From 8fa767254301af945f98a5edf8e44c61c5b0aa24 Mon Sep 17 00:00:00 2001 From: Joshua Pare Date: Sun, 8 Mar 2026 17:02:28 -0500 Subject: [PATCH 01/11] feat(ui): Add @omniview/editors package with 6 editor components New package at packages/editors/ with CodeEditor, DiffViewer, Terminal, MarkdownPreview, CommandPalette, and ObjectInspector components. - Theme integration via MutationObserver on data-ov-theme attribute - Monaco themes built from --ov-syntax-* and --ov-color-editor-* tokens - xterm themes built from --ov-color-terminal-* and --ov-color-ansi-* tokens - Heavy deps (Monaco, xterm) lazy-loaded via dynamic import + Suspense - 48 tests across all 6 components, all passing - Storybook stories with autodocs for every component --- packages/editors/package.json | 61 ++ .../code-editor/CodeEditor.module.css | 18 + .../code-editor/CodeEditor.stories.tsx | 74 ++ .../code-editor/CodeEditor.test.tsx | 65 ++ .../src/components/code-editor/CodeEditor.tsx | 159 +++ .../src/components/code-editor/index.ts | 2 + .../command-palette/CommandPalette.module.css | 133 +++ .../CommandPalette.stories.tsx | 80 ++ .../command-palette/CommandPalette.test.tsx | 104 ++ .../command-palette/CommandPalette.tsx | 212 ++++ .../src/components/command-palette/index.ts | 2 + .../diff-viewer/DiffViewer.module.css | 18 + .../diff-viewer/DiffViewer.stories.tsx | 46 + .../diff-viewer/DiffViewer.test.tsx | 61 ++ .../src/components/diff-viewer/DiffViewer.tsx | 104 ++ .../src/components/diff-viewer/index.ts | 2 + packages/editors/src/components/index.ts | 6 + .../MarkdownPreview.module.css | 133 +++ .../MarkdownPreview.stories.tsx | 89 ++ .../markdown-preview/MarkdownPreview.test.tsx | 39 + .../markdown-preview/MarkdownPreview.tsx | 58 ++ .../src/components/markdown-preview/index.ts | 2 + .../ObjectInspector.module.css | 130 +++ .../ObjectInspector.stories.tsx | 112 ++ .../object-inspector/ObjectInspector.test.tsx | 96 ++ .../object-inspector/ObjectInspector.tsx | 231 +++++ .../src/components/object-inspector/index.ts | 2 + .../components/terminal/Terminal.module.css | 9 + .../components/terminal/Terminal.stories.tsx | 54 + .../src/components/terminal/Terminal.test.tsx | 107 ++ .../src/components/terminal/Terminal.tsx | 156 +++ .../editors/src/components/terminal/index.ts | 2 + packages/editors/src/css.d.ts | 4 + packages/editors/src/index.ts | 5 + packages/editors/src/themes/index.ts | 4 + packages/editors/src/themes/monaco.ts | 119 +++ packages/editors/src/themes/useEditorTheme.ts | 31 + packages/editors/src/themes/xterm.ts | 56 + packages/editors/src/utils/language.ts | 71 ++ packages/editors/tsconfig.json | 11 + packages/editors/vite.config.ts | 34 + packages/editors/vitest.setup.ts | 11 + pnpm-lock.yaml | 965 ++++++++++++++++++ 43 files changed, 3678 insertions(+) create mode 100644 packages/editors/package.json create mode 100644 packages/editors/src/components/code-editor/CodeEditor.module.css create mode 100644 packages/editors/src/components/code-editor/CodeEditor.stories.tsx create mode 100644 packages/editors/src/components/code-editor/CodeEditor.test.tsx create mode 100644 packages/editors/src/components/code-editor/CodeEditor.tsx create mode 100644 packages/editors/src/components/code-editor/index.ts create mode 100644 packages/editors/src/components/command-palette/CommandPalette.module.css create mode 100644 packages/editors/src/components/command-palette/CommandPalette.stories.tsx create mode 100644 packages/editors/src/components/command-palette/CommandPalette.test.tsx create mode 100644 packages/editors/src/components/command-palette/CommandPalette.tsx create mode 100644 packages/editors/src/components/command-palette/index.ts create mode 100644 packages/editors/src/components/diff-viewer/DiffViewer.module.css create mode 100644 packages/editors/src/components/diff-viewer/DiffViewer.stories.tsx create mode 100644 packages/editors/src/components/diff-viewer/DiffViewer.test.tsx create mode 100644 packages/editors/src/components/diff-viewer/DiffViewer.tsx create mode 100644 packages/editors/src/components/diff-viewer/index.ts create mode 100644 packages/editors/src/components/index.ts create mode 100644 packages/editors/src/components/markdown-preview/MarkdownPreview.module.css create mode 100644 packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx create mode 100644 packages/editors/src/components/markdown-preview/MarkdownPreview.test.tsx create mode 100644 packages/editors/src/components/markdown-preview/MarkdownPreview.tsx create mode 100644 packages/editors/src/components/markdown-preview/index.ts create mode 100644 packages/editors/src/components/object-inspector/ObjectInspector.module.css create mode 100644 packages/editors/src/components/object-inspector/ObjectInspector.stories.tsx create mode 100644 packages/editors/src/components/object-inspector/ObjectInspector.test.tsx create mode 100644 packages/editors/src/components/object-inspector/ObjectInspector.tsx create mode 100644 packages/editors/src/components/object-inspector/index.ts create mode 100644 packages/editors/src/components/terminal/Terminal.module.css create mode 100644 packages/editors/src/components/terminal/Terminal.stories.tsx create mode 100644 packages/editors/src/components/terminal/Terminal.test.tsx create mode 100644 packages/editors/src/components/terminal/Terminal.tsx create mode 100644 packages/editors/src/components/terminal/index.ts create mode 100644 packages/editors/src/css.d.ts create mode 100644 packages/editors/src/index.ts create mode 100644 packages/editors/src/themes/index.ts create mode 100644 packages/editors/src/themes/monaco.ts create mode 100644 packages/editors/src/themes/useEditorTheme.ts create mode 100644 packages/editors/src/themes/xterm.ts create mode 100644 packages/editors/src/utils/language.ts create mode 100644 packages/editors/tsconfig.json create mode 100644 packages/editors/vite.config.ts create mode 100644 packages/editors/vitest.setup.ts diff --git a/packages/editors/package.json b/packages/editors/package.json new file mode 100644 index 0000000..1302c57 --- /dev/null +++ b/packages/editors/package.json @@ -0,0 +1,61 @@ +{ + "name": "@omniview/editors", + "version": "0.1.0", + "private": true, + "description": "Omniview editor components — code editor, diff viewer, terminal, markdown preview, command palette, object inspector.", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./styles.css": "./dist/style.css" + }, + "files": [ + "dist" + ], + "sideEffects": [ + "**/*.css" + ], + "scripts": { + "clean": "rm -rf dist coverage", + "build": "vite build", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@omniview/base-ui": "workspace:*" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "monaco-editor": "^0.52.2", + "@xterm/xterm": "^5.5.0", + "@xterm/addon-fit": "^0.10.0", + "@xterm/addon-search": "^0.15.0", + "@xterm/addon-webgl": "^0.18.0", + "react-markdown": "^9.0.3", + "remark-gfm": "^4.0.0", + "rehype-highlight": "^7.0.2" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^25.0.1", + "typescript": "^5.9.2", + "vite": "^5.4.14", + "vite-plugin-dts": "^4.5.1", + "vitest": "^2.1.8", + "@storybook/react": "^10.2.15", + "storybook": "^10.2.15" + } +} diff --git a/packages/editors/src/components/code-editor/CodeEditor.module.css b/packages/editors/src/components/code-editor/CodeEditor.module.css new file mode 100644 index 0000000..932d145 --- /dev/null +++ b/packages/editors/src/components/code-editor/CodeEditor.module.css @@ -0,0 +1,18 @@ +.Root { + position: relative; + overflow: hidden; + border-radius: var(--ov-radius-sm, 4px); + border: 1px solid var(--ov-color-editor-border, var(--ov-color-border-default)); + background: var(--ov-color-editor-bg, var(--ov-color-bg-surface)); +} + +.Loading { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + color: var(--ov-color-fg-muted); + font-family: var(--ov-font-family-sans, sans-serif); + font-size: var(--ov-font-size-sm, 13px); +} diff --git a/packages/editors/src/components/code-editor/CodeEditor.stories.tsx b/packages/editors/src/components/code-editor/CodeEditor.stories.tsx new file mode 100644 index 0000000..cf6b38e --- /dev/null +++ b/packages/editors/src/components/code-editor/CodeEditor.stories.tsx @@ -0,0 +1,74 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { CodeEditor } from './CodeEditor'; + +const sampleCode = `import { useState } from 'react'; + +export function Counter() { + const [count, setCount] = useState(0); + + return ( +
+

Count: {count}

+ +
+ ); +}`; + +const meta: Meta = { + title: 'Editors/CodeEditor', + component: CodeEditor, + tags: ['autodocs'], + args: { + value: sampleCode, + language: 'typescript', + height: 400, + }, + argTypes: { + language: { + control: 'select', + options: ['typescript', 'javascript', 'python', 'json', 'css', 'html', 'yaml', 'go', 'rust'], + }, + readOnly: { control: 'boolean' }, + lineNumbers: { control: 'boolean' }, + minimap: { control: 'boolean' }, + wordWrap: { control: 'boolean' }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; + +export const ReadOnly: Story = { + args: { + readOnly: true, + }, +}; + +export const WithMinimap: Story = { + args: { + minimap: true, + value: Array.from({ length: 100 }, (_, i) => `// Line ${i + 1}`).join('\n'), + }, +}; + +export const JSONContent: Story = { + args: { + value: JSON.stringify({ + name: 'omniview', + version: '1.0.0', + dependencies: { react: '^19.0.0' }, + }), + language: 'json', + }, +}; + +export const LanguageDetection: Story = { + args: { + value: 'package main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Hello")\n}', + filename: 'main.go', + }, +}; diff --git a/packages/editors/src/components/code-editor/CodeEditor.test.tsx b/packages/editors/src/components/code-editor/CodeEditor.test.tsx new file mode 100644 index 0000000..6bb8363 --- /dev/null +++ b/packages/editors/src/components/code-editor/CodeEditor.test.tsx @@ -0,0 +1,65 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { CodeEditor } from './CodeEditor'; + +// Mock @monaco-editor/react +vi.mock('@monaco-editor/react', () => ({ + __esModule: true, + default: ({ + value, + language, + options, + }: { + value: string; + language?: string; + options?: Record; + }) => ( +
+ ), +})); + +describe('CodeEditor', () => { + it('renders the editor container', () => { + render(); + expect(screen.getByTestId('code-editor')).toBeInTheDocument(); + }); + + it('passes value to the editor', () => { + render(); + expect(screen.getByTestId('monaco-editor-mock')).toHaveAttribute('data-value', 'const x = 1;'); + }); + + it('detects language from filename', () => { + render(); + expect(screen.getByTestId('monaco-editor-mock')).toHaveAttribute('data-language', 'typescript'); + }); + + it('uses explicit language over filename detection', () => { + render(); + expect(screen.getByTestId('monaco-editor-mock')).toHaveAttribute('data-language', 'python'); + }); + + it('sets readOnly via options', () => { + render(); + expect(screen.getByTestId('monaco-editor-mock')).toHaveAttribute('data-readonly', 'true'); + }); + + it('pretty-prints JSON content', () => { + const json = '{"a":1}'; + render(); + expect(screen.getByTestId('monaco-editor-mock')).toHaveAttribute( + 'data-value', + JSON.stringify({ a: 1 }, null, 2), + ); + }); + + it('merges className', () => { + render(); + expect(screen.getByTestId('code-editor')).toHaveClass('my-editor'); + }); +}); diff --git a/packages/editors/src/components/code-editor/CodeEditor.tsx b/packages/editors/src/components/code-editor/CodeEditor.tsx new file mode 100644 index 0000000..ca583df --- /dev/null +++ b/packages/editors/src/components/code-editor/CodeEditor.tsx @@ -0,0 +1,159 @@ +import { + forwardRef, + lazy, + Suspense, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import { useEditorTheme } from '../../themes/useEditorTheme'; +import { buildMonacoTheme, OV_MONACO_THEME } from '../../themes/monaco'; +import { detectLanguage } from '../../utils/language'; +import styles from './CodeEditor.module.css'; + +const MonacoEditor = lazy(() => import('@monaco-editor/react')); + +export interface CodeEditorProps { + value: string; + onChange?: (value: string) => void; + language?: string; + filename?: string; + readOnly?: boolean; + lineNumbers?: boolean; + minimap?: boolean; + wordWrap?: boolean; + height?: string | number; + width?: string | number; + className?: string; +} + +export interface CodeEditorHandle { + getEditor: () => unknown | null; + focus: () => void; +} + +function cn(...parts: Array): string { + return parts.filter(Boolean).join(' '); +} + +export const CodeEditor = forwardRef(function CodeEditor( + { + value, + onChange, + language, + filename, + readOnly = false, + lineNumbers = true, + minimap = false, + wordWrap = false, + height = '100%', + width = '100%', + className, + }, + ref, +) { + const theme = useEditorTheme(); + const editorRef = useRef(null); + const monacoRef = useRef(null); + const [isReady, setIsReady] = useState(false); + + const resolvedLanguage = language ?? (filename ? detectLanguage(filename) : undefined); + + // Format JSON content for display + const displayValue = (() => { + if (resolvedLanguage === 'json' && value) { + try { + return JSON.stringify(JSON.parse(value), null, 2); + } catch { + return value; + } + } + return value; + })(); + + const handleEditorDidMount = useCallback( + (editor: unknown, monaco: unknown) => { + editorRef.current = editor; + monacoRef.current = monaco; + + // Register and apply theme + const m = monaco as { + editor: { + defineTheme: (name: string, data: unknown) => void; + setTheme: (name: string) => void; + }; + }; + m.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); + m.editor.setTheme(OV_MONACO_THEME); + + setIsReady(true); + }, + [theme], + ); + + // Update theme when it changes + useEffect(() => { + if (!monacoRef.current || !isReady) return; + const m = monacoRef.current as { + editor: { + defineTheme: (name: string, data: unknown) => void; + setTheme: (name: string) => void; + }; + }; + m.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); + m.editor.setTheme(OV_MONACO_THEME); + }, [theme, isReady]); + + useImperativeHandle(ref, () => ({ + getEditor: () => editorRef.current, + focus: () => { + const e = editorRef.current as { focus?: () => void } | null; + e?.focus?.(); + }, + })); + + const handleChange = useCallback( + (val: string | undefined) => { + if (onChange && val !== undefined) { + onChange(val); + } + }, + [onChange], + ); + + return ( +
+ + Loading editor… +
+ } + > + + +
+ ); +}); + +CodeEditor.displayName = 'CodeEditor'; diff --git a/packages/editors/src/components/code-editor/index.ts b/packages/editors/src/components/code-editor/index.ts new file mode 100644 index 0000000..ac4f00d --- /dev/null +++ b/packages/editors/src/components/code-editor/index.ts @@ -0,0 +1,2 @@ +export { CodeEditor } from './CodeEditor'; +export type { CodeEditorProps, CodeEditorHandle } from './CodeEditor'; diff --git a/packages/editors/src/components/command-palette/CommandPalette.module.css b/packages/editors/src/components/command-palette/CommandPalette.module.css new file mode 100644 index 0000000..0d75a58 --- /dev/null +++ b/packages/editors/src/components/command-palette/CommandPalette.module.css @@ -0,0 +1,133 @@ +.Overlay { + position: fixed; + inset: 0; + z-index: var(--ov-z-modal, 1000); + display: flex; + justify-content: center; + padding-top: 20vh; + background: rgba(0, 0, 0, 0.4); +} + +.Root { + width: min(600px, 90vw); + max-height: 60vh; + display: flex; + flex-direction: column; + background: var(--ov-color-quickpick-bg, var(--ov-color-bg-surface)); + border: 1px solid var(--ov-color-quickpick-border, var(--ov-color-border-default)); + border-radius: var(--ov-radius-md, 8px); + box-shadow: var(--ov-color-quickpick-shadow, 0 8px 32px rgba(0, 0, 0, 0.4)); + overflow: hidden; +} + +.InputWrapper { + padding: var(--ov-space-2, 8px); + border-bottom: 1px solid var(--ov-color-border-default); +} + +.Input { + width: 100%; + padding: var(--ov-space-2, 8px) var(--ov-space-3, 12px); + background: var(--ov-color-input-bg, transparent); + color: var(--ov-color-input-fg, var(--ov-color-fg-default)); + border: 1px solid var(--ov-color-input-border, var(--ov-color-border-default)); + border-radius: var(--ov-radius-sm, 4px); + font-family: var(--ov-font-family-sans, sans-serif); + font-size: var(--ov-font-size-md, 14px); + outline: none; +} + +.Input::placeholder { + color: var(--ov-color-input-placeholder, var(--ov-color-fg-muted)); +} + +.Input:focus { + border-color: var(--ov-color-fg-accent); +} + +.List { + flex: 1; + overflow-y: auto; + padding: var(--ov-space-1, 4px); +} + +.Group { + margin-bottom: var(--ov-space-1, 4px); +} + +.GroupLabel { + padding: var(--ov-space-1, 4px) var(--ov-space-2, 8px); + font-size: var(--ov-font-size-xs, 11px); + font-weight: var(--ov-font-weight-semibold, 500); + color: var(--ov-color-fg-muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.Item { + display: flex; + align-items: center; + gap: var(--ov-space-2, 8px); + padding: var(--ov-space-2, 8px) var(--ov-space-3, 12px); + border-radius: var(--ov-radius-sm, 4px); + cursor: pointer; + user-select: none; +} + +.Item:hover, +.ItemActive { + background: var(--ov-color-list-hover-bg, var(--ov-color-bg-subtle)); + color: var(--ov-color-list-hover-fg, var(--ov-color-fg-default)); +} + +.Icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + color: var(--ov-color-fg-muted); +} + +.Content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 1px; +} + +.Label { + font-size: var(--ov-font-size-sm, 13px); + color: var(--ov-color-fg-default); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.Description { + font-size: var(--ov-font-size-xs, 11px); + color: var(--ov-color-fg-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.Shortcut { + flex-shrink: 0; + padding: var(--ov-space-0, 2px) var(--ov-space-1, 4px); + font-family: var(--ov-font-family-sans, sans-serif); + font-size: var(--ov-font-size-xs, 11px); + color: var(--ov-color-fg-muted); + background: var(--ov-color-bg-subtle); + border: 1px solid var(--ov-color-border-default); + border-radius: var(--ov-radius-xs, 3px); +} + +.Empty { + padding: var(--ov-space-4, 16px); + text-align: center; + color: var(--ov-color-fg-muted); + font-size: var(--ov-font-size-sm, 13px); +} diff --git a/packages/editors/src/components/command-palette/CommandPalette.stories.tsx b/packages/editors/src/components/command-palette/CommandPalette.stories.tsx new file mode 100644 index 0000000..ed8e422 --- /dev/null +++ b/packages/editors/src/components/command-palette/CommandPalette.stories.tsx @@ -0,0 +1,80 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { CommandPalette, type CommandItem, type CommandPaletteProps } from './CommandPalette'; + +const sampleCommands: CommandItem[] = [ + { id: 'new-file', label: 'New File', shortcut: 'Ctrl+N', group: 'File' }, + { id: 'open-file', label: 'Open File', shortcut: 'Ctrl+O', group: 'File' }, + { id: 'save', label: 'Save', shortcut: 'Ctrl+S', group: 'File' }, + { id: 'save-as', label: 'Save As…', shortcut: 'Ctrl+Shift+S', group: 'File' }, + { id: 'find', label: 'Find', shortcut: 'Ctrl+F', group: 'Edit' }, + { id: 'replace', label: 'Replace', shortcut: 'Ctrl+H', group: 'Edit' }, + { id: 'find-files', label: 'Find in Files', shortcut: 'Ctrl+Shift+F', group: 'Search' }, + { id: 'toggle-terminal', label: 'Toggle Terminal', shortcut: 'Ctrl+`', group: 'View' }, + { id: 'toggle-sidebar', label: 'Toggle Sidebar', shortcut: 'Ctrl+B', group: 'View' }, + { id: 'zoom-in', label: 'Zoom In', shortcut: 'Ctrl++', group: 'View' }, + { id: 'zoom-out', label: 'Zoom Out', shortcut: 'Ctrl+-', group: 'View' }, + { id: 'git-commit', label: 'Git: Commit', group: 'Source Control' }, + { id: 'git-push', label: 'Git: Push', group: 'Source Control' }, +]; + +const meta: Meta = { + title: 'Editors/CommandPalette', + component: CommandPalette, + tags: ['autodocs'], + args: { + open: true, + onClose: () => {}, + onSelect: () => {}, + commands: sampleCommands, + placeholder: 'Type a command…', + }, +}; + +export default meta; +type Story = StoryObj; + +function PlaygroundStory(args: CommandPaletteProps) { + const [open, setOpen] = useState(true); + return ( +
+ + setOpen(false)} + onSelect={() => setOpen(false)} + /> +
+ ); +} + +export const Playground: Story = { + render: (args) => , +}; + +function ManyCommandsStory(args: CommandPaletteProps) { + const [open, setOpen] = useState(true); + const manyCommands = Array.from({ length: 50 }, (_, i) => ({ + id: `cmd-${i}`, + label: `Command ${i + 1}`, + description: `Description for command ${i + 1}`, + group: `Group ${Math.floor(i / 10) + 1}`, + })); + return ( +
+ + setOpen(false)} + onSelect={() => setOpen(false)} + /> +
+ ); +} + +export const WithManyCommands: Story = { + render: (args) => , +}; diff --git a/packages/editors/src/components/command-palette/CommandPalette.test.tsx b/packages/editors/src/components/command-palette/CommandPalette.test.tsx new file mode 100644 index 0000000..7373d0c --- /dev/null +++ b/packages/editors/src/components/command-palette/CommandPalette.test.tsx @@ -0,0 +1,104 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; +import { CommandPalette, type CommandItem } from './CommandPalette'; + +const commands: CommandItem[] = [ + { id: 'open', label: 'Open File', shortcut: 'Ctrl+O', group: 'File' }, + { id: 'save', label: 'Save File', shortcut: 'Ctrl+S', group: 'File' }, + { id: 'find', label: 'Find in Files', shortcut: 'Ctrl+Shift+F', group: 'Search' }, + { id: 'terminal', label: 'Toggle Terminal', shortcut: 'Ctrl+`', group: 'View' }, + { id: 'disabled', label: 'Disabled Command', disabled: true }, +]; + +describe('CommandPalette', () => { + const defaultProps = { + open: true, + onClose: vi.fn(), + commands, + onSelect: vi.fn(), + }; + + it('renders when open', () => { + render(); + expect(screen.getByTestId('command-palette')).toBeInTheDocument(); + }); + + it('does not render when closed', () => { + render(); + expect(screen.queryByTestId('command-palette')).not.toBeInTheDocument(); + }); + + it('shows command items', () => { + render(); + expect(screen.getByTestId('command-item-open')).toBeInTheDocument(); + expect(screen.getByTestId('command-item-save')).toBeInTheDocument(); + expect(screen.getByTestId('command-item-find')).toBeInTheDocument(); + }); + + it('filters out disabled commands', () => { + render(); + expect(screen.queryByTestId('command-item-disabled')).not.toBeInTheDocument(); + }); + + it('filters commands by search text', async () => { + const user = userEvent.setup(); + render(); + const input = screen.getByTestId('command-palette-input'); + await user.type(input, 'terminal'); + expect(screen.getByTestId('command-item-terminal')).toBeInTheDocument(); + expect(screen.queryByTestId('command-item-open')).not.toBeInTheDocument(); + }); + + it('shows empty state when no matches', async () => { + const user = userEvent.setup(); + render(); + await user.type(screen.getByTestId('command-palette-input'), 'zzzzz'); + expect(screen.getByTestId('command-palette-empty')).toBeInTheDocument(); + }); + + it('selects command on click', async () => { + const onSelect = vi.fn(); + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByTestId('command-item-open')); + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'open' })); + expect(onClose).toHaveBeenCalled(); + }); + + it('closes on Escape', () => { + const onClose = vi.fn(); + render(); + fireEvent.keyDown(screen.getByTestId('command-palette-input'), { key: 'Escape' }); + expect(onClose).toHaveBeenCalled(); + }); + + it('navigates with arrow keys and selects with Enter', () => { + const onSelect = vi.fn(); + const onClose = vi.fn(); + render(); + const input = screen.getByTestId('command-palette-input'); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + fireEvent.keyDown(input, { key: 'Enter' }); + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'save' })); + }); + + it('closes on overlay click', () => { + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByTestId('command-palette-overlay')); + expect(onClose).toHaveBeenCalled(); + }); + + it('renders group labels', () => { + render(); + expect(screen.getByText('File')).toBeInTheDocument(); + expect(screen.getByText('Search')).toBeInTheDocument(); + expect(screen.getByText('View')).toBeInTheDocument(); + }); + + it('displays keyboard shortcuts', () => { + render(); + expect(screen.getByText('Ctrl+O')).toBeInTheDocument(); + }); +}); diff --git a/packages/editors/src/components/command-palette/CommandPalette.tsx b/packages/editors/src/components/command-palette/CommandPalette.tsx new file mode 100644 index 0000000..f40e78e --- /dev/null +++ b/packages/editors/src/components/command-palette/CommandPalette.tsx @@ -0,0 +1,212 @@ +import { + forwardRef, + useCallback, + useEffect, + useMemo, + useRef, + useState, + type KeyboardEvent, + type ReactNode, +} from 'react'; +import styles from './CommandPalette.module.css'; + +export interface CommandItem { + id: string; + label: string; + description?: string; + icon?: ReactNode; + shortcut?: string; + group?: string; + disabled?: boolean; +} + +export interface CommandPaletteProps { + open: boolean; + onClose: () => void; + commands: CommandItem[]; + onSelect: (command: CommandItem) => void; + placeholder?: string; +} + +function cn(...parts: Array): string { + return parts.filter(Boolean).join(' '); +} + +function fuzzyMatch(text: string, query: string): boolean { + const lower = text.toLowerCase(); + const q = query.toLowerCase(); + let qi = 0; + for (let i = 0; i < lower.length && qi < q.length; i++) { + if (lower[i] === q[qi]) qi++; + } + return qi === q.length; +} + +export const CommandPalette = forwardRef( + function CommandPalette( + { open, onClose, commands, onSelect, placeholder = 'Type a command…' }, + ref, + ) { + const [search, setSearch] = useState(''); + const [activeIndex, setActiveIndex] = useState(0); + const inputRef = useRef(null); + const listRef = useRef(null); + + const filtered = useMemo(() => { + if (!search) return commands.filter((c) => !c.disabled); + return commands.filter( + (c) => + !c.disabled && + (fuzzyMatch(c.label, search) || + (c.description && fuzzyMatch(c.description, search)) || + (c.group && fuzzyMatch(c.group, search))), + ); + }, [commands, search]); + + // Group commands + const groups = useMemo(() => { + const map = new Map(); + for (const cmd of filtered) { + const group = cmd.group || ''; + const list = map.get(group); + if (list) { + list.push(cmd); + } else { + map.set(group, [cmd]); + } + } + return map; + }, [filtered]); + + // Reset state when opened/closed + useEffect(() => { + if (open) { + setSearch(''); + setActiveIndex(0); + // Focus input after render + requestAnimationFrame(() => inputRef.current?.focus()); + } + }, [open]); + + // Scroll active item into view + useEffect(() => { + if (!listRef.current) return; + const active = listRef.current.querySelector('[data-active="true"]'); + if (active && typeof active.scrollIntoView === 'function') { + active.scrollIntoView({ block: 'nearest' }); + } + }, [activeIndex]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setActiveIndex((i) => (i + 1) % Math.max(filtered.length, 1)); + break; + case 'ArrowUp': + e.preventDefault(); + setActiveIndex((i) => (i - 1 + filtered.length) % Math.max(filtered.length, 1)); + break; + case 'Enter': { + e.preventDefault(); + const selected = filtered[activeIndex]; + if (selected) { + onSelect(selected); + onClose(); + } + break; + } + case 'Escape': + e.preventDefault(); + onClose(); + break; + } + }, + [filtered, activeIndex, onSelect, onClose], + ); + + if (!open) return null; + + let flatIndex = 0; + + return ( +
+
e.stopPropagation()} + onKeyDown={handleKeyDown} + role="dialog" + aria-modal="true" + aria-label="Command palette" + data-testid="command-palette" + > +
+ { + setSearch(e.target.value); + setActiveIndex(0); + }} + placeholder={placeholder} + aria-label="Search commands" + data-testid="command-palette-input" + /> +
+
+ {filtered.length === 0 ? ( +
+ No matching commands +
+ ) : ( + Array.from(groups.entries()).map(([group, items]) => ( +
+ {group &&
{group}
} + {items.map((cmd) => { + const index = flatIndex++; + const isActive = index === activeIndex; + return ( +
{ + onSelect(cmd); + onClose(); + }} + onMouseEnter={() => setActiveIndex(index)} + > + {cmd.icon && {cmd.icon}} +
+ {cmd.label} + {cmd.description && ( + {cmd.description} + )} +
+ {cmd.shortcut && {cmd.shortcut}} +
+ ); + })} +
+ )) + )} +
+
+
+ ); + }, +); + +CommandPalette.displayName = 'CommandPalette'; diff --git a/packages/editors/src/components/command-palette/index.ts b/packages/editors/src/components/command-palette/index.ts new file mode 100644 index 0000000..9c3711c --- /dev/null +++ b/packages/editors/src/components/command-palette/index.ts @@ -0,0 +1,2 @@ +export { CommandPalette } from './CommandPalette'; +export type { CommandPaletteProps, CommandItem } from './CommandPalette'; diff --git a/packages/editors/src/components/diff-viewer/DiffViewer.module.css b/packages/editors/src/components/diff-viewer/DiffViewer.module.css new file mode 100644 index 0000000..932d145 --- /dev/null +++ b/packages/editors/src/components/diff-viewer/DiffViewer.module.css @@ -0,0 +1,18 @@ +.Root { + position: relative; + overflow: hidden; + border-radius: var(--ov-radius-sm, 4px); + border: 1px solid var(--ov-color-editor-border, var(--ov-color-border-default)); + background: var(--ov-color-editor-bg, var(--ov-color-bg-surface)); +} + +.Loading { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + color: var(--ov-color-fg-muted); + font-family: var(--ov-font-family-sans, sans-serif); + font-size: var(--ov-font-size-sm, 13px); +} diff --git a/packages/editors/src/components/diff-viewer/DiffViewer.stories.tsx b/packages/editors/src/components/diff-viewer/DiffViewer.stories.tsx new file mode 100644 index 0000000..bf931d7 --- /dev/null +++ b/packages/editors/src/components/diff-viewer/DiffViewer.stories.tsx @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { DiffViewer } from './DiffViewer'; + +const original = `function greet(name) { + console.log("Hello, " + name); + return true; +}`; + +const modified = `function greet(name: string) { + console.log(\`Hello, \${name}\`); + return name.length > 0; +}`; + +const meta: Meta = { + title: 'Editors/DiffViewer', + component: DiffViewer, + tags: ['autodocs'], + args: { + original, + modified, + language: 'typescript', + height: 400, + }, + argTypes: { + mode: { control: 'radio', options: ['side-by-side', 'inline'] }, + readOnly: { control: 'boolean' }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; + +export const InlineMode: Story = { + args: { mode: 'inline' }, +}; + +export const LargeDiff: Story = { + args: { + original: Array.from({ length: 50 }, (_, i) => `// Original line ${i + 1}`).join('\n'), + modified: Array.from({ length: 50 }, (_, i) => + i % 5 === 0 ? `// Modified line ${i + 1}` : `// Original line ${i + 1}`, + ).join('\n'), + }, +}; diff --git a/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx b/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx new file mode 100644 index 0000000..e867887 --- /dev/null +++ b/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx @@ -0,0 +1,61 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { DiffViewer } from './DiffViewer'; + +vi.mock('@monaco-editor/react', () => ({ + __esModule: true, + default: () =>
, + DiffEditor: ({ + original, + modified, + language, + options, + }: { + original: string; + modified: string; + language?: string; + options?: Record; + }) => ( +
+ ), +})); + +describe('DiffViewer', () => { + it('renders the diff container', () => { + render(); + expect(screen.getByTestId('diff-viewer')).toBeInTheDocument(); + }); + + it('passes original and modified to the editor', () => { + render(); + const mock = screen.getByTestId('monaco-diff-mock'); + expect(mock).toHaveAttribute('data-original', 'line1'); + expect(mock).toHaveAttribute('data-modified', 'line2'); + }); + + it('applies language prop', () => { + render(); + expect(screen.getByTestId('monaco-diff-mock')).toHaveAttribute('data-language', 'yaml'); + }); + + it('defaults to side-by-side mode', () => { + render(); + expect(screen.getByTestId('monaco-diff-mock')).toHaveAttribute('data-side-by-side', 'true'); + }); + + it('switches to inline mode', () => { + render(); + expect(screen.getByTestId('monaco-diff-mock')).toHaveAttribute('data-side-by-side', 'false'); + }); + + it('merges className', () => { + render(); + expect(screen.getByTestId('diff-viewer')).toHaveClass('custom'); + }); +}); diff --git a/packages/editors/src/components/diff-viewer/DiffViewer.tsx b/packages/editors/src/components/diff-viewer/DiffViewer.tsx new file mode 100644 index 0000000..3d1be7c --- /dev/null +++ b/packages/editors/src/components/diff-viewer/DiffViewer.tsx @@ -0,0 +1,104 @@ +import { forwardRef, lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react'; +import { useEditorTheme } from '../../themes/useEditorTheme'; +import { buildMonacoTheme, OV_MONACO_THEME } from '../../themes/monaco'; +import styles from './DiffViewer.module.css'; + +const MonacoDiffEditor = lazy(() => + import('@monaco-editor/react').then((mod) => ({ default: mod.DiffEditor })), +); + +export type DiffMode = 'side-by-side' | 'inline'; + +export interface DiffViewerProps { + original: string; + modified: string; + language?: string; + mode?: DiffMode; + readOnly?: boolean; + height?: string | number; + className?: string; +} + +function cn(...parts: Array): string { + return parts.filter(Boolean).join(' '); +} + +export const DiffViewer = forwardRef(function DiffViewer( + { + original, + modified, + language, + mode = 'side-by-side', + readOnly = true, + height = '100%', + className, + }, + ref, +) { + const theme = useEditorTheme(); + const monacoRef = useRef(null); + const [isReady, setIsReady] = useState(false); + + const handleEditorDidMount = useCallback( + (_editor: unknown, monaco: unknown) => { + monacoRef.current = monaco; + const m = monaco as { + editor: { + defineTheme: (name: string, data: unknown) => void; + setTheme: (name: string) => void; + }; + }; + m.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); + m.editor.setTheme(OV_MONACO_THEME); + setIsReady(true); + }, + [theme], + ); + + useEffect(() => { + if (!monacoRef.current || !isReady) return; + const m = monacoRef.current as { + editor: { + defineTheme: (name: string, data: unknown) => void; + setTheme: (name: string) => void; + }; + }; + m.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); + m.editor.setTheme(OV_MONACO_THEME); + }, [theme, isReady]); + + return ( +
+ + Loading diff… +
+ } + > + + +
+ ); +}); + +DiffViewer.displayName = 'DiffViewer'; diff --git a/packages/editors/src/components/diff-viewer/index.ts b/packages/editors/src/components/diff-viewer/index.ts new file mode 100644 index 0000000..4e96330 --- /dev/null +++ b/packages/editors/src/components/diff-viewer/index.ts @@ -0,0 +1,2 @@ +export { DiffViewer } from './DiffViewer'; +export type { DiffViewerProps, DiffMode } from './DiffViewer'; diff --git a/packages/editors/src/components/index.ts b/packages/editors/src/components/index.ts new file mode 100644 index 0000000..e42c26a --- /dev/null +++ b/packages/editors/src/components/index.ts @@ -0,0 +1,6 @@ +export * from './code-editor'; +export * from './diff-viewer'; +export * from './terminal'; +export * from './markdown-preview'; +export * from './command-palette'; +export * from './object-inspector'; diff --git a/packages/editors/src/components/markdown-preview/MarkdownPreview.module.css b/packages/editors/src/components/markdown-preview/MarkdownPreview.module.css new file mode 100644 index 0000000..0ed404d --- /dev/null +++ b/packages/editors/src/components/markdown-preview/MarkdownPreview.module.css @@ -0,0 +1,133 @@ +.Root { + color: var(--ov-color-editor-fg, var(--ov-color-fg-default)); + font-family: var(--ov-font-family-sans, sans-serif); + font-size: var(--ov-font-size-md, 14px); + line-height: var(--ov-line-height-md, 1.6); + padding: var(--ov-space-3, 12px); + overflow-y: auto; +} + +.Root h1, +.Root h2, +.Root h3, +.Root h4, +.Root h5, +.Root h6 { + color: var(--ov-color-fg-default); + font-weight: var(--ov-font-weight-bold, 600); + margin-top: var(--ov-space-4, 16px); + margin-bottom: var(--ov-space-2, 8px); +} + +.Root h1 { + font-size: var(--ov-font-size-2xl, 24px); + border-bottom: 1px solid var(--ov-color-border-default); + padding-bottom: var(--ov-space-2, 8px); +} + +.Root h2 { + font-size: var(--ov-font-size-xl, 20px); + border-bottom: 1px solid var(--ov-color-border-default); + padding-bottom: var(--ov-space-1, 4px); +} + +.Root h3 { + font-size: var(--ov-font-size-lg, 16px); +} + +.Root p { + margin-top: 0; + margin-bottom: var(--ov-space-3, 12px); +} + +.Root a { + color: var(--ov-color-markdown-link, var(--ov-color-fg-accent)); + text-decoration: none; +} + +.Root a:hover { + text-decoration: underline; +} + +.Root code { + background: var(--ov-color-markdown-code-bg, var(--ov-color-bg-subtle)); + border-radius: var(--ov-radius-xs, 3px); + padding: 0.15em 0.4em; + font-family: var(--ov-font-family-mono, monospace); + font-size: 0.9em; +} + +.Root pre { + background: var(--ov-color-markdown-code-bg, var(--ov-color-bg-subtle)); + border-radius: var(--ov-radius-sm, 4px); + padding: var(--ov-space-3, 12px); + overflow-x: auto; + margin-bottom: var(--ov-space-3, 12px); +} + +.Root pre code { + background: none; + padding: 0; + border-radius: 0; +} + +.Root blockquote { + margin: 0 0 var(--ov-space-3, 12px) 0; + padding: var(--ov-space-1, 4px) var(--ov-space-3, 12px); + border-left: 3px solid var(--ov-color-markdown-quote-border, var(--ov-color-border-default)); + color: var(--ov-color-fg-muted); +} + +.Root ul, +.Root ol { + margin-top: 0; + margin-bottom: var(--ov-space-3, 12px); + padding-left: var(--ov-space-5, 24px); +} + +.Root li { + margin-bottom: var(--ov-space-1, 4px); +} + +.Root table { + border-collapse: collapse; + width: 100%; + margin-bottom: var(--ov-space-3, 12px); +} + +.Root th, +.Root td { + border: 1px solid var(--ov-color-border-default); + padding: var(--ov-space-2, 8px) var(--ov-space-3, 12px); + text-align: left; +} + +.Root th { + background: var(--ov-color-bg-subtle); + font-weight: var(--ov-font-weight-semibold, 500); +} + +.Root hr { + border: none; + border-top: 1px solid var(--ov-color-border-default); + margin: var(--ov-space-4, 16px) 0; +} + +.Root img { + max-width: 100%; + height: auto; + border-radius: var(--ov-radius-sm, 4px); +} + +.Root input[type='checkbox'] { + margin-right: var(--ov-space-1, 4px); +} + +.Loading { + display: flex; + align-items: center; + justify-content: center; + padding: var(--ov-space-4, 16px); + color: var(--ov-color-fg-muted); + font-size: var(--ov-font-size-sm, 13px); +} diff --git a/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx b/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx new file mode 100644 index 0000000..349e5cf --- /dev/null +++ b/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx @@ -0,0 +1,89 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { MarkdownPreview } from './MarkdownPreview'; + +const sampleMarkdown = `# Heading 1 + +## Heading 2 + +This is a paragraph with **bold**, *italic*, and \`inline code\`. + +### Code Block + +\`\`\`typescript +function greet(name: string): string { + return \`Hello, \${name}!\`; +} +\`\`\` + +### Links + +[Visit OpenAI](https://openai.com) + +### Lists + +- Item one +- Item two + - Nested item +- Item three + +1. First +2. Second +3. Third + +### Task List + +- [x] Completed task +- [ ] Incomplete task + +### Blockquote + +> This is a blockquote +> with multiple lines. + +### Table + +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Cell 1 | Cell 2 | Cell 3 | +| Cell 4 | Cell 5 | Cell 6 | + +--- + +*End of preview* +`; + +const meta: Meta = { + title: 'Editors/MarkdownPreview', + component: MarkdownPreview, + tags: ['autodocs'], + args: { + content: sampleMarkdown, + }, + argTypes: { + allowHtml: { control: 'boolean' }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; + +export const Simple: Story = { + args: { + content: '# Hello World\n\nThis is a simple markdown preview.', + }, +}; + +export const WithCode: Story = { + args: { + content: '## Code Example\n\n```js\nconst x = 42;\nconsole.log(x);\n```', + }, +}; diff --git a/packages/editors/src/components/markdown-preview/MarkdownPreview.test.tsx b/packages/editors/src/components/markdown-preview/MarkdownPreview.test.tsx new file mode 100644 index 0000000..59719e5 --- /dev/null +++ b/packages/editors/src/components/markdown-preview/MarkdownPreview.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { MarkdownPreview } from './MarkdownPreview'; + +// Mock react-markdown +vi.mock('react-markdown', () => ({ + __esModule: true, + default: ({ children }: { children: string }) => ( +
{children}
+ ), +})); + +describe('MarkdownPreview', () => { + it('renders the preview container', () => { + render(); + expect(screen.getByTestId('markdown-preview')).toBeInTheDocument(); + }); + + it('passes content to the markdown renderer', () => { + render(); + expect(screen.getByTestId('react-markdown-mock')).toHaveTextContent('# Title'); + }); + + it('merges className', () => { + render(); + expect(screen.getByTestId('markdown-preview')).toHaveClass('custom-md'); + }); + + it('forwards ref', () => { + const ref = { current: null } as unknown as React.RefObject; + render(); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); + + it('passes additional HTML attributes', () => { + render(); + expect(screen.getByTestId('markdown-preview')).toHaveAttribute('aria-label', 'Preview'); + }); +}); diff --git a/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx b/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx new file mode 100644 index 0000000..92b6b8e --- /dev/null +++ b/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx @@ -0,0 +1,58 @@ +import { forwardRef, lazy, Suspense, type HTMLAttributes } from 'react'; +import styles from './MarkdownPreview.module.css'; + +const ReactMarkdown = lazy(() => import('react-markdown')); + +export interface MarkdownPreviewProps extends HTMLAttributes { + content: string; + allowHtml?: boolean; +} + +function cn(...parts: Array): string { + return parts.filter(Boolean).join(' '); +} + +export const MarkdownPreview = forwardRef( + function MarkdownPreview({ content, allowHtml = false, className, ...props }, ref) { + return ( +
+ + Loading… +
+ } + > + + +
+ ); + }, +); + +MarkdownPreview.displayName = 'MarkdownPreview'; + +/** Inner component that uses lazy-loaded deps */ +function MarkdownContent({ content, allowHtml }: { content: string; allowHtml: boolean }) { + // Dynamic imports for remark/rehype plugins + // They're imported alongside react-markdown via the lazy boundary + return ( + ( + + {children} + + ), + }} + > + {content} + + ); +} diff --git a/packages/editors/src/components/markdown-preview/index.ts b/packages/editors/src/components/markdown-preview/index.ts new file mode 100644 index 0000000..84bb173 --- /dev/null +++ b/packages/editors/src/components/markdown-preview/index.ts @@ -0,0 +1,2 @@ +export { MarkdownPreview } from './MarkdownPreview'; +export type { MarkdownPreviewProps } from './MarkdownPreview'; diff --git a/packages/editors/src/components/object-inspector/ObjectInspector.module.css b/packages/editors/src/components/object-inspector/ObjectInspector.module.css new file mode 100644 index 0000000..d25299b --- /dev/null +++ b/packages/editors/src/components/object-inspector/ObjectInspector.module.css @@ -0,0 +1,130 @@ +.Root { + font-family: var(--ov-font-family-mono, monospace); + font-size: var(--ov-font-size-sm, 13px); + line-height: 1.5; + color: var(--ov-color-editor-fg, var(--ov-color-fg-default)); + background: var(--ov-color-editor-bg, var(--ov-color-bg-surface)); + border: 1px solid var(--ov-color-editor-border, var(--ov-color-border-default)); + border-radius: var(--ov-radius-sm, 4px); + overflow: auto; +} + +.Toolbar { + display: flex; + align-items: center; + gap: var(--ov-space-2, 8px); + padding: var(--ov-space-2, 8px); + border-bottom: 1px solid var(--ov-color-border-default); +} + +.SearchInput { + flex: 1; + padding: var(--ov-space-1, 4px) var(--ov-space-2, 8px); + background: var(--ov-color-input-bg, transparent); + color: var(--ov-color-input-fg, var(--ov-color-fg-default)); + border: 1px solid var(--ov-color-input-border, var(--ov-color-border-default)); + border-radius: var(--ov-radius-sm, 4px); + font-family: inherit; + font-size: inherit; + outline: none; +} + +.SearchInput::placeholder { + color: var(--ov-color-input-placeholder, var(--ov-color-fg-muted)); +} + +.CopyButton { + padding: var(--ov-space-1, 4px) var(--ov-space-2, 8px); + background: var(--ov-color-button-secondary-bg, var(--ov-color-bg-subtle)); + color: var(--ov-color-button-secondary-fg, var(--ov-color-fg-default)); + border: 1px solid var(--ov-color-border-default); + border-radius: var(--ov-radius-sm, 4px); + font-size: var(--ov-font-size-xs, 11px); + cursor: pointer; +} + +.CopyButton:hover { + background: var(--ov-color-button-secondary-hover-bg, var(--ov-color-bg-subtle)); +} + +.Tree { + padding: var(--ov-space-2, 8px); +} + +.Node { + /* container for tree node */ +} + +.Row { + display: flex; + align-items: center; + padding: 1px 0; + cursor: default; + white-space: nowrap; +} + +.Row:hover { + background: var(--ov-color-list-hover-bg, var(--ov-color-bg-subtle)); +} + +.RowHighlight { + background: var(--ov-color-editor-find-match-bg, rgba(255, 255, 0, 0.15)); +} + +.Chevron { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; + font-size: 10px; + color: var(--ov-color-fg-muted); + transition: transform var(--ov-duration-fast, 100ms) var(--ov-ease-standard, ease); + cursor: pointer; +} + +.ChevronExpanded { + transform: rotate(90deg); +} + +.ChevronSpacer { + display: inline-block; + width: 16px; + flex-shrink: 0; +} + +.Key { + color: var(--ov-syntax-property, var(--ov-color-fg-default)); +} + +.Colon { + color: var(--ov-syntax-punctuation, var(--ov-color-fg-muted)); +} + +.Value[data-type='string'] { + color: var(--ov-syntax-string, #ce9178); +} + +.Value[data-type='number'] { + color: var(--ov-syntax-number, #b5cea8); +} + +.Value[data-type='boolean'] { + color: var(--ov-syntax-keyword, #569cd6); +} + +.Value[data-type='null'], +.Value[data-type='undefined'] { + color: var(--ov-syntax-keyword, #569cd6); + font-style: italic; +} + +.Preview { + color: var(--ov-color-fg-muted); + font-style: italic; +} + +.Comma { + color: var(--ov-syntax-punctuation, var(--ov-color-fg-muted)); +} diff --git a/packages/editors/src/components/object-inspector/ObjectInspector.stories.tsx b/packages/editors/src/components/object-inspector/ObjectInspector.stories.tsx new file mode 100644 index 0000000..980b0cf --- /dev/null +++ b/packages/editors/src/components/object-inspector/ObjectInspector.stories.tsx @@ -0,0 +1,112 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ObjectInspector } from './ObjectInspector'; + +const sampleData = { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + name: 'my-pod', + namespace: 'default', + labels: { + app: 'web', + version: 'v1.2.3', + }, + }, + spec: { + containers: [ + { + name: 'web', + image: 'nginx:latest', + ports: [{ containerPort: 80 }], + env: [ + { name: 'NODE_ENV', value: 'production' }, + { name: 'PORT', value: '8080' }, + ], + }, + ], + restartPolicy: 'Always', + }, + status: { + phase: 'Running', + conditions: [ + { type: 'Ready', status: true }, + { type: 'PodScheduled', status: true }, + ], + startTime: '2024-01-15T10:30:00Z', + }, +}; + +const meta: Meta = { + title: 'Editors/ObjectInspector', + component: ObjectInspector, + tags: ['autodocs'], + args: { + data: sampleData, + defaultExpanded: 2, + }, + argTypes: { + format: { control: 'radio', options: ['json', 'yaml'] }, + defaultExpanded: { control: { type: 'range', min: 0, max: 5, step: 1 } }, + searchable: { control: 'boolean' }, + copyable: { control: 'boolean' }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; + +export const FullyExpanded: Story = { + args: { + defaultExpanded: true, + }, +}; + +export const Collapsed: Story = { + args: { + defaultExpanded: 0, + }, +}; + +export const WithSearch: Story = { + args: { + searchable: true, + defaultExpanded: true, + }, +}; + +export const WithCopy: Story = { + args: { + copyable: true, + }, +}; + +export const WithSearchAndCopy: Story = { + args: { + searchable: true, + copyable: true, + defaultExpanded: true, + }, +}; + +export const YamlFormat: Story = { + args: { + format: 'yaml', + copyable: true, + }, +}; + +export const SimpleArray: Story = { + args: { + data: ['hello', 42, true, null, { nested: 'value' }], + defaultExpanded: true, + }, +}; diff --git a/packages/editors/src/components/object-inspector/ObjectInspector.test.tsx b/packages/editors/src/components/object-inspector/ObjectInspector.test.tsx new file mode 100644 index 0000000..eb9a2d5 --- /dev/null +++ b/packages/editors/src/components/object-inspector/ObjectInspector.test.tsx @@ -0,0 +1,96 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; +import { ObjectInspector } from './ObjectInspector'; + +const testData = { + name: 'test', + count: 42, + active: true, + items: ['a', 'b'], + nested: { + key: 'value', + }, + nothing: null, +}; + +describe('ObjectInspector', () => { + it('renders the inspector container', () => { + render(); + expect(screen.getByTestId('object-inspector')).toBeInTheDocument(); + }); + + it('renders the tree', () => { + render(); + expect(screen.getByTestId('inspector-tree')).toBeInTheDocument(); + }); + + it('renders root node', () => { + render(); + expect(screen.getByTestId('inspector-node-root')).toBeInTheDocument(); + }); + + it('expands to default depth', () => { + render(); + // Root (depth 0) is expanded, children are visible + expect(screen.getByTestId('inspector-node-name')).toBeInTheDocument(); + expect(screen.getByTestId('inspector-node-count')).toBeInTheDocument(); + }); + + it('expands/collapses on click', () => { + render(); + const root = screen.getByTestId('inspector-node-root'); + // Initially collapsed — children not visible + expect(screen.queryByTestId('inspector-node-name')).not.toBeInTheDocument(); + fireEvent.click(root); + expect(screen.getByTestId('inspector-node-name')).toBeInTheDocument(); + }); + + it('shows search input when searchable', () => { + render(); + expect(screen.getByTestId('inspector-search')).toBeInTheDocument(); + }); + + it('filters by search query', async () => { + const user = userEvent.setup(); + render(); + const search = screen.getByTestId('inspector-search'); + await user.type(search, 'name'); + // The "name" node row should have the highlight class + const nameNode = screen.getByTestId('inspector-node-name'); + expect(nameNode.className).toContain('Highlight'); + }); + + it('shows copy button when copyable', () => { + render(); + expect(screen.getByTestId('inspector-copy')).toBeInTheDocument(); + }); + + it('copies JSON on copy click', async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, 'clipboard', { + value: { writeText }, + writable: true, + configurable: true, + }); + render(); + fireEvent.click(screen.getByTestId('inspector-copy')); + expect(writeText).toHaveBeenCalledWith(JSON.stringify(testData, null, 2)); + }); + + it('sets data-format attribute', () => { + render(); + expect(screen.getByTestId('object-inspector')).toHaveAttribute('data-format', 'yaml'); + }); + + it('merges className', () => { + render(); + expect(screen.getByTestId('object-inspector')).toHaveClass('custom'); + }); + + it('forwards ref', () => { + const ref = { current: null } as unknown as React.RefObject; + render(); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); +}); diff --git a/packages/editors/src/components/object-inspector/ObjectInspector.tsx b/packages/editors/src/components/object-inspector/ObjectInspector.tsx new file mode 100644 index 0000000..abf97ef --- /dev/null +++ b/packages/editors/src/components/object-inspector/ObjectInspector.tsx @@ -0,0 +1,231 @@ +import { forwardRef, useCallback, useMemo, useState, type HTMLAttributes } from 'react'; +import styles from './ObjectInspector.module.css'; + +export type InspectorFormat = 'json' | 'yaml'; + +export interface ObjectInspectorProps extends HTMLAttributes { + data: unknown; + format?: InspectorFormat; + defaultExpanded?: boolean | number; + searchable?: boolean; + copyable?: boolean; +} + +function cn(...parts: Array): string { + return parts.filter(Boolean).join(' '); +} + +function getType(value: unknown): string { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + if (Array.isArray(value)) return 'array'; + return typeof value; +} + +function formatValue(value: unknown): string { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + if (typeof value === 'string') return `"${value}"`; + if (typeof value === 'boolean') return String(value); + if (typeof value === 'number') return String(value); + return String(value); +} + +function shouldExpand(defaultExpanded: boolean | number, depth: number): boolean { + if (typeof defaultExpanded === 'boolean') return defaultExpanded; + return depth < defaultExpanded; +} + +function matchesSearch(key: string, value: unknown, query: string): boolean { + const q = query.toLowerCase(); + if (key.toLowerCase().includes(q)) return true; + if (typeof value === 'string' && value.toLowerCase().includes(q)) return true; + if (typeof value === 'number' && String(value).includes(q)) return true; + if (typeof value === 'boolean' && String(value).includes(q)) return true; + return false; +} + +interface TreeNodeProps { + nodeKey: string; + value: unknown; + depth: number; + defaultExpanded: boolean | number; + searchQuery: string; + isLast: boolean; +} + +function TreeNode({ nodeKey, value, depth, defaultExpanded, searchQuery, isLast }: TreeNodeProps) { + const type = getType(value); + const isExpandable = type === 'object' || type === 'array'; + const [expanded, setExpanded] = useState( + () => isExpandable && shouldExpand(defaultExpanded, depth), + ); + + const entries = useMemo(() => { + if (!isExpandable || value === null || value === undefined) return []; + return Object.entries(value as Record); + }, [isExpandable, value]); + + const toggle = useCallback(() => { + if (isExpandable) setExpanded((prev) => !prev); + }, [isExpandable]); + + const isHighlighted = searchQuery && matchesSearch(nodeKey, value, searchQuery); + + const preview = isExpandable + ? type === 'array' + ? `Array(${(value as unknown[]).length})` + : `{${entries.length}}` + : null; + + return ( +
+
+ {isExpandable ? ( + + ) : ( + + )} + {nodeKey} + : + {isExpandable ? ( + {preview} + ) : ( + + {formatValue(value)} + + )} + {!isLast && !expanded && ,} +
+ {expanded && + entries.map(([k, v], i) => ( + + ))} +
+ ); +} + +export const ObjectInspector = forwardRef( + function ObjectInspector( + { + data, + format = 'json', + defaultExpanded = 1, + searchable = false, + copyable = false, + className, + ...props + }, + ref, + ) { + const [searchQuery, setSearchQuery] = useState(''); + + const handleCopy = useCallback(async () => { + let text: string; + if (format === 'yaml') { + // Simple YAML serialization for flat/nested objects + text = toYaml(data); + } else { + text = JSON.stringify(data, null, 2); + } + await navigator.clipboard.writeText(text); + }, [data, format]); + + return ( +
+ {(searchable || copyable) && ( +
+ {searchable && ( + setSearchQuery(e.target.value)} + placeholder="Search keys/values…" + aria-label="Search object" + data-testid="inspector-search" + /> + )} + {copyable && ( + + )} +
+ )} +
+ +
+
+ ); + }, +); + +ObjectInspector.displayName = 'ObjectInspector'; + +/** Simple YAML serializer (no dependency) */ +function toYaml(value: unknown, indent: number = 0): string { + const prefix = ' '.repeat(indent); + if (value === null) return 'null'; + if (value === undefined) return 'null'; + if (typeof value === 'string') + return value.includes('\n') + ? `|\n${prefix} ${value.split('\n').join(`\n${prefix} `)}` + : value; + if (typeof value === 'number' || typeof value === 'boolean') return String(value); + if (Array.isArray(value)) { + if (value.length === 0) return '[]'; + return value.map((item) => `${prefix}- ${toYaml(item, indent + 1).trimStart()}`).join('\n'); + } + if (typeof value === 'object') { + const entries = Object.entries(value as Record); + if (entries.length === 0) return '{}'; + return entries + .map(([k, v]) => { + const serialized = toYaml(v, indent + 1); + if ( + typeof v === 'object' && + v !== null && + (Array.isArray(v) ? v.length > 0 : Object.keys(v).length > 0) + ) { + return `${prefix}${k}:\n${serialized}`; + } + return `${prefix}${k}: ${serialized}`; + }) + .join('\n'); + } + return String(value); +} diff --git a/packages/editors/src/components/object-inspector/index.ts b/packages/editors/src/components/object-inspector/index.ts new file mode 100644 index 0000000..0bcb698 --- /dev/null +++ b/packages/editors/src/components/object-inspector/index.ts @@ -0,0 +1,2 @@ +export { ObjectInspector } from './ObjectInspector'; +export type { ObjectInspectorProps, InspectorFormat } from './ObjectInspector'; diff --git a/packages/editors/src/components/terminal/Terminal.module.css b/packages/editors/src/components/terminal/Terminal.module.css new file mode 100644 index 0000000..452444d --- /dev/null +++ b/packages/editors/src/components/terminal/Terminal.module.css @@ -0,0 +1,9 @@ +.Root { + width: 100%; + height: 100%; + overflow: hidden; + background: var(--ov-color-terminal-bg, var(--ov-color-bg-surface)); + border: 1px solid var(--ov-color-terminal-border, var(--ov-color-border-default)); + border-radius: var(--ov-radius-sm, 4px); + padding: var(--ov-space-1, 4px); +} diff --git a/packages/editors/src/components/terminal/Terminal.stories.tsx b/packages/editors/src/components/terminal/Terminal.stories.tsx new file mode 100644 index 0000000..5eb90ab --- /dev/null +++ b/packages/editors/src/components/terminal/Terminal.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useRef, useEffect } from 'react'; +import { Terminal, type TerminalHandle, type TerminalProps } from './Terminal'; + +const meta: Meta = { + title: 'Editors/Terminal', + component: Terminal, + tags: ['autodocs'], + args: { + fontSize: 13, + }, + argTypes: { + fontSize: { control: { type: 'range', min: 10, max: 24, step: 1 } }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = {}; + +function WithOutputStory(args: TerminalProps) { + const ref = useRef(null); + + useEffect(() => { + const timeout = setTimeout(() => { + if (ref.current) { + ref.current.writeln('$ ls -la'); + ref.current.writeln('total 48'); + ref.current.writeln('drwxr-xr-x 8 user staff 256 Jan 1 12:00 .'); + ref.current.writeln('drwxr-xr-x 10 user staff 320 Jan 1 11:00 ..'); + ref.current.writeln('-rw-r--r-- 1 user staff 1234 Jan 1 12:00 package.json'); + ref.current.writeln('-rw-r--r-- 1 user staff 567 Jan 1 12:00 tsconfig.json'); + ref.current.writeln('drwxr-xr-x 5 user staff 160 Jan 1 12:00 src'); + ref.current.writeln(''); + ref.current.write('$ '); + } + }, 500); + return () => clearTimeout(timeout); + }, []); + + return ; +} + +export const WithOutput: Story = { + render: (args) => , +}; diff --git a/packages/editors/src/components/terminal/Terminal.test.tsx b/packages/editors/src/components/terminal/Terminal.test.tsx new file mode 100644 index 0000000..d78b45d --- /dev/null +++ b/packages/editors/src/components/terminal/Terminal.test.tsx @@ -0,0 +1,107 @@ +import { render, screen, act } from '@testing-library/react'; +import { createRef } from 'react'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { Terminal, type TerminalHandle } from './Terminal'; + +// Mock xterm modules +const mockTerminal = { + open: vi.fn(), + dispose: vi.fn(), + loadAddon: vi.fn(), + onData: vi.fn(), + onResize: vi.fn(), + write: vi.fn(), + writeln: vi.fn(), + clear: vi.fn(), + focus: vi.fn(), + options: {} as Record, +}; + +const mockFitAddon = { + fit: vi.fn(), +}; + +vi.mock('@xterm/xterm', () => ({ + Terminal: vi.fn(() => mockTerminal), +})); + +vi.mock('@xterm/addon-fit', () => ({ + FitAddon: vi.fn(() => mockFitAddon), +})); + +vi.mock('@xterm/addon-search', () => ({ + SearchAddon: vi.fn(() => ({})), +})); + +vi.mock('@xterm/addon-webgl', () => ({ + WebglAddon: vi.fn(() => ({})), +})); + +// Mock ResizeObserver +beforeEach(() => { + vi.clearAllMocks(); + global.ResizeObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })); +}); + +describe('Terminal', () => { + it('renders terminal container', () => { + render(); + expect(screen.getByTestId('terminal')).toBeInTheDocument(); + }); + + it('initializes xterm on mount', async () => { + render(); + // Wait for async init + await act(async () => { + await new Promise((r) => setTimeout(r, 10)); + }); + expect(mockTerminal.open).toHaveBeenCalled(); + }); + + it('calls onData when provided', async () => { + const onData = vi.fn(); + render(); + await act(async () => { + await new Promise((r) => setTimeout(r, 10)); + }); + expect(mockTerminal.onData).toHaveBeenCalledWith(onData); + }); + + it('calls onResize when provided', async () => { + const onResize = vi.fn(); + render(); + await act(async () => { + await new Promise((r) => setTimeout(r, 10)); + }); + expect(mockTerminal.onResize).toHaveBeenCalled(); + }); + + it('exposes imperative handle methods', async () => { + const ref = createRef(); + render(); + await act(async () => { + await new Promise((r) => setTimeout(r, 10)); + }); + + ref.current?.write('hello'); + expect(mockTerminal.write).toHaveBeenCalledWith('hello'); + + ref.current?.writeln('line'); + expect(mockTerminal.writeln).toHaveBeenCalledWith('line'); + + ref.current?.clear(); + expect(mockTerminal.clear).toHaveBeenCalled(); + + ref.current?.focus(); + expect(mockTerminal.focus).toHaveBeenCalled(); + }); + + it('merges className', () => { + render(); + expect(screen.getByTestId('terminal')).toHaveClass('custom-terminal'); + }); +}); diff --git a/packages/editors/src/components/terminal/Terminal.tsx b/packages/editors/src/components/terminal/Terminal.tsx new file mode 100644 index 0000000..7dfc04b --- /dev/null +++ b/packages/editors/src/components/terminal/Terminal.tsx @@ -0,0 +1,156 @@ +import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react'; +import { useEditorTheme } from '../../themes/useEditorTheme'; +import { buildXtermTheme } from '../../themes/xterm'; +import styles from './Terminal.module.css'; + +export interface TerminalProps { + onData?: (data: string) => void; + onResize?: (cols: number, rows: number) => void; + fontSize?: number; + fontFamily?: string; + className?: string; +} + +export interface TerminalHandle { + write: (data: string) => void; + writeln: (data: string) => void; + clear: () => void; + focus: () => void; + fit: () => void; +} + +function cn(...parts: Array): string { + return parts.filter(Boolean).join(' '); +} + +export const Terminal = forwardRef(function Terminal( + { onData, onResize, fontSize = 13, fontFamily, className }, + ref, +) { + const containerRef = useRef(null); + const termRef = useRef(null); + const fitAddonRef = useRef(null); + const theme = useEditorTheme(); + + const fit = useCallback(() => { + const addon = fitAddonRef.current as { fit?: () => void } | null; + addon?.fit?.(); + }, []); + + useImperativeHandle(ref, () => ({ + write: (data: string) => { + const t = termRef.current as { write?: (d: string) => void } | null; + t?.write?.(data); + }, + writeln: (data: string) => { + const t = termRef.current as { writeln?: (d: string) => void } | null; + t?.writeln?.(data); + }, + clear: () => { + const t = termRef.current as { clear?: () => void } | null; + t?.clear?.(); + }, + focus: () => { + const t = termRef.current as { focus?: () => void } | null; + t?.focus?.(); + }, + fit, + })); + + // Initialize xterm lazily + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + let disposed = false; + + async function init() { + const [{ Terminal: XTerminal }, { FitAddon }, { SearchAddon }] = await Promise.all([ + import('@xterm/xterm'), + import('@xterm/addon-fit'), + import('@xterm/addon-search'), + ]); + + if (disposed) return; + + const xtermTheme = buildXtermTheme(); + const term = new XTerminal({ + fontSize, + fontFamily: fontFamily || 'var(--ov-font-family-mono, monospace)', + theme: xtermTheme, + allowTransparency: true, + cursorBlink: true, + convertEol: true, + }); + + const fitAddon = new FitAddon(); + const searchAddon = new SearchAddon(); + term.loadAddon(fitAddon); + term.loadAddon(searchAddon); + + // Try WebGL renderer, fall back to canvas + try { + const { WebglAddon } = await import('@xterm/addon-webgl'); + if (!disposed) { + const webglAddon = new WebglAddon(); + term.loadAddon(webglAddon); + } + } catch { + // WebGL not available, canvas fallback is fine + } + + if (disposed) { + term.dispose(); + return; + } + + term.open(container!); + fitAddon.fit(); + + termRef.current = term; + fitAddonRef.current = fitAddon; + + if (onData) { + term.onData(onData); + } + + if (onResize) { + term.onResize(({ cols, rows }) => onResize(cols, rows)); + } + + // Observe container resize + const observer = new ResizeObserver(() => { + fitAddon.fit(); + }); + observer.observe(container!); + + // Store cleanup references + (container as HTMLDivElement & { __cleanup?: () => void }).__cleanup = () => { + observer.disconnect(); + term.dispose(); + termRef.current = null; + fitAddonRef.current = null; + }; + } + + init(); + + return () => { + disposed = true; + const cleanup = (container as HTMLDivElement & { __cleanup?: () => void }).__cleanup; + cleanup?.(); + }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + // Update theme + useEffect(() => { + const term = termRef.current as { options?: { theme: unknown } } | null; + if (term?.options) { + term.options.theme = buildXtermTheme(); + } + }, [theme]); + + return
; +}); + +Terminal.displayName = 'Terminal'; diff --git a/packages/editors/src/components/terminal/index.ts b/packages/editors/src/components/terminal/index.ts new file mode 100644 index 0000000..26c6dda --- /dev/null +++ b/packages/editors/src/components/terminal/index.ts @@ -0,0 +1,2 @@ +export { Terminal } from './Terminal'; +export type { TerminalProps, TerminalHandle } from './Terminal'; diff --git a/packages/editors/src/css.d.ts b/packages/editors/src/css.d.ts new file mode 100644 index 0000000..ab57c34 --- /dev/null +++ b/packages/editors/src/css.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: Record; + export default classes; +} diff --git a/packages/editors/src/index.ts b/packages/editors/src/index.ts new file mode 100644 index 0000000..132fc9c --- /dev/null +++ b/packages/editors/src/index.ts @@ -0,0 +1,5 @@ +export * from './components'; +export { useEditorTheme, getComputedToken } from './themes'; +export { buildMonacoTheme, OV_MONACO_THEME } from './themes'; +export { buildXtermTheme } from './themes'; +export type { XtermThemeData } from './themes'; diff --git a/packages/editors/src/themes/index.ts b/packages/editors/src/themes/index.ts new file mode 100644 index 0000000..d8ad63b --- /dev/null +++ b/packages/editors/src/themes/index.ts @@ -0,0 +1,4 @@ +export { useEditorTheme, getComputedToken } from './useEditorTheme'; +export { buildMonacoTheme, OV_MONACO_THEME } from './monaco'; +export type { XtermThemeData } from './xterm'; +export { buildXtermTheme } from './xterm'; diff --git a/packages/editors/src/themes/monaco.ts b/packages/editors/src/themes/monaco.ts new file mode 100644 index 0000000..fc00dda --- /dev/null +++ b/packages/editors/src/themes/monaco.ts @@ -0,0 +1,119 @@ +import { getComputedToken } from './useEditorTheme'; + +type ThemeMode = 'dark' | 'light' | 'high-contrast-dark' | 'high-contrast-light'; + +interface MonacoThemeData { + base: 'vs' | 'vs-dark' | 'hc-black' | 'hc-light'; + inherit: boolean; + rules: Array<{ token: string; foreground?: string; fontStyle?: string }>; + colors: Record; +} + +function stripHash(color: string): string { + return color.startsWith('#') ? color.slice(1) : color; +} + +function token(name: string): string { + return getComputedToken(name) || ''; +} + +function getBaseTheme(mode: ThemeMode): 'vs' | 'vs-dark' | 'hc-black' | 'hc-light' { + switch (mode) { + case 'light': + return 'vs'; + case 'high-contrast-dark': + return 'hc-black'; + case 'high-contrast-light': + return 'hc-light'; + default: + return 'vs-dark'; + } +} + +export function buildMonacoTheme(mode: ThemeMode): MonacoThemeData { + return { + base: getBaseTheme(mode), + inherit: true, + rules: [ + { + token: 'comment', + foreground: stripHash(token('--ov-syntax-comment')), + fontStyle: token('--ov-syntax-style-comment') || 'italic', + }, + { token: 'string', foreground: stripHash(token('--ov-syntax-string')) }, + { token: 'string.escape', foreground: stripHash(token('--ov-syntax-string-escape')) }, + { token: 'number', foreground: stripHash(token('--ov-syntax-number')) }, + { + token: 'keyword', + foreground: stripHash(token('--ov-syntax-keyword')), + fontStyle: token('--ov-syntax-style-keyword') || '', + }, + { token: 'keyword.control', foreground: stripHash(token('--ov-syntax-keyword-control')) }, + { token: 'keyword.operator', foreground: stripHash(token('--ov-syntax-keyword-operator')) }, + { + token: 'type', + foreground: stripHash(token('--ov-syntax-type')), + fontStyle: token('--ov-syntax-style-type') || '', + }, + { token: 'type.identifier', foreground: stripHash(token('--ov-syntax-type')) }, + { token: 'class', foreground: stripHash(token('--ov-syntax-class')) }, + { token: 'interface', foreground: stripHash(token('--ov-syntax-interface')) }, + { + token: 'function', + foreground: stripHash(token('--ov-syntax-function')), + fontStyle: token('--ov-syntax-style-function') || '', + }, + { token: 'function.declaration', foreground: stripHash(token('--ov-syntax-function')) }, + { token: 'method', foreground: stripHash(token('--ov-syntax-method')) }, + { token: 'variable', foreground: stripHash(token('--ov-syntax-variable')) }, + { token: 'parameter', foreground: stripHash(token('--ov-syntax-parameter')) }, + { token: 'property', foreground: stripHash(token('--ov-syntax-property')) }, + { token: 'namespace', foreground: stripHash(token('--ov-syntax-namespace')) }, + { token: 'decorator', foreground: stripHash(token('--ov-syntax-decorator')) }, + { token: 'regexp', foreground: stripHash(token('--ov-syntax-regexp')) }, + { token: 'operator', foreground: stripHash(token('--ov-syntax-operator')) }, + { token: 'delimiter', foreground: stripHash(token('--ov-syntax-punctuation')) }, + { token: 'delimiter.bracket', foreground: stripHash(token('--ov-syntax-punctuation')) }, + ], + colors: { + 'editor.background': token('--ov-color-editor-bg'), + 'editor.foreground': token('--ov-color-editor-fg'), + 'editorCursor.foreground': token('--ov-color-editor-cursor'), + 'editor.selectionBackground': token('--ov-color-editor-selection-bg'), + 'editor.inactiveSelectionBackground': token('--ov-color-editor-selection-inactive-bg'), + 'editor.lineHighlightBackground': token('--ov-color-editor-line-highlight-bg'), + 'editor.lineHighlightBorder': token('--ov-color-editor-line-highlight-border'), + 'editorLineNumber.foreground': token('--ov-color-editor-line-number'), + 'editorLineNumber.activeForeground': token('--ov-color-editor-line-number-active'), + 'editorWhitespace.foreground': token('--ov-color-editor-whitespace'), + 'editorIndentGuide.background': token('--ov-color-editor-indent-guide'), + 'editorIndentGuide.activeBackground': token('--ov-color-editor-indent-guide-active'), + 'editorRuler.foreground': token('--ov-color-editor-ruler'), + 'editor.findMatchBackground': token('--ov-color-editor-find-match-bg'), + 'editor.findMatchBorder': token('--ov-color-editor-find-match-border'), + 'editor.findMatchHighlightBackground': token('--ov-color-editor-find-range-bg'), + 'editorLink.activeForeground': token('--ov-color-editor-link'), + 'editorBracketMatch.background': token('--ov-color-editor-bracket-match-bg'), + 'editorBracketMatch.border': token('--ov-color-editor-bracket-match-border'), + 'editorBracketHighlight.foreground1': token('--ov-color-editor-bracket-1'), + 'editorBracketHighlight.foreground2': token('--ov-color-editor-bracket-2'), + 'editorBracketHighlight.foreground3': token('--ov-color-editor-bracket-3'), + 'editorBracketHighlight.foreground4': token('--ov-color-editor-bracket-4'), + 'editorBracketHighlight.foreground5': token('--ov-color-editor-bracket-5'), + 'editorBracketHighlight.foreground6': token('--ov-color-editor-bracket-6'), + 'editorGutter.background': token('--ov-color-gutter-bg'), + 'editorGutter.addedBackground': token('--ov-color-gutter-added'), + 'editorGutter.modifiedBackground': token('--ov-color-gutter-modified'), + 'editorGutter.deletedBackground': token('--ov-color-gutter-deleted'), + 'diffEditor.insertedTextBackground': token('--ov-color-diff-insert-bg'), + 'diffEditor.removedTextBackground': token('--ov-color-diff-remove-bg'), + 'minimap.background': token('--ov-color-minimap-bg'), + 'minimap.selectionHighlight': token('--ov-color-minimap-selection'), + 'minimap.errorHighlight': token('--ov-color-minimap-error'), + 'minimap.warningHighlight': token('--ov-color-minimap-warning'), + 'minimap.findMatchHighlight': token('--ov-color-minimap-find-match'), + }, + }; +} + +export const OV_MONACO_THEME = 'ov-theme'; diff --git a/packages/editors/src/themes/useEditorTheme.ts b/packages/editors/src/themes/useEditorTheme.ts new file mode 100644 index 0000000..515dad4 --- /dev/null +++ b/packages/editors/src/themes/useEditorTheme.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; + +type ThemeMode = 'dark' | 'light' | 'high-contrast-dark' | 'high-contrast-light'; + +function getCurrentTheme(): ThemeMode { + if (typeof document === 'undefined') return 'dark'; + const attr = document.documentElement.getAttribute('data-ov-theme'); + if (attr === 'light' || attr === 'high-contrast-dark' || attr === 'high-contrast-light') + return attr; + return 'dark'; +} + +export function getComputedToken(token: string): string { + if (typeof document === 'undefined') return ''; + return getComputedStyle(document.documentElement).getPropertyValue(token).trim(); +} + +export function useEditorTheme(): ThemeMode { + const [theme, setTheme] = useState(getCurrentTheme); + + useEffect(() => { + const root = document.documentElement; + const observer = new MutationObserver(() => { + setTheme(getCurrentTheme()); + }); + observer.observe(root, { attributes: true, attributeFilter: ['data-ov-theme'] }); + return () => observer.disconnect(); + }, []); + + return theme; +} diff --git a/packages/editors/src/themes/xterm.ts b/packages/editors/src/themes/xterm.ts new file mode 100644 index 0000000..002dea0 --- /dev/null +++ b/packages/editors/src/themes/xterm.ts @@ -0,0 +1,56 @@ +import { getComputedToken } from './useEditorTheme'; + +export interface XtermThemeData { + background: string; + foreground: string; + cursor: string; + cursorAccent?: string; + selectionBackground: string; + selectionForeground?: string; + black: string; + red: string; + green: string; + yellow: string; + blue: string; + magenta: string; + cyan: string; + white: string; + brightBlack: string; + brightRed: string; + brightGreen: string; + brightYellow: string; + brightBlue: string; + brightMagenta: string; + brightCyan: string; + brightWhite: string; +} + +function token(name: string): string { + return getComputedToken(name) || ''; +} + +export function buildXtermTheme(): XtermThemeData { + return { + background: token('--ov-color-terminal-bg'), + foreground: token('--ov-color-terminal-fg'), + cursor: token('--ov-color-terminal-cursor'), + selectionBackground: token('--ov-color-terminal-selection-bg'), + selectionForeground: token('--ov-color-terminal-selection-fg') || undefined, + black: token('--ov-color-ansi-black'), + red: token('--ov-color-ansi-red'), + green: token('--ov-color-ansi-green'), + yellow: token('--ov-color-ansi-yellow'), + blue: token('--ov-color-ansi-blue'), + magenta: token('--ov-color-ansi-magenta'), + cyan: token('--ov-color-ansi-cyan'), + white: token('--ov-color-ansi-white'), + brightBlack: token('--ov-color-ansi-bright-black'), + brightRed: token('--ov-color-ansi-bright-red'), + brightGreen: token('--ov-color-ansi-bright-green'), + brightYellow: token('--ov-color-ansi-bright-yellow'), + brightBlue: token('--ov-color-ansi-bright-blue'), + brightMagenta: token('--ov-color-ansi-bright-magenta'), + brightCyan: token('--ov-color-ansi-bright-cyan'), + brightWhite: token('--ov-color-ansi-bright-white'), + }; +} diff --git a/packages/editors/src/utils/language.ts b/packages/editors/src/utils/language.ts new file mode 100644 index 0000000..4c4ead4 --- /dev/null +++ b/packages/editors/src/utils/language.ts @@ -0,0 +1,71 @@ +const EXTENSION_MAP: Record = { + js: 'javascript', + jsx: 'javascript', + ts: 'typescript', + tsx: 'typescript', + py: 'python', + rb: 'ruby', + rs: 'rust', + go: 'go', + java: 'java', + kt: 'kotlin', + kts: 'kotlin', + cs: 'csharp', + fs: 'fsharp', + swift: 'swift', + c: 'c', + cpp: 'cpp', + cc: 'cpp', + cxx: 'cpp', + h: 'c', + hpp: 'cpp', + css: 'css', + scss: 'scss', + less: 'less', + html: 'html', + htm: 'html', + xml: 'xml', + json: 'json', + yaml: 'yaml', + yml: 'yaml', + md: 'markdown', + mdx: 'markdown', + sql: 'sql', + sh: 'shell', + bash: 'shell', + zsh: 'shell', + ps1: 'powershell', + dockerfile: 'dockerfile', + tf: 'hcl', + hcl: 'hcl', + toml: 'ini', + ini: 'ini', + lua: 'lua', + r: 'r', + dart: 'dart', + php: 'php', + pl: 'perl', + ex: 'elixir', + exs: 'elixir', + erl: 'erlang', + hs: 'haskell', + scala: 'scala', + clj: 'clojure', + graphql: 'graphql', + gql: 'graphql', + proto: 'protobuf', + vue: 'html', + svelte: 'html', +}; + +export function detectLanguage(filename: string): string | undefined { + const lower = filename.toLowerCase(); + // Handle dotfiles like "Dockerfile" + const base = lower.split('/').pop() ?? lower; + if (base === 'dockerfile') return 'dockerfile'; + if (base === 'makefile') return 'makefile'; + + const ext = base.split('.').pop(); + if (!ext) return undefined; + return EXTENSION_MAP[ext]; +} diff --git a/packages/editors/tsconfig.json b/packages/editors/tsconfig.json new file mode 100644 index 0000000..d0fade3 --- /dev/null +++ b/packages/editors/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "types": ["vitest/globals", "@testing-library/jest-dom"] + }, + "include": ["src", "vite.config.ts", "vitest.setup.ts"] +} diff --git a/packages/editors/vite.config.ts b/packages/editors/vite.config.ts new file mode 100644 index 0000000..a922aa8 --- /dev/null +++ b/packages/editors/vite.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; + +export default defineConfig({ + plugins: [react(), dts({ include: ['src'] })], + build: { + lib: { + entry: 'src/index.ts', + formats: ['es'], + fileName: () => 'index.js', + }, + rollupOptions: { + external: [ + 'react', + 'react-dom', + /^@omniview\/base-ui/, + /^monaco-editor/, + /^@monaco-editor/, + /^@xterm/, + /^react-markdown/, + /^remark-/, + /^rehype-/, + ], + }, + sourcemap: true, + }, + test: { + environment: 'jsdom', + globals: true, + setupFiles: './vitest.setup.ts', + css: true, + }, +}); diff --git a/packages/editors/vitest.setup.ts b/packages/editors/vitest.setup.ts new file mode 100644 index 0000000..03b305f --- /dev/null +++ b/packages/editors/vitest.setup.ts @@ -0,0 +1,11 @@ +import '@testing-library/jest-dom/vitest'; +import { cleanup } from '@testing-library/react'; +import { afterEach } from 'vitest'; + +if (typeof window !== 'undefined' && !window.PointerEvent) { + window.PointerEvent = MouseEvent as typeof PointerEvent; +} + +afterEach(() => { + cleanup(); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 410aac5..9f575a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,6 +127,85 @@ importers: specifier: ^2.1.8 version: 2.1.9(@types/node@22.19.15)(jsdom@25.0.1) + packages/editors: + dependencies: + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@omniview/base-ui': + specifier: workspace:* + version: link:../base-ui + '@xterm/addon-fit': + specifier: ^0.10.0 + version: 0.10.0(@xterm/xterm@5.5.0) + '@xterm/addon-search': + specifier: ^0.15.0 + version: 0.15.0(@xterm/xterm@5.5.0) + '@xterm/addon-webgl': + specifier: ^0.18.0 + version: 0.18.0(@xterm/xterm@5.5.0) + '@xterm/xterm': + specifier: ^5.5.0 + version: 5.5.0 + monaco-editor: + specifier: ^0.52.2 + version: 0.52.2 + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + react-markdown: + specifier: ^9.0.3 + version: 9.1.0(@types/react@19.2.14)(react@19.2.4) + rehype-highlight: + specifier: ^7.0.2 + version: 7.0.2 + remark-gfm: + specifier: ^4.0.0 + version: 4.0.1 + devDependencies: + '@storybook/react': + specifier: ^10.2.15 + version: 10.2.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.2.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) + '@types/react': + specifier: ^19.0.10 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.0.4 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@5.4.21(@types/node@22.19.15)) + jsdom: + specifier: ^25.0.1 + version: 25.0.1 + storybook: + specifier: ^10.2.15 + version: 10.2.16(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + vite: + specifier: ^5.4.14 + version: 5.4.21(@types/node@22.19.15) + vite-plugin-dts: + specifier: ^4.5.1 + version: 4.5.4(@types/node@22.19.15)(rollup@4.59.0)(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.15)) + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.19.15)(jsdom@25.0.1) + packages: '@adobe/css-tools@4.4.4': @@ -706,6 +785,16 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1012,12 +1101,18 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1027,9 +1122,15 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} @@ -1115,6 +1216,9 @@ packages: resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1191,6 +1295,24 @@ packages: '@vue/shared@3.5.29': resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + '@xterm/addon-fit@0.10.0': + resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + + '@xterm/addon-search@0.15.0': + resolution: {integrity: sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + + '@xterm/addon-webgl@0.18.0': + resolution: {integrity: sha512-xCnfMBTI+/HKPdRnSOHaJDRqEpq2Ugy8LEj9GiY4J3zJObo3joylIFaMvzBwbYRg8zLtkO0KQaStCeSfoaI2/w==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + + '@xterm/xterm@5.5.0': + resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1306,6 +1428,9 @@ packages: resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1360,6 +1485,9 @@ packages: caniuse-lite@1.0.30001777: resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1368,6 +1496,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} @@ -1493,6 +1624,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.3: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} @@ -1583,6 +1717,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-config-prettier@10.1.8: resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true @@ -1648,6 +1786,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -1665,6 +1806,9 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1809,9 +1953,21 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} @@ -1822,6 +1978,10 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} @@ -1829,6 +1989,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1865,6 +2028,9 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -1951,6 +2117,10 @@ packages: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -2076,6 +2246,9 @@ packages: lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2086,6 +2259,9 @@ packages: lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + lowlight@3.3.0: + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2107,10 +2283,142 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -2148,6 +2456,9 @@ packages: mlly@1.8.1: resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2334,6 +2645,12 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@9.1.0: + resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -2367,6 +2684,21 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + rehype-highlight@7.0.2: + resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2495,6 +2827,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -2534,6 +2869,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -2550,6 +2888,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2616,6 +2960,12 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -2680,6 +3030,27 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -2702,6 +3073,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2862,6 +3239,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -3354,6 +3734,17 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@monaco-editor/loader@1.7.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.52.2 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/pluginutils@5.3.0(rollup@4.59.0)': @@ -3649,10 +4040,18 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} '@types/doctrine@0.0.9': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} '@types/hast@3.0.4': @@ -3661,8 +4060,14 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mdx@2.0.13': {} + '@types/ms@2.1.0': {} + '@types/node@22.19.15': dependencies: undici-types: 6.21.0 @@ -3778,6 +4183,8 @@ snapshots: '@typescript-eslint/types': 8.56.1 eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.0': {} + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.15))': dependencies: '@babel/core': 7.29.0 @@ -3897,6 +4304,20 @@ snapshots: '@vue/shared@3.5.29': {} + '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + + '@xterm/addon-search@0.15.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + + '@xterm/addon-webgl@0.18.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + + '@xterm/xterm@5.5.0': {} + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -4022,6 +4443,8 @@ snapshots: axe-core@4.11.1: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -4076,6 +4499,8 @@ snapshots: caniuse-lite@1.0.30001777: {} + ccount@2.0.1: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -4089,6 +4514,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + character-entities-legacy@3.0.0: {} character-entities@2.0.2: {} @@ -4198,6 +4625,10 @@ snapshots: dequal@2.0.3: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff@8.0.3: {} doctrine@2.1.0: @@ -4388,6 +4819,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-prettier@10.1.8(eslint@9.39.3): dependencies: eslint: 9.39.3 @@ -4486,6 +4919,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -4498,6 +4933,8 @@ snapshots: exsolve@1.0.8: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -4639,10 +5076,45 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hastscript@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -4655,12 +5127,16 @@ snapshots: highlight.js@10.7.3: {} + highlight.js@11.11.1: {} + highlightjs-vue@1.0.0: {} html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 + html-url-attributes@3.0.1: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -4694,6 +5170,8 @@ snapshots: indent-string@4.0.0: {} + inline-style-parser@0.2.7: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -4784,6 +5262,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} is-regex@1.2.1: @@ -4928,6 +5408,8 @@ snapshots: lodash@4.17.23: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -4939,6 +5421,12 @@ snapshots: fault: 1.0.4 highlight.js: 10.7.3 + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + lru-cache@10.4.3: {} lru-cache@11.2.6: {} @@ -4957,8 +5445,354 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + mime-db@1.52.0: {} mime-types@2.1.35: @@ -4994,6 +5828,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 + monaco-editor@0.52.2: {} + ms@2.1.3: {} muggle-string@0.4.1: {} @@ -5196,6 +6032,24 @@ snapshots: react-is@17.0.2: {} + react-markdown@9.1.0(@types/react@19.2.14)(react@19.2.4): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.14 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.4 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-refresh@0.17.0: {} react-syntax-highlighter@16.1.1(react@19.2.4): @@ -5250,6 +6104,48 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + rehype-highlight@7.0.2: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-text: 4.0.2 + lowlight: 3.3.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + require-from-string@2.0.2: {} reselect@5.1.1: {} @@ -5411,6 +6307,8 @@ snapshots: stackback@0.0.2: {} + state-local@1.0.7: {} + std-env@3.10.0: {} stop-iteration-iterator@1.1.0: @@ -5487,6 +6385,11 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-bom@3.0.0: {} strip-indent@3.0.0: @@ -5497,6 +6400,14 @@ snapshots: strip-json-comments@3.1.1: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5546,6 +6457,10 @@ snapshots: dependencies: punycode: 2.3.1 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -5623,6 +6538,44 @@ snapshots: undici-types@6.21.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@2.0.1: {} unplugin@2.3.11: @@ -5646,6 +6599,16 @@ snapshots: dependencies: react: 19.2.4 + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-node@2.1.9(@types/node@22.19.15): dependencies: cac: 6.7.14 @@ -5816,3 +6779,5 @@ snapshots: yallist@4.0.0: {} yocto-queue@0.1.0: {} + + zwitch@2.0.4: {} From fb6944092d7da7ba9e3162de955ab85eb556cb76 Mon Sep 17 00:00:00 2001 From: Joshua Pare Date: Sun, 8 Mar 2026 17:21:54 -0500 Subject: [PATCH 02/11] feat(editors): Production-grade Terminal, circular ref safety, memoized JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terminal rewrite: - Debounced resize (10ms) via ResizeObserver + window resize listener - WebGL → Canvas → DOM renderer cascade with context loss recovery - WebLinksAddon for clickable URLs, Mac-specific options by default - Binary data support (Uint8Array), configurable scrollback buffer - Signal/error/close callback props for exec plugin integration - Additional imperative methods: getDimensions, reset, scrollToBottom - Stable callback refs to avoid terminal re-initialization - Proper disposal order: debounce → observer → listeners → addons → terminal - 26 comprehensive tests covering resize, binary writes, cleanup, etc. ObjectInspector: circular reference detection prevents infinite recursion, clipboard error handling, toYaml circular ref guard. CodeEditor: memoized JSON pretty-printing to avoid re-parsing on every render. --- packages/editors/package.json | 2 + .../src/components/code-editor/CodeEditor.tsx | 7 +- .../object-inspector/ObjectInspector.test.tsx | 32 ++ .../object-inspector/ObjectInspector.tsx | 105 ++++-- .../src/components/terminal/Terminal.test.tsx | 302 +++++++++++++++-- .../src/components/terminal/Terminal.tsx | 307 ++++++++++++++++-- .../editors/src/components/terminal/index.ts | 2 +- pnpm-lock.yaml | 24 ++ 8 files changed, 683 insertions(+), 98 deletions(-) diff --git a/packages/editors/package.json b/packages/editors/package.json index 1302c57..ac332e5 100644 --- a/packages/editors/package.json +++ b/packages/editors/package.json @@ -38,6 +38,8 @@ "@xterm/xterm": "^5.5.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-search": "^0.15.0", + "@xterm/addon-canvas": "^0.7.0", + "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "react-markdown": "^9.0.3", "remark-gfm": "^4.0.0", diff --git a/packages/editors/src/components/code-editor/CodeEditor.tsx b/packages/editors/src/components/code-editor/CodeEditor.tsx index ca583df..8b49efa 100644 --- a/packages/editors/src/components/code-editor/CodeEditor.tsx +++ b/packages/editors/src/components/code-editor/CodeEditor.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useImperativeHandle, + useMemo, useRef, useState, } from 'react'; @@ -61,8 +62,8 @@ export const CodeEditor = forwardRef(function const resolvedLanguage = language ?? (filename ? detectLanguage(filename) : undefined); - // Format JSON content for display - const displayValue = (() => { + // Format JSON content for display (memoized to avoid re-parsing on every render) + const displayValue = useMemo(() => { if (resolvedLanguage === 'json' && value) { try { return JSON.stringify(JSON.parse(value), null, 2); @@ -71,7 +72,7 @@ export const CodeEditor = forwardRef(function } } return value; - })(); + }, [resolvedLanguage, value]); const handleEditorDidMount = useCallback( (editor: unknown, monaco: unknown) => { diff --git a/packages/editors/src/components/object-inspector/ObjectInspector.test.tsx b/packages/editors/src/components/object-inspector/ObjectInspector.test.tsx index eb9a2d5..873b9a4 100644 --- a/packages/editors/src/components/object-inspector/ObjectInspector.test.tsx +++ b/packages/editors/src/components/object-inspector/ObjectInspector.test.tsx @@ -93,4 +93,36 @@ describe('ObjectInspector', () => { render(); expect(ref.current).toBeInstanceOf(HTMLDivElement); }); + + it('handles circular references without crashing', () => { + const circular: Record = { name: 'test' }; + circular.self = circular; + // Should not throw or infinite loop + expect(() => render()).not.toThrow(); + // The circular ref should show [Circular] + expect(screen.getByText('[Circular]')).toBeInTheDocument(); + }); + + it('handles empty object', () => { + render(); + expect(screen.getByTestId('inspector-node-root')).toBeInTheDocument(); + }); + + it('handles empty array', () => { + render(); + expect(screen.getByTestId('inspector-node-root')).toBeInTheDocument(); + }); + + it('handles clipboard API failure gracefully', async () => { + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: vi.fn().mockRejectedValue(new Error('Not allowed')), + }, + writable: true, + configurable: true, + }); + render(); + // Should not throw + expect(() => fireEvent.click(screen.getByTestId('inspector-copy'))).not.toThrow(); + }); }); diff --git a/packages/editors/src/components/object-inspector/ObjectInspector.tsx b/packages/editors/src/components/object-inspector/ObjectInspector.tsx index abf97ef..f87e1ea 100644 --- a/packages/editors/src/components/object-inspector/ObjectInspector.tsx +++ b/packages/editors/src/components/object-inspector/ObjectInspector.tsx @@ -45,6 +45,11 @@ function matchesSearch(key: string, value: unknown, query: string): boolean { return false; } +/** Check if a value is a non-null object that can be tracked by WeakSet. */ +function isObjectRef(value: unknown): value is object { + return value !== null && (typeof value === 'object' || typeof value === 'function'); +} + interface TreeNodeProps { nodeKey: string; value: unknown; @@ -52,31 +57,57 @@ interface TreeNodeProps { defaultExpanded: boolean | number; searchQuery: string; isLast: boolean; + /** Ancestor object references for circular reference detection. */ + ancestors: WeakSet; } -function TreeNode({ nodeKey, value, depth, defaultExpanded, searchQuery, isLast }: TreeNodeProps) { +function TreeNode({ + nodeKey, + value, + depth, + defaultExpanded, + searchQuery, + isLast, + ancestors, +}: TreeNodeProps) { const type = getType(value); const isExpandable = type === 'object' || type === 'array'; + + // Detect circular references by checking if this value is already an ancestor + const isCircular = isExpandable && isObjectRef(value) && ancestors.has(value); + const [expanded, setExpanded] = useState( - () => isExpandable && shouldExpand(defaultExpanded, depth), + () => isExpandable && !isCircular && shouldExpand(defaultExpanded, depth), ); + // Create a new ancestors set that includes this value for children. + // WeakSet doesn't support iteration, so we use a duck-typed wrapper + // that checks both the parent set and the current value. + const childAncestors = useMemo(() => { + if (!isExpandable || !isObjectRef(value) || isCircular) return ancestors; + return { + has: (v: object) => v === value || ancestors.has(v), + } as WeakSet; + }, [isExpandable, value, isCircular, ancestors]); + const entries = useMemo(() => { - if (!isExpandable || value === null || value === undefined) return []; + if (!isExpandable || value === null || value === undefined || isCircular) return []; return Object.entries(value as Record); - }, [isExpandable, value]); + }, [isExpandable, value, isCircular]); const toggle = useCallback(() => { - if (isExpandable) setExpanded((prev) => !prev); - }, [isExpandable]); + if (isExpandable && !isCircular) setExpanded((prev) => !prev); + }, [isExpandable, isCircular]); const isHighlighted = searchQuery && matchesSearch(nodeKey, value, searchQuery); - const preview = isExpandable - ? type === 'array' - ? `Array(${(value as unknown[]).length})` - : `{${entries.length}}` - : null; + const preview = isCircular + ? '[Circular]' + : isExpandable + ? type === 'array' + ? `Array(${(value as unknown[]).length})` + : `{${entries.length}}` + : null; return (
@@ -85,10 +116,10 @@ function TreeNode({ nodeKey, value, depth, defaultExpanded, searchQuery, isLast style={{ paddingLeft: depth * 16 }} onClick={toggle} role="treeitem" - aria-expanded={isExpandable ? expanded : undefined} + aria-expanded={isExpandable && !isCircular ? expanded : undefined} data-testid={`inspector-node-${nodeKey}`} > - {isExpandable ? ( + {isExpandable && !isCircular ? ( ) : ( @@ -114,12 +145,16 @@ function TreeNode({ nodeKey, value, depth, defaultExpanded, searchQuery, isLast defaultExpanded={defaultExpanded} searchQuery={searchQuery} isLast={i === entries.length - 1} + ancestors={childAncestors} /> ))}
); } +/** Shared empty WeakSet used as the initial ancestors set. */ +const EMPTY_ANCESTORS = new WeakSet(); + export const ObjectInspector = forwardRef( function ObjectInspector( { @@ -136,16 +171,23 @@ export const ObjectInspector = forwardRef( const [searchQuery, setSearchQuery] = useState(''); const handleCopy = useCallback(async () => { - let text: string; - if (format === 'yaml') { - // Simple YAML serialization for flat/nested objects - text = toYaml(data); - } else { - text = JSON.stringify(data, null, 2); + try { + let text: string; + if (format === 'yaml') { + text = toYaml(data); + } else { + text = JSON.stringify(data, null, 2); + } + await navigator.clipboard.writeText(text); + } catch { + // Clipboard API may not be available (e.g., insecure context) } - await navigator.clipboard.writeText(text); }, [data, format]); + // Root starts with an empty ancestors set — the root node itself + // is not its own ancestor, but its children will see it as one + const rootAncestors = EMPTY_ANCESTORS; + return (
( defaultExpanded={defaultExpanded} searchQuery={searchQuery} isLast + ancestors={rootAncestors} />
@@ -197,7 +240,7 @@ export const ObjectInspector = forwardRef( ObjectInspector.displayName = 'ObjectInspector'; /** Simple YAML serializer (no dependency) */ -function toYaml(value: unknown, indent: number = 0): string { +function toYaml(value: unknown, indent: number = 0, seen?: WeakSet): string { const prefix = ' '.repeat(indent); if (value === null) return 'null'; if (value === undefined) return 'null'; @@ -206,16 +249,25 @@ function toYaml(value: unknown, indent: number = 0): string { ? `|\n${prefix} ${value.split('\n').join(`\n${prefix} `)}` : value; if (typeof value === 'number' || typeof value === 'boolean') return String(value); - if (Array.isArray(value)) { - if (value.length === 0) return '[]'; - return value.map((item) => `${prefix}- ${toYaml(item, indent + 1).trimStart()}`).join('\n'); - } + + // Circular reference guard for objects/arrays if (typeof value === 'object') { + const tracking = seen ?? new WeakSet(); + if (tracking.has(value as object)) return '[Circular]'; + tracking.add(value as object); + + if (Array.isArray(value)) { + if (value.length === 0) return '[]'; + return value + .map((item) => `${prefix}- ${toYaml(item, indent + 1, tracking).trimStart()}`) + .join('\n'); + } + const entries = Object.entries(value as Record); if (entries.length === 0) return '{}'; return entries .map(([k, v]) => { - const serialized = toYaml(v, indent + 1); + const serialized = toYaml(v, indent + 1, tracking); if ( typeof v === 'object' && v !== null && @@ -227,5 +279,6 @@ function toYaml(value: unknown, indent: number = 0): string { }) .join('\n'); } + return String(value); } diff --git a/packages/editors/src/components/terminal/Terminal.test.tsx b/packages/editors/src/components/terminal/Terminal.test.tsx index d78b45d..6f887c8 100644 --- a/packages/editors/src/components/terminal/Terminal.test.tsx +++ b/packages/editors/src/components/terminal/Terminal.test.tsx @@ -1,24 +1,57 @@ import { render, screen, act } from '@testing-library/react'; import { createRef } from 'react'; -import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; import { Terminal, type TerminalHandle } from './Terminal'; // Mock xterm modules +const mockOnData = vi.fn(); +const mockOnBinary = vi.fn(); +const mockOnResize = vi.fn(); + const mockTerminal = { open: vi.fn(), dispose: vi.fn(), loadAddon: vi.fn(), - onData: vi.fn(), - onResize: vi.fn(), + onData: vi.fn((cb) => { + mockOnData.mockImplementation(cb); + }), + onBinary: vi.fn((cb) => { + mockOnBinary.mockImplementation(cb); + }), + onResize: vi.fn((cb) => { + mockOnResize.mockImplementation(cb); + }), write: vi.fn(), writeln: vi.fn(), clear: vi.fn(), focus: vi.fn(), + reset: vi.fn(), + scrollToBottom: vi.fn(), + cols: 80, + rows: 24, options: {} as Record, }; const mockFitAddon = { fit: vi.fn(), + dispose: vi.fn(), +}; + +const mockSearchAddon = { + dispose: vi.fn(), +}; + +const mockWebglAddon = { + onContextLoss: vi.fn(), + dispose: vi.fn(), +}; + +const mockCanvasAddon = { + dispose: vi.fn(), +}; + +const mockWebLinksAddon = { + dispose: vi.fn(), }; vi.mock('@xterm/xterm', () => ({ @@ -30,23 +63,49 @@ vi.mock('@xterm/addon-fit', () => ({ })); vi.mock('@xterm/addon-search', () => ({ - SearchAddon: vi.fn(() => ({})), + SearchAddon: vi.fn(() => mockSearchAddon), })); vi.mock('@xterm/addon-webgl', () => ({ - WebglAddon: vi.fn(() => ({})), + WebglAddon: vi.fn(() => mockWebglAddon), +})); + +vi.mock('@xterm/addon-canvas', () => ({ + CanvasAddon: vi.fn(() => mockCanvasAddon), +})); + +vi.mock('@xterm/addon-web-links', () => ({ + WebLinksAddon: vi.fn(() => mockWebLinksAddon), })); // Mock ResizeObserver +let resizeCallback: (() => void) | null = null; +const mockObserve = vi.fn(); +const mockDisconnect = vi.fn(); + beforeEach(() => { vi.clearAllMocks(); - global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), - })); + resizeCallback = null; + global.ResizeObserver = vi.fn().mockImplementation((cb) => { + resizeCallback = cb; + return { + observe: mockObserve, + unobserve: vi.fn(), + disconnect: mockDisconnect, + }; + }); +}); + +afterEach(() => { + vi.restoreAllMocks(); }); +async function waitForInit() { + await act(async () => { + await new Promise((r) => setTimeout(r, 10)); + }); +} + describe('Terminal', () => { it('renders terminal container', () => { render(); @@ -55,53 +114,224 @@ describe('Terminal', () => { it('initializes xterm on mount', async () => { render(); - // Wait for async init - await act(async () => { - await new Promise((r) => setTimeout(r, 10)); - }); + await waitForInit(); expect(mockTerminal.open).toHaveBeenCalled(); }); - it('calls onData when provided', async () => { + it('loads fit and search addons', async () => { + render(); + await waitForInit(); + expect(mockTerminal.loadAddon).toHaveBeenCalledWith(mockFitAddon); + expect(mockTerminal.loadAddon).toHaveBeenCalledWith(mockSearchAddon); + }); + + it('loads web links addon by default', async () => { + render(); + await waitForInit(); + expect(mockTerminal.loadAddon).toHaveBeenCalledWith(mockWebLinksAddon); + }); + + it('skips web links addon when linkHandling is false', async () => { + render(); + await waitForInit(); + expect(mockTerminal.loadAddon).not.toHaveBeenCalledWith(mockWebLinksAddon); + }); + + it('calls initial fit after opening', async () => { + render(); + await waitForInit(); + expect(mockFitAddon.fit).toHaveBeenCalled(); + }); + + it('sets up ResizeObserver on container', async () => { + render(); + await waitForInit(); + expect(mockObserve).toHaveBeenCalled(); + }); + + it('calls onData through stable ref', async () => { const onData = vi.fn(); render(); - await act(async () => { - await new Promise((r) => setTimeout(r, 10)); - }); - expect(mockTerminal.onData).toHaveBeenCalledWith(onData); + await waitForInit(); + // Simulate terminal data input + expect(mockTerminal.onData).toHaveBeenCalled(); + mockOnData('test-input'); + expect(onData).toHaveBeenCalledWith('test-input'); }); - it('calls onResize when provided', async () => { + it('calls onBinaryData through stable ref', async () => { + const onBinaryData = vi.fn(); + render(); + await waitForInit(); + expect(mockTerminal.onBinary).toHaveBeenCalled(); + mockOnBinary('binary-data'); + expect(onBinaryData).toHaveBeenCalledWith('binary-data'); + }); + + it('calls onResize through stable ref', async () => { const onResize = vi.fn(); render(); - await act(async () => { - await new Promise((r) => setTimeout(r, 10)); - }); + await waitForInit(); expect(mockTerminal.onResize).toHaveBeenCalled(); + mockOnResize({ cols: 120, rows: 40 }); + expect(onResize).toHaveBeenCalledWith(120, 40); }); - it('exposes imperative handle methods', async () => { - const ref = createRef(); - render(); - await act(async () => { - await new Promise((r) => setTimeout(r, 10)); + it('handles WebGL context loss gracefully', async () => { + render(); + await waitForInit(); + expect(mockWebglAddon.onContextLoss).toHaveBeenCalled(); + // Simulate context loss + const contextLossHandler = mockWebglAddon.onContextLoss.mock.calls[0]?.[0] as () => void; + contextLossHandler(); + expect(mockWebglAddon.dispose).toHaveBeenCalled(); + }); + + describe('imperative handle', () => { + it('write accepts string data', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.write('hello'); + expect(mockTerminal.write).toHaveBeenCalledWith('hello'); }); - ref.current?.write('hello'); - expect(mockTerminal.write).toHaveBeenCalledWith('hello'); + it('write accepts Uint8Array data', async () => { + const ref = createRef(); + render(); + await waitForInit(); + const data = new Uint8Array([72, 101, 108, 108, 111]); + ref.current?.write(data); + expect(mockTerminal.write).toHaveBeenCalledWith(data); + }); - ref.current?.writeln('line'); - expect(mockTerminal.writeln).toHaveBeenCalledWith('line'); + it('writeln writes a line', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.writeln('line'); + expect(mockTerminal.writeln).toHaveBeenCalledWith('line'); + }); - ref.current?.clear(); - expect(mockTerminal.clear).toHaveBeenCalled(); + it('clear clears the terminal', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.clear(); + expect(mockTerminal.clear).toHaveBeenCalled(); + }); + + it('focus focuses the terminal', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.focus(); + expect(mockTerminal.focus).toHaveBeenCalled(); + }); + + it('fit re-fits the terminal', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.fit(); + expect(mockFitAddon.fit).toHaveBeenCalled(); + }); + + it('getDimensions returns current cols/rows', async () => { + const ref = createRef(); + render(); + await waitForInit(); + const dims = ref.current?.getDimensions(); + expect(dims).toEqual({ cols: 80, rows: 24 }); + }); + + it('reset resets the terminal', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.reset(); + expect(mockTerminal.reset).toHaveBeenCalled(); + }); - ref.current?.focus(); - expect(mockTerminal.focus).toHaveBeenCalled(); + it('scrollToBottom scrolls to bottom', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.scrollToBottom(); + expect(mockTerminal.scrollToBottom).toHaveBeenCalled(); + }); + + it('handles methods called before init gracefully', () => { + const ref = createRef(); + render(); + // Call before async init completes — should not throw + expect(() => ref.current?.write('test')).not.toThrow(); + expect(() => ref.current?.clear()).not.toThrow(); + expect(() => ref.current?.focus()).not.toThrow(); + expect(ref.current?.getDimensions()).toBeNull(); + }); }); it('merges className', () => { render(); expect(screen.getByTestId('terminal')).toHaveClass('custom-terminal'); }); + + it('disposes terminal and addons on unmount', async () => { + const { unmount } = render(); + await waitForInit(); + unmount(); + expect(mockDisconnect).toHaveBeenCalled(); + expect(mockTerminal.dispose).toHaveBeenCalled(); + }); + + it('handles debounced fit on resize', async () => { + vi.useFakeTimers(); + render(); + + // Wait for init with real timers briefly + vi.useRealTimers(); + await waitForInit(); + vi.useFakeTimers(); + + // Clear fit calls from init + mockFitAddon.fit.mockClear(); + + // Trigger resize + resizeCallback?.(); + resizeCallback?.(); + resizeCallback?.(); + + // Should not have called fit yet (debounced) + expect(mockFitAddon.fit).not.toHaveBeenCalled(); + + // Advance past debounce period + vi.advanceTimersByTime(15); + expect(mockFitAddon.fit).toHaveBeenCalledTimes(1); + + vi.useRealTimers(); + }); + + it('passes mac-specific options', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ + macOptionIsMeta: true, + macOptionClickForcesSelection: true, + }), + ); + }); + + it('passes scrollback option', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ + scrollback: 10000, + }), + ); + }); }); diff --git a/packages/editors/src/components/terminal/Terminal.tsx b/packages/editors/src/components/terminal/Terminal.tsx index 7dfc04b..ee5f633 100644 --- a/packages/editors/src/components/terminal/Terminal.tsx +++ b/packages/editors/src/components/terminal/Terminal.tsx @@ -3,43 +3,148 @@ import { useEditorTheme } from '../../themes/useEditorTheme'; import { buildXtermTheme } from '../../themes/xterm'; import styles from './Terminal.module.css'; +/** Unix signal types that exec plugins may emit. */ +export type TerminalSignal = + | 'ERROR' + | 'CLOSE' + | 'SIGINT' + | 'SIGQUIT' + | 'SIGTERM' + | 'SIGKILL' + | 'SIGHUP' + | 'SIGUSR1' + | 'SIGUSR2' + | 'SIGWINCH'; + export interface TerminalProps { + /** Called when user types input. */ onData?: (data: string) => void; + /** Called when user pastes binary data. */ + onBinaryData?: (data: string) => void; + /** Called on terminal resize with new dimensions. */ onResize?: (cols: number, rows: number) => void; + /** Called when a signal is received (for exec plugin integration). */ + onSignal?: (signal: TerminalSignal, payload?: unknown) => void; + /** Called when the terminal session encounters an error. */ + onError?: (error: Error) => void; + /** Called when the terminal session closes. */ + onClose?: (code?: number) => void; + /** Font size in pixels. */ fontSize?: number; + /** Font family override. */ fontFamily?: string; + /** Maximum scrollback buffer lines. 0 = unlimited. */ + scrollback?: number; + /** Enable cursor blink. */ + cursorBlink?: boolean; + /** Convert \\n to \\r\\n for proper line endings. */ + convertEol?: boolean; + /** Allow transparent background. */ + allowTransparency?: boolean; + /** Treat macOS Option key as Meta (for proper Alt-key sequences). */ + macOptionIsMeta?: boolean; + /** Allow macOS Option+Click to force selection. */ + macOptionClickForcesSelection?: boolean; + /** Enable clickable URLs in terminal output. */ + linkHandling?: boolean; + /** Custom CSS class. */ className?: string; } export interface TerminalHandle { - write: (data: string) => void; + /** Write a string to the terminal. */ + write: (data: string | Uint8Array) => void; + /** Write a string followed by a newline. */ writeln: (data: string) => void; + /** Clear the terminal viewport and scrollback. */ clear: () => void; + /** Focus the terminal. */ focus: () => void; + /** Re-fit the terminal to its container. */ fit: () => void; + /** Get the current terminal dimensions. */ + getDimensions: () => { cols: number; rows: number } | null; + /** Reset the terminal (clear + reset state). */ + reset: () => void; + /** Scroll to the bottom of the terminal. */ + scrollToBottom: () => void; } function cn(...parts: Array): string { return parts.filter(Boolean).join(' '); } +/** + * Simple debounce for resize handlers. Returns a debounced function + * and a cancel function for cleanup. + */ +function debounce void>( + fn: T, + ms: number, +): { run: (...args: Parameters) => void; cancel: () => void } { + let timer: ReturnType | null = null; + return { + run: (...args: Parameters) => { + if (timer !== null) clearTimeout(timer); + timer = setTimeout(() => { + timer = null; + fn(...args); + }, ms); + }, + cancel: () => { + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + }, + }; +} + export const Terminal = forwardRef(function Terminal( - { onData, onResize, fontSize = 13, fontFamily, className }, + { + onData, + onBinaryData, + onResize, + onSignal, + onError, + onClose, + fontSize = 13, + fontFamily, + scrollback = 5000, + cursorBlink = true, + convertEol = true, + allowTransparency = true, + macOptionIsMeta = true, + macOptionClickForcesSelection = true, + linkHandling = true, + className, + }, ref, ) { const containerRef = useRef(null); const termRef = useRef(null); const fitAddonRef = useRef(null); + const addonsRef = useRef void }>>([]); + const observerRef = useRef(null); + const debouncedFitRef = useRef<{ run: () => void; cancel: () => void } | null>(null); const theme = useEditorTheme(); + // Stable refs for callbacks to avoid re-initializing terminal + const callbacksRef = useRef({ onData, onBinaryData, onResize, onSignal, onError, onClose }); + callbacksRef.current = { onData, onBinaryData, onResize, onSignal, onError, onClose }; + const fit = useCallback(() => { - const addon = fitAddonRef.current as { fit?: () => void } | null; - addon?.fit?.(); + try { + const addon = fitAddonRef.current as { fit?: () => void } | null; + addon?.fit?.(); + } catch { + // fit() can throw if container has zero dimensions (e.g., hidden tab) + } }, []); useImperativeHandle(ref, () => ({ - write: (data: string) => { - const t = termRef.current as { write?: (d: string) => void } | null; + write: (data: string | Uint8Array) => { + const t = termRef.current as { write?: (d: string | Uint8Array) => void } | null; t?.write?.(data); }, writeln: (data: string) => { @@ -55,6 +160,21 @@ export const Terminal = forwardRef(function Termi t?.focus?.(); }, fit, + getDimensions: () => { + const t = termRef.current as { cols?: number; rows?: number } | null; + if (t && typeof t.cols === 'number' && typeof t.rows === 'number') { + return { cols: t.cols, rows: t.rows }; + } + return null; + }, + reset: () => { + const t = termRef.current as { reset?: () => void } | null; + t?.reset?.(); + }, + scrollToBottom: () => { + const t = termRef.current as { scrollToBottom?: () => void } | null; + t?.scrollToBottom?.(); + }, })); // Initialize xterm lazily @@ -78,71 +198,184 @@ export const Terminal = forwardRef(function Termi fontSize, fontFamily: fontFamily || 'var(--ov-font-family-mono, monospace)', theme: xtermTheme, - allowTransparency: true, - cursorBlink: true, - convertEol: true, + allowTransparency, + cursorBlink, + convertEol, + scrollback, + macOptionIsMeta, + macOptionClickForcesSelection, + allowProposedApi: true, + drawBoldTextInBrightColors: true, }); + const loadedAddons: Array<{ dispose: () => void }> = []; + const fitAddon = new FitAddon(); const searchAddon = new SearchAddon(); term.loadAddon(fitAddon); term.loadAddon(searchAddon); + loadedAddons.push(fitAddon, searchAddon); - // Try WebGL renderer, fall back to canvas + // Load web links addon for clickable URLs + if (linkHandling) { + try { + const { WebLinksAddon } = await import('@xterm/addon-web-links'); + if (!disposed) { + const webLinksAddon = new WebLinksAddon(); + term.loadAddon(webLinksAddon); + loadedAddons.push(webLinksAddon); + } + } catch { + // Web links addon not available, non-critical + } + } + + if (disposed) { + term.dispose(); + return; + } + + // Open terminal in container + term.open(container!); + + // Try WebGL renderer first, then Canvas fallback + let rendererLoaded = false; try { const { WebglAddon } = await import('@xterm/addon-webgl'); if (!disposed) { const webglAddon = new WebglAddon(); + // Handle WebGL context loss gracefully + webglAddon.onContextLoss(() => { + webglAddon.dispose(); + }); term.loadAddon(webglAddon); + loadedAddons.push(webglAddon); + rendererLoaded = true; } } catch { - // WebGL not available, canvas fallback is fine + // WebGL not available + } + + if (!rendererLoaded && !disposed) { + try { + const { CanvasAddon } = await import('@xterm/addon-canvas'); + if (!disposed) { + const canvasAddon = new CanvasAddon(); + term.loadAddon(canvasAddon); + loadedAddons.push(canvasAddon); + } + } catch { + // Canvas addon not available, DOM renderer is used as final fallback + } } if (disposed) { + for (const addon of loadedAddons) { + try { + addon.dispose(); + } catch { + // ignore + } + } term.dispose(); return; } - term.open(container!); + // Initial fit fitAddon.fit(); + // Store refs termRef.current = term; fitAddonRef.current = fitAddon; + addonsRef.current = loadedAddons; - if (onData) { - term.onData(onData); - } + // Wire up callbacks via stable refs + term.onData((data: string) => { + callbacksRef.current.onData?.(data); + }); - if (onResize) { - term.onResize(({ cols, rows }) => onResize(cols, rows)); - } + term.onBinary((data: string) => { + callbacksRef.current.onBinaryData?.(data); + }); - // Observe container resize - const observer = new ResizeObserver(() => { - fitAddon.fit(); + term.onResize(({ cols, rows }: { cols: number; rows: number }) => { + callbacksRef.current.onResize?.(cols, rows); }); - observer.observe(container!); - // Store cleanup references - (container as HTMLDivElement & { __cleanup?: () => void }).__cleanup = () => { - observer.disconnect(); - term.dispose(); - termRef.current = null; - fitAddonRef.current = null; + // Debounced fit for resize handling (10ms debounce like legacy impl) + const debouncedFit = debounce(() => { + try { + fitAddon.fit(); + } catch { + // Ignore fit errors (zero-size container, etc.) + } + }, 10); + debouncedFitRef.current = debouncedFit; + + // ResizeObserver for container resize + const resizeObserver = new ResizeObserver(() => { + debouncedFit.run(); + }); + resizeObserver.observe(container!); + observerRef.current = resizeObserver; + + // Window resize handler as additional safety + const handleWindowResize = () => { + debouncedFit.run(); }; + window.addEventListener('resize', handleWindowResize); + + // Store window resize cleanup + (container as HTMLDivElement & { __windowResizeCleanup?: () => void }).__windowResizeCleanup = + () => { + window.removeEventListener('resize', handleWindowResize); + }; } - init(); + init().catch((err) => { + callbacksRef.current.onError?.(err instanceof Error ? err : new Error(String(err))); + }); return () => { disposed = true; - const cleanup = (container as HTMLDivElement & { __cleanup?: () => void }).__cleanup; - cleanup?.(); + + // Cancel pending debounced fit + debouncedFitRef.current?.cancel(); + debouncedFitRef.current = null; + + // Disconnect resize observer + observerRef.current?.disconnect(); + observerRef.current = null; + + // Remove window resize listener + const windowCleanup = (container as HTMLDivElement & { __windowResizeCleanup?: () => void }) + .__windowResizeCleanup; + windowCleanup?.(); + + // Dispose addons first, then terminal (proper order) + for (const addon of addonsRef.current) { + try { + addon.dispose(); + } catch { + // ignore addon disposal errors + } + } + addonsRef.current = []; + + // Dispose terminal + const term = termRef.current as { dispose?: () => void } | null; + try { + term?.dispose?.(); + } catch { + // ignore terminal disposal errors + } + + termRef.current = null; + fitAddonRef.current = null; }; }, []); // eslint-disable-line react-hooks/exhaustive-deps - // Update theme + // Update theme dynamically without recreating terminal useEffect(() => { const term = termRef.current as { options?: { theme: unknown } } | null; if (term?.options) { @@ -150,6 +383,16 @@ export const Terminal = forwardRef(function Termi } }, [theme]); + // Update font size dynamically + useEffect(() => { + const term = termRef.current as { options?: { fontSize: number } } | null; + if (term?.options) { + term.options.fontSize = fontSize; + // Re-fit after font size change + debouncedFitRef.current?.run(); + } + }, [fontSize]); + return
; }); diff --git a/packages/editors/src/components/terminal/index.ts b/packages/editors/src/components/terminal/index.ts index 26c6dda..f6059c7 100644 --- a/packages/editors/src/components/terminal/index.ts +++ b/packages/editors/src/components/terminal/index.ts @@ -1,2 +1,2 @@ export { Terminal } from './Terminal'; -export type { TerminalProps, TerminalHandle } from './Terminal'; +export type { TerminalProps, TerminalHandle, TerminalSignal } from './Terminal'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f575a9..40751d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,12 +135,18 @@ importers: '@omniview/base-ui': specifier: workspace:* version: link:../base-ui + '@xterm/addon-canvas': + specifier: ^0.7.0 + version: 0.7.0(@xterm/xterm@5.5.0) '@xterm/addon-fit': specifier: ^0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) '@xterm/addon-search': specifier: ^0.15.0 version: 0.15.0(@xterm/xterm@5.5.0) + '@xterm/addon-web-links': + specifier: ^0.11.0 + version: 0.11.0(@xterm/xterm@5.5.0) '@xterm/addon-webgl': specifier: ^0.18.0 version: 0.18.0(@xterm/xterm@5.5.0) @@ -1295,6 +1301,11 @@ packages: '@vue/shared@3.5.29': resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + '@xterm/addon-canvas@0.7.0': + resolution: {integrity: sha512-LF5LYcfvefJuJ7QotNRdRSPc9YASAVDeoT5uyXS/nZshZXjYplGXRECBGiznwvhNL2I8bq1Lf5MzRwstsYQ2Iw==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + '@xterm/addon-fit@0.10.0': resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} peerDependencies: @@ -1305,6 +1316,11 @@ packages: peerDependencies: '@xterm/xterm': ^5.0.0 + '@xterm/addon-web-links@0.11.0': + resolution: {integrity: sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + '@xterm/addon-webgl@0.18.0': resolution: {integrity: sha512-xCnfMBTI+/HKPdRnSOHaJDRqEpq2Ugy8LEj9GiY4J3zJObo3joylIFaMvzBwbYRg8zLtkO0KQaStCeSfoaI2/w==} peerDependencies: @@ -4304,6 +4320,10 @@ snapshots: '@vue/shared@3.5.29': {} + '@xterm/addon-canvas@0.7.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': dependencies: '@xterm/xterm': 5.5.0 @@ -4312,6 +4332,10 @@ snapshots: dependencies: '@xterm/xterm': 5.5.0 + '@xterm/addon-web-links@0.11.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + '@xterm/addon-webgl@0.18.0(@xterm/xterm@5.5.0)': dependencies: '@xterm/xterm': 5.5.0 From 2c58846aff5b9a593a59fe0464bfbdc6d9fdd587 Mon Sep 17 00:00:00 2001 From: Joshua Pare Date: Mon, 9 Mar 2026 17:36:28 -0500 Subject: [PATCH 03/11] feat(editors): Monaco 0.52 downgrade, YAML schema completions, diagnostics API - Downgrade monaco-editor to 0.52.2 for monaco-yaml@5.4.1 compatibility - Add EditorSchemaRegistry with runtime YAML/JSON schema registration - Add onDiagnostics/onCursorChange callbacks and getDebugState() handle - Fix model disposal (detach before dispose) to prevent Canceled errors - Fix test mocks for monaco.languages.typescript (0.52 API shape) - Add resolve alias for monaco-editor ESM entry in Vite config - Suppress marked.js sourcemap warning in Storybook via Vite plugin - Document monaco-yaml compatibility issues and fork considerations --- packages/editors/.storybook/main.ts | 53 + packages/editors/.storybook/preview.tsx | 48 + packages/editors/docs/MONACO_YAML_COMPAT.md | 155 + packages/editors/package.json | 32 +- .../code-editor/CodeEditor.stories.tsx | 587 +- .../code-editor/CodeEditor.test.tsx | 209 +- .../src/components/code-editor/CodeEditor.tsx | 673 +- .../src/components/code-editor/index.ts | 9 +- .../diff-viewer/DiffViewer.stories.tsx | 118 + .../diff-viewer/DiffViewer.test.tsx | 148 +- .../src/components/diff-viewer/DiffViewer.tsx | 141 +- .../MarkdownPreview.module.css | 78 +- .../MarkdownPreview.stories.tsx | 438 +- .../markdown-preview/MarkdownPreview.test.tsx | 164 +- .../markdown-preview/MarkdownPreview.tsx | 153 +- .../ObjectInspector.stories.tsx | 162 +- .../components/terminal/Terminal.stories.tsx | 237 +- packages/editors/src/index.ts | 3 + .../src/schemas/EditorSchemaRegistry.ts | 260 + packages/editors/src/schemas/index.ts | 2 + .../src/schemas/kubernetes/configmap-v1.json | 316 + .../kubernetes/deployment-apps-v1.json | 10291 ++++++++++++++ .../editors/src/schemas/kubernetes/index.ts | 68 + .../kubernetes/ingress-networking-v1.json | 656 + .../src/schemas/kubernetes/pod-v1.json | 11148 ++++++++++++++++ .../src/schemas/kubernetes/service-v1.json | 697 + packages/editors/src/setup/css.worker.ts | 1 + packages/editors/src/setup/editor.worker.ts | 1 + packages/editors/src/setup/html.worker.ts | 1 + packages/editors/src/setup/index.ts | 1 + packages/editors/src/setup/json.worker.ts | 1 + .../editors/src/setup/setupMonacoWorkers.ts | 113 + packages/editors/src/setup/ts.worker.ts | 1 + packages/editors/src/setup/yaml.worker.ts | 3 + packages/editors/src/themes/monaco.ts | 216 +- packages/editors/tsconfig.json | 4 +- packages/editors/vite.config.ts | 9 +- pnpm-lock.yaml | 251 +- 38 files changed, 26945 insertions(+), 503 deletions(-) create mode 100644 packages/editors/.storybook/main.ts create mode 100644 packages/editors/.storybook/preview.tsx create mode 100644 packages/editors/docs/MONACO_YAML_COMPAT.md create mode 100644 packages/editors/src/schemas/EditorSchemaRegistry.ts create mode 100644 packages/editors/src/schemas/index.ts create mode 100644 packages/editors/src/schemas/kubernetes/configmap-v1.json create mode 100644 packages/editors/src/schemas/kubernetes/deployment-apps-v1.json create mode 100644 packages/editors/src/schemas/kubernetes/index.ts create mode 100644 packages/editors/src/schemas/kubernetes/ingress-networking-v1.json create mode 100644 packages/editors/src/schemas/kubernetes/pod-v1.json create mode 100644 packages/editors/src/schemas/kubernetes/service-v1.json create mode 100644 packages/editors/src/setup/css.worker.ts create mode 100644 packages/editors/src/setup/editor.worker.ts create mode 100644 packages/editors/src/setup/html.worker.ts create mode 100644 packages/editors/src/setup/index.ts create mode 100644 packages/editors/src/setup/json.worker.ts create mode 100644 packages/editors/src/setup/setupMonacoWorkers.ts create mode 100644 packages/editors/src/setup/ts.worker.ts create mode 100644 packages/editors/src/setup/yaml.worker.ts diff --git a/packages/editors/.storybook/main.ts b/packages/editors/.storybook/main.ts new file mode 100644 index 0000000..cf94686 --- /dev/null +++ b/packages/editors/.storybook/main.ts @@ -0,0 +1,53 @@ +import type { StorybookConfig } from '@storybook/react-vite'; +import type { Plugin } from 'vite'; + +/** + * Strip the broken `//# sourceMappingURL=marked.umd.js.map` reference from + * monaco-editor@0.52's bundled `marked.js`. The .map file isn't shipped in + * the npm package, so Vite logs a noisy warning every time it transforms it. + */ +function stripBrokenSourcemaps(): Plugin { + return { + name: 'strip-broken-sourcemaps', + transform(code, id) { + if (id.includes('marked') && code.includes('sourceMappingURL=marked.umd.js.map')) { + return { + code: code.replace('//# sourceMappingURL=marked.umd.js.map', ''), + map: null, + }; + } + }, + }; +} + +const config: StorybookConfig = { + stories: ['../src/**/*.stories.@(ts|tsx)'], + addons: ['@storybook/addon-docs', '@storybook/addon-a11y'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, + docs: { + autodocs: 'tag', + }, + viteFinal(config) { + config.optimizeDeps ??= {}; + + // monaco-editor is huge — skip esbuild pre-bundling, let Vite serve on-the-fly. + config.optimizeDeps.exclude ??= []; + config.optimizeDeps.exclude.push('monaco-editor'); + + // Pre-bundle the yaml worker entry so esbuild transforms its CJS deps + // (path-browserify, etc.) into ESM — required for native ESM workers in dev. + config.optimizeDeps.include ??= []; + config.optimizeDeps.include.push('monaco-yaml/yaml.worker.js'); + + // Strip broken sourcemap reference from monaco's bundled marked.js + config.plugins ??= []; + (config.plugins as Plugin[]).push(stripBrokenSourcemaps()); + + return config; + }, +}; + +export default config; diff --git a/packages/editors/.storybook/preview.tsx b/packages/editors/.storybook/preview.tsx new file mode 100644 index 0000000..32afd51 --- /dev/null +++ b/packages/editors/.storybook/preview.tsx @@ -0,0 +1,48 @@ +import type { Preview } from '@storybook/react'; +import { ThemeProvider } from '../../base-ui/src/theme/ThemeProvider'; +import '../../base-ui/src/theme/styles.css'; +// Side-effect import — configures Monaco workers + local build before any editor mounts +import '../src/setup/setupMonacoWorkers'; + +const preview: Preview = { + globalTypes: { + theme: { + name: 'Theme', + defaultValue: 'dark', + toolbar: { + icon: 'paintbrush', + items: ['dark', 'light', 'high-contrast-dark', 'high-contrast-light'], + }, + }, + }, + decorators: [ + (Story, context) => { + const theme = context.globals.theme ?? 'dark'; + + return ( + +
+ +
+
+ ); + }, + ], + parameters: { + controls: { + expanded: true, + }, + layout: 'fullscreen', + }, +}; + +export default preview; diff --git a/packages/editors/docs/MONACO_YAML_COMPAT.md b/packages/editors/docs/MONACO_YAML_COMPAT.md new file mode 100644 index 0000000..d7c7746 --- /dev/null +++ b/packages/editors/docs/MONACO_YAML_COMPAT.md @@ -0,0 +1,155 @@ +# Monaco + YAML: Compatibility Notes + +This document records the compatibility constraints and known issues between +`monaco-editor`, `monaco-yaml`, and `monaco-worker-manager` so that future +maintainers (or a potential fork) have full context. + +--- + +## Version Matrix (as of March 2025) + +| Package | Version | Notes | +| ---------------------- | -------- | ----------------------------------------------- | +| `monaco-editor` | `0.52.2` | Latest in the 0.52 line; **required** for yaml | +| `monaco-yaml` | `5.4.1` | Peer dep says `>=0.36` but only tested on 0.52 | +| `monaco-worker-manager`| `2.0.1` | Transitive dep of `monaco-yaml` | + +## Why Not Monaco 0.55? + +Monaco 0.55 introduced two breaking changes that are **fundamentally +incompatible** with `monaco-yaml@5.4.1` (via `monaco-worker-manager@2.0.1`): + +### 1. `WebWorker` constructor crash + +In `vs/base/browser/webWorkerFactory.js`, the `WebWorker` class constructor +tries `'then' in descriptorOrWorker` (line ~110). When `descriptorOrWorker` is +`undefined` — which happens because `monaco-worker-manager` doesn't pass +`opts.worker` — this crashes with: + +``` +TypeError: Cannot use 'in' operator to search for 'then' in undefined +``` + +**Workaround attempted:** Monkey-patching `monaco.editor.createWebWorker` to +inject `opts.worker` from `MonacoEnvironment.getWorker`. This prevents the +crash but doesn't fix completions. + +### 2. Worker protocol mismatch (`$initialize` consumed) + +Monaco 0.55's `WebWorkerClient` (in `vs/base/common/worker/webWorker.js`) sends +a `$initialize` message to the worker. Inside the worker, two different +initialization paths compete: + +- **Default workers** (`editor.worker.js`): Use a single-message protocol via + `start()` → `WebWorkerServer` — this works fine. +- **monaco-worker-manager workers** (yaml, etc.): Use `common/initialize.js` + which creates a **two-message** protocol. The first message is consumed by + `initialize()` before `WebWorkerServer` is even created. The `$initialize` + message from `WebWorkerClient` is therefore never processed by + `WebWorkerServer`, causing the proxy to hang forever at "Loading...". + +**This is not patchable.** The worker initialization protocol in 0.55 is +structurally incompatible with how `monaco-worker-manager` bootstraps workers. + +### Summary + +``` +monaco-editor@0.55 + monaco-yaml@5.4.1 = broken (hangs on "Loading...") +monaco-editor@0.52 + monaco-yaml@5.4.1 = working (completions + validation) +``` + +## Vite Worker Loading + +Monaco workers in Vite require special handling: + +### `?worker` imports (current approach) + +```ts +import YamlWorker from './yaml.worker?worker'; +``` + +Vite bundles each worker with its dependencies, transforming CJS → ESM. This is +necessary because `yaml-language-server` (used by `monaco-yaml`) depends on +CJS-only packages like `path-browserify`. Without bundling, native ESM workers +fail with `module is not defined`. + +### Storybook `viteFinal` config + +```ts +viteFinal(config) { + config.optimizeDeps ??= {}; + // monaco-editor is too large for esbuild pre-bundling + config.optimizeDeps.exclude ??= []; + config.optimizeDeps.exclude.push('monaco-editor'); + // Pre-bundle yaml worker so CJS deps become ESM + config.optimizeDeps.include ??= []; + config.optimizeDeps.include.push('monaco-yaml/yaml.worker.js'); + return config; +} +``` + +## Schema Registration + +Schemas are registered at runtime via the `EditorSchemaRegistry` singleton. +The `configureMonacoYaml()` call happens once at setup time (side-effect import), +returning a `{ update, dispose }` handle. The registry calls `handle.update()` +whenever schemas change. + +### Schema URI Bug — YAML(768) "No schema request service" + +``` +Unable to load schema from '...'. No schema request service available YAML(768) +``` + +This error has **two** causes: + +#### 1. Custom protocol URIs (e.g. `k8s://`) + +Schema URIs must use `https://`. Custom protocols cause `monaco-yaml` to attempt +a fetch even with `enableSchemaRequest: false`. + +#### 2. URI normalization mismatch (the deeper bug) + +`yaml-language-server`'s `JSONSchemaService` has a bug: it stores schema handles +keyed by `normalizeId(uri)` (which runs `URI.parse(uri).toString()`) but looks +them up via `createCombinedSchema` → `getOrAddSchemaHandle` using the **raw** URI +from `filePatternAssociations`. If `URI.parse` changes the URI (e.g. encoding +`::` to `%3A%3A`), the lookup misses the stored handle and creates a new one +**without** the inline schema content, triggering a fetch attempt. + +**Fix:** Schema URIs must be "URI-safe" — avoid characters that `URI.parse` +normalizes differently. Use `encodeURIComponent()` for dynamic segments: + +```ts +// BAD — :: gets normalized by URI.parse, causing handle lookup miss +uri: `https://omniview.dev/schemas/k8s/core::v1::Pod` + +// GOOD — encoded segment survives URI normalization +uri: `https://omniview.dev/schemas/k8s/${encodeURIComponent('core::v1::Pod')}` +``` + +## If We Fork `monaco-yaml` + +Key areas to consider: + +1. **Drop `monaco-worker-manager` dependency** — use Monaco's built-in + `createWebWorker` directly with explicit worker injection. This would fix + Monaco 0.55+ compatibility. + +2. **Worker initialization** — replace the two-message `common/initialize.js` + protocol with the single-message `start()` pattern that Monaco's own workers + use (compatible across all versions). + +3. **Schema URI handling** — fix the `normalizeId` mismatch bug in + `JSONSchemaService` (store and look up with the same normalized key). + Allow custom URI protocols without requiring `enableSchemaRequest: true`. + The schema object is already provided inline; the URI should just be an + identifier, not a fetch target. + +4. **`yaml-language-server` bundling** — the LSP server has heavy CJS + dependencies. A fork could pre-bundle or replace them to simplify worker + loading. + +5. **Singleton enforcement** — `configureMonacoYaml` currently throws if called + twice. A fork could support reconfiguration or multiple instances for + different editor contexts. diff --git a/packages/editors/package.json b/packages/editors/package.json index ac332e5..d78061b 100644 --- a/packages/editors/package.json +++ b/packages/editors/package.json @@ -21,31 +21,38 @@ "**/*.css" ], "scripts": { - "clean": "rm -rf dist coverage", + "clean": "rm -rf dist storybook-static coverage", "build": "vite build", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "storybook": "storybook dev -p 6007", + "build-storybook": "storybook build" }, "peerDependencies": { + "@omniview/base-ui": "workspace:*", "react": "^19.0.0", - "react-dom": "^19.0.0", - "@omniview/base-ui": "workspace:*" + "react-dom": "^19.0.0" }, "dependencies": { - "@monaco-editor/react": "^4.7.0", - "monaco-editor": "^0.52.2", - "@xterm/xterm": "^5.5.0", + "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-search": "^0.15.0", - "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", + "@xterm/xterm": "^5.5.0", + "monaco-editor": "^0.52.2", + "monaco-yaml": "^5.4.1", "react-markdown": "^9.0.3", - "remark-gfm": "^4.0.0", - "rehype-highlight": "^7.0.2" + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.0" }, "devDependencies": { + "@storybook/addon-a11y": "^10.2.15", + "@storybook/addon-docs": "^10.2.15", + "@storybook/react": "^10.2.15", + "@storybook/react-vite": "^10.2.15", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.6.1", @@ -53,11 +60,10 @@ "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "jsdom": "^25.0.1", + "storybook": "^10.2.15", "typescript": "^5.9.2", "vite": "^5.4.14", "vite-plugin-dts": "^4.5.1", - "vitest": "^2.1.8", - "@storybook/react": "^10.2.15", - "storybook": "^10.2.15" + "vitest": "^2.1.8" } } diff --git a/packages/editors/src/components/code-editor/CodeEditor.stories.tsx b/packages/editors/src/components/code-editor/CodeEditor.stories.tsx index cf6b38e..ce3198f 100644 --- a/packages/editors/src/components/code-editor/CodeEditor.stories.tsx +++ b/packages/editors/src/components/code-editor/CodeEditor.stories.tsx @@ -1,5 +1,8 @@ +import { useState, useEffect, useCallback } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { CodeEditor } from './CodeEditor'; +import { CodeEditor, type CodeEditorProps, type EditorDiagnostic, type CursorPosition } from './CodeEditor'; +import { editorSchemas, type EditorSchema } from '../../schemas'; +import { k8sSchemas, gvrFileMatch, type K8sSchemaEntry } from '../../schemas/kubernetes'; const sampleCode = `import { useState } from 'react'; @@ -34,6 +37,7 @@ const meta: Meta = { lineNumbers: { control: 'boolean' }, minimap: { control: 'boolean' }, wordWrap: { control: 'boolean' }, + height: { control: { type: 'number', min: 100, max: 800, step: 50 } }, }, }; @@ -51,7 +55,26 @@ export const ReadOnly: Story = { export const WithMinimap: Story = { args: { minimap: true, - value: Array.from({ length: 100 }, (_, i) => `// Line ${i + 1}`).join('\n'), + value: Array.from({ length: 100 }, (_, i) => `// Line ${i + 1}: sample code content`).join( + '\n', + ), + }, +}; + +export const WordWrap: Story = { + args: { + wordWrap: true, + value: `// This is a very long line that should wrap when wordWrap is enabled. ${'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(5)} +// Another long line follows. ${'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. '.repeat(3)} +const shortLine = true;`, + }, +}; + +export const NoLineNumbers: Story = { + args: { + lineNumbers: false, + value: '# Markdown-style preview\n\nNo line numbers shown.\n\n- Item 1\n- Item 2\n- Item 3', + language: 'markdown', }, }; @@ -60,7 +83,8 @@ export const JSONContent: Story = { value: JSON.stringify({ name: 'omniview', version: '1.0.0', - dependencies: { react: '^19.0.0' }, + dependencies: { react: '^19.0.0', 'react-dom': '^19.0.0' }, + devDependencies: { typescript: '^5.9.0', vite: '^5.4.0' }, }), language: 'json', }, @@ -70,5 +94,562 @@ export const LanguageDetection: Story = { args: { value: 'package main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Hello")\n}', filename: 'main.go', + language: undefined, + }, +}; + +export const PythonCode: Story = { + args: { + value: `import os +from typing import List, Optional + +class FileProcessor: + """Process files in a directory.""" + + def __init__(self, root: str) -> None: + self.root = root + self._files: List[str] = [] + + def scan(self, extensions: Optional[List[str]] = None) -> List[str]: + for dirpath, _, filenames in os.walk(self.root): + for f in filenames: + if extensions is None or any(f.endswith(ext) for ext in extensions): + self._files.append(os.path.join(dirpath, f)) + return self._files + +if __name__ == "__main__": + proc = FileProcessor("/tmp") + print(proc.scan([".py", ".txt"]))`, + language: 'python', + }, +}; + +export const YAMLContent: Story = { + args: { + value: `apiVersion: apps/v1 +kind: Deployment +metadata: + name: web-server + namespace: production + labels: + app: web + tier: frontend +spec: + replicas: 3 + selector: + matchLabels: + app: web + template: + metadata: + labels: + app: web + spec: + containers: + - name: nginx + image: nginx:1.25 + ports: + - containerPort: 80 + resources: + limits: + cpu: "500m" + memory: "128Mi"`, + language: 'yaml', + }, +}; + +export const CSSContent: Story = { + args: { + value: `:root { + --ov-color-bg-base: #1e1e1e; + --ov-color-fg-default: #d4d4d4; + --ov-radius-sm: 4px; +} + +.container { + display: flex; + flex-direction: column; + gap: var(--ov-space-2); + padding: var(--ov-space-4); + background: var(--ov-color-bg-base); + border-radius: var(--ov-radius-sm); +} + +.container:hover { + border-color: var(--ov-color-border-focus); +} + +@media (prefers-reduced-motion: reduce) { + * { transition: none !important; } +}`, + language: 'css', + }, +}; + +export const SyntaxHighlightingOff: Story = { + args: { + value: sampleCode, + language: 'typescript', + syntaxHighlighting: false, + }, +}; + +/** Demonstrates toggling syntax highlighting at runtime. */ +function SyntaxToggleStory(args: CodeEditorProps) { + const [enabled, setEnabled] = useState(true); + return ( +
+ + +
+ ); +} + +export const SyntaxHighlightingToggle: Story = { + render: (args) => , + args: { + value: sampleCode, + language: 'typescript', + height: 400, + }, +}; + +/** Demonstrates the onChange callback with a live character count. */ +function ControlledStory(args: CodeEditorProps) { + const [value, setValue] = useState(args.value); + return ( +
+
+ Characters: {value.length} | Lines: {value.split('\n').length} +
+ +
+ ); +} + +export const Controlled: Story = { + render: (args) => , + args: { + value: '// Type here to see live stats\nconst x = 1;\n', + }, +}; + +export const EmptyEditor: Story = { + args: { + value: '', + language: 'typescript', + }, +}; + +export const CustomDimensions: Story = { + args: { + height: 200, + width: 500, + value: 'const small = true;\n', + language: 'typescript', + }, +}; + +// --------------------------------------------------------------------------- +// YAML Schema Completion — Real Kubernetes Schemas (demo-only) +// --------------------------------------------------------------------------- + +const podSampleYaml = `apiVersion: v1 +kind: Pod +metadata: + name: my-app + namespace: default + labels: + app: my-app +spec: + containers: + - name: app + image: nginx:1.25 + ports: + - containerPort: 80 + resources: + limits: + cpu: "500m" + memory: "128Mi" + restartPolicy: Always +`; + +const deploymentSampleYaml = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: web-server + namespace: production +spec: + replicas: 3 + selector: + matchLabels: + app: web + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: "25%" + maxUnavailable: "25%" + template: + metadata: + labels: + app: web + spec: + containers: + - name: nginx + image: nginx:1.25 + ports: + - containerPort: 80 +`; + +const serviceSampleYaml = `apiVersion: v1 +kind: Service +metadata: + name: web-svc + namespace: production +spec: + type: ClusterIP + selector: + app: web + ports: + - port: 80 + targetPort: 8080 +`; + +/** + * Sample YAML content keyed by GVR, matching the structured filename convention: + * //::::.yaml + */ +const sampleYamlByGvr: Record = { + 'core::v1::Pod': podSampleYaml, + 'apps::v1::Deployment': deploymentSampleYaml, + 'core::v1::Service': serviceSampleYaml, + 'core::v1::ConfigMap': `apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config + namespace: default +data: + APP_ENV: production + LOG_LEVEL: info +`, + 'networking.k8s.io::v1::Ingress': `apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: web-ingress + namespace: production +spec: + rules: + - host: example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: web-svc + port: + number: 80 +`, +}; + +/** Simulated plugin/connection context for structured filenames. */ +const DEMO_PLUGIN = 'kubernetes'; +const DEMO_CONNECTION = 'demo-cluster'; + +/** Build a structured filename for a GVR, e.g. "kubernetes/demo-cluster/core::v1::Pod.yaml" */ +function gvrFilename(gvr: string): string { + return `${DEMO_PLUGIN}/${DEMO_CONNECTION}/${gvr}.yaml`; +} + +/** + * Demonstrates runtime schema registration with real Kubernetes JSON schemas. + * + * This mirrors what happens in production: schemas are loaded per-cluster at + * runtime. Before loading, the editor has no autocompletion. After loading, + * full schema-driven completions appear as you type. + * + * Click "Load" buttons to dynamically register schemas. Click "Unload All" + * to remove them. The editor updates in real time. + */ +function RuntimeSchemaStory(args: CodeEditorProps) { + const [value, setValue] = useState(args.value); + const [loaded, setLoaded] = useState>({}); + const [loading, setLoading] = useState>({}); + const [activeGvr, setActiveGvr] = useState('core::v1::Pod'); + const [diagnosticsList, setDiagnosticsList] = useState([]); + const [cursor, setCursor] = useState({ lineNumber: 1, column: 1 }); + + // Clean up on unmount + useEffect(() => { + return () => { + editorSchemas.clear(); + }; + }, []); + + const loadSchema = useCallback(async (entry: K8sSchemaEntry) => { + setLoading((prev) => ({ ...prev, [entry.gvr]: true })); + try { + const mod = await entry.load(); + const schema: EditorSchema = { + uri: `https://omniview.dev/schemas/k8s/${encodeURIComponent(entry.gvr)}`, + fileMatch: gvrFileMatch(entry.gvr), + schema: mod.default, + name: `${entry.kind} (${entry.apiVersion})`, + description: `Kubernetes ${entry.kind} resource schema`, + }; + editorSchemas.register(schema); + setLoaded((prev) => ({ ...prev, [entry.gvr]: true })); + } finally { + setLoading((prev) => ({ ...prev, [entry.gvr]: false })); + } + }, []); + + const unloadAll = useCallback(() => { + editorSchemas.clear(); + setLoaded({}); + }, []); + + const loadedCount = Object.values(loaded).filter(Boolean).length; + + return ( +
+
+ Runtime Schema Loading +
+
+ Simulates per-cluster schema loading. Schemas are fetched lazily and registered at runtime. + Each schema matches a structured filename:{' '} + <plugin>/<connectionId>/<group>::<version>::<resource>.yaml +
+ +
+ {k8sSchemas.map((entry) => ( + + ))} + {loadedCount > 0 && ( + + )} +
+ +
+ {k8sSchemas.map((entry) => ( + + ))} +
+ +
+ file: {gvrFilename(activeGvr)} +
+
+ Loaded: {loadedCount === 0 ? 'none — no completions available' : `${loadedCount} schema(s)`} + {loaded[activeGvr] ? ' — active file has schema' : loaded[activeGvr] === undefined && loadedCount > 0 ? ' — active file has NO schema loaded' : ''} +
+ + + + {/* Status bar */} +
+ Ln {cursor.lineNumber}, Col {cursor.column} + + {diagnosticsList.length === 0 + ? 'No problems' + : `${diagnosticsList.filter(d => d.severity === 'error').length} errors, ${diagnosticsList.filter(d => d.severity === 'warning').length} warnings`} + +
+ + {/* Problems panel */} + {diagnosticsList.length > 0 && ( +
+ {diagnosticsList.map((d, i) => ( +
+ [{d.severity}] Ln {d.startLineNumber}:{d.startColumn} — {d.message} + {d.source && ({d.source})} +
+ ))} +
+ )} +
+ ); +} + +export const RuntimeSchemaRegistration: Story = { + render: (args) => , + args: { + value: podSampleYaml, + language: 'yaml', + height: 500, + }, +}; + +/** + * Demonstrates the onMount callback for advanced Monaco configuration. + * Plugins can use this to register custom completion providers, set up + * language workers, or perform any Monaco API call. + */ +function OnMountCallbackStory(args: CodeEditorProps) { + const [value, setValue] = useState(args.value); + const [log, setLog] = useState([]); + + const handleMount = (_editor: unknown, monaco: unknown) => { + setLog((prev) => [...prev, 'Editor mounted — Monaco instance available']); + + const m = monaco as { + languages: { + registerCompletionItemProvider: ( + language: string, + provider: Record, + ) => unknown; + CompletionItemKind: Record; + }; + }; + + m.languages.registerCompletionItemProvider('yaml', { + provideCompletionItems: () => ({ + suggestions: [ + { + label: 'apiVersion: v1', + kind: m.languages.CompletionItemKind.Snippet, + insertText: 'apiVersion: v1', + documentation: 'Core API version', + }, + { + label: 'apiVersion: apps/v1', + kind: m.languages.CompletionItemKind.Snippet, + insertText: 'apiVersion: apps/v1', + documentation: 'Apps API version', + }, + ], + }), + }); + + setLog((prev) => [...prev, 'Custom YAML completion provider registered']); + }; + + return ( +
+
+ Uses onMount to register a custom completion provider via raw Monaco API. +
+ +
+ {log.map((entry, i) => ( +
{entry}
+ ))} +
+
+ ); +} + +export const OnMountCallback: Story = { + render: (args) => , + args: { + value: `# Type here and press Ctrl+Space to see custom completions\n`, + language: 'yaml', + height: 300, }, }; diff --git a/packages/editors/src/components/code-editor/CodeEditor.test.tsx b/packages/editors/src/components/code-editor/CodeEditor.test.tsx index 6bb8363..d3634f0 100644 --- a/packages/editors/src/components/code-editor/CodeEditor.test.tsx +++ b/packages/editors/src/components/code-editor/CodeEditor.test.tsx @@ -1,60 +1,151 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; -import { CodeEditor } from './CodeEditor'; - -// Mock @monaco-editor/react -vi.mock('@monaco-editor/react', () => ({ - __esModule: true, - default: ({ - value, - language, - options, - }: { - value: string; - language?: string; - options?: Record; - }) => ( -
- ), +import { render, screen, act } from '@testing-library/react'; +import { createRef } from 'react'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { CodeEditor, type CodeEditorHandle } from './CodeEditor'; + +let mockEditorInstance: Record; + +const mockCreate = vi.fn(); +const mockGetModel = vi.fn(); +const mockCreateModel = vi.fn(); +const mockSetModelLanguage = vi.fn(); +const mockDefineTheme = vi.fn(); +const mockSetTheme = vi.fn(); + +vi.mock('monaco-editor', () => { + const Uri = { + parse: (s: string) => ({ toString: () => s, path: s }), + }; + + return { + default: undefined, + Uri, + editor: { + create: (...args: unknown[]) => { + mockCreate(...args); + mockEditorInstance = { + getValue: vi.fn(() => ''), + setValue: vi.fn(), + getModel: vi.fn(() => ({ + getFullModelRange: vi.fn(() => ({})), + uri: { toString: () => 'test', path: 'test' }, + dispose: vi.fn(), + })), + updateOptions: vi.fn(), + executeEdits: vi.fn(), + pushUndoStop: vi.fn(), + setModel: vi.fn(), + onDidChangeModelContent: vi.fn(() => ({ dispose: vi.fn() })), + dispose: vi.fn(), + focus: vi.fn(), + getOption: vi.fn(() => false), + }; + return mockEditorInstance; + }, + getModel: (...args: unknown[]) => mockGetModel(...args), + createModel: (...args: unknown[]) => { + mockCreateModel(...args); + return { + getFullModelRange: vi.fn(() => ({})), + uri: { toString: () => 'test', path: 'test' }, + dispose: vi.fn(), + }; + }, + setModelLanguage: (...args: unknown[]) => mockSetModelLanguage(...args), + defineTheme: (...args: unknown[]) => mockDefineTheme(...args), + setTheme: (...args: unknown[]) => mockSetTheme(...args), + EditorOption: { readOnly: 81 }, + }, + languages: { + register: vi.fn(), + typescript: { + typescriptDefaults: { + setDiagnosticsOptions: vi.fn(), + setCompilerOptions: vi.fn(), + }, + javascriptDefaults: { + setDiagnosticsOptions: vi.fn(), + }, + }, + }, + }; +}); + +vi.mock('../../schemas', () => ({ + editorSchemas: { + applyYamlSchemas: vi.fn(), + applyJsonSchemas: vi.fn(), + onChange: vi.fn(() => vi.fn()), + }, +})); + +vi.mock('../../themes/useEditorTheme', () => ({ + useEditorTheme: () => ({ isDark: false }), +})); + +vi.mock('../../themes/monaco', () => ({ + buildMonacoTheme: () => ({ base: 'vs', inherit: true, rules: [], colors: {} }), + OV_MONACO_THEME: 'ov-theme', })); +beforeEach(() => { + vi.clearAllMocks(); + mockGetModel.mockReturnValue(null); +}); + describe('CodeEditor', () => { it('renders the editor container', () => { render(); expect(screen.getByTestId('code-editor')).toBeInTheDocument(); }); - it('passes value to the editor', () => { + it('creates a monaco editor on mount', () => { render(); - expect(screen.getByTestId('monaco-editor-mock')).toHaveAttribute('data-value', 'const x = 1;'); + expect(mockCreate).toHaveBeenCalled(); }); it('detects language from filename', () => { render(); - expect(screen.getByTestId('monaco-editor-mock')).toHaveAttribute('data-language', 'typescript'); + expect(mockCreateModel).toHaveBeenCalledWith( + '', + 'typescript', + expect.anything(), + ); }); it('uses explicit language over filename detection', () => { render(); - expect(screen.getByTestId('monaco-editor-mock')).toHaveAttribute('data-language', 'python'); + expect(mockCreateModel).toHaveBeenCalledWith( + '', + 'python', + expect.anything(), + ); }); - it('sets readOnly via options', () => { + it('passes readOnly to editor options', () => { render(); - expect(screen.getByTestId('monaco-editor-mock')).toHaveAttribute('data-readonly', 'true'); + expect(mockCreate).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ readOnly: true }), + ); }); it('pretty-prints JSON content', () => { const json = '{"a":1}'; render(); - expect(screen.getByTestId('monaco-editor-mock')).toHaveAttribute( - 'data-value', - JSON.stringify({ a: 1 }, null, 2), + const formatted = JSON.stringify({ a: 1 }, null, 2); + expect(mockCreateModel).toHaveBeenCalledWith( + formatted, + 'json', + ); + }); + + it('passes through invalid JSON unchanged', () => { + const badJson = '{not valid json}'; + render(); + expect(mockCreateModel).toHaveBeenCalledWith( + badJson, + 'json', ); }); @@ -62,4 +153,58 @@ describe('CodeEditor', () => { render(); expect(screen.getByTestId('code-editor')).toHaveClass('my-editor'); }); + + it('disables line numbers via options', () => { + render(); + expect(mockCreate).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ lineNumbers: 'off' }), + ); + }); + + it('enables minimap', () => { + render(); + expect(mockCreate).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ minimap: { enabled: true } }), + ); + }); + + it('enables word wrap', () => { + render(); + expect(mockCreate).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ wordWrap: 'on' }), + ); + }); + + it('applies height and width styles', () => { + render(); + const container = screen.getByTestId('code-editor'); + expect(container).toHaveStyle({ height: '400px', width: '600px' }); + }); + + it('exposes imperative handle via ref', async () => { + const ref = createRef(); + render(); + + // Editor is created synchronously in useEffect, wait for it + await act(async () => {}); + + expect(ref.current?.getEditor()).toBeTruthy(); + expect(ref.current?.getMonaco()).toBeTruthy(); + ref.current?.focus(); + expect(mockEditorInstance.focus).toHaveBeenCalled(); + }); + + it('handles empty value', () => { + render(); + expect(mockCreate).toHaveBeenCalled(); + }); + + it('disposes editor on unmount', () => { + const { unmount } = render(); + unmount(); + expect(mockEditorInstance.dispose).toHaveBeenCalled(); + }); }); diff --git a/packages/editors/src/components/code-editor/CodeEditor.tsx b/packages/editors/src/components/code-editor/CodeEditor.tsx index 8b49efa..bfe9bf8 100644 --- a/packages/editors/src/components/code-editor/CodeEditor.tsx +++ b/packages/editors/src/components/code-editor/CodeEditor.tsx @@ -1,160 +1,611 @@ import { forwardRef, - lazy, - Suspense, - useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; +import * as monaco from 'monaco-editor'; import { useEditorTheme } from '../../themes/useEditorTheme'; import { buildMonacoTheme, OV_MONACO_THEME } from '../../themes/monaco'; import { detectLanguage } from '../../utils/language'; +import { editorSchemas } from '../../schemas'; import styles from './CodeEditor.module.css'; -const MonacoEditor = lazy(() => import('@monaco-editor/react')); +// --------------------------------------------------------------------------- +// Debug logging (set to false for production) +// --------------------------------------------------------------------------- + +const DEBUG = false; +function log(...args: unknown[]) { + if (DEBUG) console.log('[CodeEditor]', ...args); +} + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +/** Severity levels matching Monaco's MarkerSeverity enum. */ +export type DiagnosticSeverity = 'error' | 'warning' | 'info' | 'hint'; + +/** A parsed diagnostic from the editor's validation markers. */ +export interface EditorDiagnostic { + /** Human-readable error message. */ + message: string; + /** Severity level. */ + severity: DiagnosticSeverity; + /** 1-based line number. */ + startLineNumber: number; + /** 1-based column number. */ + startColumn: number; + /** 1-based end line number. */ + endLineNumber: number; + /** 1-based end column number. */ + endColumn: number; + /** The owner/source of the marker (e.g. "yaml", "typescript"). */ + source?: string; + /** Optional error code. */ + code?: string; +} + +/** Cursor position info passed to `onCursorChange`. */ +export interface CursorPosition { + /** 1-based line number. */ + lineNumber: number; + /** 1-based column number. */ + column: number; +} export interface CodeEditorProps { + /** The editor content. Controlled — updates push new text into the editor. */ value: string; + /** Fires when the user edits content. Omit for a read-only display. */ onChange?: (value: string) => void; + + // -- File identity ---------------------------------------------------------- + + /** Explicit Monaco language id (e.g. `"yaml"`, `"typescript"`). */ language?: string; + /** + * Virtual filename / path used to derive the language (when `language` is + * omitted) and, more importantly, to set the Monaco model URI so that + * `monaco-yaml` / JSON schema `fileMatch` patterns can match. + * + * For structured resources use the convention: + * `//::::.yaml` + * + * @example "kubernetes/demo-cluster/core::v1::Pod.yaml" + */ filename?: string; + + // -- Editor behaviour ------------------------------------------------------- + readOnly?: boolean; lineNumbers?: boolean; minimap?: boolean; wordWrap?: boolean; + tabSize?: number; + /** Show TypeScript / JavaScript diagnostics (squiggly error markers). @default false */ + diagnostics?: boolean; + /** Trigger completions automatically as the user types. @default false */ + quickSuggestions?: boolean; + /** + * Enable syntax highlighting. When false the editor still renders text but + * without any token colouring — useful for plain log output. + * @default true + */ + syntaxHighlighting?: boolean; + + // -- Lifecycle callbacks ---------------------------------------------------- + + /** + * Called once after the Monaco editor mounts, exposing the raw editor and + * `monaco` namespace for advanced configuration (custom keybindings, + * language registration, LSP server attachment, etc.). + */ + onMount?: ( + editor: monaco.editor.IStandaloneCodeEditor, + monacoInstance: typeof monaco, + ) => void; + + /** + * Called whenever the editor's validation markers change. Provides a parsed + * array of diagnostics that can be displayed in a problems panel, status bar, + * or custom UI. + * + * Fires for all marker owners (YAML, TypeScript, JSON, etc.). + */ + onDiagnostics?: (diagnostics: EditorDiagnostic[]) => void; + + /** + * Called when the cursor position changes. Useful for breadcrumb navigation, + * status bar line/column display, or position tracking. + */ + onCursorChange?: (position: CursorPosition) => void; + + // -- Layout ----------------------------------------------------------------- + height?: string | number; width?: string | number; className?: string; } +/** + * Snapshot of editor internal state for debug panels / troubleshooting. + * Obtained via `handle.getDebugState()`. + */ +export interface EditorDebugState { + // -- Model ------------------------------------------------------------------ + /** The model URI string (drives schema matching). */ + modelUri: string | null; + /** The resolved language id (e.g. "yaml", "typescript"). */ + modelLanguage: string | null; + /** Total number of lines. */ + modelLineCount: number; + /** Total character count. */ + modelContentLength: number; + /** Monaco's internal version id — incremented on every edit including undo. */ + modelVersionId: number; + /** End-of-line sequence (`\n` or `\r\n`). */ + modelEOL: string | null; + + // -- Cursor / Selection ----------------------------------------------------- + cursorPosition: CursorPosition | null; + /** All active selections (multi-cursor). */ + selections: Array<{ + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + }>; + + // -- Diagnostics ------------------------------------------------------------ + /** Total marker count across all owners. */ + diagnosticsTotal: number; + /** Breakdown by severity. */ + diagnosticsBySeverity: Record; + /** Breakdown by owner/source (e.g. { yaml: 2, typescript: 1 }). */ + diagnosticsBySource: Record; + + // -- Schema ----------------------------------------------------------------- + /** Total registered schemas in the EditorSchemaRegistry. */ + registeredSchemaCount: number; + /** Schemas whose fileMatch patterns match the current model URI. */ + matchingSchemas: Array<{ uri: string; name?: string; fileMatch: string[] }>; + + // -- Editor ----------------------------------------------------------------- + /** Whether the editor is read-only. */ + isReadOnly: boolean; + /** Whether the editor or its widgets have focus. */ + isFocused: boolean; + + // -- Monaco environment ----------------------------------------------------- + /** Total number of Monaco models in memory. */ + totalModelCount: number; + /** Number of registered languages. */ + registeredLanguageCount: number; + /** Whether the YAML handle is set (worker initialized). */ + yamlHandleSet: boolean; +} + export interface CodeEditorHandle { - getEditor: () => unknown | null; + /** The underlying Monaco standalone code editor, or null before mount. */ + getEditor: () => monaco.editor.IStandaloneCodeEditor | null; + /** The `monaco-editor` module namespace. */ + getMonaco: () => typeof monaco; + /** Focus the editor. */ focus: () => void; + /** Get the current diagnostics (validation markers) for the editor model. */ + getDiagnostics: () => EditorDiagnostic[]; + /** + * Snapshot the full editor debug state — model info, cursor, diagnostics, + * schemas, environment. Designed for debug panels and troubleshooting. + */ + getDebugState: () => EditorDebugState; } +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + function cn(...parts: Array): string { return parts.filter(Boolean).join(' '); } -export const CodeEditor = forwardRef(function CodeEditor( - { - value, - onChange, - language, - filename, - readOnly = false, - lineNumbers = true, - minimap = false, - wordWrap = false, - height = '100%', - width = '100%', - className, - }, - ref, -) { - const theme = useEditorTheme(); - const editorRef = useRef(null); - const monacoRef = useRef(null); - const [isReady, setIsReady] = useState(false); - - const resolvedLanguage = language ?? (filename ? detectLanguage(filename) : undefined); - - // Format JSON content for display (memoized to avoid re-parsing on every render) - const displayValue = useMemo(() => { - if (resolvedLanguage === 'json' && value) { - try { - return JSON.stringify(JSON.parse(value), null, 2); - } catch { - return value; - } +function getOrCreateModel( + value: string, + language: string | undefined, + path: string | undefined, +): monaco.editor.ITextModel { + if (path) { + const uri = monaco.Uri.parse(path); + log('getOrCreateModel — parsed URI:', uri.toString(), '| language:', language); + const existing = monaco.editor.getModel(uri); + if (existing) { + log(' reusing existing model'); + return existing; } - return value; - }, [resolvedLanguage, value]); + log(' creating new model with URI'); + return monaco.editor.createModel(value, language, uri); + } + log('getOrCreateModel — no path, creating anonymous model | language:', language); + return monaco.editor.createModel(value, language); +} + +function severityToString(severity: monaco.MarkerSeverity): DiagnosticSeverity { + switch (severity) { + case monaco.MarkerSeverity.Error: return 'error'; + case monaco.MarkerSeverity.Warning: return 'warning'; + case monaco.MarkerSeverity.Info: return 'info'; + default: return 'hint'; + } +} + +function markersTodiagnostics(model: monaco.editor.ITextModel): EditorDiagnostic[] { + return monaco.editor.getModelMarkers({ resource: model.uri }).map((m) => ({ + message: m.message, + severity: severityToString(m.severity), + startLineNumber: m.startLineNumber, + startColumn: m.startColumn, + endLineNumber: m.endLineNumber, + endColumn: m.endColumn, + source: m.source ?? undefined, + code: typeof m.code === 'string' ? m.code : m.code?.value, + })); +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export const CodeEditor = forwardRef( + function CodeEditor( + { + value, + onChange, + onMount: onMountProp, + onDiagnostics, + onCursorChange, + language, + filename, + readOnly = false, + lineNumbers = true, + minimap = false, + wordWrap = false, + tabSize = 2, + diagnostics = false, + quickSuggestions = false, + syntaxHighlighting = true, + height = '100%', + width = '100%', + className, + }, + ref, + ) { + const theme = useEditorTheme(); + const editorRef = useRef(null); + const containerRef = useRef(null); + const [isReady, setIsReady] = useState(false); + const preventTriggerChangeEvent = useRef(false); + const subscriptionRef = useRef(null); + const markerSubscriptionRef = useRef(null); + const cursorSubscriptionRef = useRef(null); + + const resolvedLanguage = language ?? (filename ? detectLanguage(filename) : undefined); + + log('render — filename:', filename, '| resolvedLanguage:', resolvedLanguage); + + // Pretty-print JSON when the language is JSON + const displayValue = useMemo(() => { + if (resolvedLanguage === 'json' && value) { + try { + return JSON.stringify(JSON.parse(value), null, 2); + } catch { + return value; + } + } + return value; + }, [resolvedLanguage, value]); + + // ----------------------------------------------------------------------- + // Mount / Unmount + // ----------------------------------------------------------------------- + + useEffect(() => { + if (!containerRef.current) return; + + log('=== MOUNT ==='); + + // Theme + monaco.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); + monaco.editor.setTheme(OV_MONACO_THEME); + + // TypeScript / JavaScript diagnostics & compiler options + const diagOpts = diagnostics + ? {} + : { + noSemanticValidation: true, + noSyntaxValidation: true, + noSuggestionDiagnostics: true, + }; + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(diagOpts); + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(diagOpts); + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + jsx: 2, // JsxEmit.React + jsxFactory: 'React.createElement', + allowNonTsExtensions: true, + allowJs: true, + target: 99, // ESNext + }); + + // Model — the URI drives schema fileMatch + const model = getOrCreateModel( + displayValue, + syntaxHighlighting ? resolvedLanguage : 'plaintext', + filename, + ); + log(' model created — uri:', model.uri.toString()); + + const editor = monaco.editor.create(containerRef.current, { + model, + readOnly, + lineNumbers: lineNumbers ? 'on' : 'off', + minimap: { enabled: minimap }, + wordWrap: wordWrap ? 'on' : 'off', + quickSuggestions: quickSuggestions + ? { other: true, strings: true, comments: false } + : false, + scrollBeyondLastLine: false, + automaticLayout: true, + fontSize: 13, + fontFamily: 'var(--ov-font-family-mono, monospace)', + tabSize, + }); - const handleEditorDidMount = useCallback( - (editor: unknown, monaco: unknown) => { editorRef.current = editor; - monacoRef.current = monaco; - // Register and apply theme - const m = monaco as { - editor: { - defineTheme: (name: string, data: unknown) => void; - setTheme: (name: string) => void; - }; - }; - m.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); - m.editor.setTheme(OV_MONACO_THEME); + // Apply registered schemas so completions/validation are live + editorSchemas.applyYamlSchemas(); + editorSchemas.applyJsonSchemas(monaco); + onMountProp?.(editor, monaco); setIsReady(true); - }, - [theme], - ); - - // Update theme when it changes - useEffect(() => { - if (!monacoRef.current || !isReady) return; - const m = monacoRef.current as { - editor: { - defineTheme: (name: string, data: unknown) => void; - setTheme: (name: string) => void; + log('=== MOUNT COMPLETE ==='); + + return () => { + log('=== UNMOUNT ==='); + subscriptionRef.current?.dispose(); + subscriptionRef.current = null; + markerSubscriptionRef.current?.dispose(); + markerSubscriptionRef.current = null; + cursorSubscriptionRef.current?.dispose(); + cursorSubscriptionRef.current = null; + // Detach model before disposing to avoid WordHighlighter "Canceled" errors + const m = editor.getModel(); + editor.setModel(null); + m?.dispose(); + editor.dispose(); + editorRef.current = null; + setIsReady(false); }; - }; - m.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); - m.editor.setTheme(OV_MONACO_THEME); - }, [theme, isReady]); - - useImperativeHandle(ref, () => ({ - getEditor: () => editorRef.current, - focus: () => { - const e = editorRef.current as { focus?: () => void } | null; - e?.focus?.(); - }, - })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // ----------------------------------------------------------------------- + // Prop → editor syncing + // ----------------------------------------------------------------------- - const handleChange = useCallback( - (val: string | undefined) => { - if (onChange && val !== undefined) { - onChange(val); + // Value + useEffect(() => { + const editor = editorRef.current; + if (!editor || !isReady || displayValue === undefined) return; + if (editor.getOption(monaco.editor.EditorOption.readOnly)) { + editor.setValue(displayValue); + } else if (displayValue !== editor.getValue()) { + preventTriggerChangeEvent.current = true; + editor.executeEdits('', [ + { + range: editor.getModel()!.getFullModelRange(), + text: displayValue, + forceMoveMarkers: true, + }, + ]); + editor.pushUndoStop(); + preventTriggerChangeEvent.current = false; } - }, - [onChange], - ); - - return ( -
- - Loading editor… -
+ }, [displayValue, isReady]); + + // Options + useEffect(() => { + if (!editorRef.current || !isReady) return; + editorRef.current.updateOptions({ + readOnly, + lineNumbers: lineNumbers ? 'on' : 'off', + minimap: { enabled: minimap }, + wordWrap: wordWrap ? 'on' : 'off', + quickSuggestions: quickSuggestions + ? { other: true, strings: true, comments: false } + : false, + tabSize, + }); + }, [readOnly, lineNumbers, minimap, wordWrap, quickSuggestions, tabSize, isReady]); + + // Language (including syntax highlighting toggle) + useEffect(() => { + if (!editorRef.current || !isReady) return; + const model = editorRef.current.getModel(); + if (model) { + const lang = syntaxHighlighting ? (resolvedLanguage ?? 'plaintext') : 'plaintext'; + log('setModelLanguage →', lang); + monaco.editor.setModelLanguage(model, lang); + } + }, [resolvedLanguage, syntaxHighlighting, isReady]); + + // Filename → model URI swap (for schema fileMatch) + const prevFilenameRef = useRef(filename); + useEffect(() => { + const editor = editorRef.current; + if (!editor || !isReady) return; + if (prevFilenameRef.current === filename) return; + log('filename changed:', prevFilenameRef.current, '→', filename); + prevFilenameRef.current = filename; + + const currentModel = editor.getModel(); + const currentValue = currentModel?.getValue?.() ?? displayValue; + // Detach model before disposing to avoid WordHighlighter "Canceled" errors + editor.setModel(null); + currentModel?.dispose(); + const newModel = getOrCreateModel(currentValue, resolvedLanguage, filename); + editor.setModel(newModel); + log(' new model set — uri:', newModel.uri.toString()); + }, [filename, isReady, resolvedLanguage, displayValue]); + + // Theme + useEffect(() => { + if (!isReady) return; + monaco.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); + monaco.editor.setTheme(OV_MONACO_THEME); + }, [theme, isReady]); + + // onChange subscription + useEffect(() => { + if (!editorRef.current || !isReady) return; + subscriptionRef.current?.dispose(); + if (onChange) { + subscriptionRef.current = editorRef.current.onDidChangeModelContent(() => { + if (!preventTriggerChangeEvent.current) { + onChange(editorRef.current!.getValue()); + } + }); + } + }, [onChange, isReady]); + + // onDiagnostics subscription — listen to marker changes on the model + useEffect(() => { + if (!isReady) return; + markerSubscriptionRef.current?.dispose(); + if (onDiagnostics) { + markerSubscriptionRef.current = monaco.editor.onDidChangeMarkers((uris) => { + const model = editorRef.current?.getModel(); + if (!model) return; + const modelUri = model.uri.toString(); + if (uris.some((u) => u.toString() === modelUri)) { + onDiagnostics(markersTodiagnostics(model)); + } + }); + } + }, [onDiagnostics, isReady]); + + // onCursorChange subscription + useEffect(() => { + if (!editorRef.current || !isReady) return; + cursorSubscriptionRef.current?.dispose(); + if (onCursorChange) { + cursorSubscriptionRef.current = editorRef.current.onDidChangeCursorPosition((e) => { + onCursorChange({ + lineNumber: e.position.lineNumber, + column: e.position.column, + }); + }); + } + }, [onCursorChange, isReady]); + + // Live schema updates — re-apply whenever the registry changes + useEffect(() => { + if (!isReady) return; + return editorSchemas.onChange(() => { + editorSchemas.applyYamlSchemas(); + editorSchemas.applyJsonSchemas(monaco); + }); + }, [isReady]); + + // ----------------------------------------------------------------------- + // Imperative handle + // ----------------------------------------------------------------------- + + useImperativeHandle(ref, () => ({ + getEditor: () => editorRef.current, + getMonaco: () => monaco, + focus: () => editorRef.current?.focus(), + getDiagnostics: () => { + const model = editorRef.current?.getModel(); + return model ? markersTodiagnostics(model) : []; + }, + getDebugState: (): EditorDebugState => { + const editor = editorRef.current; + const model = editor?.getModel() ?? null; + const markers = model + ? monaco.editor.getModelMarkers({ resource: model.uri }) + : []; + const modelUri = model?.uri.toString() ?? null; + + // Diagnostics breakdown + const diagnosticsBySeverity: Record = { + error: 0, warning: 0, info: 0, hint: 0, + }; + const diagnosticsBySource: Record = {}; + for (const m of markers) { + const sev = severityToString(m.severity); + diagnosticsBySeverity[sev]++; + const src = m.source ?? 'unknown'; + diagnosticsBySource[src] = (diagnosticsBySource[src] ?? 0) + 1; } + + // Schema matching + const matchingSchemas = modelUri + ? editorSchemas.getSchemasForUri(modelUri).map((s) => ({ + uri: s.uri, + name: s.name, + fileMatch: s.fileMatch, + })) + : []; + + const pos = editor?.getPosition() ?? null; + const sels = editor?.getSelections() ?? []; + + return { + modelUri, + modelLanguage: model?.getLanguageId() ?? null, + modelLineCount: model?.getLineCount() ?? 0, + modelContentLength: model?.getValueLength() ?? 0, + modelVersionId: model?.getVersionId() ?? 0, + modelEOL: model?.getEOL() ?? null, + cursorPosition: pos ? { lineNumber: pos.lineNumber, column: pos.column } : null, + selections: sels.map((s) => ({ + startLineNumber: s.startLineNumber, + startColumn: s.startColumn, + endLineNumber: s.endLineNumber, + endColumn: s.endColumn, + })), + diagnosticsTotal: markers.length, + diagnosticsBySeverity, + diagnosticsBySource, + registeredSchemaCount: editorSchemas.schemas.length, + matchingSchemas, + isReadOnly: editor?.getOption(monaco.editor.EditorOption.readOnly) ?? false, + isFocused: editor?.hasWidgetFocus() ?? false, + totalModelCount: monaco.editor.getModels().length, + registeredLanguageCount: monaco.languages.getLanguages().length, + yamlHandleSet: editorSchemas.isYamlReady, + }; + }, + })); + + // ----------------------------------------------------------------------- + // Render + // ----------------------------------------------------------------------- + + return ( +
- - -
- ); -}); +
+
+ ); + }, +); CodeEditor.displayName = 'CodeEditor'; diff --git a/packages/editors/src/components/code-editor/index.ts b/packages/editors/src/components/code-editor/index.ts index ac4f00d..c65fb66 100644 --- a/packages/editors/src/components/code-editor/index.ts +++ b/packages/editors/src/components/code-editor/index.ts @@ -1,2 +1,9 @@ export { CodeEditor } from './CodeEditor'; -export type { CodeEditorProps, CodeEditorHandle } from './CodeEditor'; +export type { + CodeEditorProps, + CodeEditorHandle, + EditorDiagnostic, + DiagnosticSeverity, + CursorPosition, + EditorDebugState, +} from './CodeEditor'; diff --git a/packages/editors/src/components/diff-viewer/DiffViewer.stories.tsx b/packages/editors/src/components/diff-viewer/DiffViewer.stories.tsx index bf931d7..707789c 100644 --- a/packages/editors/src/components/diff-viewer/DiffViewer.stories.tsx +++ b/packages/editors/src/components/diff-viewer/DiffViewer.stories.tsx @@ -24,6 +24,11 @@ const meta: Meta = { argTypes: { mode: { control: 'radio', options: ['side-by-side', 'inline'] }, readOnly: { control: 'boolean' }, + language: { + control: 'select', + options: ['typescript', 'javascript', 'python', 'json', 'yaml', 'css', 'go'], + }, + height: { control: { type: 'number', min: 200, max: 800, step: 50 } }, }, }; @@ -36,6 +41,16 @@ export const InlineMode: Story = { args: { mode: 'inline' }, }; +export const SideBySide: Story = { + args: { mode: 'side-by-side' }, +}; + +export const Editable: Story = { + args: { + readOnly: false, + }, +}; + export const LargeDiff: Story = { args: { original: Array.from({ length: 50 }, (_, i) => `// Original line ${i + 1}`).join('\n'), @@ -44,3 +59,106 @@ export const LargeDiff: Story = { ).join('\n'), }, }; + +export const NoDifferences: Story = { + args: { + original: 'const x = 1;\nconst y = 2;\n', + modified: 'const x = 1;\nconst y = 2;\n', + language: 'typescript', + }, +}; + +export const CompletelyDifferent: Story = { + args: { + original: `package main + +import "fmt" + +func main() { + fmt.Println("Hello, World!") +}`, + modified: `from typing import Optional + +def greet(name: Optional[str] = None) -> str: + if name: + return f"Hello, {name}!" + return "Hello, World!" + +if __name__ == "__main__": + print(greet())`, + language: undefined, + }, +}; + +export const JSONDiff: Story = { + args: { + original: JSON.stringify( + { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { name: 'app-config', namespace: 'default' }, + data: { DATABASE_URL: 'postgres://localhost:5432/mydb', LOG_LEVEL: 'info' }, + }, + null, + 2, + ), + modified: JSON.stringify( + { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { name: 'app-config', namespace: 'production' }, + data: { + DATABASE_URL: 'postgres://prod-host:5432/mydb', + LOG_LEVEL: 'warn', + CACHE_TTL: '3600', + }, + }, + null, + 2, + ), + language: 'json', + }, +}; + +export const YAMLDiff: Story = { + args: { + original: `replicas: 1 +image: nginx:1.24 +resources: + limits: + cpu: "250m" + memory: "64Mi" +env: + - name: NODE_ENV + value: development`, + modified: `replicas: 3 +image: nginx:1.25 +resources: + limits: + cpu: "500m" + memory: "128Mi" + requests: + cpu: "100m" + memory: "32Mi" +env: + - name: NODE_ENV + value: production + - name: LOG_LEVEL + value: warn`, + language: 'yaml', + }, +}; + +export const EmptyOriginal: Story = { + args: { + original: '', + modified: 'const newFile = true;\nexport default newFile;\n', + language: 'typescript', + }, +}; + +export const CompactHeight: Story = { + args: { + height: 200, + }, +}; diff --git a/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx b/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx index e867887..ea04d25 100644 --- a/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx +++ b/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx @@ -1,61 +1,147 @@ import { render, screen } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; import { DiffViewer } from './DiffViewer'; -vi.mock('@monaco-editor/react', () => ({ - __esModule: true, - default: () =>
, - DiffEditor: ({ - original, - modified, - language, - options, - }: { - original: string; - modified: string; - language?: string; - options?: Record; - }) => ( -
- ), +const mockCreateDiffEditor = vi.fn(); +const mockDefineTheme = vi.fn(); +const mockSetTheme = vi.fn(); +const mockSetModelLanguage = vi.fn(); + +let mockDiffEditorInstance: Record; + +vi.mock('monaco-editor', () => ({ + default: undefined, + Uri: { + parse: (s: string) => ({ toString: () => s, path: s }), + }, + editor: { + createDiffEditor: (...args: unknown[]) => { + mockCreateDiffEditor(...args); + const originalModel = { + setValue: vi.fn(), + dispose: vi.fn(), + }; + const modifiedModel = { + setValue: vi.fn(), + getValue: vi.fn(() => ''), + dispose: vi.fn(), + }; + mockDiffEditorInstance = { + setModel: vi.fn(), + getModel: vi.fn(() => ({ original: originalModel, modified: modifiedModel })), + getModifiedEditor: vi.fn(() => ({ getValue: vi.fn(() => '') })), + updateOptions: vi.fn(), + dispose: vi.fn(), + }; + return mockDiffEditorInstance; + }, + createModel: vi.fn((_value: string, _lang?: string) => ({ + setValue: vi.fn(), + getValue: vi.fn(() => ''), + dispose: vi.fn(), + })), + getModel: vi.fn(() => null), + setModelLanguage: (...args: unknown[]) => mockSetModelLanguage(...args), + defineTheme: (...args: unknown[]) => mockDefineTheme(...args), + setTheme: (...args: unknown[]) => mockSetTheme(...args), + EditorOption: { readOnly: 81 }, + }, + languages: { + register: vi.fn(), + typescript: { + typescriptDefaults: { + setDiagnosticsOptions: vi.fn(), + setCompilerOptions: vi.fn(), + }, + javascriptDefaults: { + setDiagnosticsOptions: vi.fn(), + }, + }, + }, +})); + +vi.mock('../../themes/useEditorTheme', () => ({ + useEditorTheme: () => ({ isDark: false }), +})); + +vi.mock('../../themes/monaco', () => ({ + buildMonacoTheme: () => ({ base: 'vs', inherit: true, rules: [], colors: {} }), + OV_MONACO_THEME: 'ov-theme', })); +beforeEach(() => { + vi.clearAllMocks(); +}); + describe('DiffViewer', () => { it('renders the diff container', () => { render(); expect(screen.getByTestId('diff-viewer')).toBeInTheDocument(); }); - it('passes original and modified to the editor', () => { + it('creates a diff editor on mount', () => { render(); - const mock = screen.getByTestId('monaco-diff-mock'); - expect(mock).toHaveAttribute('data-original', 'line1'); - expect(mock).toHaveAttribute('data-modified', 'line2'); + expect(mockCreateDiffEditor).toHaveBeenCalled(); }); - it('applies language prop', () => { + it('passes language to createModel', async () => { + const monaco = await import('monaco-editor'); render(); - expect(screen.getByTestId('monaco-diff-mock')).toHaveAttribute('data-language', 'yaml'); + expect(monaco.editor.createModel).toHaveBeenCalledWith('', 'yaml'); }); it('defaults to side-by-side mode', () => { render(); - expect(screen.getByTestId('monaco-diff-mock')).toHaveAttribute('data-side-by-side', 'true'); + expect(mockCreateDiffEditor).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ renderSideBySide: true }), + ); }); it('switches to inline mode', () => { render(); - expect(screen.getByTestId('monaco-diff-mock')).toHaveAttribute('data-side-by-side', 'false'); + expect(mockCreateDiffEditor).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ renderSideBySide: false }), + ); }); it('merges className', () => { render(); expect(screen.getByTestId('diff-viewer')).toHaveClass('custom'); }); + + it('defaults to readOnly true', () => { + render(); + expect(mockCreateDiffEditor).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ readOnly: true }), + ); + }); + + it('supports readOnly false', () => { + render(); + expect(mockCreateDiffEditor).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ readOnly: false }), + ); + }); + + it('applies height prop as style', () => { + render(); + const container = screen.getByTestId('diff-viewer'); + expect(container).toHaveStyle({ height: '500px' }); + }); + + it('forwards ref', () => { + const ref = { current: null } as unknown as React.RefObject; + render(); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); + + it('disposes editor on unmount', () => { + const { unmount } = render(); + unmount(); + expect(mockDiffEditorInstance.dispose).toHaveBeenCalled(); + }); }); diff --git a/packages/editors/src/components/diff-viewer/DiffViewer.tsx b/packages/editors/src/components/diff-viewer/DiffViewer.tsx index 3d1be7c..52641f8 100644 --- a/packages/editors/src/components/diff-viewer/DiffViewer.tsx +++ b/packages/editors/src/components/diff-viewer/DiffViewer.tsx @@ -1,12 +1,9 @@ -import { forwardRef, lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react'; +import { forwardRef, useEffect, useRef, useState } from 'react'; +import * as monaco from 'monaco-editor'; import { useEditorTheme } from '../../themes/useEditorTheme'; import { buildMonacoTheme, OV_MONACO_THEME } from '../../themes/monaco'; import styles from './DiffViewer.module.css'; -const MonacoDiffEditor = lazy(() => - import('@monaco-editor/react').then((mod) => ({ default: mod.DiffEditor })), -); - export type DiffMode = 'side-by-side' | 'inline'; export interface DiffViewerProps { @@ -36,35 +33,95 @@ export const DiffViewer = forwardRef(function D ref, ) { const theme = useEditorTheme(); - const monacoRef = useRef(null); + const containerRef = useRef(null); + const editorRef = useRef(null); const [isReady, setIsReady] = useState(false); - const handleEditorDidMount = useCallback( - (_editor: unknown, monaco: unknown) => { - monacoRef.current = monaco; - const m = monaco as { - editor: { - defineTheme: (name: string, data: unknown) => void; - setTheme: (name: string) => void; - }; - }; - m.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); - m.editor.setTheme(OV_MONACO_THEME); - setIsReady(true); - }, - [theme], - ); - + // Create editor on mount useEffect(() => { - if (!monacoRef.current || !isReady) return; - const m = monacoRef.current as { - editor: { - defineTheme: (name: string, data: unknown) => void; - setTheme: (name: string) => void; - }; + if (!containerRef.current) return; + + monaco.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); + monaco.editor.setTheme(OV_MONACO_THEME); + + // Disable diagnostics — diff viewer is for viewing, not linting + const diagOpts = { + noSemanticValidation: true, + noSyntaxValidation: true, + noSuggestionDiagnostics: true, + }; + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(diagOpts); + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(diagOpts); + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + jsx: 2, + allowNonTsExtensions: true, + allowJs: true, + target: 99, + }); + + const editor = monaco.editor.createDiffEditor(containerRef.current, { + readOnly, + renderSideBySide: mode === 'side-by-side', + scrollBeyondLastLine: false, + automaticLayout: true, + fontSize: 13, + fontFamily: 'var(--ov-font-family-mono, monospace)', + }); + + const originalModel = monaco.editor.createModel(original, language); + const modifiedModel = monaco.editor.createModel(modified, language); + editor.setModel({ original: originalModel, modified: modifiedModel }); + + editorRef.current = editor; + setIsReady(true); + + return () => { + editor.getModel()?.original?.dispose(); + editor.getModel()?.modified?.dispose(); + editor.dispose(); + editorRef.current = null; + setIsReady(false); }; - m.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); - m.editor.setTheme(OV_MONACO_THEME); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Update original content + useEffect(() => { + if (!editorRef.current || !isReady) return; + editorRef.current.getModel()?.original?.setValue(original); + }, [original, isReady]); + + // Update modified content + useEffect(() => { + if (!editorRef.current || !isReady) return; + const modifiedEditor = editorRef.current.getModifiedEditor(); + if (modified !== modifiedEditor.getValue()) { + editorRef.current.getModel()?.modified?.setValue(modified); + } + }, [modified, isReady]); + + // Update language + useEffect(() => { + if (!editorRef.current || !isReady || !language) return; + const models = editorRef.current.getModel(); + if (models?.original) monaco.editor.setModelLanguage(models.original, language); + if (models?.modified) monaco.editor.setModelLanguage(models.modified, language); + }, [language, isReady]); + + // Update options + useEffect(() => { + if (!editorRef.current || !isReady) return; + editorRef.current.updateOptions({ + readOnly, + renderSideBySide: mode === 'side-by-side', + }); + }, [readOnly, mode, isReady]); + + // Update theme + useEffect(() => { + if (!isReady) return; + monaco.editor.defineTheme(OV_MONACO_THEME, buildMonacoTheme(theme)); + monaco.editor.setTheme(OV_MONACO_THEME); }, [theme, isReady]); return ( @@ -74,29 +131,7 @@ export const DiffViewer = forwardRef(function D style={{ height }} data-testid="diff-viewer" > - - Loading diff… -
- } - > - - +
); }); diff --git a/packages/editors/src/components/markdown-preview/MarkdownPreview.module.css b/packages/editors/src/components/markdown-preview/MarkdownPreview.module.css index 0ed404d..8479f43 100644 --- a/packages/editors/src/components/markdown-preview/MarkdownPreview.module.css +++ b/packages/editors/src/components/markdown-preview/MarkdownPreview.module.css @@ -40,44 +40,6 @@ margin-bottom: var(--ov-space-3, 12px); } -.Root a { - color: var(--ov-color-markdown-link, var(--ov-color-fg-accent)); - text-decoration: none; -} - -.Root a:hover { - text-decoration: underline; -} - -.Root code { - background: var(--ov-color-markdown-code-bg, var(--ov-color-bg-subtle)); - border-radius: var(--ov-radius-xs, 3px); - padding: 0.15em 0.4em; - font-family: var(--ov-font-family-mono, monospace); - font-size: 0.9em; -} - -.Root pre { - background: var(--ov-color-markdown-code-bg, var(--ov-color-bg-subtle)); - border-radius: var(--ov-radius-sm, 4px); - padding: var(--ov-space-3, 12px); - overflow-x: auto; - margin-bottom: var(--ov-space-3, 12px); -} - -.Root pre code { - background: none; - padding: 0; - border-radius: 0; -} - -.Root blockquote { - margin: 0 0 var(--ov-space-3, 12px) 0; - padding: var(--ov-space-1, 4px) var(--ov-space-3, 12px); - border-left: 3px solid var(--ov-color-markdown-quote-border, var(--ov-color-border-default)); - color: var(--ov-color-fg-muted); -} - .Root ul, .Root ol { margin-top: 0; @@ -89,28 +51,31 @@ margin-bottom: var(--ov-space-1, 4px); } -.Root table { - border-collapse: collapse; - width: 100%; - margin-bottom: var(--ov-space-3, 12px); +.Root figure { + margin: 0 0 var(--ov-space-3, 12px) 0; } -.Root th, -.Root td { - border: 1px solid var(--ov-color-border-default); - padding: var(--ov-space-2, 8px) var(--ov-space-3, 12px); - text-align: left; +/* Strip bottom margin from last paragraph inside blockquotes */ +.Root blockquote > p:last-child { + margin-bottom: 0; } -.Root th { - background: var(--ov-color-bg-subtle); - font-weight: var(--ov-font-weight-semibold, 500); +/* Blockquote spacing between siblings */ +.Root blockquote { + margin-bottom: var(--ov-space-3, 12px); +} + +/* Task list checkboxes — keep inline with text */ +.Root :global(.ov-md-checkbox) { + display: inline-grid; + width: auto; + vertical-align: middle; + margin-right: var(--ov-space-1, 4px); } -.Root hr { - border: none; - border-top: 1px solid var(--ov-color-border-default); - margin: var(--ov-space-4, 16px) 0; +/* Task list items should not show bullet */ +.Root li:has(> :global(.ov-md-checkbox)) { + list-style: none; } .Root img { @@ -119,8 +84,9 @@ border-radius: var(--ov-radius-sm, 4px); } -.Root input[type='checkbox'] { - margin-right: var(--ov-space-1, 4px); +/* Accordion spacing for details/summary blocks */ +.Root [data-ov-component='accordion'] { + margin-bottom: var(--ov-space-3, 12px); } .Loading { diff --git a/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx b/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx index 349e5cf..3ce922c 100644 --- a/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx +++ b/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx @@ -1,70 +1,196 @@ import type { Meta, StoryObj } from '@storybook/react'; import { MarkdownPreview } from './MarkdownPreview'; -const sampleMarkdown = `# Heading 1 +const richMarkdown = `# Omniview Platform Guide -## Heading 2 +Welcome to the **Omniview** platform. This document exercises every markdown surface supported by the preview component. -This is a paragraph with **bold**, *italic*, and \`inline code\`. +## Inline Formatting -### Code Block +Regular text with **bold**, *italic*, ~~strikethrough~~, and **_bold italic_** combined. Here is some \`inline code\` and a [link to the docs](https://docs.omniview.dev). + +## Headings + +### Third-level heading +#### Fourth-level heading +##### Fifth-level heading +###### Sixth-level heading + +## Lists + +### Unordered + +- Cluster management +- Resource monitoring + - CPU & memory + - Disk I/O + - Read throughput + - Write throughput +- Alerting & notifications + +### Ordered + +1. Connect to the cluster +2. Select a namespace +3. Choose a resource type + 1. Pods + 2. Services + 3. Deployments + +### Task List + +- [x] Set up project structure +- [x] Implement CodeEditor component +- [x] Implement DiffViewer component +- [ ] Add integration tests +- [ ] Performance benchmarks +- [ ] Release v0.1.0 + +## Code Blocks + +A quick shell command: + +\`\`\`bash +kubectl get pods -n production --sort-by='.status.startTime' +\`\`\` + +TypeScript with syntax highlighting: \`\`\`typescript -function greet(name: string): string { - return \`Hello, \${name}!\`; +import { createClient } from './api/client'; + +interface PodListResponse { + items: Array<{ + metadata: { name: string; namespace: string }; + status: { phase: 'Running' | 'Pending' | 'Failed' }; + }>; +} + +export async function fetchPods(namespace: string): Promise { + const { data } = await client.get( + \\\`/api/v1/namespaces/\\\${namespace}/pods\\\`, + ); + return data; } \`\`\` -### Links +JSON configuration: + +\`\`\`json +{ + "cluster": "production", + "context": "omniview-prod", + "preferences": { + "theme": "dark", + "refreshInterval": 5000, + "maxLogLines": 1000 + } +} +\`\`\` -[Visit OpenAI](https://openai.com) +YAML manifest: -### Lists +\`\`\`yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: omniview-api + labels: + app: omniview +spec: + replicas: 3 + selector: + matchLabels: + app: omniview + template: + spec: + containers: + - name: api + image: omniview/api:latest + ports: + - containerPort: 8080 +\`\`\` -- Item one -- Item two - - Nested item -- Item three +A plain code block with no language: -1. First -2. Second -3. Third +\`\`\` +no syntax highlighting here +just plain preformatted text +\`\`\` -### Task List +## Tables -- [x] Completed task -- [ ] Incomplete task +| Kind | API Version | Namespaced | Description | +|------|:----------:|:----------:|-------------| +| Pod | v1 | Yes | Smallest deployable unit | +| Service | v1 | Yes | Network endpoint abstraction | +| Deployment | apps/v1 | Yes | Declarative pod management | +| ConfigMap | v1 | Yes | Non-confidential configuration | +| Secret | v1 | Yes | Sensitive data storage | +| Namespace | v1 | No | Cluster partitioning | +| Node | v1 | No | Worker machine in cluster | -### Blockquote +## Blockquotes -> This is a blockquote -> with multiple lines. +> **Note:** All API calls go through the centralized client at \`src/api/client.ts\`. -### Table +> **Warning:** This API is experimental and may change in future releases. +> +> Proceed with caution when using the \`--force\` flag in production. + +> Nested blockquote with code: +> +> \`\`\`bash +> kubectl port-forward svc/my-service 8080:80 +> \`\`\` + +## Links + +- [GitHub Repository](https://github.com/omniviewdev/omniview) +- [Documentation](https://docs.omniview.dev) +- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook) + +Inline link: Visit [GitHub](https://github.com) for source code. + +## Horizontal Rules + +Content above the rule. + +--- -| Column 1 | Column 2 | Column 3 | -|----------|----------|----------| -| Cell 1 | Cell 2 | Cell 3 | -| Cell 4 | Cell 5 | Cell 6 | +Content below the rule. --- -*End of preview* -`; +## Images + +![Placeholder](https://placehold.co/600x200/1a1a2e/e0e0e0?text=Omniview+Preview) + +## Paragraphs & Emphasis + +This is a regular paragraph with enough text to demonstrate line wrapping behavior in the preview component. The markdown renderer should handle long paragraphs gracefully without breaking the layout. + +*This entire paragraph is italicized for emphasis. It demonstrates how the preview handles blocks of styled text rather than just inline fragments.* + +**This entire paragraph is bold. It is useful for call-outs or important notices that need to stand out from the surrounding content.** + +--- + +*Last updated: 2025-03-09*`; const meta: Meta = { title: 'Editors/MarkdownPreview', component: MarkdownPreview, tags: ['autodocs'], args: { - content: sampleMarkdown, + content: richMarkdown, }, argTypes: { allowHtml: { control: 'boolean' }, }, decorators: [ (Story) => ( -
+
), @@ -78,12 +204,256 @@ export const Playground: Story = {}; export const Simple: Story = { args: { - content: '# Hello World\n\nThis is a simple markdown preview.', + content: + '# Hello World\n\nThis is a simple markdown preview.\n\nIt supports **bold**, *italic*, and `inline code`.', + }, +}; + +export const WithCodeBlocks: Story = { + args: { + content: `## Code Examples + +### JavaScript +\`\`\`javascript +function fibonacci(n) { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} +\`\`\` + +### Python +\`\`\`python +def fibonacci(n: int) -> int: + if n <= 1: + return n + return fibonacci(n - 1) + fibonacci(n - 2) +\`\`\` + +### Go +\`\`\`go +func fibonacci(n int) int { + if n <= 1 { + return n + } + return fibonacci(n-1) + fibonacci(n-2) +} +\`\`\``, + }, +}; + +export const Tables: Story = { + args: { + content: `## Kubernetes Resource Types + +| Kind | API Version | Namespaced | Description | +|------|------------|------------|-------------| +| Pod | v1 | Yes | Smallest deployable unit | +| Service | v1 | Yes | Network endpoint abstraction | +| Deployment | apps/v1 | Yes | Declarative pod management | +| ConfigMap | v1 | Yes | Non-confidential configuration | +| Secret | v1 | Yes | Sensitive data storage | +| Namespace | v1 | No | Cluster partitioning | +| Node | v1 | No | Worker machine in cluster | +| PersistentVolume | v1 | No | Storage resource |`, + }, +}; + +export const TaskLists: Story = { + args: { + content: `## Sprint Checklist + +- [x] Set up project structure +- [x] Implement CodeEditor component +- [x] Implement DiffViewer component +- [x] Implement Terminal component +- [ ] Add integration tests +- [ ] Performance benchmarks +- [ ] Documentation review +- [ ] Release v0.1.0`, + }, +}; + +export const NestedLists: Story = { + args: { + content: `## Project Structure + +- \`packages/\` + - \`base-ui/\` — Core UI primitives + - \`src/components/\` — Component library + - \`src/theme/\` — Theme system + - \`editors/\` — Editor components + - \`src/components/\` + - \`code-editor/\` — Monaco-based editor + - \`terminal/\` — xterm.js terminal + - \`diff-viewer/\` — Side-by-side diff + - \`src/themes/\` — Editor theme integration +- \`docs/\` — Documentation +- \`scripts/\` — Build tooling`, }, }; -export const WithCode: Story = { +export const Blockquotes: Story = { args: { - content: '## Code Example\n\n```js\nconst x = 42;\nconsole.log(x);\n```', + content: `## Important Notes + +> **Warning:** This API is experimental and may change in future releases. + +> **Tip:** Use \`kubectl port-forward\` to access services locally: +> \`\`\`bash +> kubectl port-forward svc/my-service 8080:80 +> \`\`\``, + }, +}; + +export const Links: Story = { + args: { + content: `## Resources + +Links open in new tabs with \`rel="noopener noreferrer"\`: + +- [React Documentation](https://react.dev) +- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook) +- [Kubernetes Documentation](https://kubernetes.io/docs) + +Inline link: Visit [GitHub](https://github.com) for source code.`, + }, +}; + +export const EmptyContent: Story = { + args: { + content: '', + }, +}; + +export const LongDocument: Story = { + args: { + content: Array.from( + { length: 20 }, + (_, i) => + `## Section ${i + 1}\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.\n\n\`\`\`\ncode block ${i + 1}\n\`\`\`\n`, + ).join('\n'), + }, +}; + +export const WithHtml: Story = { + args: { + content: `## HTML Content + +This story has \`allowHtml\` enabled, so raw HTML is parsed and rendered. + +### Details / Summary + +
+Click to expand — deployment notes + +The v2.0 release requires a database migration. Run the following **before** deploying: + +\`\`\`bash +pnpm db:migrate --env production +\`\`\` + +After migration, verify with \`pnpm db:status\`. + +
+ +
+Architecture decision records + +- **ADR-001** — Use Monaco Editor for code editing +- **ADR-002** — Use xterm.js for terminal emulation +- **ADR-003** — Monorepo with pnpm workspaces + +
+ +### Badges & Shields + +![Build](https://img.shields.io/badge/build-passing-brightgreen) +![Coverage](https://img.shields.io/badge/coverage-94%25-brightgreen) +![License](https://img.shields.io/badge/license-MIT-blue) +![Version](https://img.shields.io/badge/version-0.1.0-orange) + +### Inline HTML elements + +This paragraph has strong, emphasis, strikethrough, inline code, and a link. + +### Definition list (via HTML) + +
+
Kubernetes
+
An open-source container orchestration platform.
+
Helm
+
A package manager for Kubernetes.
+
+ +### Keyboard shortcuts + +Press Ctrl + Shift + P to open the command palette.`, + allowHtml: true, + }, +}; + +export const GitHubReadme: Story = { + args: { + content: `

+ Omniview — A unified platform for Kubernetes management +

+ +

+ Build + Coverage + License + PRs Welcome +

+ +--- + +## Features + +- Multi-cluster management with a unified dashboard +- Real-time resource monitoring and alerting +- Integrated code editor with **YAML** schema validation +- Built-in terminal for \`kubectl\` access + +## Quick Start + +\`\`\`bash +# Install +pnpm add @omniview/app + +# Run +pnpm dev +\`\`\` + +## Configuration + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| \`theme\` | \`string\` | \`"dark"\` | UI theme | +| \`refreshInterval\` | \`number\` | \`5000\` | Poll interval in ms | +| \`maxLogLines\` | \`number\` | \`1000\` | Terminal log buffer | + +## Contributing + +- [x] Read the contributing guide +- [ ] Fork the repository +- [ ] Create a feature branch +- [ ] Submit a pull request + +
+Development setup + +\`\`\`bash +git clone https://github.com/omniviewdev/omniview.git +cd omniview +pnpm install +pnpm dev +\`\`\` + +
+ +## License + +MIT — see [LICENSE](LICENSE) for details.`, + allowHtml: true, }, }; diff --git a/packages/editors/src/components/markdown-preview/MarkdownPreview.test.tsx b/packages/editors/src/components/markdown-preview/MarkdownPreview.test.tsx index 59719e5..d41d2c6 100644 --- a/packages/editors/src/components/markdown-preview/MarkdownPreview.test.tsx +++ b/packages/editors/src/components/markdown-preview/MarkdownPreview.test.tsx @@ -2,11 +2,101 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { MarkdownPreview } from './MarkdownPreview'; +// Mock remark/rehype plugins +vi.mock('remark-gfm', () => ({ + __esModule: true, + default: () => {}, +})); + +vi.mock('rehype-raw', () => ({ + __esModule: true, + default: () => {}, +})); + +vi.mock('rehype-sanitize', () => ({ + __esModule: true, + default: () => {}, + defaultSchema: { tagNames: [], attributes: {} }, +})); + +// Mock base-ui components as simple pass-throughs +vi.mock('@omniview/base-ui', () => ({ + CodeBlock: ({ children, language }: { children: string; language?: string }) => ( +
+      {children}
+    
+ ), + Code: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + Table: { + Root: ({ children }: { children: React.ReactNode }) => ( + {children}
+ ), + Head: ({ children }: { children: React.ReactNode }) => {children}, + Body: ({ children }: { children: React.ReactNode }) => {children}, + Row: ({ children }: { children: React.ReactNode }) => {children}, + HeaderCell: ({ children }: { children: React.ReactNode }) => {children}, + Cell: ({ children }: { children: React.ReactNode }) => {children}, + }, + Link: ({ children, ...props }: { children: React.ReactNode; [key: string]: unknown }) => ( + + {children} + + ), + Blockquote: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + Separator: () =>
, + Checkbox: Object.assign( + ({ checked }: { checked?: boolean }) => ( + + ), + { + Root: ({ children }: { children: React.ReactNode }) => {children}, + Control: ({ children }: { children: React.ReactNode }) => {children}, + Indicator: () => , + }, + ), + Accordion: Object.assign( + ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + { + Item: ({ children, title }: { children: React.ReactNode; title: string }) => ( +
+ {children} +
+ ), + }, + ), +})); + // Mock react-markdown vi.mock('react-markdown', () => ({ __esModule: true, - default: ({ children }: { children: string }) => ( -
{children}
+ default: ({ + children, + skipHtml, + remarkPlugins, + rehypePlugins, + components, + }: { + children: string; + skipHtml?: boolean; + remarkPlugins?: unknown[]; + rehypePlugins?: unknown[]; + components?: Record; + }) => ( +
0)} + data-has-rehype-plugins={String(Array.isArray(rehypePlugins) && rehypePlugins.length > 0)} + data-component-keys={components ? Object.keys(components).sort().join(',') : ''} + > + {children} +
), })); @@ -36,4 +126,74 @@ describe('MarkdownPreview', () => { render(); expect(screen.getByTestId('markdown-preview')).toHaveAttribute('aria-label', 'Preview'); }); + + it('sets skipHtml=true by default (allowHtml=false)', () => { + render(); + expect(screen.getByTestId('react-markdown-mock')).toHaveAttribute('data-skip-html', 'true'); + }); + + it('sets skipHtml=false when allowHtml=true', () => { + render(); + expect(screen.getByTestId('react-markdown-mock')).toHaveAttribute('data-skip-html', 'false'); + }); + + it('adds rehype plugins when allowHtml=true', () => { + render(); + expect(screen.getByTestId('react-markdown-mock')).toHaveAttribute( + 'data-has-rehype-plugins', + 'true', + ); + }); + + it('does not add rehype plugins when allowHtml=false', () => { + render(); + expect(screen.getByTestId('react-markdown-mock')).toHaveAttribute( + 'data-has-rehype-plugins', + 'false', + ); + }); + + it('passes remarkPlugins to the renderer', () => { + render(); + expect(screen.getByTestId('react-markdown-mock')).toHaveAttribute( + 'data-has-remark-plugins', + 'true', + ); + }); + + it('provides component overrides for all mapped elements', () => { + render(); + const md = screen.getByTestId('react-markdown-mock'); + const keys = md.getAttribute('data-component-keys'); + expect(keys).toContain('code'); + expect(keys).toContain('pre'); + expect(keys).toContain('table'); + expect(keys).toContain('thead'); + expect(keys).toContain('tbody'); + expect(keys).toContain('tr'); + expect(keys).toContain('th'); + expect(keys).toContain('td'); + expect(keys).toContain('a'); + expect(keys).toContain('blockquote'); + expect(keys).toContain('hr'); + expect(keys).toContain('details'); + expect(keys).toContain('summary'); + expect(keys).toContain('input'); + }); + + it('renders with empty content', () => { + render(); + expect(screen.getByTestId('markdown-preview')).toBeInTheDocument(); + expect(screen.getByTestId('react-markdown-mock')).toHaveTextContent(''); + }); + + it('passes style prop through', () => { + render(); + expect(screen.getByTestId('markdown-preview')).toHaveStyle({ maxWidth: '500px' }); + }); + + it('passes data attributes through', () => { + render(); + expect(screen.getByTestId('markdown-preview')).toHaveAttribute('data-section', 'readme'); + }); }); diff --git a/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx b/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx index 92b6b8e..0b2753e 100644 --- a/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx +++ b/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx @@ -1,4 +1,17 @@ -import { forwardRef, lazy, Suspense, type HTMLAttributes } from 'react'; +import { Children, forwardRef, isValidElement, lazy, Suspense, useMemo, type HTMLAttributes } from 'react'; +import remarkGfm from 'remark-gfm'; +import rehypeRaw from 'rehype-raw'; +import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; +import { + Accordion, + Blockquote, + Checkbox, + Code, + CodeBlock, + Link, + Separator, + Table, +} from '@omniview/base-ui'; import styles from './MarkdownPreview.module.css'; const ReactMarkdown = lazy(() => import('react-markdown')); @@ -12,6 +25,117 @@ function cn(...parts: Array): string { return parts.filter(Boolean).join(' '); } +/** Recursively extract plain text from a hast node tree. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function hastText(node: any): string { + if (!node) return ''; + if (node.type === 'text') return node.value ?? ''; + if (Array.isArray(node.children)) return node.children.map(hastText).join(''); + return ''; +} + +/** Stable component overrides for react-markdown */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type MdProps = Record; + +const markdownComponents: Record> = { + // Code blocks & inline code + pre: ({ children }: MdProps) => <>{children}, + code: ({ className, children }: MdProps) => { + const match = /language-(\w+)/.exec(className ?? ''); + if (match) { + return ( + + {String(children).replace(/\n$/, '')} + + ); + } + return {children}; + }, + + // Tables + table: ({ children }: MdProps) => ( + + {children} + + ), + thead: ({ children }: MdProps) => {children}, + tbody: ({ children }: MdProps) => {children}, + tr: ({ children }: MdProps) => {children}, + th: ({ children, style }: MdProps) => ( + {children} + ), + td: ({ children, style }: MdProps) => ( + {children} + ), + + // Links + a: ({ children, href }: MdProps) => ( + + {children} + + ), + + // Blockquotes + blockquote: ({ children }: MdProps) =>
{children}
, + + // Horizontal rules + hr: () => , + + // Details/summary → Accordion — extract title from the hast node's summary child + details: ({ children, node }: MdProps) => { + // Extract summary text from the hast AST (before React renders it) + let summaryText = 'Details'; + if (node?.children) { + const summaryNode = node.children.find( + (c: MdProps) => c.tagName === 'summary', + ); + if (summaryNode) { + const text = hastText(summaryNode).trim(); + if (text) summaryText = text; + } + } + + // Filter out the rendered summary element from React children + const body: React.ReactNode[] = []; + Children.forEach(children, (child) => { + // Skip the hidden summary marker + if (isValidElement(child) && (child.props as MdProps)?.['data-md-summary']) { + return; + } + body.push(child); + }); + + const itemId = `md-details-${summaryText.replace(/\s+/g, '-').toLowerCase().slice(0, 32)}`; + + return ( + + + {body} + + + ); + }, + // Mark summary so details can filter it out; text is extracted from hast node + summary: ({ children }: MdProps) => ( + + ), + + // Task list checkboxes — use compound parts for inline rendering + input: ({ type, checked, disabled }: MdProps) => { + if (type === 'checkbox') { + return ( + + + + + + ); + } + return ; + }, +}; + export const MarkdownPreview = forwardRef( function MarkdownPreview({ content, allowHtml = false, className, ...props }, ref) { return ( @@ -37,20 +161,29 @@ export const MarkdownPreview = forwardRef( MarkdownPreview.displayName = 'MarkdownPreview'; +/** Sanitize schema that extends the default to allow class on any element (needed for alerts). */ +const sanitizeSchema = { + ...defaultSchema, + attributes: { + ...defaultSchema.attributes, + '*': [...(defaultSchema.attributes?.['*'] ?? []), 'className'], + }, +}; + /** Inner component that uses lazy-loaded deps */ function MarkdownContent({ content, allowHtml }: { content: string; allowHtml: boolean }) { - // Dynamic imports for remark/rehype plugins - // They're imported alongside react-markdown via the lazy boundary + const rehypePlugins = useMemo( + () => (allowHtml ? [rehypeRaw, [rehypeSanitize, sanitizeSchema]] : []), + [allowHtml], + ); + return ( ( - - {children} - - ), - }} + remarkPlugins={[remarkGfm]} + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rehypePlugins={rehypePlugins as any} + components={markdownComponents} > {content} diff --git a/packages/editors/src/components/object-inspector/ObjectInspector.stories.tsx b/packages/editors/src/components/object-inspector/ObjectInspector.stories.tsx index 980b0cf..94a1378 100644 --- a/packages/editors/src/components/object-inspector/ObjectInspector.stories.tsx +++ b/packages/editors/src/components/object-inspector/ObjectInspector.stories.tsx @@ -11,28 +11,62 @@ const sampleData = { app: 'web', version: 'v1.2.3', }, + annotations: { + 'kubectl.kubernetes.io/last-applied-configuration': '...', + }, + creationTimestamp: '2024-01-15T10:30:00Z', + uid: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', }, spec: { containers: [ { name: 'web', image: 'nginx:latest', - ports: [{ containerPort: 80 }], + ports: [{ containerPort: 80, protocol: 'TCP' }], env: [ { name: 'NODE_ENV', value: 'production' }, { name: 'PORT', value: '8080' }, ], + resources: { + limits: { cpu: '500m', memory: '128Mi' }, + requests: { cpu: '100m', memory: '64Mi' }, + }, + }, + { + name: 'sidecar', + image: 'envoy:v1.28', + ports: [{ containerPort: 9901 }], }, ], restartPolicy: 'Always', + serviceAccountName: 'default', + nodeName: 'worker-node-1', }, status: { phase: 'Running', conditions: [ - { type: 'Ready', status: true }, - { type: 'PodScheduled', status: true }, + { type: 'Ready', status: true, lastTransitionTime: '2024-01-15T10:31:00Z' }, + { type: 'PodScheduled', status: true, lastTransitionTime: '2024-01-15T10:30:00Z' }, + { type: 'ContainersReady', status: true, lastTransitionTime: '2024-01-15T10:31:00Z' }, + { type: 'Initialized', status: true, lastTransitionTime: '2024-01-15T10:30:05Z' }, + ], + containerStatuses: [ + { + name: 'web', + ready: true, + restartCount: 0, + state: { running: { startedAt: '2024-01-15T10:30:30Z' } }, + }, + { + name: 'sidecar', + ready: true, + restartCount: 2, + state: { running: { startedAt: '2024-01-15T10:30:45Z' } }, + }, ], startTime: '2024-01-15T10:30:00Z', + podIP: '10.244.0.5', + hostIP: '192.168.1.100', }, }; @@ -52,7 +86,7 @@ const meta: Meta = { }, decorators: [ (Story) => ( -
+
), @@ -110,3 +144,123 @@ export const SimpleArray: Story = { defaultExpanded: true, }, }; + +/** Demonstrates handling of all primitive types. */ +export const PrimitiveTypes: Story = { + args: { + data: { + string: 'hello world', + number: 42, + float: 3.14159, + booleanTrue: true, + booleanFalse: false, + nullValue: null, + emptyString: '', + zero: 0, + negativeNumber: -100, + largeNumber: 9007199254740991, + }, + defaultExpanded: true, + }, +}; + +/** Deeply nested data structure. */ +export const DeepNesting: Story = { + args: { + data: { + level1: { + level2: { + level3: { + level4: { + level5: { + level6: { + value: 'deeply nested', + }, + }, + }, + }, + }, + }, + }, + defaultExpanded: 3, + }, +}; + +/** Circular reference handling — should display [Circular] instead of crashing. */ +export const CircularReference: Story = { + args: (() => { + const obj: Record = { + name: 'root', + children: [ + { name: 'child-1', type: 'leaf' }, + { name: 'child-2', type: 'leaf' }, + ], + }; + obj.self = obj; + (obj.children as Record[])[0]!.parent = obj; + return { data: obj, defaultExpanded: true }; + })(), +}; + +/** Empty object and array edge cases. */ +export const EmptyContainers: Story = { + args: { + data: { + emptyObject: {}, + emptyArray: [], + nonEmpty: { key: 'value' }, + nonEmptyArray: [1, 2], + }, + defaultExpanded: true, + }, +}; + +/** Large flat object with many properties. */ +export const ManyProperties: Story = { + args: { + data: Object.fromEntries( + Array.from({ length: 50 }, (_, i) => [`property_${i + 1}`, `value_${i + 1}`]), + ), + defaultExpanded: true, + }, +}; + +/** Complex Kubernetes service list response. */ +export const KubernetesServiceList: Story = { + args: { + data: { + apiVersion: 'v1', + kind: 'ServiceList', + items: [ + { + metadata: { name: 'kubernetes', namespace: 'default' }, + spec: { + type: 'ClusterIP', + clusterIP: '10.96.0.1', + ports: [{ port: 443, targetPort: 6443, protocol: 'TCP' }], + }, + }, + { + metadata: { name: 'web-frontend', namespace: 'production' }, + spec: { + type: 'LoadBalancer', + clusterIP: '10.96.1.50', + ports: [ + { name: 'http', port: 80, targetPort: 8080, protocol: 'TCP' }, + { name: 'https', port: 443, targetPort: 8443, protocol: 'TCP' }, + ], + selector: { app: 'web', tier: 'frontend' }, + }, + status: { + loadBalancer: { + ingress: [{ ip: '203.0.113.50' }], + }, + }, + }, + ], + }, + defaultExpanded: 2, + searchable: true, + copyable: true, + }, +}; diff --git a/packages/editors/src/components/terminal/Terminal.stories.tsx b/packages/editors/src/components/terminal/Terminal.stories.tsx index 5eb90ab..05c07b4 100644 --- a/packages/editors/src/components/terminal/Terminal.stories.tsx +++ b/packages/editors/src/components/terminal/Terminal.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { useRef, useEffect } from 'react'; +import { useRef, useEffect, useState } from 'react'; import { Terminal, type TerminalHandle, type TerminalProps } from './Terminal'; const meta: Meta = { @@ -11,6 +11,13 @@ const meta: Meta = { }, argTypes: { fontSize: { control: { type: 'range', min: 10, max: 24, step: 1 } }, + scrollback: { control: { type: 'number', min: 0, max: 50000, step: 500 } }, + cursorBlink: { control: 'boolean' }, + convertEol: { control: 'boolean' }, + macOptionIsMeta: { control: 'boolean' }, + macOptionClickForcesSelection: { control: 'boolean' }, + linkHandling: { control: 'boolean' }, + allowTransparency: { control: 'boolean' }, }, decorators: [ (Story) => ( @@ -26,6 +33,7 @@ type Story = StoryObj; export const Playground: Story = {}; +/** Writes simulated `ls -la` output to demonstrate basic text rendering. */ function WithOutputStory(args: TerminalProps) { const ref = useRef(null); @@ -52,3 +60,230 @@ function WithOutputStory(args: TerminalProps) { export const WithOutput: Story = { render: (args) => , }; + +/** Demonstrates ANSI color codes in terminal output. */ +function AnsiColorsStory(args: TerminalProps) { + const ref = useRef(null); + + useEffect(() => { + const timeout = setTimeout(() => { + if (!ref.current) return; + const t = ref.current; + t.writeln('\x1b[1mBold text\x1b[0m'); + t.writeln('\x1b[3mItalic text\x1b[0m'); + t.writeln('\x1b[4mUnderlined text\x1b[0m'); + t.writeln(''); + t.writeln('Standard colors:'); + t.writeln( + ' \x1b[30mBlack\x1b[0m \x1b[31mRed\x1b[0m \x1b[32mGreen\x1b[0m \x1b[33mYellow\x1b[0m \x1b[34mBlue\x1b[0m \x1b[35mMagenta\x1b[0m \x1b[36mCyan\x1b[0m \x1b[37mWhite\x1b[0m', + ); + t.writeln(''); + t.writeln('Bright colors:'); + t.writeln( + ' \x1b[90mBlack\x1b[0m \x1b[91mRed\x1b[0m \x1b[92mGreen\x1b[0m \x1b[93mYellow\x1b[0m \x1b[94mBlue\x1b[0m \x1b[95mMagenta\x1b[0m \x1b[96mCyan\x1b[0m \x1b[97mWhite\x1b[0m', + ); + t.writeln(''); + t.writeln('Background colors:'); + t.writeln( + ' \x1b[41m Red \x1b[0m \x1b[42m Green \x1b[0m \x1b[43m Yellow \x1b[0m \x1b[44m Blue \x1b[0m \x1b[45m Magenta \x1b[0m \x1b[46m Cyan \x1b[0m', + ); + t.writeln(''); + t.write('$ '); + }, 300); + return () => clearTimeout(timeout); + }, []); + + return ; +} + +export const AnsiColors: Story = { + render: (args) => , +}; + +/** Demonstrates high-throughput output with rapid line writes. */ +function HighThroughputStory(args: TerminalProps) { + const ref = useRef(null); + + useEffect(() => { + const timeout = setTimeout(() => { + if (!ref.current) return; + ref.current.writeln('$ cat /var/log/system.log | head -200'); + for (let i = 0; i < 200; i++) { + const level = + i % 10 === 0 + ? '\x1b[31mERROR\x1b[0m' + : i % 5 === 0 + ? '\x1b[33mWARN\x1b[0m' + : '\x1b[32mINFO\x1b[0m'; + ref.current.writeln( + `2024-01-15T10:${String(i % 60).padStart(2, '0')}:00Z ${level} service.handler Request processed in ${Math.floor(Math.random() * 500)}ms`, + ); + } + ref.current.writeln(''); + ref.current.write('$ '); + }, 300); + return () => clearTimeout(timeout); + }, []); + + return ; +} + +export const HighThroughput: Story = { + render: (args) => , +}; + +/** Interactive terminal that echoes typed input. */ +function InteractiveStory(args: TerminalProps) { + const ref = useRef(null); + const [line, setLine] = useState(''); + + const handleData = (data: string) => { + if (!ref.current) return; + if (data === '\r') { + ref.current.writeln(''); + if (line.trim()) { + ref.current.writeln(`\x1b[36m> ${line}\x1b[0m`); + } + setLine(''); + ref.current.write('$ '); + } else if (data === '\x7f') { + // Backspace + if (line.length > 0) { + setLine((prev) => prev.slice(0, -1)); + ref.current.write('\b \b'); + } + } else { + setLine((prev) => prev + data); + ref.current.write(data); + } + }; + + useEffect(() => { + const timeout = setTimeout(() => { + ref.current?.writeln('Interactive echo terminal. Type something and press Enter.'); + ref.current?.write('$ '); + }, 300); + return () => clearTimeout(timeout); + }, []); + + return ; +} + +export const Interactive: Story = { + render: (args) => , +}; + +/** Demonstrates the onResize callback by displaying current dimensions. */ +function ResizeDemoStory(args: TerminalProps) { + const ref = useRef(null); + + const handleResize = (cols: number, rows: number) => { + ref.current?.writeln(`\x1b[33m[resize] ${cols} cols x ${rows} rows\x1b[0m`); + }; + + useEffect(() => { + const timeout = setTimeout(() => { + const dims = ref.current?.getDimensions(); + ref.current?.writeln('Resize this panel to see dimension changes.'); + if (dims) { + ref.current?.writeln(`Current: ${dims.cols} cols x ${dims.rows} rows`); + } + ref.current?.writeln(''); + }, 500); + return () => clearTimeout(timeout); + }, []); + + return ; +} + +export const ResizeDemo: Story = { + render: (args) => , + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +/** Shows different font sizes side by side. */ +function FontSizesStory() { + return ( +
+ {[10, 13, 16, 20].map((size) => ( + + ))} +
+ ); +} + +function FontSizePanel({ fontSize }: { fontSize: number }) { + const ref = useRef(null); + useEffect(() => { + const t = setTimeout(() => { + ref.current?.writeln(`fontSize: ${fontSize}`); + ref.current?.writeln('$ echo "Hello"'); + ref.current?.writeln('Hello'); + }, 300); + return () => clearTimeout(t); + }, [fontSize]); + return ( +
+ +
+ ); +} + +export const FontSizes: Story = { + render: () => , + decorators: [], +}; + +/** Large scrollback buffer demonstration. */ +function LargeScrollbackStory(args: TerminalProps) { + const ref = useRef(null); + + useEffect(() => { + const timeout = setTimeout(() => { + if (!ref.current) return; + for (let i = 1; i <= 1000; i++) { + ref.current.writeln(`Line ${i}: ${'x'.repeat(40)}`); + } + ref.current.writeln(''); + ref.current.writeln('\x1b[32mScroll up to see 1000 lines of output.\x1b[0m'); + }, 300); + return () => clearTimeout(timeout); + }, []); + + return ; +} + +export const LargeScrollback: Story = { + render: (args) => , +}; + +/** Demonstrates clickable URL links in terminal output. */ +function ClickableLinksStory(args: TerminalProps) { + const ref = useRef(null); + + useEffect(() => { + const timeout = setTimeout(() => { + if (!ref.current) return; + ref.current.writeln('Terminal with clickable links (hover to see):'); + ref.current.writeln(''); + ref.current.writeln(' https://github.com/omniviewdev/omniview'); + ref.current.writeln(' https://example.com/api/v1/resources'); + ref.current.writeln(' http://localhost:3000/dashboard'); + ref.current.writeln(''); + ref.current.write('$ '); + }, 300); + return () => clearTimeout(timeout); + }, []); + + return ; +} + +export const ClickableLinks: Story = { + render: (args) => , +}; diff --git a/packages/editors/src/index.ts b/packages/editors/src/index.ts index 132fc9c..4195284 100644 --- a/packages/editors/src/index.ts +++ b/packages/editors/src/index.ts @@ -3,3 +3,6 @@ export { useEditorTheme, getComputedToken } from './themes'; export { buildMonacoTheme, OV_MONACO_THEME } from './themes'; export { buildXtermTheme } from './themes'; export type { XtermThemeData } from './themes'; +export { editorSchemas } from './schemas'; +export type { EditorSchema, EditorSchemaRegistry } from './schemas'; +export { setupMonacoWorkers } from './setup'; diff --git a/packages/editors/src/schemas/EditorSchemaRegistry.ts b/packages/editors/src/schemas/EditorSchemaRegistry.ts new file mode 100644 index 0000000..8cdb5d3 --- /dev/null +++ b/packages/editors/src/schemas/EditorSchemaRegistry.ts @@ -0,0 +1,260 @@ +/** + * EditorSchemaRegistry — runtime registry for adding language schemas + * (primarily YAML/JSON) to Monaco editors. + * + * Plugins register schemas at runtime via `register()`. The CodeEditor + * subscribes via `onChange()` and calls `applyYamlSchemas()` / `applyJsonSchemas()` + * whenever schemas change. + * + * @example + * ```ts + * import { editorSchemas } from '@omniview/editors'; + * + * // Plugin registers a Kubernetes Deployment schema + * editorSchemas.register({ + * uri: 'https://kubernetesjsonschema.dev/v1.25.0/deployment-apps-v1.json', + * fileMatch: ['*apps%3A%3Av1%3A%3ADeployment.yaml'], + * schema: deploymentJsonSchema, + * }); + * ``` + */ + +const DEBUG = false; +function log(...args: unknown[]) { + if (DEBUG) console.log('[schema-registry]', ...args); +} +function warn(...args: unknown[]) { + if (DEBUG) console.warn('[schema-registry]', ...args); +} + +export interface EditorSchema { + /** Unique URI identifying this schema. Must be a valid https:// URL. */ + uri: string; + /** + * Glob patterns for filenames this schema applies to. + * Tested against the full Monaco model URI string — use URL-encoded + * colons (`%3A`) for structured GVR filenames. + * + * @example ['*core%3A%3Av1%3A%3APod.yaml'] + */ + fileMatch: string[]; + /** The JSON Schema object. */ + schema: Record; + /** + * Human-readable name shown in hover tooltips as the "Source:" link text. + * If omitted, the URI basename is used. + * @example "Pod (v1)" + */ + name?: string; + /** + * Description shown in hover tooltips below the schema title. + * Supports plain text (markdown special chars will be escaped). + */ + description?: string; +} + +type SchemaChangeListener = () => void; + +/** + * Simple glob matcher — supports `*` (matches any sequence of chars). + * Used to replicate the fileMatch behavior of monaco-yaml. + */ +function globMatch(pattern: string, str: string): boolean { + const escaped = pattern.replace(/[-[\]{}()+?.\\^$|]/g, '\\$&').replace(/\*/g, '.*'); + return new RegExp(`^${escaped}$`).test(str); +} + +type YamlHandle = { + update: (options: Record) => void; + dispose: () => void; +}; + +class EditorSchemaRegistryImpl { + private _schemas = new Map(); + private _listeners = new Set(); + private _yamlHandle: YamlHandle | null = null; + + /** All currently registered schemas. */ + get schemas(): EditorSchema[] { + return Array.from(this._schemas.values()); + } + + /** + * Inject the yaml handle created by `setupMonacoWorkers`. + * Called once at startup before any editor mounts. + * @internal + */ + _setYamlHandle(handle: YamlHandle): void { + log('_setYamlHandle called — handle keys:', Object.keys(handle)); + this._yamlHandle = handle; + } + + /** + * Register a schema. If a schema with the same URI exists, it is replaced. + * Notifies all listeners (active editors) to reconfigure. + */ + register(schema: EditorSchema): void { + log('register() — uri:', schema.uri, 'fileMatch:', schema.fileMatch, + 'schema keys:', Object.keys(schema.schema).slice(0, 10)); + this._schemas.set(schema.uri, schema); + this._notify(); + } + + /** Register multiple schemas at once. */ + registerAll(schemas: EditorSchema[]): void { + log('registerAll() —', schemas.length, 'schemas:', + schemas.map(s => s.uri)); + for (const s of schemas) { + this._schemas.set(s.uri, s); + } + this._notify(); + } + + /** Unregister a schema by URI. */ + unregister(uri: string): boolean { + log('unregister() — uri:', uri); + const deleted = this._schemas.delete(uri); + if (deleted) this._notify(); + else log(' schema not found, nothing to unregister'); + return deleted; + } + + /** Clear all registered schemas. */ + clear(): void { + log('clear() — removing', this._schemas.size, 'schemas'); + this._schemas.clear(); + this._notify(); + } + + /** + * Find all registered schemas whose `fileMatch` patterns match the given + * model URI string. Uses the same glob-like matching that monaco-yaml uses. + */ + getSchemasForUri(modelUri: string): EditorSchema[] { + return this.schemas.filter((s) => + s.fileMatch.some((pattern) => globMatch(pattern, modelUri)), + ); + } + + /** Whether the YAML handle has been injected (workers initialized). */ + get isYamlReady(): boolean { + return this._yamlHandle !== null; + } + + /** Subscribe to schema changes. Returns an unsubscribe function. */ + onChange(listener: SchemaChangeListener): () => void { + this._listeners.add(listener); + log('onChange() — listener added, total listeners:', this._listeners.size); + return () => { + this._listeners.delete(listener); + log('onChange() — listener removed, total listeners:', this._listeners.size); + }; + } + + /** + * Push all registered YAML schemas to the monaco-yaml worker via `update()`. + * The yaml handle must have been injected by `setupMonacoWorkers` at startup. + */ + applyYamlSchemas(): void { + if (!this._yamlHandle) { + warn('applyYamlSchemas() — NO yaml handle! Was setupMonacoWorkers imported?'); + return; + } + const schemas = this.schemas; + const mapped = schemas.map((s) => ({ + uri: s.uri, + fileMatch: s.fileMatch, + schema: s.schema as Record, + // name/description are passed through to yaml-language-server's + // registerExternalSchema — they appear in hover tooltips. + ...(s.name && { name: s.name }), + ...(s.description && { description: s.description }), + })); + log('applyYamlSchemas() — pushing', schemas.length, 'schemas'); + this._yamlHandle.update({ + enableSchemaRequest: false, + schemas: mapped, + }); + log('applyYamlSchemas() — update() called successfully'); + } + + /** + * Apply all JSON schemas to Monaco's built-in JSON language service. + * Accepts the `monaco` namespace so the registry itself doesn't import + * `monaco-editor` — keeping it usable before Monaco loads. + */ + applyJsonSchemas(monacoInstance: unknown): void { + const m = monacoInstance as { + json?: { + jsonDefaults?: { + setDiagnosticsOptions: (opts: { + validate: boolean; + schemas: Array<{ + uri: string; + fileMatch: string[]; + schema: Record; + }>; + }) => void; + }; + }; + // Fallback for pre-0.55 (languages.json.jsonDefaults) + languages?: { + json?: { + jsonDefaults?: { + setDiagnosticsOptions: (opts: { + validate: boolean; + schemas: Array<{ + uri: string; + fileMatch: string[]; + schema: Record; + }>; + }) => void; + }; + }; + }; + }; + + const diagnosticsOptions = { + validate: true, + schemas: this.schemas.map((s) => ({ + uri: s.uri, + fileMatch: s.fileMatch, + schema: s.schema, + })), + }; + + // 0.55+ top-level namespace + const jsonDefaults = m.json?.jsonDefaults ?? m.languages?.json?.jsonDefaults; + log('applyJsonSchemas() —', this.schemas.length, 'schemas, jsonDefaults available:', + !!jsonDefaults); + jsonDefaults?.setDiagnosticsOptions(diagnosticsOptions); + } + + /** Log the current state of the registry for debugging. */ + debugState(): void { + log('=== Registry State ==='); + log(' Schemas:', this._schemas.size); + for (const [uri, schema] of this._schemas) { + log(` ${uri} → fileMatch: ${JSON.stringify(schema.fileMatch)}`); + } + log(' Listeners:', this._listeners.size); + log(' YAML handle:', this._yamlHandle ? 'SET' : 'NOT SET'); + log('======================'); + } + + private _notify(): void { + log('_notify() — notifying', this._listeners.size, 'listeners'); + for (const fn of this._listeners) { + try { + fn(); + } catch { + // listener errors shouldn't break registry + } + } + } +} + +/** Singleton schema registry instance. */ +export const editorSchemas = new EditorSchemaRegistryImpl(); + +export type { EditorSchemaRegistryImpl as EditorSchemaRegistry }; diff --git a/packages/editors/src/schemas/index.ts b/packages/editors/src/schemas/index.ts new file mode 100644 index 0000000..b910e01 --- /dev/null +++ b/packages/editors/src/schemas/index.ts @@ -0,0 +1,2 @@ +export { editorSchemas } from './EditorSchemaRegistry'; +export type { EditorSchema, EditorSchemaRegistry } from './EditorSchemaRegistry'; diff --git a/packages/editors/src/schemas/kubernetes/configmap-v1.json b/packages/editors/src/schemas/kubernetes/configmap-v1.json new file mode 100644 index 0000000..5634403 --- /dev/null +++ b/packages/editors/src/schemas/kubernetes/configmap-v1.json @@ -0,0 +1,316 @@ +{ + "description": "ConfigMap holds configuration data for pods to consume.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": [ + "string", + "null" + ], + "enum": [ + "v1" + ] + }, + "binaryData": { + "additionalProperties": { + "format": "byte", + "type": [ + "string", + "null" + ] + }, + "description": "BinaryData contains the binary data. Each key must consist of alphanumeric characters, '-', '_' or '.'. BinaryData can contain byte sequences that are not in the UTF-8 range. The keys stored in BinaryData must not overlap with the ones in the Data field, this is enforced during validation process. Using this field will require 1.10+ apiserver and kubelet.", + "type": [ + "object", + "null" + ] + }, + "data": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte sequences must use the BinaryData field. The keys stored in Data must not overlap with the keys in the BinaryData field, this is enforced during validation process.", + "type": [ + "object", + "null" + ] + }, + "immutable": { + "description": "Immutable, if set to true, ensures that data stored in the ConfigMap cannot be updated (only object metadata can be modified). If not set to true, the field can be modified at any time. Defaulted to nil.", + "type": [ + "boolean", + "null" + ] + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": [ + "string", + "null" + ], + "enum": [ + "ConfigMap" + ] + }, + "metadata": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "properties": { + "annotations": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": [ + "object", + "null" + ] + }, + "creationTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "deletionTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": [ + "string", + "null" + ] + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "labels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": [ + "object", + "null" + ] + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "items": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": [ + "string", + "null" + ] + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": [ + "string", + "null" + ] + }, + "fieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": [ + "object", + "null" + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": [ + "string", + "null" + ] + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": [ + "string", + "null" + ] + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": [ + "string", + "null" + ] + }, + "time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": [ + "string", + "null" + ] + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": [ + "string", + "null" + ] + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "items": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": [ + "boolean", + "null" + ] + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": [ + "boolean", + "null" + ] + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + }, + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": [ + "string", + "null" + ] + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": [ + "string", + "null" + ] + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "", + "kind": "ConfigMap", + "version": "v1" + } + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/schema#" +} \ No newline at end of file diff --git a/packages/editors/src/schemas/kubernetes/deployment-apps-v1.json b/packages/editors/src/schemas/kubernetes/deployment-apps-v1.json new file mode 100644 index 0000000..b530c96 --- /dev/null +++ b/packages/editors/src/schemas/kubernetes/deployment-apps-v1.json @@ -0,0 +1,10291 @@ +{ + "description": "Deployment enables declarative updates for Pods and ReplicaSets.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": [ + "string", + "null" + ], + "enum": [ + "apps/v1" + ] + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": [ + "string", + "null" + ], + "enum": [ + "Deployment" + ] + }, + "metadata": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "properties": { + "annotations": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": [ + "object", + "null" + ] + }, + "creationTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "deletionTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": [ + "string", + "null" + ] + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "labels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": [ + "object", + "null" + ] + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "items": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": [ + "string", + "null" + ] + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": [ + "string", + "null" + ] + }, + "fieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": [ + "object", + "null" + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": [ + "string", + "null" + ] + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": [ + "string", + "null" + ] + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": [ + "string", + "null" + ] + }, + "time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": [ + "string", + "null" + ] + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": [ + "string", + "null" + ] + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "items": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": [ + "boolean", + "null" + ] + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": [ + "boolean", + "null" + ] + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + }, + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": [ + "string", + "null" + ] + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": [ + "string", + "null" + ] + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "spec": { + "description": "DeploymentSpec is the specification of the desired behavior of the Deployment.", + "properties": { + "minReadySeconds": { + "description": "Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, for it to be considered available. Defaults to 0 (pod will be considered available as soon as it is ready)", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "paused": { + "description": "Indicates that the deployment is paused.", + "type": [ + "boolean", + "null" + ] + }, + "progressDeadlineSeconds": { + "description": "The maximum time in seconds for a deployment to make progress before it is considered to be failed. The deployment controller will continue to process failed deployments and a condition with a ProgressDeadlineExceeded reason will be surfaced in the deployment status. Note that progress will not be estimated during the time a deployment is paused. Defaults to 600s.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "replicas": { + "description": "Number of desired pods. This is a pointer to distinguish between explicit zero and not specified. Defaults to 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "revisionHistoryLimit": { + "description": "The number of old ReplicaSets to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 10.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "selector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": "object", + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "strategy": { + "description": "DeploymentStrategy describes how to replace existing pods with new ones.", + "properties": { + "rollingUpdate": { + "description": "Spec to control the desired behavior of rolling update.", + "properties": { + "maxSurge": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "maxUnavailable": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": { + "description": "Type of deployment. Can be \"Recreate\" or \"RollingUpdate\". Default is RollingUpdate.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "template": { + "description": "PodTemplateSpec describes the data a pod should have when created from a template", + "properties": { + "metadata": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "properties": { + "annotations": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": [ + "object", + "null" + ] + }, + "creationTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "deletionTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": [ + "string", + "null" + ] + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "labels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": [ + "object", + "null" + ] + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "items": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": [ + "string", + "null" + ] + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": [ + "string", + "null" + ] + }, + "fieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": [ + "object", + "null" + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": [ + "string", + "null" + ] + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": [ + "string", + "null" + ] + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": [ + "string", + "null" + ] + }, + "time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": [ + "string", + "null" + ] + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": [ + "string", + "null" + ] + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "items": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": [ + "boolean", + "null" + ] + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": [ + "boolean", + "null" + ] + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + }, + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": [ + "string", + "null" + ] + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": [ + "string", + "null" + ] + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "spec": { + "description": "PodSpec is a description of a pod.", + "properties": { + "activeDeadlineSeconds": { + "description": "Optional duration in seconds the pod may be active on the node relative to StartTime before the system will actively try to mark it failed and kill associated containers. Value must be a positive integer.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "affinity": { + "description": "Affinity is a group of affinity scheduling rules.", + "properties": { + "nodeAffinity": { + "description": "Node affinity is a group of node affinity scheduling rules.", + "properties": { + "preferredDuringSchedulingIgnoredDuringExecution": { + "description": "The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node matches the corresponding matchExpressions; the node(s) with the highest sum are the most preferred.", + "items": { + "description": "An empty preferred scheduling term matches all objects with implicit weight 0 (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).", + "properties": { + "preference": { + "description": "A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.", + "properties": { + "matchExpressions": { + "description": "A list of node selector requirements by node's labels.", + "items": { + "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "The label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + "type": "string" + }, + "values": { + "description": "An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchFields": { + "description": "A list of node selector requirements by node's fields.", + "items": { + "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "The label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + "type": "string" + }, + "values": { + "description": "An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object", + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "weight": { + "description": "Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100.", + "format": "int32", + "type": "integer" + } + }, + "required": [ + "weight", + "preference" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "requiredDuringSchedulingIgnoredDuringExecution": { + "description": "A node selector represents the union of the results of one or more label queries over a set of nodes; that is, it represents the OR of the selectors represented by the node selector terms.", + "properties": { + "nodeSelectorTerms": { + "description": "Required. A list of node selector terms. The terms are ORed.", + "items": { + "description": "A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.", + "properties": { + "matchExpressions": { + "description": "A list of node selector requirements by node's labels.", + "items": { + "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "The label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + "type": "string" + }, + "values": { + "description": "An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchFields": { + "description": "A list of node selector requirements by node's fields.", + "items": { + "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "The label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + "type": "string" + }, + "values": { + "description": "An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": "array" + } + }, + "required": [ + "nodeSelectorTerms" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "podAffinity": { + "description": "Pod affinity is a group of inter pod affinity scheduling rules.", + "properties": { + "preferredDuringSchedulingIgnoredDuringExecution": { + "description": "The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred.", + "items": { + "description": "The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)", + "properties": { + "podAffinityTerm": { + "description": "Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running", + "properties": { + "labelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaceSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaces": { + "description": "namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means \"this pod's namespace\".", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "topologyKey": { + "description": "This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed.", + "type": "string" + } + }, + "required": [ + "topologyKey" + ], + "type": "object", + "additionalProperties": false + }, + "weight": { + "description": "weight associated with matching the corresponding podAffinityTerm, in the range 1-100.", + "format": "int32", + "type": "integer" + } + }, + "required": [ + "weight", + "podAffinityTerm" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "requiredDuringSchedulingIgnoredDuringExecution": { + "description": "If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied.", + "items": { + "description": "Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running", + "properties": { + "labelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaceSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaces": { + "description": "namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means \"this pod's namespace\".", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "topologyKey": { + "description": "This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed.", + "type": "string" + } + }, + "required": [ + "topologyKey" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "podAntiAffinity": { + "description": "Pod anti affinity is a group of inter pod anti affinity scheduling rules.", + "properties": { + "preferredDuringSchedulingIgnoredDuringExecution": { + "description": "The scheduler will prefer to schedule pods to nodes that satisfy the anti-affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling anti-affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred.", + "items": { + "description": "The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)", + "properties": { + "podAffinityTerm": { + "description": "Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running", + "properties": { + "labelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaceSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaces": { + "description": "namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means \"this pod's namespace\".", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "topologyKey": { + "description": "This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed.", + "type": "string" + } + }, + "required": [ + "topologyKey" + ], + "type": "object", + "additionalProperties": false + }, + "weight": { + "description": "weight associated with matching the corresponding podAffinityTerm, in the range 1-100.", + "format": "int32", + "type": "integer" + } + }, + "required": [ + "weight", + "podAffinityTerm" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "requiredDuringSchedulingIgnoredDuringExecution": { + "description": "If the anti-affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the anti-affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied.", + "items": { + "description": "Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running", + "properties": { + "labelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaceSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaces": { + "description": "namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means \"this pod's namespace\".", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "topologyKey": { + "description": "This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed.", + "type": "string" + } + }, + "required": [ + "topologyKey" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "automountServiceAccountToken": { + "description": "AutomountServiceAccountToken indicates whether a service account token should be automatically mounted.", + "type": [ + "boolean", + "null" + ] + }, + "containers": { + "description": "List of containers belonging to the pod. Containers cannot currently be added or removed. There must be at least one container in a Pod. Cannot be updated.", + "items": { + "description": "A single application container that you want to run within a pod.", + "properties": { + "args": { + "description": "Arguments to the entrypoint. The container image's CMD is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "command": { + "description": "Entrypoint array. Not executed within a shell. The container image's ENTRYPOINT is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "env": { + "description": "List of environment variables to set in the container. Cannot be updated.", + "items": { + "description": "EnvVar represents an environment variable present in a Container.", + "properties": { + "name": { + "description": "Name of the environment variable. Must be a C_IDENTIFIER.", + "type": "string" + }, + "value": { + "description": "Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to \"\".", + "type": [ + "string", + "null" + ] + }, + "valueFrom": { + "description": "EnvVarSource represents a source for the value of an EnvVar.", + "properties": { + "configMapKeyRef": { + "description": "Selects a key from a ConfigMap.", + "properties": { + "key": { + "description": "The key to select.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "fieldRef": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": [ + "string", + "null" + ] + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "resourceFieldRef": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": [ + "string", + "null" + ] + }, + "divisor": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "secretKeyRef": { + "description": "SecretKeySelector selects a key of a Secret.", + "properties": { + "key": { + "description": "The key of the secret to select from. Must be a valid secret key.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "envFrom": { + "description": "List of sources to populate environment variables in the container. The keys defined within a source must be a C_IDENTIFIER. All invalid keys will be reported as an event when the container is starting. When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated.", + "items": { + "description": "EnvFromSource represents the source of a set of ConfigMaps", + "properties": { + "configMapRef": { + "description": "ConfigMapEnvSource selects a ConfigMap to populate the environment variables with.\n\nThe contents of the target ConfigMap's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "prefix": { + "description": "An optional identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER.", + "type": [ + "string", + "null" + ] + }, + "secretRef": { + "description": "SecretEnvSource selects a Secret to populate the environment variables with.\n\nThe contents of the target Secret's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "image": { + "description": "Container image name. More info: https://kubernetes.io/docs/concepts/containers/images This field is optional to allow higher level config management to default or override container images in workload controllers like Deployments and StatefulSets.", + "type": [ + "string", + "null" + ] + }, + "imagePullPolicy": { + "description": "Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. Cannot be updated. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images", + "type": [ + "string", + "null" + ] + }, + "lifecycle": { + "description": "Lifecycle describes actions that the management system should take in response to container lifecycle events. For the PostStart and PreStop lifecycle handlers, management of the container blocks until the action is complete, unless the container process fails, in which case the handler is aborted.", + "properties": { + "postStart": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "preStop": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "livenessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "Name of the container specified as a DNS_LABEL. Each container in a pod must have a unique name (DNS_LABEL). Cannot be updated.", + "type": "string" + }, + "ports": { + "description": "List of ports to expose from the container. Not specifying a port here DOES NOT prevent that port from being exposed. Any port which is listening on the default \"0.0.0.0\" address inside a container will be accessible from the network. Modifying this array with strategic merge patch may corrupt the data. For more information See https://github.com/kubernetes/kubernetes/issues/108255. Cannot be updated.", + "items": { + "description": "ContainerPort represents a network port in a single container.", + "properties": { + "containerPort": { + "description": "Number of port to expose on the pod's IP address. This must be a valid port number, 0 < x < 65536.", + "format": "int32", + "type": "integer" + }, + "hostIP": { + "description": "What host IP to bind the external port to.", + "type": [ + "string", + "null" + ] + }, + "hostPort": { + "description": "Number of port to expose on the host. If specified, this must be a valid port number, 0 < x < 65536. If HostNetwork is specified, this must match ContainerPort. Most containers do not need this.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "name": { + "description": "If specified, this must be an IANA_SVC_NAME and unique within the pod. Each named port in a pod must have a unique name. Name for the port that can be referred to by services.", + "type": [ + "string", + "null" + ] + }, + "protocol": { + "description": "Protocol for port. Must be UDP, TCP, or SCTP. Defaults to \"TCP\".", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "containerPort" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "containerPort", + "protocol" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "containerPort", + "x-kubernetes-patch-strategy": "merge" + }, + "readinessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "resizePolicy": { + "description": "Resources resize policy for the container.", + "items": { + "description": "ContainerResizePolicy represents resource resize policy for the container.", + "properties": { + "resourceName": { + "description": "Name of the resource to which this resource resize policy applies. Supported values: cpu, memory.", + "type": "string" + }, + "restartPolicy": { + "description": "Restart policy to apply when specified resource is resized. If not specified, it defaults to NotRequired.", + "type": "string" + } + }, + "required": [ + "resourceName", + "restartPolicy" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "restartPolicy": { + "description": "RestartPolicy defines the restart behavior of individual containers in a pod. This field may only be set for init containers, and the only allowed value is \"Always\". For non-init containers or when this field is not specified, the restart behavior is defined by the Pod's restart policy and the container type. Setting the RestartPolicy as \"Always\" for the init container will have the following effect: this init container will be continually restarted on exit until all regular containers have terminated. Once all regular containers have completed, all init containers with restartPolicy \"Always\" will be shut down. This lifecycle differs from normal init containers and is often referred to as a \"sidecar\" container. Although this init container still starts in the init container sequence, it does not wait for the container to complete before proceeding to the next init container. Instead, the next init container starts immediately after this init container is started, or after any startupProbe has successfully completed.", + "type": [ + "string", + "null" + ] + }, + "securityContext": { + "description": "SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. When both are set, the values in SecurityContext take precedence.", + "properties": { + "allowPrivilegeEscalation": { + "description": "AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "capabilities": { + "description": "Adds and removes POSIX capabilities from running containers.", + "properties": { + "add": { + "description": "Added capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "drop": { + "description": "Removed capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "privileged": { + "description": "Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "procMount": { + "description": "procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "string", + "null" + ] + }, + "readOnlyRootFilesystem": { + "description": "Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "runAsGroup": { + "description": "The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "runAsNonRoot": { + "description": "Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUser": { + "description": "The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "seLinuxOptions": { + "description": "SELinuxOptions are the labels to be applied to the container", + "properties": { + "level": { + "description": "Level is SELinux level label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "role": { + "description": "Role is a SELinux role label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "Type is a SELinux type label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "user": { + "description": "User is a SELinux user label that applies to the container.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "seccompProfile": { + "description": "SeccompProfile defines a pod/container's seccomp profile settings. Only one profile source may be set.", + "properties": { + "localhostProfile": { + "description": "localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is \"Localhost\". Must NOT be set for any other type.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "type indicates which kind of seccomp profile will be applied. Valid options are:\n\nLocalhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-unions": [ + { + "discriminator": "type", + "fields-to-discriminateBy": { + "localhostProfile": "LocalhostProfile" + } + } + ], + "additionalProperties": false + }, + "windowsOptions": { + "description": "WindowsSecurityContextOptions contain Windows-specific options and credentials.", + "properties": { + "gmsaCredentialSpec": { + "description": "GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.", + "type": [ + "string", + "null" + ] + }, + "gmsaCredentialSpecName": { + "description": "GMSACredentialSpecName is the name of the GMSA credential spec to use.", + "type": [ + "string", + "null" + ] + }, + "hostProcess": { + "description": "HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUserName": { + "description": "The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "startupProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "stdin": { + "description": "Whether this container should allocate a buffer for stdin in the container runtime. If this is not set, reads from stdin in the container will always result in EOF. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "stdinOnce": { + "description": "Whether the container runtime should close the stdin channel after it has been opened by a single attach. When stdin is true the stdin stream will remain open across multiple attach sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the first client attaches to stdin, and then remains open and accepts data until the client disconnects, at which time stdin is closed and remains closed until the container is restarted. If this flag is false, a container processes that reads from stdin will never receive an EOF. Default is false", + "type": [ + "boolean", + "null" + ] + }, + "terminationMessagePath": { + "description": "Optional: Path at which the file to which the container's termination message will be written is mounted into the container's filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "terminationMessagePolicy": { + "description": "Indicate how the termination message should be populated. File will use the contents of terminationMessagePath to populate the container status message on both success and failure. FallbackToLogsOnError will use the last chunk of container log output if the termination message file is empty and the container exited with an error. The log output is limited to 2048 bytes or 80 lines, whichever is smaller. Defaults to File. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "tty": { + "description": "Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "volumeDevices": { + "description": "volumeDevices is the list of block devices to be used by the container.", + "items": { + "description": "volumeDevice describes a mapping of a raw block device within a container.", + "properties": { + "devicePath": { + "description": "devicePath is the path inside of the container that the device will be mapped to.", + "type": "string" + }, + "name": { + "description": "name must match the name of a persistentVolumeClaim in the pod", + "type": "string" + } + }, + "required": [ + "name", + "devicePath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "devicePath", + "x-kubernetes-patch-strategy": "merge" + }, + "volumeMounts": { + "description": "Pod volumes to mount into the container's filesystem. Cannot be updated.", + "items": { + "description": "VolumeMount describes a mounting of a Volume within a container.", + "properties": { + "mountPath": { + "description": "Path within the container at which the volume should be mounted. Must not contain ':'.", + "type": "string" + }, + "mountPropagation": { + "description": "mountPropagation determines how mounts are propagated from the host to container and the other way around. When not set, MountPropagationNone is used. This field is beta in 1.10.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "This must match the Name of a Volume.", + "type": "string" + }, + "readOnly": { + "description": "Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, + "subPath": { + "description": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).", + "type": [ + "string", + "null" + ] + }, + "subPathExpr": { + "description": "Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "mountPath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "mountPath", + "x-kubernetes-patch-strategy": "merge" + }, + "workingDir": { + "description": "Container's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": "array", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "dnsConfig": { + "description": "PodDNSConfig defines the DNS parameters of a pod in addition to those generated from DNSPolicy.", + "properties": { + "nameservers": { + "description": "A list of DNS name server IP addresses. This will be appended to the base nameservers generated from DNSPolicy. Duplicated nameservers will be removed.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "options": { + "description": "A list of DNS resolver options. This will be merged with the base options generated from DNSPolicy. Duplicated entries will be removed. Resolution options given in Options will override those that appear in the base DNSPolicy.", + "items": { + "description": "PodDNSConfigOption defines DNS resolver options of a pod.", + "properties": { + "name": { + "description": "Required.", + "type": [ + "string", + "null" + ] + }, + "value": { + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "searches": { + "description": "A list of DNS search domains for host-name lookup. This will be appended to the base search paths generated from DNSPolicy. Duplicated search paths will be removed.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "dnsPolicy": { + "description": "Set DNS policy for the pod. Defaults to \"ClusterFirst\". Valid values are 'ClusterFirstWithHostNet', 'ClusterFirst', 'Default' or 'None'. DNS parameters given in DNSConfig will be merged with the policy selected with DNSPolicy. To have DNS options set along with hostNetwork, you have to specify DNS policy explicitly to 'ClusterFirstWithHostNet'.", + "type": [ + "string", + "null" + ] + }, + "enableServiceLinks": { + "description": "EnableServiceLinks indicates whether information about services should be injected into pod's environment variables, matching the syntax of Docker links. Optional: Defaults to true.", + "type": [ + "boolean", + "null" + ] + }, + "ephemeralContainers": { + "description": "List of ephemeral containers run in this pod. Ephemeral containers may be run in an existing pod to perform user-initiated actions such as debugging. This list cannot be specified when creating a pod, and it cannot be modified by updating the pod spec. In order to add an ephemeral container to an existing pod, use the pod's ephemeralcontainers subresource.", + "items": { + "description": "An EphemeralContainer is a temporary container that you may add to an existing Pod for user-initiated activities such as debugging. Ephemeral containers have no resource or scheduling guarantees, and they will not be restarted when they exit or when a Pod is removed or restarted. The kubelet may evict a Pod if an ephemeral container causes the Pod to exceed its resource allocation.\n\nTo add an ephemeral container, use the ephemeralcontainers subresource of an existing Pod. Ephemeral containers may not be removed or restarted.", + "properties": { + "args": { + "description": "Arguments to the entrypoint. The image's CMD is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "command": { + "description": "Entrypoint array. Not executed within a shell. The image's ENTRYPOINT is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "env": { + "description": "List of environment variables to set in the container. Cannot be updated.", + "items": { + "description": "EnvVar represents an environment variable present in a Container.", + "properties": { + "name": { + "description": "Name of the environment variable. Must be a C_IDENTIFIER.", + "type": "string" + }, + "value": { + "description": "Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to \"\".", + "type": [ + "string", + "null" + ] + }, + "valueFrom": { + "description": "EnvVarSource represents a source for the value of an EnvVar.", + "properties": { + "configMapKeyRef": { + "description": "Selects a key from a ConfigMap.", + "properties": { + "key": { + "description": "The key to select.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "fieldRef": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": [ + "string", + "null" + ] + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "resourceFieldRef": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": [ + "string", + "null" + ] + }, + "divisor": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "secretKeyRef": { + "description": "SecretKeySelector selects a key of a Secret.", + "properties": { + "key": { + "description": "The key of the secret to select from. Must be a valid secret key.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "envFrom": { + "description": "List of sources to populate environment variables in the container. The keys defined within a source must be a C_IDENTIFIER. All invalid keys will be reported as an event when the container is starting. When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated.", + "items": { + "description": "EnvFromSource represents the source of a set of ConfigMaps", + "properties": { + "configMapRef": { + "description": "ConfigMapEnvSource selects a ConfigMap to populate the environment variables with.\n\nThe contents of the target ConfigMap's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "prefix": { + "description": "An optional identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER.", + "type": [ + "string", + "null" + ] + }, + "secretRef": { + "description": "SecretEnvSource selects a Secret to populate the environment variables with.\n\nThe contents of the target Secret's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "image": { + "description": "Container image name. More info: https://kubernetes.io/docs/concepts/containers/images", + "type": [ + "string", + "null" + ] + }, + "imagePullPolicy": { + "description": "Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. Cannot be updated. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images", + "type": [ + "string", + "null" + ] + }, + "lifecycle": { + "description": "Lifecycle describes actions that the management system should take in response to container lifecycle events. For the PostStart and PreStop lifecycle handlers, management of the container blocks until the action is complete, unless the container process fails, in which case the handler is aborted.", + "properties": { + "postStart": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "preStop": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "livenessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "Name of the ephemeral container specified as a DNS_LABEL. This name must be unique among all containers, init containers and ephemeral containers.", + "type": "string" + }, + "ports": { + "description": "Ports are not allowed for ephemeral containers.", + "items": { + "description": "ContainerPort represents a network port in a single container.", + "properties": { + "containerPort": { + "description": "Number of port to expose on the pod's IP address. This must be a valid port number, 0 < x < 65536.", + "format": "int32", + "type": "integer" + }, + "hostIP": { + "description": "What host IP to bind the external port to.", + "type": [ + "string", + "null" + ] + }, + "hostPort": { + "description": "Number of port to expose on the host. If specified, this must be a valid port number, 0 < x < 65536. If HostNetwork is specified, this must match ContainerPort. Most containers do not need this.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "name": { + "description": "If specified, this must be an IANA_SVC_NAME and unique within the pod. Each named port in a pod must have a unique name. Name for the port that can be referred to by services.", + "type": [ + "string", + "null" + ] + }, + "protocol": { + "description": "Protocol for port. Must be UDP, TCP, or SCTP. Defaults to \"TCP\".", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "containerPort" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "containerPort", + "protocol" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "containerPort", + "x-kubernetes-patch-strategy": "merge" + }, + "readinessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "resizePolicy": { + "description": "Resources resize policy for the container.", + "items": { + "description": "ContainerResizePolicy represents resource resize policy for the container.", + "properties": { + "resourceName": { + "description": "Name of the resource to which this resource resize policy applies. Supported values: cpu, memory.", + "type": "string" + }, + "restartPolicy": { + "description": "Restart policy to apply when specified resource is resized. If not specified, it defaults to NotRequired.", + "type": "string" + } + }, + "required": [ + "resourceName", + "restartPolicy" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "restartPolicy": { + "description": "Restart policy for the container to manage the restart behavior of each container within a pod. This may only be set for init containers. You cannot set this field on ephemeral containers.", + "type": [ + "string", + "null" + ] + }, + "securityContext": { + "description": "SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. When both are set, the values in SecurityContext take precedence.", + "properties": { + "allowPrivilegeEscalation": { + "description": "AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "capabilities": { + "description": "Adds and removes POSIX capabilities from running containers.", + "properties": { + "add": { + "description": "Added capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "drop": { + "description": "Removed capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "privileged": { + "description": "Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "procMount": { + "description": "procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "string", + "null" + ] + }, + "readOnlyRootFilesystem": { + "description": "Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "runAsGroup": { + "description": "The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "runAsNonRoot": { + "description": "Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUser": { + "description": "The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "seLinuxOptions": { + "description": "SELinuxOptions are the labels to be applied to the container", + "properties": { + "level": { + "description": "Level is SELinux level label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "role": { + "description": "Role is a SELinux role label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "Type is a SELinux type label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "user": { + "description": "User is a SELinux user label that applies to the container.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "seccompProfile": { + "description": "SeccompProfile defines a pod/container's seccomp profile settings. Only one profile source may be set.", + "properties": { + "localhostProfile": { + "description": "localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is \"Localhost\". Must NOT be set for any other type.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "type indicates which kind of seccomp profile will be applied. Valid options are:\n\nLocalhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-unions": [ + { + "discriminator": "type", + "fields-to-discriminateBy": { + "localhostProfile": "LocalhostProfile" + } + } + ], + "additionalProperties": false + }, + "windowsOptions": { + "description": "WindowsSecurityContextOptions contain Windows-specific options and credentials.", + "properties": { + "gmsaCredentialSpec": { + "description": "GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.", + "type": [ + "string", + "null" + ] + }, + "gmsaCredentialSpecName": { + "description": "GMSACredentialSpecName is the name of the GMSA credential spec to use.", + "type": [ + "string", + "null" + ] + }, + "hostProcess": { + "description": "HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUserName": { + "description": "The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "startupProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "stdin": { + "description": "Whether this container should allocate a buffer for stdin in the container runtime. If this is not set, reads from stdin in the container will always result in EOF. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "stdinOnce": { + "description": "Whether the container runtime should close the stdin channel after it has been opened by a single attach. When stdin is true the stdin stream will remain open across multiple attach sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the first client attaches to stdin, and then remains open and accepts data until the client disconnects, at which time stdin is closed and remains closed until the container is restarted. If this flag is false, a container processes that reads from stdin will never receive an EOF. Default is false", + "type": [ + "boolean", + "null" + ] + }, + "targetContainerName": { + "description": "If set, the name of the container from PodSpec that this ephemeral container targets. The ephemeral container will be run in the namespaces (IPC, PID, etc) of this container. If not set then the ephemeral container uses the namespaces configured in the Pod spec.\n\nThe container runtime must implement support for this feature. If the runtime does not support namespace targeting then the result of setting this field is undefined.", + "type": [ + "string", + "null" + ] + }, + "terminationMessagePath": { + "description": "Optional: Path at which the file to which the container's termination message will be written is mounted into the container's filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "terminationMessagePolicy": { + "description": "Indicate how the termination message should be populated. File will use the contents of terminationMessagePath to populate the container status message on both success and failure. FallbackToLogsOnError will use the last chunk of container log output if the termination message file is empty and the container exited with an error. The log output is limited to 2048 bytes or 80 lines, whichever is smaller. Defaults to File. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "tty": { + "description": "Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "volumeDevices": { + "description": "volumeDevices is the list of block devices to be used by the container.", + "items": { + "description": "volumeDevice describes a mapping of a raw block device within a container.", + "properties": { + "devicePath": { + "description": "devicePath is the path inside of the container that the device will be mapped to.", + "type": "string" + }, + "name": { + "description": "name must match the name of a persistentVolumeClaim in the pod", + "type": "string" + } + }, + "required": [ + "name", + "devicePath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "devicePath", + "x-kubernetes-patch-strategy": "merge" + }, + "volumeMounts": { + "description": "Pod volumes to mount into the container's filesystem. Subpath mounts are not allowed for ephemeral containers. Cannot be updated.", + "items": { + "description": "VolumeMount describes a mounting of a Volume within a container.", + "properties": { + "mountPath": { + "description": "Path within the container at which the volume should be mounted. Must not contain ':'.", + "type": "string" + }, + "mountPropagation": { + "description": "mountPropagation determines how mounts are propagated from the host to container and the other way around. When not set, MountPropagationNone is used. This field is beta in 1.10.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "This must match the Name of a Volume.", + "type": "string" + }, + "readOnly": { + "description": "Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, + "subPath": { + "description": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).", + "type": [ + "string", + "null" + ] + }, + "subPathExpr": { + "description": "Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "mountPath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "mountPath", + "x-kubernetes-patch-strategy": "merge" + }, + "workingDir": { + "description": "Container's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "hostAliases": { + "description": "HostAliases is an optional list of hosts and IPs that will be injected into the pod's hosts file if specified. This is only valid for non-hostNetwork pods.", + "items": { + "description": "HostAlias holds the mapping between IP and hostnames that will be injected as an entry in the pod's hosts file.", + "properties": { + "hostnames": { + "description": "Hostnames for the above IP address.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "ip": { + "description": "IP address of the host file entry.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "ip", + "x-kubernetes-patch-strategy": "merge" + }, + "hostIPC": { + "description": "Use the host's ipc namespace. Optional: Default to false.", + "type": [ + "boolean", + "null" + ] + }, + "hostNetwork": { + "description": "Host networking requested for this pod. Use the host's network namespace. If this option is set, the ports that will be used must be specified. Default to false.", + "type": [ + "boolean", + "null" + ] + }, + "hostPID": { + "description": "Use the host's pid namespace. Optional: Default to false.", + "type": [ + "boolean", + "null" + ] + }, + "hostUsers": { + "description": "Use the host's user namespace. Optional: Default to true. If set to true or not present, the pod will be run in the host user namespace, useful for when the pod needs a feature only available to the host user namespace, such as loading a kernel module with CAP_SYS_MODULE. When set to false, a new userns is created for the pod. Setting false is useful for mitigating container breakout vulnerabilities even allowing users to run their containers as root without actually having root privileges on the host. This field is alpha-level and is only honored by servers that enable the UserNamespacesSupport feature.", + "type": [ + "boolean", + "null" + ] + }, + "hostname": { + "description": "Specifies the hostname of the Pod If not specified, the pod's hostname will be set to a system-defined value.", + "type": [ + "string", + "null" + ] + }, + "imagePullSecrets": { + "description": "ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. If specified, these secrets will be passed to individual puller implementations for them to use. More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod", + "items": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "initContainers": { + "description": "List of initialization containers belonging to the pod. Init containers are executed in order prior to containers being started. If any init container fails, the pod is considered to have failed and is handled according to its restartPolicy. The name for an init container or normal container must be unique among all containers. Init containers may not have Lifecycle actions, Readiness probes, Liveness probes, or Startup probes. The resourceRequirements of an init container are taken into account during scheduling by finding the highest request/limit for each resource type, and then using the max of of that value or the sum of the normal containers. Limits are applied to init containers in a similar fashion. Init containers cannot currently be added or removed. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/", + "items": { + "description": "A single application container that you want to run within a pod.", + "properties": { + "args": { + "description": "Arguments to the entrypoint. The container image's CMD is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "command": { + "description": "Entrypoint array. Not executed within a shell. The container image's ENTRYPOINT is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "env": { + "description": "List of environment variables to set in the container. Cannot be updated.", + "items": { + "description": "EnvVar represents an environment variable present in a Container.", + "properties": { + "name": { + "description": "Name of the environment variable. Must be a C_IDENTIFIER.", + "type": "string" + }, + "value": { + "description": "Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to \"\".", + "type": [ + "string", + "null" + ] + }, + "valueFrom": { + "description": "EnvVarSource represents a source for the value of an EnvVar.", + "properties": { + "configMapKeyRef": { + "description": "Selects a key from a ConfigMap.", + "properties": { + "key": { + "description": "The key to select.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "fieldRef": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": [ + "string", + "null" + ] + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "resourceFieldRef": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": [ + "string", + "null" + ] + }, + "divisor": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "secretKeyRef": { + "description": "SecretKeySelector selects a key of a Secret.", + "properties": { + "key": { + "description": "The key of the secret to select from. Must be a valid secret key.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "envFrom": { + "description": "List of sources to populate environment variables in the container. The keys defined within a source must be a C_IDENTIFIER. All invalid keys will be reported as an event when the container is starting. When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated.", + "items": { + "description": "EnvFromSource represents the source of a set of ConfigMaps", + "properties": { + "configMapRef": { + "description": "ConfigMapEnvSource selects a ConfigMap to populate the environment variables with.\n\nThe contents of the target ConfigMap's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "prefix": { + "description": "An optional identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER.", + "type": [ + "string", + "null" + ] + }, + "secretRef": { + "description": "SecretEnvSource selects a Secret to populate the environment variables with.\n\nThe contents of the target Secret's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "image": { + "description": "Container image name. More info: https://kubernetes.io/docs/concepts/containers/images This field is optional to allow higher level config management to default or override container images in workload controllers like Deployments and StatefulSets.", + "type": [ + "string", + "null" + ] + }, + "imagePullPolicy": { + "description": "Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. Cannot be updated. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images", + "type": [ + "string", + "null" + ] + }, + "lifecycle": { + "description": "Lifecycle describes actions that the management system should take in response to container lifecycle events. For the PostStart and PreStop lifecycle handlers, management of the container blocks until the action is complete, unless the container process fails, in which case the handler is aborted.", + "properties": { + "postStart": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "preStop": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "livenessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "Name of the container specified as a DNS_LABEL. Each container in a pod must have a unique name (DNS_LABEL). Cannot be updated.", + "type": "string" + }, + "ports": { + "description": "List of ports to expose from the container. Not specifying a port here DOES NOT prevent that port from being exposed. Any port which is listening on the default \"0.0.0.0\" address inside a container will be accessible from the network. Modifying this array with strategic merge patch may corrupt the data. For more information See https://github.com/kubernetes/kubernetes/issues/108255. Cannot be updated.", + "items": { + "description": "ContainerPort represents a network port in a single container.", + "properties": { + "containerPort": { + "description": "Number of port to expose on the pod's IP address. This must be a valid port number, 0 < x < 65536.", + "format": "int32", + "type": "integer" + }, + "hostIP": { + "description": "What host IP to bind the external port to.", + "type": [ + "string", + "null" + ] + }, + "hostPort": { + "description": "Number of port to expose on the host. If specified, this must be a valid port number, 0 < x < 65536. If HostNetwork is specified, this must match ContainerPort. Most containers do not need this.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "name": { + "description": "If specified, this must be an IANA_SVC_NAME and unique within the pod. Each named port in a pod must have a unique name. Name for the port that can be referred to by services.", + "type": [ + "string", + "null" + ] + }, + "protocol": { + "description": "Protocol for port. Must be UDP, TCP, or SCTP. Defaults to \"TCP\".", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "containerPort" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "containerPort", + "protocol" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "containerPort", + "x-kubernetes-patch-strategy": "merge" + }, + "readinessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "resizePolicy": { + "description": "Resources resize policy for the container.", + "items": { + "description": "ContainerResizePolicy represents resource resize policy for the container.", + "properties": { + "resourceName": { + "description": "Name of the resource to which this resource resize policy applies. Supported values: cpu, memory.", + "type": "string" + }, + "restartPolicy": { + "description": "Restart policy to apply when specified resource is resized. If not specified, it defaults to NotRequired.", + "type": "string" + } + }, + "required": [ + "resourceName", + "restartPolicy" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "restartPolicy": { + "description": "RestartPolicy defines the restart behavior of individual containers in a pod. This field may only be set for init containers, and the only allowed value is \"Always\". For non-init containers or when this field is not specified, the restart behavior is defined by the Pod's restart policy and the container type. Setting the RestartPolicy as \"Always\" for the init container will have the following effect: this init container will be continually restarted on exit until all regular containers have terminated. Once all regular containers have completed, all init containers with restartPolicy \"Always\" will be shut down. This lifecycle differs from normal init containers and is often referred to as a \"sidecar\" container. Although this init container still starts in the init container sequence, it does not wait for the container to complete before proceeding to the next init container. Instead, the next init container starts immediately after this init container is started, or after any startupProbe has successfully completed.", + "type": [ + "string", + "null" + ] + }, + "securityContext": { + "description": "SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. When both are set, the values in SecurityContext take precedence.", + "properties": { + "allowPrivilegeEscalation": { + "description": "AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "capabilities": { + "description": "Adds and removes POSIX capabilities from running containers.", + "properties": { + "add": { + "description": "Added capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "drop": { + "description": "Removed capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "privileged": { + "description": "Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "procMount": { + "description": "procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "string", + "null" + ] + }, + "readOnlyRootFilesystem": { + "description": "Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "runAsGroup": { + "description": "The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "runAsNonRoot": { + "description": "Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUser": { + "description": "The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "seLinuxOptions": { + "description": "SELinuxOptions are the labels to be applied to the container", + "properties": { + "level": { + "description": "Level is SELinux level label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "role": { + "description": "Role is a SELinux role label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "Type is a SELinux type label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "user": { + "description": "User is a SELinux user label that applies to the container.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "seccompProfile": { + "description": "SeccompProfile defines a pod/container's seccomp profile settings. Only one profile source may be set.", + "properties": { + "localhostProfile": { + "description": "localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is \"Localhost\". Must NOT be set for any other type.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "type indicates which kind of seccomp profile will be applied. Valid options are:\n\nLocalhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-unions": [ + { + "discriminator": "type", + "fields-to-discriminateBy": { + "localhostProfile": "LocalhostProfile" + } + } + ], + "additionalProperties": false + }, + "windowsOptions": { + "description": "WindowsSecurityContextOptions contain Windows-specific options and credentials.", + "properties": { + "gmsaCredentialSpec": { + "description": "GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.", + "type": [ + "string", + "null" + ] + }, + "gmsaCredentialSpecName": { + "description": "GMSACredentialSpecName is the name of the GMSA credential spec to use.", + "type": [ + "string", + "null" + ] + }, + "hostProcess": { + "description": "HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUserName": { + "description": "The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "startupProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "stdin": { + "description": "Whether this container should allocate a buffer for stdin in the container runtime. If this is not set, reads from stdin in the container will always result in EOF. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "stdinOnce": { + "description": "Whether the container runtime should close the stdin channel after it has been opened by a single attach. When stdin is true the stdin stream will remain open across multiple attach sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the first client attaches to stdin, and then remains open and accepts data until the client disconnects, at which time stdin is closed and remains closed until the container is restarted. If this flag is false, a container processes that reads from stdin will never receive an EOF. Default is false", + "type": [ + "boolean", + "null" + ] + }, + "terminationMessagePath": { + "description": "Optional: Path at which the file to which the container's termination message will be written is mounted into the container's filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "terminationMessagePolicy": { + "description": "Indicate how the termination message should be populated. File will use the contents of terminationMessagePath to populate the container status message on both success and failure. FallbackToLogsOnError will use the last chunk of container log output if the termination message file is empty and the container exited with an error. The log output is limited to 2048 bytes or 80 lines, whichever is smaller. Defaults to File. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "tty": { + "description": "Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "volumeDevices": { + "description": "volumeDevices is the list of block devices to be used by the container.", + "items": { + "description": "volumeDevice describes a mapping of a raw block device within a container.", + "properties": { + "devicePath": { + "description": "devicePath is the path inside of the container that the device will be mapped to.", + "type": "string" + }, + "name": { + "description": "name must match the name of a persistentVolumeClaim in the pod", + "type": "string" + } + }, + "required": [ + "name", + "devicePath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "devicePath", + "x-kubernetes-patch-strategy": "merge" + }, + "volumeMounts": { + "description": "Pod volumes to mount into the container's filesystem. Cannot be updated.", + "items": { + "description": "VolumeMount describes a mounting of a Volume within a container.", + "properties": { + "mountPath": { + "description": "Path within the container at which the volume should be mounted. Must not contain ':'.", + "type": "string" + }, + "mountPropagation": { + "description": "mountPropagation determines how mounts are propagated from the host to container and the other way around. When not set, MountPropagationNone is used. This field is beta in 1.10.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "This must match the Name of a Volume.", + "type": "string" + }, + "readOnly": { + "description": "Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, + "subPath": { + "description": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).", + "type": [ + "string", + "null" + ] + }, + "subPathExpr": { + "description": "Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "mountPath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "mountPath", + "x-kubernetes-patch-strategy": "merge" + }, + "workingDir": { + "description": "Container's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "nodeName": { + "description": "NodeName is a request to schedule this pod onto a specific node. If it is non-empty, the scheduler simply schedules this pod onto that node, assuming that it fits resource requirements.", + "type": [ + "string", + "null" + ] + }, + "nodeSelector": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "NodeSelector is a selector which must be true for the pod to fit on a node. Selector which must match a node's labels for the pod to be scheduled on that node. More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/", + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic" + }, + "os": { + "description": "PodOS defines the OS parameters of a pod.", + "properties": { + "name": { + "description": "Name is the name of the operating system. The currently supported values are linux and windows. Additional value may be defined in future and can be one of: https://github.com/opencontainers/runtime-spec/blob/master/config.md#platform-specific-configuration Clients should expect to handle additional values and treat unrecognized values in this field as os: null", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "overhead": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Overhead represents the resource overhead associated with running a pod for a given RuntimeClass. This field will be autopopulated at admission time by the RuntimeClass admission controller. If the RuntimeClass admission controller is enabled, overhead must not be set in Pod create requests. The RuntimeClass admission controller will reject Pod create requests which have the overhead already set. If RuntimeClass is configured and selected in the PodSpec, Overhead will be set to the value defined in the corresponding RuntimeClass, otherwise it will remain unset and treated as zero. More info: https://git.k8s.io/enhancements/keps/sig-node/688-pod-overhead/README.md", + "type": [ + "object", + "null" + ] + }, + "preemptionPolicy": { + "description": "PreemptionPolicy is the Policy for preempting pods with lower priority. One of Never, PreemptLowerPriority. Defaults to PreemptLowerPriority if unset.", + "type": [ + "string", + "null" + ] + }, + "priority": { + "description": "The priority value. Various system components use this field to find the priority of the pod. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "priorityClassName": { + "description": "If specified, indicates the pod's priority. \"system-node-critical\" and \"system-cluster-critical\" are two special keywords which indicate the highest priorities with the former being the highest priority. Any other name must be defined by creating a PriorityClass object with that name. If not specified, the pod priority will be default or zero if there is no default.", + "type": [ + "string", + "null" + ] + }, + "readinessGates": { + "description": "If specified, all readiness gates will be evaluated for pod readiness. A pod is ready when all its containers are ready AND all conditions specified in the readiness gates have status equal to \"True\" More info: https://git.k8s.io/enhancements/keps/sig-network/580-pod-readiness-gates", + "items": { + "description": "PodReadinessGate contains the reference to a pod condition", + "properties": { + "conditionType": { + "description": "ConditionType refers to a condition in the pod's condition list with matching type.", + "type": "string" + } + }, + "required": [ + "conditionType" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "resourceClaims": { + "description": "ResourceClaims defines which ResourceClaims must be allocated and reserved before the Pod is allowed to start. The resources will be made available to those containers which consume them by name.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable.", + "items": { + "description": "PodResourceClaim references exactly one ResourceClaim through a ClaimSource. It adds a name to it that uniquely identifies the ResourceClaim inside the Pod. Containers that need access to the ResourceClaim reference it with this name.", + "properties": { + "name": { + "description": "Name uniquely identifies this resource claim inside the pod. This must be a DNS_LABEL.", + "type": "string" + }, + "source": { + "description": "ClaimSource describes a reference to a ResourceClaim.\n\nExactly one of these fields should be set. Consumers of this type must treat an empty object as if it has an unknown value.", + "properties": { + "resourceClaimName": { + "description": "ResourceClaimName is the name of a ResourceClaim object in the same namespace as this pod.", + "type": [ + "string", + "null" + ] + }, + "resourceClaimTemplateName": { + "description": "ResourceClaimTemplateName is the name of a ResourceClaimTemplate object in the same namespace as this pod.\n\nThe template will be used to create a new ResourceClaim, which will be bound to this pod. When this pod is deleted, the ResourceClaim will also be deleted. The pod name and resource name, along with a generated component, will be used to form a unique name for the ResourceClaim, which will be recorded in pod.status.resourceClaimStatuses.\n\nThis field is immutable and no changes will be made to the corresponding ResourceClaim by the control plane after creating the ResourceClaim.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge,retainKeys" + }, + "restartPolicy": { + "description": "Restart policy for all containers within the pod. One of Always, OnFailure, Never. In some contexts, only a subset of those values may be permitted. Default to Always. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy", + "type": [ + "string", + "null" + ] + }, + "runtimeClassName": { + "description": "RuntimeClassName refers to a RuntimeClass object in the node.k8s.io group, which should be used to run this pod. If no RuntimeClass resource matches the named class, the pod will not be run. If unset or empty, the \"legacy\" RuntimeClass will be used, which is an implicit class with an empty definition that uses the default runtime handler. More info: https://git.k8s.io/enhancements/keps/sig-node/585-runtime-class", + "type": [ + "string", + "null" + ] + }, + "schedulerName": { + "description": "If specified, the pod will be dispatched by specified scheduler. If not specified, the pod will be dispatched by default scheduler.", + "type": [ + "string", + "null" + ] + }, + "schedulingGates": { + "description": "SchedulingGates is an opaque list of values that if specified will block scheduling the pod. If schedulingGates is not empty, the pod will stay in the SchedulingGated state and the scheduler will not attempt to schedule the pod.\n\nSchedulingGates can only be set at pod creation time, and be removed only afterwards.\n\nThis is a beta feature enabled by the PodSchedulingReadiness feature gate.", + "items": { + "description": "PodSchedulingGate is associated to a Pod to guard its scheduling.", + "properties": { + "name": { + "description": "Name of the scheduling gate. Each scheduling gate must have a unique name field.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "securityContext": { + "description": "PodSecurityContext holds pod-level security attributes and common container settings. Some fields are also present in container.securityContext. Field values of container.securityContext take precedence over field values of PodSecurityContext.", + "properties": { + "fsGroup": { + "description": "A special supplemental group that applies to all containers in a pod. Some volume types allow the Kubelet to change the ownership of that volume to be owned by the pod:\n\n1. The owning GID will be the FSGroup 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) 3. The permission bits are OR'd with rw-rw----\n\nIf unset, the Kubelet will not modify the ownership and permissions of any volume. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "fsGroupChangePolicy": { + "description": "fsGroupChangePolicy defines behavior of changing ownership and permission of the volume before being exposed inside Pod. This field will only apply to volume types which support fsGroup based ownership(and permissions). It will have no effect on ephemeral volume types such as: secret, configmaps and emptydir. Valid values are \"OnRootMismatch\" and \"Always\". If not specified, \"Always\" is used. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "string", + "null" + ] + }, + "runAsGroup": { + "description": "The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "runAsNonRoot": { + "description": "Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUser": { + "description": "The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "seLinuxOptions": { + "description": "SELinuxOptions are the labels to be applied to the container", + "properties": { + "level": { + "description": "Level is SELinux level label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "role": { + "description": "Role is a SELinux role label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "Type is a SELinux type label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "user": { + "description": "User is a SELinux user label that applies to the container.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "seccompProfile": { + "description": "SeccompProfile defines a pod/container's seccomp profile settings. Only one profile source may be set.", + "properties": { + "localhostProfile": { + "description": "localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is \"Localhost\". Must NOT be set for any other type.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "type indicates which kind of seccomp profile will be applied. Valid options are:\n\nLocalhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-unions": [ + { + "discriminator": "type", + "fields-to-discriminateBy": { + "localhostProfile": "LocalhostProfile" + } + } + ], + "additionalProperties": false + }, + "supplementalGroups": { + "description": "A list of groups applied to the first process run in each container, in addition to the container's primary GID, the fsGroup (if specified), and group memberships defined in the container image for the uid of the container process. If unspecified, no additional groups are added to any container. Note that group memberships defined in the container image for the uid of the container process are still effective, even if they are not included in this list. Note that this field cannot be set when spec.os.name is windows.", + "items": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "sysctls": { + "description": "Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported sysctls (by the container runtime) might fail to launch. Note that this field cannot be set when spec.os.name is windows.", + "items": { + "description": "Sysctl defines a kernel parameter to be set", + "properties": { + "name": { + "description": "Name of a property to set", + "type": "string" + }, + "value": { + "description": "Value of a property to set", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "windowsOptions": { + "description": "WindowsSecurityContextOptions contain Windows-specific options and credentials.", + "properties": { + "gmsaCredentialSpec": { + "description": "GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.", + "type": [ + "string", + "null" + ] + }, + "gmsaCredentialSpecName": { + "description": "GMSACredentialSpecName is the name of the GMSA credential spec to use.", + "type": [ + "string", + "null" + ] + }, + "hostProcess": { + "description": "HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUserName": { + "description": "The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "serviceAccount": { + "description": "DeprecatedServiceAccount is a depreciated alias for ServiceAccountName. Deprecated: Use serviceAccountName instead.", + "type": [ + "string", + "null" + ] + }, + "serviceAccountName": { + "description": "ServiceAccountName is the name of the ServiceAccount to use to run this pod. More info: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/", + "type": [ + "string", + "null" + ] + }, + "setHostnameAsFQDN": { + "description": "If true the pod's hostname will be configured as the pod's FQDN, rather than the leaf name (the default). In Linux containers, this means setting the FQDN in the hostname field of the kernel (the nodename field of struct utsname). In Windows containers, this means setting the registry value of hostname for the registry key HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters to FQDN. If a pod does not have FQDN, this has no effect. Default to false.", + "type": [ + "boolean", + "null" + ] + }, + "shareProcessNamespace": { + "description": "Share a single process namespace between all of the containers in a pod. When this is set containers will be able to view and signal processes from other containers in the same pod, and the first process in each container will not be assigned PID 1. HostPID and ShareProcessNamespace cannot both be set. Optional: Default to false.", + "type": [ + "boolean", + "null" + ] + }, + "subdomain": { + "description": "If specified, the fully qualified Pod hostname will be \"...svc.\". If not specified, the pod will not have a domainname at all.", + "type": [ + "string", + "null" + ] + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully. May be decreased in delete request. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). If this value is nil, the default grace period will be used instead. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. Defaults to 30 seconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "tolerations": { + "description": "If specified, the pod's tolerations.", + "items": { + "description": "The pod this Toleration is attached to tolerates any taint that matches the triple using the matching operator .", + "properties": { + "effect": { + "description": "Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.", + "type": [ + "string", + "null" + ] + }, + "key": { + "description": "Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys.", + "type": [ + "string", + "null" + ] + }, + "operator": { + "description": "Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category.", + "type": [ + "string", + "null" + ] + }, + "tolerationSeconds": { + "description": "TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "value": { + "description": "Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "topologySpreadConstraints": { + "description": "TopologySpreadConstraints describes how a group of pods ought to spread across topology domains. Scheduler will schedule pods in a way which abides by the constraints. All topologySpreadConstraints are ANDed.", + "items": { + "description": "TopologySpreadConstraint specifies how to spread matching pods among the given topology.", + "properties": { + "labelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "matchLabelKeys": { + "description": "MatchLabelKeys is a set of pod label keys to select the pods over which spreading will be calculated. The keys are used to lookup values from the incoming pod labels, those key-value labels are ANDed with labelSelector to select the group of existing pods over which spreading will be calculated for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. MatchLabelKeys cannot be set when LabelSelector isn't set. Keys that don't exist in the incoming pod labels will be ignored. A null or empty list means only match against labelSelector.\n\nThis is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default).", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "maxSkew": { + "description": "MaxSkew describes the degree to which pods may be unevenly distributed. When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference between the number of matching pods in the target topology and the global minimum. The global minimum is the minimum number of matching pods in an eligible domain or zero if the number of eligible domains is less than MinDomains. For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same labelSelector spread as 2/2/1: In this case, the global minimum is 1. | zone1 | zone2 | zone3 | | P P | P P | P | - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) violate MaxSkew(1). - if MaxSkew is 2, incoming pod can be scheduled onto any zone. When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence to topologies that satisfy it. It's a required field. Default value is 1 and 0 is not allowed.", + "format": "int32", + "type": "integer" + }, + "minDomains": { + "description": "MinDomains indicates a minimum number of eligible domains. When the number of eligible domains with matching topology keys is less than minDomains, Pod Topology Spread treats \"global minimum\" as 0, and then the calculation of Skew is performed. And when the number of eligible domains with matching topology keys equals or greater than minDomains, this value has no effect on scheduling. As a result, when the number of eligible domains is less than minDomains, scheduler won't schedule more than maxSkew Pods to those domains. If value is nil, the constraint behaves as if MinDomains is equal to 1. Valid values are integers greater than 0. When value is not nil, WhenUnsatisfiable must be DoNotSchedule.\n\nFor example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same labelSelector spread as 2/2/2: | zone1 | zone2 | zone3 | | P P | P P | P P | The number of domains is less than 5(MinDomains), so \"global minimum\" is treated as 0. In this situation, new pod with the same labelSelector cannot be scheduled, because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, it will violate MaxSkew.\n\nThis is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default).", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "nodeAffinityPolicy": { + "description": "NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector when calculating pod topology spread skew. Options are: - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations.\n\nIf this value is nil, the behavior is equivalent to the Honor policy. This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag.", + "type": [ + "string", + "null" + ] + }, + "nodeTaintsPolicy": { + "description": "NodeTaintsPolicy indicates how we will treat node taints when calculating pod topology spread skew. Options are: - Honor: nodes without taints, along with tainted nodes for which the incoming pod has a toleration, are included. - Ignore: node taints are ignored. All nodes are included.\n\nIf this value is nil, the behavior is equivalent to the Ignore policy. This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag.", + "type": [ + "string", + "null" + ] + }, + "topologyKey": { + "description": "TopologyKey is the key of node labels. Nodes that have a label with this key and identical values are considered to be in the same topology. We consider each as a \"bucket\", and try to put balanced number of pods into each bucket. We define a domain as a particular instance of a topology. Also, we define an eligible domain as a domain whose nodes meet the requirements of nodeAffinityPolicy and nodeTaintsPolicy. e.g. If TopologyKey is \"kubernetes.io/hostname\", each Node is a domain of that topology. And, if TopologyKey is \"topology.kubernetes.io/zone\", each zone is a domain of that topology. It's a required field.", + "type": "string" + }, + "whenUnsatisfiable": { + "description": "WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - DoNotSchedule (default) tells the scheduler not to schedule it. - ScheduleAnyway tells the scheduler to schedule the pod in any location,\n but giving higher precedence to topologies that would help reduce the\n skew.\nA constraint is considered \"Unsatisfiable\" for an incoming pod if and only if every possible node assignment for that pod would violate \"MaxSkew\" on some topology. For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same labelSelector spread as 3/1/1: | zone1 | zone2 | zone3 | | P P P | P | P | If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler won't make it *more* imbalanced. It's a required field.", + "type": "string" + } + }, + "required": [ + "maxSkew", + "topologyKey", + "whenUnsatisfiable" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "topologyKey", + "whenUnsatisfiable" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "topologyKey", + "x-kubernetes-patch-strategy": "merge" + }, + "volumes": { + "description": "List of volumes that can be mounted by containers belonging to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes", + "items": { + "description": "Volume represents a named volume in a pod that may be accessed by any container in the pod.", + "properties": { + "awsElasticBlockStore": { + "description": "Represents a Persistent Disk resource in AWS.\n\nAn AWS EBS disk must exist before mounting to a container. The disk must also be in the same AWS zone as the kubelet. An AWS EBS disk can only be mounted as read/write once. AWS EBS volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", + "type": [ + "string", + "null" + ] + }, + "partition": { + "description": "partition is the partition in the volume that you want to mount. If omitted, the default is to mount by volume name. Examples: For volume /dev/sda1, you specify the partition as \"1\". Similarly, the volume partition for /dev/sda is \"0\" (or you can leave the property empty).", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "readOnly": { + "description": "readOnly value true will force the readOnly setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", + "type": [ + "boolean", + "null" + ] + }, + "volumeID": { + "description": "volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", + "type": "string" + } + }, + "required": [ + "volumeID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "azureDisk": { + "description": "AzureDisk represents an Azure Data Disk mount on the host and bind mount to the pod.", + "properties": { + "cachingMode": { + "description": "cachingMode is the Host Caching mode: None, Read Only, Read Write.", + "type": [ + "string", + "null" + ] + }, + "diskName": { + "description": "diskName is the Name of the data disk in the blob storage", + "type": "string" + }, + "diskURI": { + "description": "diskURI is the URI of data disk in the blob storage", + "type": "string" + }, + "fsType": { + "description": "fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "kind": { + "description": "kind expected values are Shared: multiple blob disks per storage account Dedicated: single blob disk per storage account Managed: azure managed data disk (only in managed availability set). defaults to shared", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "diskName", + "diskURI" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "azureFile": { + "description": "AzureFile represents an Azure File Service mount on the host and bind mount to the pod.", + "properties": { + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "secretName": { + "description": "secretName is the name of secret that contains Azure Storage Account Name and Key", + "type": "string" + }, + "shareName": { + "description": "shareName is the azure share Name", + "type": "string" + } + }, + "required": [ + "secretName", + "shareName" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "cephfs": { + "description": "Represents a Ceph Filesystem mount that lasts the lifetime of a pod Cephfs volumes do not support ownership management or SELinux relabeling.", + "properties": { + "monitors": { + "description": "monitors is Required: Monitors is a collection of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": "array" + }, + "path": { + "description": "path is Optional: Used as the mounted root, rather than the full Ceph tree, default is /", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly is Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "type": [ + "boolean", + "null" + ] + }, + "secretFile": { + "description": "secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "type": [ + "string", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "user": { + "description": "user is optional: User is the rados user name, default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "monitors" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "cinder": { + "description": "Represents a cinder volume resource in Openstack. A Cinder volume must exist before mounting to a container. The volume must also be in the same region as the kubelet. Cinder volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "volumeID": { + "description": "volumeID used to identify the volume in cinder. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", + "type": "string" + } + }, + "required": [ + "volumeID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "configMap": { + "description": "Adapts a ConfigMap into a volume.\n\nThe contents of the target ConfigMap's Data field will be presented in a volume as files using the keys in the Data field as the file names, unless the items element is populated with specific mappings of keys to paths. ConfigMap volumes support ownership management and SELinux relabeling.", + "properties": { + "defaultMode": { + "description": "defaultMode is optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "items": { + "description": "items if unspecified, each key-value pair in the Data field of the referenced ConfigMap will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the ConfigMap, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "description": "Maps a string key to a path within a volume.", + "properties": { + "key": { + "description": "key is the key to project.", + "type": "string" + }, + "mode": { + "description": "mode is Optional: mode bits used to set permissions on this file. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.", + "type": "string" + } + }, + "required": [ + "key", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "optional specify whether the ConfigMap or its keys must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "csi": { + "description": "Represents a source location of a volume to mount, managed by an external CSI driver", + "properties": { + "driver": { + "description": "driver is the name of the CSI driver that handles this volume. Consult with your admin for the correct name as registered in the cluster.", + "type": "string" + }, + "fsType": { + "description": "fsType to mount. Ex. \"ext4\", \"xfs\", \"ntfs\". If not provided, the empty value is passed to the associated CSI driver which will determine the default filesystem to apply.", + "type": [ + "string", + "null" + ] + }, + "nodePublishSecretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "readOnly": { + "description": "readOnly specifies a read-only configuration for the volume. Defaults to false (read/write).", + "type": [ + "boolean", + "null" + ] + }, + "volumeAttributes": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "volumeAttributes stores driver-specific properties that are passed to the CSI driver. Consult your driver's documentation for supported values.", + "type": [ + "object", + "null" + ] + } + }, + "required": [ + "driver" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "downwardAPI": { + "description": "DownwardAPIVolumeSource represents a volume containing downward API info. Downward API volumes support ownership management and SELinux relabeling.", + "properties": { + "defaultMode": { + "description": "Optional: mode bits to use on created files by default. Must be a Optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "items": { + "description": "Items is a list of downward API volume file", + "items": { + "description": "DownwardAPIVolumeFile represents information to create the file containing the pod field", + "properties": { + "fieldRef": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": [ + "string", + "null" + ] + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "mode": { + "description": "Optional: mode bits used to set permissions on this file, must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "Required: Path is the relative path name of the file to be created. Must not be absolute or contain the '..' path. Must be utf-8 encoded. The first item of the relative path must not start with '..'", + "type": "string" + }, + "resourceFieldRef": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": [ + "string", + "null" + ] + }, + "divisor": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "required": [ + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "emptyDir": { + "description": "Represents an empty directory for a pod. Empty directory volumes support ownership management and SELinux relabeling.", + "properties": { + "medium": { + "description": "medium represents what type of storage medium should back this directory. The default is \"\" which means to use the node's default medium. Must be an empty string (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir", + "type": [ + "string", + "null" + ] + }, + "sizeLimit": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "ephemeral": { + "description": "Represents an ephemeral volume that is handled by a normal storage driver.", + "properties": { + "volumeClaimTemplate": { + "description": "PersistentVolumeClaimTemplate is used to produce PersistentVolumeClaim objects as part of an EphemeralVolumeSource.", + "properties": { + "metadata": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "properties": { + "annotations": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": [ + "object", + "null" + ] + }, + "creationTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "deletionTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": [ + "string", + "null" + ] + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "labels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": [ + "object", + "null" + ] + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "items": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": [ + "string", + "null" + ] + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": [ + "string", + "null" + ] + }, + "fieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": [ + "object", + "null" + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": [ + "string", + "null" + ] + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": [ + "string", + "null" + ] + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": [ + "string", + "null" + ] + }, + "time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": [ + "string", + "null" + ] + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": [ + "string", + "null" + ] + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "items": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": [ + "boolean", + "null" + ] + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": [ + "boolean", + "null" + ] + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + }, + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": [ + "string", + "null" + ] + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": [ + "string", + "null" + ] + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "spec": { + "description": "PersistentVolumeClaimSpec describes the common attributes of storage devices and allows a Source for provider-specific attributes", + "properties": { + "accessModes": { + "description": "accessModes contains the desired access modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "dataSource": { + "description": "TypedLocalObjectReference contains enough information to let you locate the typed referenced object inside the same namespace.", + "properties": { + "apiGroup": { + "description": "APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.", + "type": [ + "string", + "null" + ] + }, + "kind": { + "description": "Kind is the type of resource being referenced", + "type": "string" + }, + "name": { + "description": "Name is the name of resource being referenced", + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "dataSourceRef": { + "properties": { + "apiGroup": { + "description": "APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.", + "type": [ + "string", + "null" + ] + }, + "kind": { + "description": "Kind is the type of resource being referenced", + "type": "string" + }, + "name": { + "description": "Name is the name of resource being referenced", + "type": "string" + }, + "namespace": { + "description": "Namespace is the namespace of resource being referenced Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind", + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "selector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "storageClassName": { + "description": "storageClassName is the name of the StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1", + "type": [ + "string", + "null" + ] + }, + "volumeMode": { + "description": "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.", + "type": [ + "string", + "null" + ] + }, + "volumeName": { + "description": "volumeName is the binding reference to the PersistentVolume backing this claim.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object", + "additionalProperties": false + } + }, + "required": [ + "spec" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "fc": { + "description": "Represents a Fibre Channel volume. Fibre Channel volumes can only be mounted as read/write once. Fibre Channel volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "lun": { + "description": "lun is Optional: FC target lun number", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "readOnly": { + "description": "readOnly is Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "targetWWNs": { + "description": "targetWWNs is Optional: FC target worldwide names (WWNs)", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "wwids": { + "description": "wwids Optional: FC volume world wide identifiers (wwids) Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "flexVolume": { + "description": "FlexVolume represents a generic volume resource that is provisioned/attached using an exec based plugin.", + "properties": { + "driver": { + "description": "driver is the name of the driver to use for this volume.", + "type": "string" + }, + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". The default filesystem depends on FlexVolume script.", + "type": [ + "string", + "null" + ] + }, + "options": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "options is Optional: this field holds extra command options if any.", + "type": [ + "object", + "null" + ] + }, + "readOnly": { + "description": "readOnly is Optional: defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "required": [ + "driver" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "flocker": { + "description": "Represents a Flocker volume mounted by the Flocker agent. One and only one of datasetName and datasetUUID should be set. Flocker volumes do not support ownership management or SELinux relabeling.", + "properties": { + "datasetName": { + "description": "datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker should be considered as deprecated", + "type": [ + "string", + "null" + ] + }, + "datasetUUID": { + "description": "datasetUUID is the UUID of the dataset. This is unique identifier of a Flocker dataset", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "gcePersistentDisk": { + "description": "Represents a Persistent Disk resource in Google Compute Engine.\n\nA GCE PD must exist before mounting to a container. The disk must also be in the same GCE project and zone as the kubelet. A GCE PD can only be mounted as read/write once or read-only many times. GCE PDs support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "type": [ + "string", + "null" + ] + }, + "partition": { + "description": "partition is the partition in the volume that you want to mount. If omitted, the default is to mount by volume name. Examples: For volume /dev/sda1, you specify the partition as \"1\". Similarly, the volume partition for /dev/sda is \"0\" (or you can leave the property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "pdName": { + "description": "pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "pdName" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "gitRepo": { + "description": "Represents a volume that is populated with the contents of a git repository. Git repo volumes do not support ownership management. Git repo volumes support SELinux relabeling.\n\nDEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir into the Pod's container.", + "properties": { + "directory": { + "description": "directory is the target directory name. Must not contain or start with '..'. If '.' is supplied, the volume directory will be the git repository. Otherwise, if specified, the volume will contain the git repository in the subdirectory with the given name.", + "type": [ + "string", + "null" + ] + }, + "repository": { + "description": "repository is the URL", + "type": "string" + }, + "revision": { + "description": "revision is the commit hash for the specified revision.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "repository" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "glusterfs": { + "description": "Represents a Glusterfs mount that lasts the lifetime of a pod. Glusterfs volumes do not support ownership management or SELinux relabeling.", + "properties": { + "endpoints": { + "description": "endpoints is the endpoint name that details Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", + "type": "string" + }, + "path": { + "description": "path is the Glusterfs volume path. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the Glusterfs volume to be mounted with read-only permissions. Defaults to false. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "endpoints", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "hostPath": { + "description": "Represents a host path mapped into a pod. Host path volumes do not support ownership management or SELinux relabeling.", + "properties": { + "path": { + "description": "path of the directory on the host. If the path is a symlink, it will follow the link to the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath", + "type": "string" + }, + "type": { + "description": "type for HostPath Volume Defaults to \"\" More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "iscsi": { + "description": "Represents an ISCSI disk. ISCSI volumes can only be mounted as read/write once. ISCSI volumes support ownership management and SELinux relabeling.", + "properties": { + "chapAuthDiscovery": { + "description": "chapAuthDiscovery defines whether support iSCSI Discovery CHAP authentication", + "type": [ + "boolean", + "null" + ] + }, + "chapAuthSession": { + "description": "chapAuthSession defines whether support iSCSI Session CHAP authentication", + "type": [ + "boolean", + "null" + ] + }, + "fsType": { + "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi", + "type": [ + "string", + "null" + ] + }, + "initiatorName": { + "description": "initiatorName is the custom iSCSI Initiator Name. If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface : will be created for the connection.", + "type": [ + "string", + "null" + ] + }, + "iqn": { + "description": "iqn is the target iSCSI Qualified Name.", + "type": "string" + }, + "iscsiInterface": { + "description": "iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp).", + "type": [ + "string", + "null" + ] + }, + "lun": { + "description": "lun represents iSCSI Target Lun number.", + "format": "int32", + "type": "integer" + }, + "portals": { + "description": "portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port is other than default (typically TCP ports 860 and 3260).", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "readOnly": { + "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "targetPortal": { + "description": "targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port is other than default (typically TCP ports 860 and 3260).", + "type": "string" + } + }, + "required": [ + "targetPortal", + "iqn", + "lun" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "name of the volume. Must be a DNS_LABEL and unique within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "nfs": { + "description": "Represents an NFS mount that lasts the lifetime of a pod. NFS volumes do not support ownership management or SELinux relabeling.", + "properties": { + "path": { + "description": "path that is exported by the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the NFS export to be mounted with read-only permissions. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", + "type": [ + "boolean", + "null" + ] + }, + "server": { + "description": "server is the hostname or IP address of the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", + "type": "string" + } + }, + "required": [ + "server", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "persistentVolumeClaim": { + "description": "PersistentVolumeClaimVolumeSource references the user's PVC in the same namespace. This volume finds the bound PV and mounts that volume for the pod. A PersistentVolumeClaimVolumeSource is, essentially, a wrapper around another type of volume that is owned by someone else (the system).", + "properties": { + "claimName": { + "description": "claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims", + "type": "string" + }, + "readOnly": { + "description": "readOnly Will force the ReadOnly setting in VolumeMounts. Default false.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "claimName" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "photonPersistentDisk": { + "description": "Represents a Photon Controller persistent disk resource.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "pdID": { + "description": "pdID is the ID that identifies Photon Controller persistent disk", + "type": "string" + } + }, + "required": [ + "pdID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "portworxVolume": { + "description": "PortworxVolumeSource represents a Portworx volume resource.", + "properties": { + "fsType": { + "description": "fSType represents the filesystem type to mount Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "volumeID": { + "description": "volumeID uniquely identifies a Portworx volume", + "type": "string" + } + }, + "required": [ + "volumeID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "projected": { + "description": "Represents a projected volume source", + "properties": { + "defaultMode": { + "description": "defaultMode are the mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "sources": { + "description": "sources is the list of volume projections", + "items": { + "description": "Projection that may be projected along with other supported volume types", + "properties": { + "configMap": { + "description": "Adapts a ConfigMap into a projected volume.\n\nThe contents of the target ConfigMap's Data field will be presented in a projected volume as files using the keys in the Data field as the file names, unless the items element is populated with specific mappings of keys to paths. Note that this is identical to a configmap volume source without the default mode.", + "properties": { + "items": { + "description": "items if unspecified, each key-value pair in the Data field of the referenced ConfigMap will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the ConfigMap, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "description": "Maps a string key to a path within a volume.", + "properties": { + "key": { + "description": "key is the key to project.", + "type": "string" + }, + "mode": { + "description": "mode is Optional: mode bits used to set permissions on this file. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.", + "type": "string" + } + }, + "required": [ + "key", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "optional specify whether the ConfigMap or its keys must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "downwardAPI": { + "description": "Represents downward API info for projecting into a projected volume. Note that this is identical to a downwardAPI volume source without the default mode.", + "properties": { + "items": { + "description": "Items is a list of DownwardAPIVolume file", + "items": { + "description": "DownwardAPIVolumeFile represents information to create the file containing the pod field", + "properties": { + "fieldRef": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": [ + "string", + "null" + ] + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "mode": { + "description": "Optional: mode bits used to set permissions on this file, must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "Required: Path is the relative path name of the file to be created. Must not be absolute or contain the '..' path. Must be utf-8 encoded. The first item of the relative path must not start with '..'", + "type": "string" + }, + "resourceFieldRef": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": [ + "string", + "null" + ] + }, + "divisor": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "required": [ + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "secret": { + "description": "Adapts a secret into a projected volume.\n\nThe contents of the target Secret's Data field will be presented in a projected volume as files using the keys in the Data field as the file names. Note that this is identical to a secret volume source without the default mode.", + "properties": { + "items": { + "description": "items if unspecified, each key-value pair in the Data field of the referenced Secret will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the Secret, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "description": "Maps a string key to a path within a volume.", + "properties": { + "key": { + "description": "key is the key to project.", + "type": "string" + }, + "mode": { + "description": "mode is Optional: mode bits used to set permissions on this file. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.", + "type": "string" + } + }, + "required": [ + "key", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "optional field specify whether the Secret or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "serviceAccountToken": { + "description": "ServiceAccountTokenProjection represents a projected service account token volume. This projection can be used to insert a service account token into the pods runtime filesystem for use against APIs (Kubernetes API Server or otherwise).", + "properties": { + "audience": { + "description": "audience is the intended audience of the token. A recipient of a token must identify itself with an identifier specified in the audience of the token, and otherwise should reject the token. The audience defaults to the identifier of the apiserver.", + "type": [ + "string", + "null" + ] + }, + "expirationSeconds": { + "description": "expirationSeconds is the requested duration of validity of the service account token. As the token approaches expiration, the kubelet volume plugin will proactively rotate the service account token. The kubelet will start trying to rotate the token if the token is older than 80 percent of its time to live or if the token is older than 24 hours.Defaults to 1 hour and must be at least 10 minutes.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "path is the path relative to the mount point of the file to project the token into.", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "quobyte": { + "description": "Represents a Quobyte mount that lasts the lifetime of a pod. Quobyte volumes do not support ownership management or SELinux relabeling.", + "properties": { + "group": { + "description": "group to map volume access to Default is no group", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly here will force the Quobyte volume to be mounted with read-only permissions. Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, + "registry": { + "description": "registry represents a single or multiple Quobyte Registry services specified as a string as host:port pair (multiple entries are separated with commas) which acts as the central registry for volumes", + "type": "string" + }, + "tenant": { + "description": "tenant owning the given Quobyte volume in the Backend Used with dynamically provisioned Quobyte volumes, value is set by the plugin", + "type": [ + "string", + "null" + ] + }, + "user": { + "description": "user to map volume access to Defaults to serivceaccount user", + "type": [ + "string", + "null" + ] + }, + "volume": { + "description": "volume is a string that references an already created Quobyte volume by name.", + "type": "string" + } + }, + "required": [ + "registry", + "volume" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "rbd": { + "description": "Represents a Rados Block Device mount that lasts the lifetime of a pod. RBD volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd", + "type": [ + "string", + "null" + ] + }, + "image": { + "description": "image is the rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": "string" + }, + "keyring": { + "description": "keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": [ + "string", + "null" + ] + }, + "monitors": { + "description": "monitors is a collection of Ceph monitors. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": "array" + }, + "pool": { + "description": "pool is the rados pool name. Default is rbd. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "user": { + "description": "user is the rados user name. Default is admin. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "monitors", + "image" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "scaleIO": { + "description": "ScaleIOVolumeSource represents a persistent ScaleIO volume", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Default is \"xfs\".", + "type": [ + "string", + "null" + ] + }, + "gateway": { + "description": "gateway is the host address of the ScaleIO API Gateway.", + "type": "string" + }, + "protectionDomain": { + "description": "protectionDomain is the name of the ScaleIO Protection Domain for the configured storage.", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": "object", + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "sslEnabled": { + "description": "sslEnabled Flag enable/disable SSL communication with Gateway, default false", + "type": [ + "boolean", + "null" + ] + }, + "storageMode": { + "description": "storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. Default is ThinProvisioned.", + "type": [ + "string", + "null" + ] + }, + "storagePool": { + "description": "storagePool is the ScaleIO Storage Pool associated with the protection domain.", + "type": [ + "string", + "null" + ] + }, + "system": { + "description": "system is the name of the storage system as configured in ScaleIO.", + "type": "string" + }, + "volumeName": { + "description": "volumeName is the name of a volume already created in the ScaleIO system that is associated with this volume source.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "gateway", + "system", + "secretRef" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "secret": { + "description": "Adapts a Secret into a volume.\n\nThe contents of the target Secret's Data field will be presented in a volume as files using the keys in the Data field as the file names. Secret volumes support ownership management and SELinux relabeling.", + "properties": { + "defaultMode": { + "description": "defaultMode is Optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "items": { + "description": "items If unspecified, each key-value pair in the Data field of the referenced Secret will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the Secret, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "description": "Maps a string key to a path within a volume.", + "properties": { + "key": { + "description": "key is the key to project.", + "type": "string" + }, + "mode": { + "description": "mode is Optional: mode bits used to set permissions on this file. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.", + "type": "string" + } + }, + "required": [ + "key", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "optional": { + "description": "optional field specify whether the Secret or its keys must be defined", + "type": [ + "boolean", + "null" + ] + }, + "secretName": { + "description": "secretName is the name of the secret in the pod's namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "storageos": { + "description": "Represents a StorageOS persistent volume resource.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "volumeName": { + "description": "volumeName is the human-readable name of the StorageOS volume. Volume names are only unique within a namespace.", + "type": [ + "string", + "null" + ] + }, + "volumeNamespace": { + "description": "volumeNamespace specifies the scope of the volume within StorageOS. If no namespace is specified then the Pod's namespace will be used. This allows the Kubernetes name scoping to be mirrored within StorageOS for tighter integration. Set VolumeName to any name to override the default behaviour. Set to \"default\" if you are not using namespaces within StorageOS. Namespaces that do not pre-exist within StorageOS will be created.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "vsphereVolume": { + "description": "Represents a vSphere volume resource.", + "properties": { + "fsType": { + "description": "fsType is filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "storagePolicyID": { + "description": "storagePolicyID is the storage Policy Based Management (SPBM) profile ID associated with the StoragePolicyName.", + "type": [ + "string", + "null" + ] + }, + "storagePolicyName": { + "description": "storagePolicyName is the storage Policy Based Management (SPBM) profile name.", + "type": [ + "string", + "null" + ] + }, + "volumePath": { + "description": "volumePath is the path that identifies vSphere volume vmdk", + "type": "string" + } + }, + "required": [ + "volumePath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge,retainKeys" + } + }, + "required": [ + "containers" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": "object", + "additionalProperties": false + } + }, + "required": [ + "selector", + "template" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "status": { + "description": "DeploymentStatus is the most recently observed status of the Deployment.", + "properties": { + "availableReplicas": { + "description": "Total number of available pods (ready for at least minReadySeconds) targeted by this deployment.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "collisionCount": { + "description": "Count of hash collisions for the Deployment. The Deployment controller uses this field as a collision avoidance mechanism when it needs to create the name for the newest ReplicaSet.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "conditions": { + "description": "Represents the latest available observations of a deployment's current state.", + "items": { + "description": "DeploymentCondition describes the state of a deployment at a certain point.", + "properties": { + "lastTransitionTime": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "lastUpdateTime": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "message": { + "description": "A human readable message indicating details about the transition.", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "The reason for the condition's last transition.", + "type": [ + "string", + "null" + ] + }, + "status": { + "description": "Status of the condition, one of True, False, Unknown.", + "type": "string" + }, + "type": { + "description": "Type of deployment condition.", + "type": "string" + } + }, + "required": [ + "type", + "status" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "type", + "x-kubernetes-patch-strategy": "merge" + }, + "observedGeneration": { + "description": "The generation observed by the deployment controller.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "readyReplicas": { + "description": "readyReplicas is the number of pods targeted by this Deployment with a Ready Condition.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "replicas": { + "description": "Total number of non-terminated pods targeted by this deployment (their labels match the selector).", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "unavailableReplicas": { + "description": "Total number of unavailable pods targeted by this deployment. This is the total number of pods that are still required for the deployment to have 100% available capacity. They may either be pods that are running but not yet available or pods that still have not been created.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "updatedReplicas": { + "description": "Total number of non-terminated pods targeted by this deployment that have the desired template spec.", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "apps", + "kind": "Deployment", + "version": "v1" + } + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/schema#" +} \ No newline at end of file diff --git a/packages/editors/src/schemas/kubernetes/index.ts b/packages/editors/src/schemas/kubernetes/index.ts new file mode 100644 index 0000000..0fea2bf --- /dev/null +++ b/packages/editors/src/schemas/kubernetes/index.ts @@ -0,0 +1,68 @@ +/** + * Official Kubernetes JSON Schemas (v1.28.0, standalone-strict). + * Source: https://github.com/yannh/kubernetes-json-schema + * + * Loaded lazily via dynamic import() so they don't bloat the main bundle. + */ + +export interface K8sSchemaEntry { + name: string; + kind: string; + /** Full API group/version, e.g. "v1", "apps/v1", "networking.k8s.io/v1". */ + apiVersion: string; + /** The group::version::resource segment used in structured filenames. */ + gvr: string; + load: () => Promise<{ default: Record }>; +} + +/** + * Build a fileMatch pattern for the structured naming convention: + * |"_global"/|"_global"/::::.yaml + * + * Monaco URL-encodes `:` to `%3A` in model URIs, so fileMatch patterns use + * the encoded form. The `*` prefix matches any scheme/path prefix. + */ +function gvrFileMatch(gvr: string): string[] { + const encoded = gvr.replace(/:/g, '%3A'); + return [`*${encoded}.yaml`, `*${encoded}.yml`]; +} + +export const k8sSchemas: K8sSchemaEntry[] = [ + { + name: 'Pod', + kind: 'Pod', + apiVersion: 'v1', + gvr: 'core::v1::Pod', + load: () => import('./pod-v1.json'), + }, + { + name: 'Deployment', + kind: 'Deployment', + apiVersion: 'apps/v1', + gvr: 'apps::v1::Deployment', + load: () => import('./deployment-apps-v1.json'), + }, + { + name: 'Service', + kind: 'Service', + apiVersion: 'v1', + gvr: 'core::v1::Service', + load: () => import('./service-v1.json'), + }, + { + name: 'ConfigMap', + kind: 'ConfigMap', + apiVersion: 'v1', + gvr: 'core::v1::ConfigMap', + load: () => import('./configmap-v1.json'), + }, + { + name: 'Ingress', + kind: 'Ingress', + apiVersion: 'networking.k8s.io/v1', + gvr: 'networking.k8s.io::v1::Ingress', + load: () => import('./ingress-networking-v1.json'), + }, +]; + +export { gvrFileMatch }; diff --git a/packages/editors/src/schemas/kubernetes/ingress-networking-v1.json b/packages/editors/src/schemas/kubernetes/ingress-networking-v1.json new file mode 100644 index 0000000..a4d1021 --- /dev/null +++ b/packages/editors/src/schemas/kubernetes/ingress-networking-v1.json @@ -0,0 +1,656 @@ +{ + "description": "Ingress is a collection of rules that allow inbound connections to reach the endpoints defined by a backend. An Ingress can be configured to give services externally-reachable urls, load balance traffic, terminate SSL, offer name based virtual hosting etc.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": [ + "string", + "null" + ], + "enum": [ + "networking.k8s.io/v1" + ] + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": [ + "string", + "null" + ], + "enum": [ + "Ingress" + ] + }, + "metadata": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "properties": { + "annotations": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": [ + "object", + "null" + ] + }, + "creationTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "deletionTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": [ + "string", + "null" + ] + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "labels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": [ + "object", + "null" + ] + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "items": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": [ + "string", + "null" + ] + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": [ + "string", + "null" + ] + }, + "fieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": [ + "object", + "null" + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": [ + "string", + "null" + ] + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": [ + "string", + "null" + ] + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": [ + "string", + "null" + ] + }, + "time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": [ + "string", + "null" + ] + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": [ + "string", + "null" + ] + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "items": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": [ + "boolean", + "null" + ] + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": [ + "boolean", + "null" + ] + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + }, + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": [ + "string", + "null" + ] + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": [ + "string", + "null" + ] + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "spec": { + "description": "IngressSpec describes the Ingress the user wishes to exist.", + "properties": { + "defaultBackend": { + "description": "IngressBackend describes all endpoints for a given service and port.", + "properties": { + "resource": { + "description": "TypedLocalObjectReference contains enough information to let you locate the typed referenced object inside the same namespace.", + "properties": { + "apiGroup": { + "description": "APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.", + "type": [ + "string", + "null" + ] + }, + "kind": { + "description": "Kind is the type of resource being referenced", + "type": "string" + }, + "name": { + "description": "Name is the name of resource being referenced", + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "service": { + "description": "IngressServiceBackend references a Kubernetes Service as a Backend.", + "properties": { + "name": { + "description": "name is the referenced service. The service must exist in the same namespace as the Ingress object.", + "type": "string" + }, + "port": { + "description": "ServiceBackendPort is the service port being referenced.", + "properties": { + "name": { + "description": "name is the name of the port on the Service. This is a mutually exclusive setting with \"Number\".", + "type": [ + "string", + "null" + ] + }, + "number": { + "description": "number is the numerical port number (e.g. 80) on the Service. This is a mutually exclusive setting with \"Name\".", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "ingressClassName": { + "description": "ingressClassName is the name of an IngressClass cluster resource. Ingress controller implementations use this field to know whether they should be serving this Ingress resource, by a transitive connection (controller -> IngressClass -> Ingress resource). Although the `kubernetes.io/ingress.class` annotation (simple constant name) was never formally defined, it was widely supported by Ingress controllers to create a direct binding between Ingress controller and Ingress resources. Newly created Ingress resources should prefer using the field. However, even though the annotation is officially deprecated, for backwards compatibility reasons, ingress controllers should still honor that annotation if present.", + "type": [ + "string", + "null" + ] + }, + "rules": { + "description": "rules is a list of host rules used to configure the Ingress. If unspecified, or no rule matches, all traffic is sent to the default backend.", + "items": { + "description": "IngressRule represents the rules mapping the paths under a specified host to the related backend services. Incoming requests are first evaluated for a host match, then routed to the backend associated with the matching IngressRuleValue.", + "properties": { + "host": { + "description": "host is the fully qualified domain name of a network host, as defined by RFC 3986. Note the following deviations from the \"host\" part of the URI as defined in RFC 3986: 1. IPs are not allowed. Currently an IngressRuleValue can only apply to\n the IP in the Spec of the parent Ingress.\n2. The `:` delimiter is not respected because ports are not allowed.\n\t Currently the port of an Ingress is implicitly :80 for http and\n\t :443 for https.\nBoth these may change in the future. Incoming requests are matched against the host before the IngressRuleValue. If the host is unspecified, the Ingress routes all traffic based on the specified IngressRuleValue.\n\nhost can be \"precise\" which is a domain name without the terminating dot of a network host (e.g. \"foo.bar.com\") or \"wildcard\", which is a domain name prefixed with a single wildcard label (e.g. \"*.foo.com\"). The wildcard character '*' must appear by itself as the first DNS label and matches only a single label. You cannot have a wildcard label by itself (e.g. Host == \"*\"). Requests will be matched against the Host field in the following way: 1. If host is precise, the request matches this rule if the http host header is equal to Host. 2. If host is a wildcard, then the request matches this rule if the http host header is to equal to the suffix (removing the first label) of the wildcard rule.", + "type": [ + "string", + "null" + ] + }, + "http": { + "description": "HTTPIngressRuleValue is a list of http selectors pointing to backends. In the example: http:///? -> backend where where parts of the url correspond to RFC 3986, this resource will be used to match against everything after the last '/' and before the first '?' or '#'.", + "properties": { + "paths": { + "description": "paths is a collection of paths that map requests to backends.", + "items": { + "description": "HTTPIngressPath associates a path with a backend. Incoming urls matching the path are forwarded to the backend.", + "properties": { + "backend": { + "description": "IngressBackend describes all endpoints for a given service and port.", + "properties": { + "resource": { + "description": "TypedLocalObjectReference contains enough information to let you locate the typed referenced object inside the same namespace.", + "properties": { + "apiGroup": { + "description": "APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.", + "type": [ + "string", + "null" + ] + }, + "kind": { + "description": "Kind is the type of resource being referenced", + "type": "string" + }, + "name": { + "description": "Name is the name of resource being referenced", + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "service": { + "description": "IngressServiceBackend references a Kubernetes Service as a Backend.", + "properties": { + "name": { + "description": "name is the referenced service. The service must exist in the same namespace as the Ingress object.", + "type": "string" + }, + "port": { + "description": "ServiceBackendPort is the service port being referenced.", + "properties": { + "name": { + "description": "name is the name of the port on the Service. This is a mutually exclusive setting with \"Number\".", + "type": [ + "string", + "null" + ] + }, + "number": { + "description": "number is the numerical port number (e.g. 80) on the Service. This is a mutually exclusive setting with \"Name\".", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": "object", + "additionalProperties": false + }, + "path": { + "description": "path is matched against the path of an incoming request. Currently it can contain characters disallowed from the conventional \"path\" part of a URL as defined by RFC 3986. Paths must begin with a '/' and must be present when using PathType with value \"Exact\" or \"Prefix\".", + "type": [ + "string", + "null" + ] + }, + "pathType": { + "description": "pathType determines the interpretation of the path matching. PathType can be one of the following values: * Exact: Matches the URL path exactly. * Prefix: Matches based on a URL path prefix split by '/'. Matching is\n done on a path element by element basis. A path element refers is the\n list of labels in the path split by the '/' separator. A request is a\n match for path p if every p is an element-wise prefix of p of the\n request path. Note that if the last element of the path is a substring\n of the last element in request path, it is not a match (e.g. /foo/bar\n matches /foo/bar/baz, but does not match /foo/barbaz).\n* ImplementationSpecific: Interpretation of the Path matching is up to\n the IngressClass. Implementations can treat this as a separate PathType\n or treat it identically to Prefix or Exact path types.\nImplementations are required to support all path types.", + "type": "string" + } + }, + "required": [ + "pathType", + "backend" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "required": [ + "paths" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "tls": { + "description": "tls represents the TLS configuration. Currently the Ingress only supports a single TLS port, 443. If multiple members of this list specify different hosts, they will be multiplexed on the same port according to the hostname specified through the SNI TLS extension, if the ingress controller fulfilling the ingress supports SNI.", + "items": { + "description": "IngressTLS describes the transport layer security associated with an ingress.", + "properties": { + "hosts": { + "description": "hosts is a list of hosts included in the TLS certificate. The values in this list must match the name/s used in the tlsSecret. Defaults to the wildcard host setting for the loadbalancer controller fulfilling this Ingress, if left unspecified.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "secretName": { + "description": "secretName is the name of the secret used to terminate TLS traffic on port 443. Field is left optional to allow TLS routing based on SNI hostname alone. If the SNI host in a listener conflicts with the \"Host\" header field used by an IngressRule, the SNI host is used for termination and value of the \"Host\" header is used for routing.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "status": { + "description": "IngressStatus describe the current state of the Ingress.", + "properties": { + "loadBalancer": { + "description": "IngressLoadBalancerStatus represents the status of a load-balancer.", + "properties": { + "ingress": { + "description": "ingress is a list containing ingress points for the load-balancer.", + "items": { + "description": "IngressLoadBalancerIngress represents the status of a load-balancer ingress point.", + "properties": { + "hostname": { + "description": "hostname is set for load-balancer ingress points that are DNS based.", + "type": [ + "string", + "null" + ] + }, + "ip": { + "description": "ip is set for load-balancer ingress points that are IP based.", + "type": [ + "string", + "null" + ] + }, + "ports": { + "description": "ports provides information about the ports exposed by this LoadBalancer.", + "items": { + "description": "IngressPortStatus represents the error condition of a service port", + "properties": { + "error": { + "description": "error is to record the problem with the service port The format of the error shall comply with the following rules: - built-in error values shall be specified in this file and those shall use\n CamelCase names\n- cloud provider specific error values must have names that comply with the\n format foo.example.com/CamelCase.", + "type": [ + "string", + "null" + ] + }, + "port": { + "description": "port is the port number of the ingress port.", + "format": "int32", + "type": "integer" + }, + "protocol": { + "description": "protocol is the protocol of the ingress port. The supported values are: \"TCP\", \"UDP\", \"SCTP\"", + "type": "string" + } + }, + "required": [ + "port", + "protocol" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "networking.k8s.io", + "kind": "Ingress", + "version": "v1" + } + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/schema#" +} \ No newline at end of file diff --git a/packages/editors/src/schemas/kubernetes/pod-v1.json b/packages/editors/src/schemas/kubernetes/pod-v1.json new file mode 100644 index 0000000..8eb5efd --- /dev/null +++ b/packages/editors/src/schemas/kubernetes/pod-v1.json @@ -0,0 +1,11148 @@ +{ + "description": "Pod is a collection of containers that can run on a host. This resource is created by clients and scheduled onto hosts.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": [ + "string", + "null" + ], + "enum": [ + "v1" + ] + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": [ + "string", + "null" + ], + "enum": [ + "Pod" + ] + }, + "metadata": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "properties": { + "annotations": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": [ + "object", + "null" + ] + }, + "creationTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "deletionTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": [ + "string", + "null" + ] + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "labels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": [ + "object", + "null" + ] + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "items": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": [ + "string", + "null" + ] + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": [ + "string", + "null" + ] + }, + "fieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": [ + "object", + "null" + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": [ + "string", + "null" + ] + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": [ + "string", + "null" + ] + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": [ + "string", + "null" + ] + }, + "time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": [ + "string", + "null" + ] + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": [ + "string", + "null" + ] + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "items": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": [ + "boolean", + "null" + ] + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": [ + "boolean", + "null" + ] + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + }, + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": [ + "string", + "null" + ] + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": [ + "string", + "null" + ] + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "spec": { + "description": "PodSpec is a description of a pod.", + "properties": { + "activeDeadlineSeconds": { + "description": "Optional duration in seconds the pod may be active on the node relative to StartTime before the system will actively try to mark it failed and kill associated containers. Value must be a positive integer.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "affinity": { + "description": "Affinity is a group of affinity scheduling rules.", + "properties": { + "nodeAffinity": { + "description": "Node affinity is a group of node affinity scheduling rules.", + "properties": { + "preferredDuringSchedulingIgnoredDuringExecution": { + "description": "The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node matches the corresponding matchExpressions; the node(s) with the highest sum are the most preferred.", + "items": { + "description": "An empty preferred scheduling term matches all objects with implicit weight 0 (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).", + "properties": { + "preference": { + "description": "A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.", + "properties": { + "matchExpressions": { + "description": "A list of node selector requirements by node's labels.", + "items": { + "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "The label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + "type": "string" + }, + "values": { + "description": "An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchFields": { + "description": "A list of node selector requirements by node's fields.", + "items": { + "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "The label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + "type": "string" + }, + "values": { + "description": "An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object", + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "weight": { + "description": "Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100.", + "format": "int32", + "type": "integer" + } + }, + "required": [ + "weight", + "preference" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "requiredDuringSchedulingIgnoredDuringExecution": { + "description": "A node selector represents the union of the results of one or more label queries over a set of nodes; that is, it represents the OR of the selectors represented by the node selector terms.", + "properties": { + "nodeSelectorTerms": { + "description": "Required. A list of node selector terms. The terms are ORed.", + "items": { + "description": "A null or empty node selector term matches no objects. The requirements of them are ANDed. The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.", + "properties": { + "matchExpressions": { + "description": "A list of node selector requirements by node's labels.", + "items": { + "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "The label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + "type": "string" + }, + "values": { + "description": "An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchFields": { + "description": "A list of node selector requirements by node's fields.", + "items": { + "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "The label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "Represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.", + "type": "string" + }, + "values": { + "description": "An array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. If the operator is Gt or Lt, the values array must have a single element, which will be interpreted as an integer. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": "array" + } + }, + "required": [ + "nodeSelectorTerms" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "podAffinity": { + "description": "Pod affinity is a group of inter pod affinity scheduling rules.", + "properties": { + "preferredDuringSchedulingIgnoredDuringExecution": { + "description": "The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred.", + "items": { + "description": "The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)", + "properties": { + "podAffinityTerm": { + "description": "Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running", + "properties": { + "labelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaceSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaces": { + "description": "namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means \"this pod's namespace\".", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "topologyKey": { + "description": "This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed.", + "type": "string" + } + }, + "required": [ + "topologyKey" + ], + "type": "object", + "additionalProperties": false + }, + "weight": { + "description": "weight associated with matching the corresponding podAffinityTerm, in the range 1-100.", + "format": "int32", + "type": "integer" + } + }, + "required": [ + "weight", + "podAffinityTerm" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "requiredDuringSchedulingIgnoredDuringExecution": { + "description": "If the affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied.", + "items": { + "description": "Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running", + "properties": { + "labelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaceSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaces": { + "description": "namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means \"this pod's namespace\".", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "topologyKey": { + "description": "This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed.", + "type": "string" + } + }, + "required": [ + "topologyKey" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "podAntiAffinity": { + "description": "Pod anti affinity is a group of inter pod anti affinity scheduling rules.", + "properties": { + "preferredDuringSchedulingIgnoredDuringExecution": { + "description": "The scheduler will prefer to schedule pods to nodes that satisfy the anti-affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling anti-affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding \"weight\" to the sum if the node has pods which matches the corresponding podAffinityTerm; the node(s) with the highest sum are the most preferred.", + "items": { + "description": "The weights of all of the matched WeightedPodAffinityTerm fields are added per-node to find the most preferred node(s)", + "properties": { + "podAffinityTerm": { + "description": "Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running", + "properties": { + "labelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaceSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaces": { + "description": "namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means \"this pod's namespace\".", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "topologyKey": { + "description": "This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed.", + "type": "string" + } + }, + "required": [ + "topologyKey" + ], + "type": "object", + "additionalProperties": false + }, + "weight": { + "description": "weight associated with matching the corresponding podAffinityTerm, in the range 1-100.", + "format": "int32", + "type": "integer" + } + }, + "required": [ + "weight", + "podAffinityTerm" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "requiredDuringSchedulingIgnoredDuringExecution": { + "description": "If the anti-affinity requirements specified by this field are not met at scheduling time, the pod will not be scheduled onto the node. If the anti-affinity requirements specified by this field cease to be met at some point during pod execution (e.g. due to a pod label update), the system may or may not try to eventually evict the pod from its node. When there are multiple elements, the lists of nodes corresponding to each podAffinityTerm are intersected, i.e. all terms must be satisfied.", + "items": { + "description": "Defines a set of pods (namely those matching the labelSelector relative to the given namespace(s)) that this pod should be co-located (affinity) or not co-located (anti-affinity) with, where co-located is defined as running on a node whose value of the label with key matches that of any node on which a pod of the set of pods is running", + "properties": { + "labelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaceSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "namespaces": { + "description": "namespaces specifies a static list of namespace names that the term applies to. The term is applied to the union of the namespaces listed in this field and the ones selected by namespaceSelector. null or empty namespaces list and null namespaceSelector means \"this pod's namespace\".", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "topologyKey": { + "description": "This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching the labelSelector in the specified namespaces, where co-located is defined as running on a node whose value of the label with key topologyKey matches that of any node on which any of the selected pods is running. Empty topologyKey is not allowed.", + "type": "string" + } + }, + "required": [ + "topologyKey" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "automountServiceAccountToken": { + "description": "AutomountServiceAccountToken indicates whether a service account token should be automatically mounted.", + "type": [ + "boolean", + "null" + ] + }, + "containers": { + "description": "List of containers belonging to the pod. Containers cannot currently be added or removed. There must be at least one container in a Pod. Cannot be updated.", + "items": { + "description": "A single application container that you want to run within a pod.", + "properties": { + "args": { + "description": "Arguments to the entrypoint. The container image's CMD is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "command": { + "description": "Entrypoint array. Not executed within a shell. The container image's ENTRYPOINT is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "env": { + "description": "List of environment variables to set in the container. Cannot be updated.", + "items": { + "description": "EnvVar represents an environment variable present in a Container.", + "properties": { + "name": { + "description": "Name of the environment variable. Must be a C_IDENTIFIER.", + "type": "string" + }, + "value": { + "description": "Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to \"\".", + "type": [ + "string", + "null" + ] + }, + "valueFrom": { + "description": "EnvVarSource represents a source for the value of an EnvVar.", + "properties": { + "configMapKeyRef": { + "description": "Selects a key from a ConfigMap.", + "properties": { + "key": { + "description": "The key to select.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "fieldRef": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": [ + "string", + "null" + ] + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "resourceFieldRef": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": [ + "string", + "null" + ] + }, + "divisor": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "secretKeyRef": { + "description": "SecretKeySelector selects a key of a Secret.", + "properties": { + "key": { + "description": "The key of the secret to select from. Must be a valid secret key.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "envFrom": { + "description": "List of sources to populate environment variables in the container. The keys defined within a source must be a C_IDENTIFIER. All invalid keys will be reported as an event when the container is starting. When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated.", + "items": { + "description": "EnvFromSource represents the source of a set of ConfigMaps", + "properties": { + "configMapRef": { + "description": "ConfigMapEnvSource selects a ConfigMap to populate the environment variables with.\n\nThe contents of the target ConfigMap's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "prefix": { + "description": "An optional identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER.", + "type": [ + "string", + "null" + ] + }, + "secretRef": { + "description": "SecretEnvSource selects a Secret to populate the environment variables with.\n\nThe contents of the target Secret's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "image": { + "description": "Container image name. More info: https://kubernetes.io/docs/concepts/containers/images This field is optional to allow higher level config management to default or override container images in workload controllers like Deployments and StatefulSets.", + "type": [ + "string", + "null" + ] + }, + "imagePullPolicy": { + "description": "Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. Cannot be updated. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images", + "type": [ + "string", + "null" + ] + }, + "lifecycle": { + "description": "Lifecycle describes actions that the management system should take in response to container lifecycle events. For the PostStart and PreStop lifecycle handlers, management of the container blocks until the action is complete, unless the container process fails, in which case the handler is aborted.", + "properties": { + "postStart": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "preStop": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "livenessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "Name of the container specified as a DNS_LABEL. Each container in a pod must have a unique name (DNS_LABEL). Cannot be updated.", + "type": "string" + }, + "ports": { + "description": "List of ports to expose from the container. Not specifying a port here DOES NOT prevent that port from being exposed. Any port which is listening on the default \"0.0.0.0\" address inside a container will be accessible from the network. Modifying this array with strategic merge patch may corrupt the data. For more information See https://github.com/kubernetes/kubernetes/issues/108255. Cannot be updated.", + "items": { + "description": "ContainerPort represents a network port in a single container.", + "properties": { + "containerPort": { + "description": "Number of port to expose on the pod's IP address. This must be a valid port number, 0 < x < 65536.", + "format": "int32", + "type": "integer" + }, + "hostIP": { + "description": "What host IP to bind the external port to.", + "type": [ + "string", + "null" + ] + }, + "hostPort": { + "description": "Number of port to expose on the host. If specified, this must be a valid port number, 0 < x < 65536. If HostNetwork is specified, this must match ContainerPort. Most containers do not need this.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "name": { + "description": "If specified, this must be an IANA_SVC_NAME and unique within the pod. Each named port in a pod must have a unique name. Name for the port that can be referred to by services.", + "type": [ + "string", + "null" + ] + }, + "protocol": { + "description": "Protocol for port. Must be UDP, TCP, or SCTP. Defaults to \"TCP\".", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "containerPort" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "containerPort", + "protocol" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "containerPort", + "x-kubernetes-patch-strategy": "merge" + }, + "readinessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "resizePolicy": { + "description": "Resources resize policy for the container.", + "items": { + "description": "ContainerResizePolicy represents resource resize policy for the container.", + "properties": { + "resourceName": { + "description": "Name of the resource to which this resource resize policy applies. Supported values: cpu, memory.", + "type": "string" + }, + "restartPolicy": { + "description": "Restart policy to apply when specified resource is resized. If not specified, it defaults to NotRequired.", + "type": "string" + } + }, + "required": [ + "resourceName", + "restartPolicy" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "restartPolicy": { + "description": "RestartPolicy defines the restart behavior of individual containers in a pod. This field may only be set for init containers, and the only allowed value is \"Always\". For non-init containers or when this field is not specified, the restart behavior is defined by the Pod's restart policy and the container type. Setting the RestartPolicy as \"Always\" for the init container will have the following effect: this init container will be continually restarted on exit until all regular containers have terminated. Once all regular containers have completed, all init containers with restartPolicy \"Always\" will be shut down. This lifecycle differs from normal init containers and is often referred to as a \"sidecar\" container. Although this init container still starts in the init container sequence, it does not wait for the container to complete before proceeding to the next init container. Instead, the next init container starts immediately after this init container is started, or after any startupProbe has successfully completed.", + "type": [ + "string", + "null" + ] + }, + "securityContext": { + "description": "SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. When both are set, the values in SecurityContext take precedence.", + "properties": { + "allowPrivilegeEscalation": { + "description": "AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "capabilities": { + "description": "Adds and removes POSIX capabilities from running containers.", + "properties": { + "add": { + "description": "Added capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "drop": { + "description": "Removed capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "privileged": { + "description": "Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "procMount": { + "description": "procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "string", + "null" + ] + }, + "readOnlyRootFilesystem": { + "description": "Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "runAsGroup": { + "description": "The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "runAsNonRoot": { + "description": "Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUser": { + "description": "The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "seLinuxOptions": { + "description": "SELinuxOptions are the labels to be applied to the container", + "properties": { + "level": { + "description": "Level is SELinux level label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "role": { + "description": "Role is a SELinux role label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "Type is a SELinux type label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "user": { + "description": "User is a SELinux user label that applies to the container.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "seccompProfile": { + "description": "SeccompProfile defines a pod/container's seccomp profile settings. Only one profile source may be set.", + "properties": { + "localhostProfile": { + "description": "localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is \"Localhost\". Must NOT be set for any other type.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "type indicates which kind of seccomp profile will be applied. Valid options are:\n\nLocalhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-unions": [ + { + "discriminator": "type", + "fields-to-discriminateBy": { + "localhostProfile": "LocalhostProfile" + } + } + ], + "additionalProperties": false + }, + "windowsOptions": { + "description": "WindowsSecurityContextOptions contain Windows-specific options and credentials.", + "properties": { + "gmsaCredentialSpec": { + "description": "GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.", + "type": [ + "string", + "null" + ] + }, + "gmsaCredentialSpecName": { + "description": "GMSACredentialSpecName is the name of the GMSA credential spec to use.", + "type": [ + "string", + "null" + ] + }, + "hostProcess": { + "description": "HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUserName": { + "description": "The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "startupProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "stdin": { + "description": "Whether this container should allocate a buffer for stdin in the container runtime. If this is not set, reads from stdin in the container will always result in EOF. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "stdinOnce": { + "description": "Whether the container runtime should close the stdin channel after it has been opened by a single attach. When stdin is true the stdin stream will remain open across multiple attach sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the first client attaches to stdin, and then remains open and accepts data until the client disconnects, at which time stdin is closed and remains closed until the container is restarted. If this flag is false, a container processes that reads from stdin will never receive an EOF. Default is false", + "type": [ + "boolean", + "null" + ] + }, + "terminationMessagePath": { + "description": "Optional: Path at which the file to which the container's termination message will be written is mounted into the container's filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "terminationMessagePolicy": { + "description": "Indicate how the termination message should be populated. File will use the contents of terminationMessagePath to populate the container status message on both success and failure. FallbackToLogsOnError will use the last chunk of container log output if the termination message file is empty and the container exited with an error. The log output is limited to 2048 bytes or 80 lines, whichever is smaller. Defaults to File. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "tty": { + "description": "Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "volumeDevices": { + "description": "volumeDevices is the list of block devices to be used by the container.", + "items": { + "description": "volumeDevice describes a mapping of a raw block device within a container.", + "properties": { + "devicePath": { + "description": "devicePath is the path inside of the container that the device will be mapped to.", + "type": "string" + }, + "name": { + "description": "name must match the name of a persistentVolumeClaim in the pod", + "type": "string" + } + }, + "required": [ + "name", + "devicePath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "devicePath", + "x-kubernetes-patch-strategy": "merge" + }, + "volumeMounts": { + "description": "Pod volumes to mount into the container's filesystem. Cannot be updated.", + "items": { + "description": "VolumeMount describes a mounting of a Volume within a container.", + "properties": { + "mountPath": { + "description": "Path within the container at which the volume should be mounted. Must not contain ':'.", + "type": "string" + }, + "mountPropagation": { + "description": "mountPropagation determines how mounts are propagated from the host to container and the other way around. When not set, MountPropagationNone is used. This field is beta in 1.10.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "This must match the Name of a Volume.", + "type": "string" + }, + "readOnly": { + "description": "Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, + "subPath": { + "description": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).", + "type": [ + "string", + "null" + ] + }, + "subPathExpr": { + "description": "Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "mountPath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "mountPath", + "x-kubernetes-patch-strategy": "merge" + }, + "workingDir": { + "description": "Container's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": "array", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "dnsConfig": { + "description": "PodDNSConfig defines the DNS parameters of a pod in addition to those generated from DNSPolicy.", + "properties": { + "nameservers": { + "description": "A list of DNS name server IP addresses. This will be appended to the base nameservers generated from DNSPolicy. Duplicated nameservers will be removed.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "options": { + "description": "A list of DNS resolver options. This will be merged with the base options generated from DNSPolicy. Duplicated entries will be removed. Resolution options given in Options will override those that appear in the base DNSPolicy.", + "items": { + "description": "PodDNSConfigOption defines DNS resolver options of a pod.", + "properties": { + "name": { + "description": "Required.", + "type": [ + "string", + "null" + ] + }, + "value": { + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "searches": { + "description": "A list of DNS search domains for host-name lookup. This will be appended to the base search paths generated from DNSPolicy. Duplicated search paths will be removed.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "dnsPolicy": { + "description": "Set DNS policy for the pod. Defaults to \"ClusterFirst\". Valid values are 'ClusterFirstWithHostNet', 'ClusterFirst', 'Default' or 'None'. DNS parameters given in DNSConfig will be merged with the policy selected with DNSPolicy. To have DNS options set along with hostNetwork, you have to specify DNS policy explicitly to 'ClusterFirstWithHostNet'.", + "type": [ + "string", + "null" + ] + }, + "enableServiceLinks": { + "description": "EnableServiceLinks indicates whether information about services should be injected into pod's environment variables, matching the syntax of Docker links. Optional: Defaults to true.", + "type": [ + "boolean", + "null" + ] + }, + "ephemeralContainers": { + "description": "List of ephemeral containers run in this pod. Ephemeral containers may be run in an existing pod to perform user-initiated actions such as debugging. This list cannot be specified when creating a pod, and it cannot be modified by updating the pod spec. In order to add an ephemeral container to an existing pod, use the pod's ephemeralcontainers subresource.", + "items": { + "description": "An EphemeralContainer is a temporary container that you may add to an existing Pod for user-initiated activities such as debugging. Ephemeral containers have no resource or scheduling guarantees, and they will not be restarted when they exit or when a Pod is removed or restarted. The kubelet may evict a Pod if an ephemeral container causes the Pod to exceed its resource allocation.\n\nTo add an ephemeral container, use the ephemeralcontainers subresource of an existing Pod. Ephemeral containers may not be removed or restarted.", + "properties": { + "args": { + "description": "Arguments to the entrypoint. The image's CMD is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "command": { + "description": "Entrypoint array. Not executed within a shell. The image's ENTRYPOINT is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "env": { + "description": "List of environment variables to set in the container. Cannot be updated.", + "items": { + "description": "EnvVar represents an environment variable present in a Container.", + "properties": { + "name": { + "description": "Name of the environment variable. Must be a C_IDENTIFIER.", + "type": "string" + }, + "value": { + "description": "Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to \"\".", + "type": [ + "string", + "null" + ] + }, + "valueFrom": { + "description": "EnvVarSource represents a source for the value of an EnvVar.", + "properties": { + "configMapKeyRef": { + "description": "Selects a key from a ConfigMap.", + "properties": { + "key": { + "description": "The key to select.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "fieldRef": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": [ + "string", + "null" + ] + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "resourceFieldRef": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": [ + "string", + "null" + ] + }, + "divisor": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "secretKeyRef": { + "description": "SecretKeySelector selects a key of a Secret.", + "properties": { + "key": { + "description": "The key of the secret to select from. Must be a valid secret key.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "envFrom": { + "description": "List of sources to populate environment variables in the container. The keys defined within a source must be a C_IDENTIFIER. All invalid keys will be reported as an event when the container is starting. When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated.", + "items": { + "description": "EnvFromSource represents the source of a set of ConfigMaps", + "properties": { + "configMapRef": { + "description": "ConfigMapEnvSource selects a ConfigMap to populate the environment variables with.\n\nThe contents of the target ConfigMap's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "prefix": { + "description": "An optional identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER.", + "type": [ + "string", + "null" + ] + }, + "secretRef": { + "description": "SecretEnvSource selects a Secret to populate the environment variables with.\n\nThe contents of the target Secret's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "image": { + "description": "Container image name. More info: https://kubernetes.io/docs/concepts/containers/images", + "type": [ + "string", + "null" + ] + }, + "imagePullPolicy": { + "description": "Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. Cannot be updated. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images", + "type": [ + "string", + "null" + ] + }, + "lifecycle": { + "description": "Lifecycle describes actions that the management system should take in response to container lifecycle events. For the PostStart and PreStop lifecycle handlers, management of the container blocks until the action is complete, unless the container process fails, in which case the handler is aborted.", + "properties": { + "postStart": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "preStop": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "livenessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "Name of the ephemeral container specified as a DNS_LABEL. This name must be unique among all containers, init containers and ephemeral containers.", + "type": "string" + }, + "ports": { + "description": "Ports are not allowed for ephemeral containers.", + "items": { + "description": "ContainerPort represents a network port in a single container.", + "properties": { + "containerPort": { + "description": "Number of port to expose on the pod's IP address. This must be a valid port number, 0 < x < 65536.", + "format": "int32", + "type": "integer" + }, + "hostIP": { + "description": "What host IP to bind the external port to.", + "type": [ + "string", + "null" + ] + }, + "hostPort": { + "description": "Number of port to expose on the host. If specified, this must be a valid port number, 0 < x < 65536. If HostNetwork is specified, this must match ContainerPort. Most containers do not need this.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "name": { + "description": "If specified, this must be an IANA_SVC_NAME and unique within the pod. Each named port in a pod must have a unique name. Name for the port that can be referred to by services.", + "type": [ + "string", + "null" + ] + }, + "protocol": { + "description": "Protocol for port. Must be UDP, TCP, or SCTP. Defaults to \"TCP\".", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "containerPort" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "containerPort", + "protocol" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "containerPort", + "x-kubernetes-patch-strategy": "merge" + }, + "readinessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "resizePolicy": { + "description": "Resources resize policy for the container.", + "items": { + "description": "ContainerResizePolicy represents resource resize policy for the container.", + "properties": { + "resourceName": { + "description": "Name of the resource to which this resource resize policy applies. Supported values: cpu, memory.", + "type": "string" + }, + "restartPolicy": { + "description": "Restart policy to apply when specified resource is resized. If not specified, it defaults to NotRequired.", + "type": "string" + } + }, + "required": [ + "resourceName", + "restartPolicy" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "restartPolicy": { + "description": "Restart policy for the container to manage the restart behavior of each container within a pod. This may only be set for init containers. You cannot set this field on ephemeral containers.", + "type": [ + "string", + "null" + ] + }, + "securityContext": { + "description": "SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. When both are set, the values in SecurityContext take precedence.", + "properties": { + "allowPrivilegeEscalation": { + "description": "AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "capabilities": { + "description": "Adds and removes POSIX capabilities from running containers.", + "properties": { + "add": { + "description": "Added capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "drop": { + "description": "Removed capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "privileged": { + "description": "Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "procMount": { + "description": "procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "string", + "null" + ] + }, + "readOnlyRootFilesystem": { + "description": "Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "runAsGroup": { + "description": "The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "runAsNonRoot": { + "description": "Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUser": { + "description": "The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "seLinuxOptions": { + "description": "SELinuxOptions are the labels to be applied to the container", + "properties": { + "level": { + "description": "Level is SELinux level label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "role": { + "description": "Role is a SELinux role label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "Type is a SELinux type label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "user": { + "description": "User is a SELinux user label that applies to the container.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "seccompProfile": { + "description": "SeccompProfile defines a pod/container's seccomp profile settings. Only one profile source may be set.", + "properties": { + "localhostProfile": { + "description": "localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is \"Localhost\". Must NOT be set for any other type.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "type indicates which kind of seccomp profile will be applied. Valid options are:\n\nLocalhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-unions": [ + { + "discriminator": "type", + "fields-to-discriminateBy": { + "localhostProfile": "LocalhostProfile" + } + } + ], + "additionalProperties": false + }, + "windowsOptions": { + "description": "WindowsSecurityContextOptions contain Windows-specific options and credentials.", + "properties": { + "gmsaCredentialSpec": { + "description": "GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.", + "type": [ + "string", + "null" + ] + }, + "gmsaCredentialSpecName": { + "description": "GMSACredentialSpecName is the name of the GMSA credential spec to use.", + "type": [ + "string", + "null" + ] + }, + "hostProcess": { + "description": "HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUserName": { + "description": "The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "startupProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "stdin": { + "description": "Whether this container should allocate a buffer for stdin in the container runtime. If this is not set, reads from stdin in the container will always result in EOF. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "stdinOnce": { + "description": "Whether the container runtime should close the stdin channel after it has been opened by a single attach. When stdin is true the stdin stream will remain open across multiple attach sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the first client attaches to stdin, and then remains open and accepts data until the client disconnects, at which time stdin is closed and remains closed until the container is restarted. If this flag is false, a container processes that reads from stdin will never receive an EOF. Default is false", + "type": [ + "boolean", + "null" + ] + }, + "targetContainerName": { + "description": "If set, the name of the container from PodSpec that this ephemeral container targets. The ephemeral container will be run in the namespaces (IPC, PID, etc) of this container. If not set then the ephemeral container uses the namespaces configured in the Pod spec.\n\nThe container runtime must implement support for this feature. If the runtime does not support namespace targeting then the result of setting this field is undefined.", + "type": [ + "string", + "null" + ] + }, + "terminationMessagePath": { + "description": "Optional: Path at which the file to which the container's termination message will be written is mounted into the container's filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "terminationMessagePolicy": { + "description": "Indicate how the termination message should be populated. File will use the contents of terminationMessagePath to populate the container status message on both success and failure. FallbackToLogsOnError will use the last chunk of container log output if the termination message file is empty and the container exited with an error. The log output is limited to 2048 bytes or 80 lines, whichever is smaller. Defaults to File. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "tty": { + "description": "Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "volumeDevices": { + "description": "volumeDevices is the list of block devices to be used by the container.", + "items": { + "description": "volumeDevice describes a mapping of a raw block device within a container.", + "properties": { + "devicePath": { + "description": "devicePath is the path inside of the container that the device will be mapped to.", + "type": "string" + }, + "name": { + "description": "name must match the name of a persistentVolumeClaim in the pod", + "type": "string" + } + }, + "required": [ + "name", + "devicePath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "devicePath", + "x-kubernetes-patch-strategy": "merge" + }, + "volumeMounts": { + "description": "Pod volumes to mount into the container's filesystem. Subpath mounts are not allowed for ephemeral containers. Cannot be updated.", + "items": { + "description": "VolumeMount describes a mounting of a Volume within a container.", + "properties": { + "mountPath": { + "description": "Path within the container at which the volume should be mounted. Must not contain ':'.", + "type": "string" + }, + "mountPropagation": { + "description": "mountPropagation determines how mounts are propagated from the host to container and the other way around. When not set, MountPropagationNone is used. This field is beta in 1.10.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "This must match the Name of a Volume.", + "type": "string" + }, + "readOnly": { + "description": "Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, + "subPath": { + "description": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).", + "type": [ + "string", + "null" + ] + }, + "subPathExpr": { + "description": "Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "mountPath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "mountPath", + "x-kubernetes-patch-strategy": "merge" + }, + "workingDir": { + "description": "Container's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "hostAliases": { + "description": "HostAliases is an optional list of hosts and IPs that will be injected into the pod's hosts file if specified. This is only valid for non-hostNetwork pods.", + "items": { + "description": "HostAlias holds the mapping between IP and hostnames that will be injected as an entry in the pod's hosts file.", + "properties": { + "hostnames": { + "description": "Hostnames for the above IP address.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "ip": { + "description": "IP address of the host file entry.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "ip", + "x-kubernetes-patch-strategy": "merge" + }, + "hostIPC": { + "description": "Use the host's ipc namespace. Optional: Default to false.", + "type": [ + "boolean", + "null" + ] + }, + "hostNetwork": { + "description": "Host networking requested for this pod. Use the host's network namespace. If this option is set, the ports that will be used must be specified. Default to false.", + "type": [ + "boolean", + "null" + ] + }, + "hostPID": { + "description": "Use the host's pid namespace. Optional: Default to false.", + "type": [ + "boolean", + "null" + ] + }, + "hostUsers": { + "description": "Use the host's user namespace. Optional: Default to true. If set to true or not present, the pod will be run in the host user namespace, useful for when the pod needs a feature only available to the host user namespace, such as loading a kernel module with CAP_SYS_MODULE. When set to false, a new userns is created for the pod. Setting false is useful for mitigating container breakout vulnerabilities even allowing users to run their containers as root without actually having root privileges on the host. This field is alpha-level and is only honored by servers that enable the UserNamespacesSupport feature.", + "type": [ + "boolean", + "null" + ] + }, + "hostname": { + "description": "Specifies the hostname of the Pod If not specified, the pod's hostname will be set to a system-defined value.", + "type": [ + "string", + "null" + ] + }, + "imagePullSecrets": { + "description": "ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. If specified, these secrets will be passed to individual puller implementations for them to use. More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod", + "items": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "initContainers": { + "description": "List of initialization containers belonging to the pod. Init containers are executed in order prior to containers being started. If any init container fails, the pod is considered to have failed and is handled according to its restartPolicy. The name for an init container or normal container must be unique among all containers. Init containers may not have Lifecycle actions, Readiness probes, Liveness probes, or Startup probes. The resourceRequirements of an init container are taken into account during scheduling by finding the highest request/limit for each resource type, and then using the max of of that value or the sum of the normal containers. Limits are applied to init containers in a similar fashion. Init containers cannot currently be added or removed. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/", + "items": { + "description": "A single application container that you want to run within a pod.", + "properties": { + "args": { + "description": "Arguments to the entrypoint. The container image's CMD is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "command": { + "description": "Entrypoint array. Not executed within a shell. The container image's ENTRYPOINT is used if this is not provided. Variable references $(VAR_NAME) are expanded using the container's environment. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "env": { + "description": "List of environment variables to set in the container. Cannot be updated.", + "items": { + "description": "EnvVar represents an environment variable present in a Container.", + "properties": { + "name": { + "description": "Name of the environment variable. Must be a C_IDENTIFIER.", + "type": "string" + }, + "value": { + "description": "Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. \"$$(VAR_NAME)\" will produce the string literal \"$(VAR_NAME)\". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to \"\".", + "type": [ + "string", + "null" + ] + }, + "valueFrom": { + "description": "EnvVarSource represents a source for the value of an EnvVar.", + "properties": { + "configMapKeyRef": { + "description": "Selects a key from a ConfigMap.", + "properties": { + "key": { + "description": "The key to select.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "fieldRef": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": [ + "string", + "null" + ] + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "resourceFieldRef": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": [ + "string", + "null" + ] + }, + "divisor": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "secretKeyRef": { + "description": "SecretKeySelector selects a key of a Secret.", + "properties": { + "key": { + "description": "The key of the secret to select from. Must be a valid secret key.", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "key" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "envFrom": { + "description": "List of sources to populate environment variables in the container. The keys defined within a source must be a C_IDENTIFIER. All invalid keys will be reported as an event when the container is starting. When a key exists in multiple sources, the value associated with the last source will take precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be updated.", + "items": { + "description": "EnvFromSource represents the source of a set of ConfigMaps", + "properties": { + "configMapRef": { + "description": "ConfigMapEnvSource selects a ConfigMap to populate the environment variables with.\n\nThe contents of the target ConfigMap's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the ConfigMap must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "prefix": { + "description": "An optional identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER.", + "type": [ + "string", + "null" + ] + }, + "secretRef": { + "description": "SecretEnvSource selects a Secret to populate the environment variables with.\n\nThe contents of the target Secret's Data field will represent the key-value pairs as environment variables.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "Specify whether the Secret must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "image": { + "description": "Container image name. More info: https://kubernetes.io/docs/concepts/containers/images This field is optional to allow higher level config management to default or override container images in workload controllers like Deployments and StatefulSets.", + "type": [ + "string", + "null" + ] + }, + "imagePullPolicy": { + "description": "Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. Cannot be updated. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images", + "type": [ + "string", + "null" + ] + }, + "lifecycle": { + "description": "Lifecycle describes actions that the management system should take in response to container lifecycle events. For the PostStart and PreStop lifecycle handlers, management of the container blocks until the action is complete, unless the container process fails, in which case the handler is aborted.", + "properties": { + "postStart": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "preStop": { + "description": "LifecycleHandler defines a specific action that should be taken in a lifecycle hook. One and only one of the fields, except TCPSocket must be specified.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "livenessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "Name of the container specified as a DNS_LABEL. Each container in a pod must have a unique name (DNS_LABEL). Cannot be updated.", + "type": "string" + }, + "ports": { + "description": "List of ports to expose from the container. Not specifying a port here DOES NOT prevent that port from being exposed. Any port which is listening on the default \"0.0.0.0\" address inside a container will be accessible from the network. Modifying this array with strategic merge patch may corrupt the data. For more information See https://github.com/kubernetes/kubernetes/issues/108255. Cannot be updated.", + "items": { + "description": "ContainerPort represents a network port in a single container.", + "properties": { + "containerPort": { + "description": "Number of port to expose on the pod's IP address. This must be a valid port number, 0 < x < 65536.", + "format": "int32", + "type": "integer" + }, + "hostIP": { + "description": "What host IP to bind the external port to.", + "type": [ + "string", + "null" + ] + }, + "hostPort": { + "description": "Number of port to expose on the host. If specified, this must be a valid port number, 0 < x < 65536. If HostNetwork is specified, this must match ContainerPort. Most containers do not need this.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "name": { + "description": "If specified, this must be an IANA_SVC_NAME and unique within the pod. Each named port in a pod must have a unique name. Name for the port that can be referred to by services.", + "type": [ + "string", + "null" + ] + }, + "protocol": { + "description": "Protocol for port. Must be UDP, TCP, or SCTP. Defaults to \"TCP\".", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "containerPort" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "containerPort", + "protocol" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "containerPort", + "x-kubernetes-patch-strategy": "merge" + }, + "readinessProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "resizePolicy": { + "description": "Resources resize policy for the container.", + "items": { + "description": "ContainerResizePolicy represents resource resize policy for the container.", + "properties": { + "resourceName": { + "description": "Name of the resource to which this resource resize policy applies. Supported values: cpu, memory.", + "type": "string" + }, + "restartPolicy": { + "description": "Restart policy to apply when specified resource is resized. If not specified, it defaults to NotRequired.", + "type": "string" + } + }, + "required": [ + "resourceName", + "restartPolicy" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "restartPolicy": { + "description": "RestartPolicy defines the restart behavior of individual containers in a pod. This field may only be set for init containers, and the only allowed value is \"Always\". For non-init containers or when this field is not specified, the restart behavior is defined by the Pod's restart policy and the container type. Setting the RestartPolicy as \"Always\" for the init container will have the following effect: this init container will be continually restarted on exit until all regular containers have terminated. Once all regular containers have completed, all init containers with restartPolicy \"Always\" will be shut down. This lifecycle differs from normal init containers and is often referred to as a \"sidecar\" container. Although this init container still starts in the init container sequence, it does not wait for the container to complete before proceeding to the next init container. Instead, the next init container starts immediately after this init container is started, or after any startupProbe has successfully completed.", + "type": [ + "string", + "null" + ] + }, + "securityContext": { + "description": "SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. When both are set, the values in SecurityContext take precedence.", + "properties": { + "allowPrivilegeEscalation": { + "description": "AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "capabilities": { + "description": "Adds and removes POSIX capabilities from running containers.", + "properties": { + "add": { + "description": "Added capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "drop": { + "description": "Removed capabilities", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "privileged": { + "description": "Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "procMount": { + "description": "procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "string", + "null" + ] + }, + "readOnlyRootFilesystem": { + "description": "Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "boolean", + "null" + ] + }, + "runAsGroup": { + "description": "The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "runAsNonRoot": { + "description": "Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUser": { + "description": "The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "seLinuxOptions": { + "description": "SELinuxOptions are the labels to be applied to the container", + "properties": { + "level": { + "description": "Level is SELinux level label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "role": { + "description": "Role is a SELinux role label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "Type is a SELinux type label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "user": { + "description": "User is a SELinux user label that applies to the container.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "seccompProfile": { + "description": "SeccompProfile defines a pod/container's seccomp profile settings. Only one profile source may be set.", + "properties": { + "localhostProfile": { + "description": "localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is \"Localhost\". Must NOT be set for any other type.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "type indicates which kind of seccomp profile will be applied. Valid options are:\n\nLocalhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-unions": [ + { + "discriminator": "type", + "fields-to-discriminateBy": { + "localhostProfile": "LocalhostProfile" + } + } + ], + "additionalProperties": false + }, + "windowsOptions": { + "description": "WindowsSecurityContextOptions contain Windows-specific options and credentials.", + "properties": { + "gmsaCredentialSpec": { + "description": "GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.", + "type": [ + "string", + "null" + ] + }, + "gmsaCredentialSpecName": { + "description": "GMSACredentialSpecName is the name of the GMSA credential spec to use.", + "type": [ + "string", + "null" + ] + }, + "hostProcess": { + "description": "HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUserName": { + "description": "The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "startupProbe": { + "description": "Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.", + "properties": { + "exec": { + "description": "ExecAction describes a \"run in container\" action.", + "properties": { + "command": { + "description": "Command is the command line to execute inside the container, the working directory for the command is root ('/') in the container's filesystem. The command is simply exec'd, it is not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell, you need to explicitly call out to that shell. Exit status of 0 is treated as live/healthy and non-zero is unhealthy.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "failureThreshold": { + "description": "Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "grpc": { + "properties": { + "port": { + "description": "Port number of the gRPC service. Number must be in the range 1 to 65535.", + "format": "int32", + "type": "integer" + }, + "service": { + "description": "Service is the name of the service to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md).\n\nIf this is not specified, the default behavior is defined by gRPC.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "httpGet": { + "description": "HTTPGetAction describes an action based on HTTP Get requests.", + "properties": { + "host": { + "description": "Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.", + "type": [ + "string", + "null" + ] + }, + "httpHeaders": { + "description": "Custom headers to set in the request. HTTP allows repeated headers.", + "items": { + "description": "HTTPHeader describes a custom header to be used in HTTP probes", + "properties": { + "name": { + "description": "The header field name. This will be canonicalized upon output, so case-variant names will be understood as the same header.", + "type": "string" + }, + "value": { + "description": "The header field value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "path": { + "description": "Path to access on the HTTP server.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + }, + "scheme": { + "description": "Scheme to use for connecting to the host. Defaults to HTTP.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "initialDelaySeconds": { + "description": "Number of seconds after the container has started before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "periodSeconds": { + "description": "How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "successThreshold": { + "description": "Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "tcpSocket": { + "description": "TCPSocketAction describes an action based on opening a socket", + "properties": { + "host": { + "description": "Optional: Host name to connect to, defaults to the pod IP.", + "type": [ + "string", + "null" + ] + }, + "port": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "timeoutSeconds": { + "description": "Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "stdin": { + "description": "Whether this container should allocate a buffer for stdin in the container runtime. If this is not set, reads from stdin in the container will always result in EOF. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "stdinOnce": { + "description": "Whether the container runtime should close the stdin channel after it has been opened by a single attach. When stdin is true the stdin stream will remain open across multiple attach sessions. If stdinOnce is set to true, stdin is opened on container start, is empty until the first client attaches to stdin, and then remains open and accepts data until the client disconnects, at which time stdin is closed and remains closed until the container is restarted. If this flag is false, a container processes that reads from stdin will never receive an EOF. Default is false", + "type": [ + "boolean", + "null" + ] + }, + "terminationMessagePath": { + "description": "Optional: Path at which the file to which the container's termination message will be written is mounted into the container's filesystem. Message written is intended to be brief final status, such as an assertion failure message. Will be truncated by the node if greater than 4096 bytes. The total message length across all containers will be limited to 12kb. Defaults to /dev/termination-log. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "terminationMessagePolicy": { + "description": "Indicate how the termination message should be populated. File will use the contents of terminationMessagePath to populate the container status message on both success and failure. FallbackToLogsOnError will use the last chunk of container log output if the termination message file is empty and the container exited with an error. The log output is limited to 2048 bytes or 80 lines, whichever is smaller. Defaults to File. Cannot be updated.", + "type": [ + "string", + "null" + ] + }, + "tty": { + "description": "Whether this container should allocate a TTY for itself, also requires 'stdin' to be true. Default is false.", + "type": [ + "boolean", + "null" + ] + }, + "volumeDevices": { + "description": "volumeDevices is the list of block devices to be used by the container.", + "items": { + "description": "volumeDevice describes a mapping of a raw block device within a container.", + "properties": { + "devicePath": { + "description": "devicePath is the path inside of the container that the device will be mapped to.", + "type": "string" + }, + "name": { + "description": "name must match the name of a persistentVolumeClaim in the pod", + "type": "string" + } + }, + "required": [ + "name", + "devicePath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "devicePath", + "x-kubernetes-patch-strategy": "merge" + }, + "volumeMounts": { + "description": "Pod volumes to mount into the container's filesystem. Cannot be updated.", + "items": { + "description": "VolumeMount describes a mounting of a Volume within a container.", + "properties": { + "mountPath": { + "description": "Path within the container at which the volume should be mounted. Must not contain ':'.", + "type": "string" + }, + "mountPropagation": { + "description": "mountPropagation determines how mounts are propagated from the host to container and the other way around. When not set, MountPropagationNone is used. This field is beta in 1.10.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "This must match the Name of a Volume.", + "type": "string" + }, + "readOnly": { + "description": "Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, + "subPath": { + "description": "Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root).", + "type": [ + "string", + "null" + ] + }, + "subPathExpr": { + "description": "Expanded path within the volume from which the container's volume should be mounted. Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. Defaults to \"\" (volume's root). SubPathExpr and SubPath are mutually exclusive.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "mountPath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "mountPath", + "x-kubernetes-patch-strategy": "merge" + }, + "workingDir": { + "description": "Container's working directory. If not specified, the container runtime's default will be used, which might be configured in the container image. Cannot be updated.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "nodeName": { + "description": "NodeName is a request to schedule this pod onto a specific node. If it is non-empty, the scheduler simply schedules this pod onto that node, assuming that it fits resource requirements.", + "type": [ + "string", + "null" + ] + }, + "nodeSelector": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "NodeSelector is a selector which must be true for the pod to fit on a node. Selector which must match a node's labels for the pod to be scheduled on that node. More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/", + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic" + }, + "os": { + "description": "PodOS defines the OS parameters of a pod.", + "properties": { + "name": { + "description": "Name is the name of the operating system. The currently supported values are linux and windows. Additional value may be defined in future and can be one of: https://github.com/opencontainers/runtime-spec/blob/master/config.md#platform-specific-configuration Clients should expect to handle additional values and treat unrecognized values in this field as os: null", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "overhead": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Overhead represents the resource overhead associated with running a pod for a given RuntimeClass. This field will be autopopulated at admission time by the RuntimeClass admission controller. If the RuntimeClass admission controller is enabled, overhead must not be set in Pod create requests. The RuntimeClass admission controller will reject Pod create requests which have the overhead already set. If RuntimeClass is configured and selected in the PodSpec, Overhead will be set to the value defined in the corresponding RuntimeClass, otherwise it will remain unset and treated as zero. More info: https://git.k8s.io/enhancements/keps/sig-node/688-pod-overhead/README.md", + "type": [ + "object", + "null" + ] + }, + "preemptionPolicy": { + "description": "PreemptionPolicy is the Policy for preempting pods with lower priority. One of Never, PreemptLowerPriority. Defaults to PreemptLowerPriority if unset.", + "type": [ + "string", + "null" + ] + }, + "priority": { + "description": "The priority value. Various system components use this field to find the priority of the pod. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "priorityClassName": { + "description": "If specified, indicates the pod's priority. \"system-node-critical\" and \"system-cluster-critical\" are two special keywords which indicate the highest priorities with the former being the highest priority. Any other name must be defined by creating a PriorityClass object with that name. If not specified, the pod priority will be default or zero if there is no default.", + "type": [ + "string", + "null" + ] + }, + "readinessGates": { + "description": "If specified, all readiness gates will be evaluated for pod readiness. A pod is ready when all its containers are ready AND all conditions specified in the readiness gates have status equal to \"True\" More info: https://git.k8s.io/enhancements/keps/sig-network/580-pod-readiness-gates", + "items": { + "description": "PodReadinessGate contains the reference to a pod condition", + "properties": { + "conditionType": { + "description": "ConditionType refers to a condition in the pod's condition list with matching type.", + "type": "string" + } + }, + "required": [ + "conditionType" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "resourceClaims": { + "description": "ResourceClaims defines which ResourceClaims must be allocated and reserved before the Pod is allowed to start. The resources will be made available to those containers which consume them by name.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable.", + "items": { + "description": "PodResourceClaim references exactly one ResourceClaim through a ClaimSource. It adds a name to it that uniquely identifies the ResourceClaim inside the Pod. Containers that need access to the ResourceClaim reference it with this name.", + "properties": { + "name": { + "description": "Name uniquely identifies this resource claim inside the pod. This must be a DNS_LABEL.", + "type": "string" + }, + "source": { + "description": "ClaimSource describes a reference to a ResourceClaim.\n\nExactly one of these fields should be set. Consumers of this type must treat an empty object as if it has an unknown value.", + "properties": { + "resourceClaimName": { + "description": "ResourceClaimName is the name of a ResourceClaim object in the same namespace as this pod.", + "type": [ + "string", + "null" + ] + }, + "resourceClaimTemplateName": { + "description": "ResourceClaimTemplateName is the name of a ResourceClaimTemplate object in the same namespace as this pod.\n\nThe template will be used to create a new ResourceClaim, which will be bound to this pod. When this pod is deleted, the ResourceClaim will also be deleted. The pod name and resource name, along with a generated component, will be used to form a unique name for the ResourceClaim, which will be recorded in pod.status.resourceClaimStatuses.\n\nThis field is immutable and no changes will be made to the corresponding ResourceClaim by the control plane after creating the ResourceClaim.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge,retainKeys" + }, + "restartPolicy": { + "description": "Restart policy for all containers within the pod. One of Always, OnFailure, Never. In some contexts, only a subset of those values may be permitted. Default to Always. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy", + "type": [ + "string", + "null" + ] + }, + "runtimeClassName": { + "description": "RuntimeClassName refers to a RuntimeClass object in the node.k8s.io group, which should be used to run this pod. If no RuntimeClass resource matches the named class, the pod will not be run. If unset or empty, the \"legacy\" RuntimeClass will be used, which is an implicit class with an empty definition that uses the default runtime handler. More info: https://git.k8s.io/enhancements/keps/sig-node/585-runtime-class", + "type": [ + "string", + "null" + ] + }, + "schedulerName": { + "description": "If specified, the pod will be dispatched by specified scheduler. If not specified, the pod will be dispatched by default scheduler.", + "type": [ + "string", + "null" + ] + }, + "schedulingGates": { + "description": "SchedulingGates is an opaque list of values that if specified will block scheduling the pod. If schedulingGates is not empty, the pod will stay in the SchedulingGated state and the scheduler will not attempt to schedule the pod.\n\nSchedulingGates can only be set at pod creation time, and be removed only afterwards.\n\nThis is a beta feature enabled by the PodSchedulingReadiness feature gate.", + "items": { + "description": "PodSchedulingGate is associated to a Pod to guard its scheduling.", + "properties": { + "name": { + "description": "Name of the scheduling gate. Each scheduling gate must have a unique name field.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "securityContext": { + "description": "PodSecurityContext holds pod-level security attributes and common container settings. Some fields are also present in container.securityContext. Field values of container.securityContext take precedence over field values of PodSecurityContext.", + "properties": { + "fsGroup": { + "description": "A special supplemental group that applies to all containers in a pod. Some volume types allow the Kubelet to change the ownership of that volume to be owned by the pod:\n\n1. The owning GID will be the FSGroup 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) 3. The permission bits are OR'd with rw-rw----\n\nIf unset, the Kubelet will not modify the ownership and permissions of any volume. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "fsGroupChangePolicy": { + "description": "fsGroupChangePolicy defines behavior of changing ownership and permission of the volume before being exposed inside Pod. This field will only apply to volume types which support fsGroup based ownership(and permissions). It will have no effect on ephemeral volume types such as: secret, configmaps and emptydir. Valid values are \"OnRootMismatch\" and \"Always\". If not specified, \"Always\" is used. Note that this field cannot be set when spec.os.name is windows.", + "type": [ + "string", + "null" + ] + }, + "runAsGroup": { + "description": "The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "runAsNonRoot": { + "description": "Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUser": { + "description": "The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "seLinuxOptions": { + "description": "SELinuxOptions are the labels to be applied to the container", + "properties": { + "level": { + "description": "Level is SELinux level label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "role": { + "description": "Role is a SELinux role label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "Type is a SELinux type label that applies to the container.", + "type": [ + "string", + "null" + ] + }, + "user": { + "description": "User is a SELinux user label that applies to the container.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "seccompProfile": { + "description": "SeccompProfile defines a pod/container's seccomp profile settings. Only one profile source may be set.", + "properties": { + "localhostProfile": { + "description": "localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is \"Localhost\". Must NOT be set for any other type.", + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "type indicates which kind of seccomp profile will be applied. Valid options are:\n\nLocalhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-unions": [ + { + "discriminator": "type", + "fields-to-discriminateBy": { + "localhostProfile": "LocalhostProfile" + } + } + ], + "additionalProperties": false + }, + "supplementalGroups": { + "description": "A list of groups applied to the first process run in each container, in addition to the container's primary GID, the fsGroup (if specified), and group memberships defined in the container image for the uid of the container process. If unspecified, no additional groups are added to any container. Note that group memberships defined in the container image for the uid of the container process are still effective, even if they are not included in this list. Note that this field cannot be set when spec.os.name is windows.", + "items": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "sysctls": { + "description": "Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported sysctls (by the container runtime) might fail to launch. Note that this field cannot be set when spec.os.name is windows.", + "items": { + "description": "Sysctl defines a kernel parameter to be set", + "properties": { + "name": { + "description": "Name of a property to set", + "type": "string" + }, + "value": { + "description": "Value of a property to set", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "windowsOptions": { + "description": "WindowsSecurityContextOptions contain Windows-specific options and credentials.", + "properties": { + "gmsaCredentialSpec": { + "description": "GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.", + "type": [ + "string", + "null" + ] + }, + "gmsaCredentialSpecName": { + "description": "GMSACredentialSpecName is the name of the GMSA credential spec to use.", + "type": [ + "string", + "null" + ] + }, + "hostProcess": { + "description": "HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.", + "type": [ + "boolean", + "null" + ] + }, + "runAsUserName": { + "description": "The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "serviceAccount": { + "description": "DeprecatedServiceAccount is a depreciated alias for ServiceAccountName. Deprecated: Use serviceAccountName instead.", + "type": [ + "string", + "null" + ] + }, + "serviceAccountName": { + "description": "ServiceAccountName is the name of the ServiceAccount to use to run this pod. More info: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/", + "type": [ + "string", + "null" + ] + }, + "setHostnameAsFQDN": { + "description": "If true the pod's hostname will be configured as the pod's FQDN, rather than the leaf name (the default). In Linux containers, this means setting the FQDN in the hostname field of the kernel (the nodename field of struct utsname). In Windows containers, this means setting the registry value of hostname for the registry key HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters to FQDN. If a pod does not have FQDN, this has no effect. Default to false.", + "type": [ + "boolean", + "null" + ] + }, + "shareProcessNamespace": { + "description": "Share a single process namespace between all of the containers in a pod. When this is set containers will be able to view and signal processes from other containers in the same pod, and the first process in each container will not be assigned PID 1. HostPID and ShareProcessNamespace cannot both be set. Optional: Default to false.", + "type": [ + "boolean", + "null" + ] + }, + "subdomain": { + "description": "If specified, the fully qualified Pod hostname will be \"...svc.\". If not specified, the pod will not have a domainname at all.", + "type": [ + "string", + "null" + ] + }, + "terminationGracePeriodSeconds": { + "description": "Optional duration in seconds the pod needs to terminate gracefully. May be decreased in delete request. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). If this value is nil, the default grace period will be used instead. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. Defaults to 30 seconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "tolerations": { + "description": "If specified, the pod's tolerations.", + "items": { + "description": "The pod this Toleration is attached to tolerates any taint that matches the triple using the matching operator .", + "properties": { + "effect": { + "description": "Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.", + "type": [ + "string", + "null" + ] + }, + "key": { + "description": "Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys.", + "type": [ + "string", + "null" + ] + }, + "operator": { + "description": "Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category.", + "type": [ + "string", + "null" + ] + }, + "tolerationSeconds": { + "description": "TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "value": { + "description": "Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "topologySpreadConstraints": { + "description": "TopologySpreadConstraints describes how a group of pods ought to spread across topology domains. Scheduler will schedule pods in a way which abides by the constraints. All topologySpreadConstraints are ANDed.", + "items": { + "description": "TopologySpreadConstraint specifies how to spread matching pods among the given topology.", + "properties": { + "labelSelector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "matchLabelKeys": { + "description": "MatchLabelKeys is a set of pod label keys to select the pods over which spreading will be calculated. The keys are used to lookup values from the incoming pod labels, those key-value labels are ANDed with labelSelector to select the group of existing pods over which spreading will be calculated for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. MatchLabelKeys cannot be set when LabelSelector isn't set. Keys that don't exist in the incoming pod labels will be ignored. A null or empty list means only match against labelSelector.\n\nThis is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default).", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "maxSkew": { + "description": "MaxSkew describes the degree to which pods may be unevenly distributed. When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference between the number of matching pods in the target topology and the global minimum. The global minimum is the minimum number of matching pods in an eligible domain or zero if the number of eligible domains is less than MinDomains. For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same labelSelector spread as 2/2/1: In this case, the global minimum is 1. | zone1 | zone2 | zone3 | | P P | P P | P | - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) violate MaxSkew(1). - if MaxSkew is 2, incoming pod can be scheduled onto any zone. When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence to topologies that satisfy it. It's a required field. Default value is 1 and 0 is not allowed.", + "format": "int32", + "type": "integer" + }, + "minDomains": { + "description": "MinDomains indicates a minimum number of eligible domains. When the number of eligible domains with matching topology keys is less than minDomains, Pod Topology Spread treats \"global minimum\" as 0, and then the calculation of Skew is performed. And when the number of eligible domains with matching topology keys equals or greater than minDomains, this value has no effect on scheduling. As a result, when the number of eligible domains is less than minDomains, scheduler won't schedule more than maxSkew Pods to those domains. If value is nil, the constraint behaves as if MinDomains is equal to 1. Valid values are integers greater than 0. When value is not nil, WhenUnsatisfiable must be DoNotSchedule.\n\nFor example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same labelSelector spread as 2/2/2: | zone1 | zone2 | zone3 | | P P | P P | P P | The number of domains is less than 5(MinDomains), so \"global minimum\" is treated as 0. In this situation, new pod with the same labelSelector cannot be scheduled, because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, it will violate MaxSkew.\n\nThis is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default).", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "nodeAffinityPolicy": { + "description": "NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector when calculating pod topology spread skew. Options are: - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations.\n\nIf this value is nil, the behavior is equivalent to the Honor policy. This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag.", + "type": [ + "string", + "null" + ] + }, + "nodeTaintsPolicy": { + "description": "NodeTaintsPolicy indicates how we will treat node taints when calculating pod topology spread skew. Options are: - Honor: nodes without taints, along with tainted nodes for which the incoming pod has a toleration, are included. - Ignore: node taints are ignored. All nodes are included.\n\nIf this value is nil, the behavior is equivalent to the Ignore policy. This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag.", + "type": [ + "string", + "null" + ] + }, + "topologyKey": { + "description": "TopologyKey is the key of node labels. Nodes that have a label with this key and identical values are considered to be in the same topology. We consider each as a \"bucket\", and try to put balanced number of pods into each bucket. We define a domain as a particular instance of a topology. Also, we define an eligible domain as a domain whose nodes meet the requirements of nodeAffinityPolicy and nodeTaintsPolicy. e.g. If TopologyKey is \"kubernetes.io/hostname\", each Node is a domain of that topology. And, if TopologyKey is \"topology.kubernetes.io/zone\", each zone is a domain of that topology. It's a required field.", + "type": "string" + }, + "whenUnsatisfiable": { + "description": "WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy the spread constraint. - DoNotSchedule (default) tells the scheduler not to schedule it. - ScheduleAnyway tells the scheduler to schedule the pod in any location,\n but giving higher precedence to topologies that would help reduce the\n skew.\nA constraint is considered \"Unsatisfiable\" for an incoming pod if and only if every possible node assignment for that pod would violate \"MaxSkew\" on some topology. For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same labelSelector spread as 3/1/1: | zone1 | zone2 | zone3 | | P P P | P | P | If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler won't make it *more* imbalanced. It's a required field.", + "type": "string" + } + }, + "required": [ + "maxSkew", + "topologyKey", + "whenUnsatisfiable" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "topologyKey", + "whenUnsatisfiable" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "topologyKey", + "x-kubernetes-patch-strategy": "merge" + }, + "volumes": { + "description": "List of volumes that can be mounted by containers belonging to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes", + "items": { + "description": "Volume represents a named volume in a pod that may be accessed by any container in the pod.", + "properties": { + "awsElasticBlockStore": { + "description": "Represents a Persistent Disk resource in AWS.\n\nAn AWS EBS disk must exist before mounting to a container. The disk must also be in the same AWS zone as the kubelet. An AWS EBS disk can only be mounted as read/write once. AWS EBS volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", + "type": [ + "string", + "null" + ] + }, + "partition": { + "description": "partition is the partition in the volume that you want to mount. If omitted, the default is to mount by volume name. Examples: For volume /dev/sda1, you specify the partition as \"1\". Similarly, the volume partition for /dev/sda is \"0\" (or you can leave the property empty).", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "readOnly": { + "description": "readOnly value true will force the readOnly setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", + "type": [ + "boolean", + "null" + ] + }, + "volumeID": { + "description": "volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore", + "type": "string" + } + }, + "required": [ + "volumeID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "azureDisk": { + "description": "AzureDisk represents an Azure Data Disk mount on the host and bind mount to the pod.", + "properties": { + "cachingMode": { + "description": "cachingMode is the Host Caching mode: None, Read Only, Read Write.", + "type": [ + "string", + "null" + ] + }, + "diskName": { + "description": "diskName is the Name of the data disk in the blob storage", + "type": "string" + }, + "diskURI": { + "description": "diskURI is the URI of data disk in the blob storage", + "type": "string" + }, + "fsType": { + "description": "fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "kind": { + "description": "kind expected values are Shared: multiple blob disks per storage account Dedicated: single blob disk per storage account Managed: azure managed data disk (only in managed availability set). defaults to shared", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "diskName", + "diskURI" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "azureFile": { + "description": "AzureFile represents an Azure File Service mount on the host and bind mount to the pod.", + "properties": { + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "secretName": { + "description": "secretName is the name of secret that contains Azure Storage Account Name and Key", + "type": "string" + }, + "shareName": { + "description": "shareName is the azure share Name", + "type": "string" + } + }, + "required": [ + "secretName", + "shareName" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "cephfs": { + "description": "Represents a Ceph Filesystem mount that lasts the lifetime of a pod Cephfs volumes do not support ownership management or SELinux relabeling.", + "properties": { + "monitors": { + "description": "monitors is Required: Monitors is a collection of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": "array" + }, + "path": { + "description": "path is Optional: Used as the mounted root, rather than the full Ceph tree, default is /", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly is Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "type": [ + "boolean", + "null" + ] + }, + "secretFile": { + "description": "secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "type": [ + "string", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "user": { + "description": "user is optional: User is the rados user name, default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "monitors" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "cinder": { + "description": "Represents a cinder volume resource in Openstack. A Cinder volume must exist before mounting to a container. The volume must also be in the same region as the kubelet. Cinder volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "volumeID": { + "description": "volumeID used to identify the volume in cinder. More info: https://examples.k8s.io/mysql-cinder-pd/README.md", + "type": "string" + } + }, + "required": [ + "volumeID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "configMap": { + "description": "Adapts a ConfigMap into a volume.\n\nThe contents of the target ConfigMap's Data field will be presented in a volume as files using the keys in the Data field as the file names, unless the items element is populated with specific mappings of keys to paths. ConfigMap volumes support ownership management and SELinux relabeling.", + "properties": { + "defaultMode": { + "description": "defaultMode is optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "items": { + "description": "items if unspecified, each key-value pair in the Data field of the referenced ConfigMap will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the ConfigMap, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "description": "Maps a string key to a path within a volume.", + "properties": { + "key": { + "description": "key is the key to project.", + "type": "string" + }, + "mode": { + "description": "mode is Optional: mode bits used to set permissions on this file. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.", + "type": "string" + } + }, + "required": [ + "key", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "optional specify whether the ConfigMap or its keys must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "csi": { + "description": "Represents a source location of a volume to mount, managed by an external CSI driver", + "properties": { + "driver": { + "description": "driver is the name of the CSI driver that handles this volume. Consult with your admin for the correct name as registered in the cluster.", + "type": "string" + }, + "fsType": { + "description": "fsType to mount. Ex. \"ext4\", \"xfs\", \"ntfs\". If not provided, the empty value is passed to the associated CSI driver which will determine the default filesystem to apply.", + "type": [ + "string", + "null" + ] + }, + "nodePublishSecretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "readOnly": { + "description": "readOnly specifies a read-only configuration for the volume. Defaults to false (read/write).", + "type": [ + "boolean", + "null" + ] + }, + "volumeAttributes": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "volumeAttributes stores driver-specific properties that are passed to the CSI driver. Consult your driver's documentation for supported values.", + "type": [ + "object", + "null" + ] + } + }, + "required": [ + "driver" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "downwardAPI": { + "description": "DownwardAPIVolumeSource represents a volume containing downward API info. Downward API volumes support ownership management and SELinux relabeling.", + "properties": { + "defaultMode": { + "description": "Optional: mode bits to use on created files by default. Must be a Optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "items": { + "description": "Items is a list of downward API volume file", + "items": { + "description": "DownwardAPIVolumeFile represents information to create the file containing the pod field", + "properties": { + "fieldRef": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": [ + "string", + "null" + ] + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "mode": { + "description": "Optional: mode bits used to set permissions on this file, must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "Required: Path is the relative path name of the file to be created. Must not be absolute or contain the '..' path. Must be utf-8 encoded. The first item of the relative path must not start with '..'", + "type": "string" + }, + "resourceFieldRef": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": [ + "string", + "null" + ] + }, + "divisor": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "required": [ + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "emptyDir": { + "description": "Represents an empty directory for a pod. Empty directory volumes support ownership management and SELinux relabeling.", + "properties": { + "medium": { + "description": "medium represents what type of storage medium should back this directory. The default is \"\" which means to use the node's default medium. Must be an empty string (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir", + "type": [ + "string", + "null" + ] + }, + "sizeLimit": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "ephemeral": { + "description": "Represents an ephemeral volume that is handled by a normal storage driver.", + "properties": { + "volumeClaimTemplate": { + "description": "PersistentVolumeClaimTemplate is used to produce PersistentVolumeClaim objects as part of an EphemeralVolumeSource.", + "properties": { + "metadata": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "properties": { + "annotations": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": [ + "object", + "null" + ] + }, + "creationTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "deletionTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": [ + "string", + "null" + ] + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "labels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": [ + "object", + "null" + ] + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "items": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": [ + "string", + "null" + ] + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": [ + "string", + "null" + ] + }, + "fieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": [ + "object", + "null" + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": [ + "string", + "null" + ] + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": [ + "string", + "null" + ] + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": [ + "string", + "null" + ] + }, + "time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": [ + "string", + "null" + ] + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": [ + "string", + "null" + ] + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "items": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": [ + "boolean", + "null" + ] + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": [ + "boolean", + "null" + ] + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + }, + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": [ + "string", + "null" + ] + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": [ + "string", + "null" + ] + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "spec": { + "description": "PersistentVolumeClaimSpec describes the common attributes of storage devices and allows a Source for provider-specific attributes", + "properties": { + "accessModes": { + "description": "accessModes contains the desired access modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "dataSource": { + "description": "TypedLocalObjectReference contains enough information to let you locate the typed referenced object inside the same namespace.", + "properties": { + "apiGroup": { + "description": "APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.", + "type": [ + "string", + "null" + ] + }, + "kind": { + "description": "Kind is the type of resource being referenced", + "type": "string" + }, + "name": { + "description": "Name is the name of resource being referenced", + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "dataSourceRef": { + "properties": { + "apiGroup": { + "description": "APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required.", + "type": [ + "string", + "null" + ] + }, + "kind": { + "description": "Kind is the type of resource being referenced", + "type": "string" + }, + "name": { + "description": "Name is the name of resource being referenced", + "type": "string" + }, + "namespace": { + "description": "Namespace is the namespace of resource being referenced Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "kind", + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "selector": { + "description": "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "key", + "operator" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "matchLabels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "storageClassName": { + "description": "storageClassName is the name of the StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1", + "type": [ + "string", + "null" + ] + }, + "volumeMode": { + "description": "volumeMode defines what type of volume is required by the claim. Value of Filesystem is implied when not included in claim spec.", + "type": [ + "string", + "null" + ] + }, + "volumeName": { + "description": "volumeName is the binding reference to the PersistentVolume backing this claim.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object", + "additionalProperties": false + } + }, + "required": [ + "spec" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "fc": { + "description": "Represents a Fibre Channel volume. Fibre Channel volumes can only be mounted as read/write once. Fibre Channel volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "lun": { + "description": "lun is Optional: FC target lun number", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "readOnly": { + "description": "readOnly is Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "targetWWNs": { + "description": "targetWWNs is Optional: FC target worldwide names (WWNs)", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "wwids": { + "description": "wwids Optional: FC volume world wide identifiers (wwids) Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "flexVolume": { + "description": "FlexVolume represents a generic volume resource that is provisioned/attached using an exec based plugin.", + "properties": { + "driver": { + "description": "driver is the name of the driver to use for this volume.", + "type": "string" + }, + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". The default filesystem depends on FlexVolume script.", + "type": [ + "string", + "null" + ] + }, + "options": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "options is Optional: this field holds extra command options if any.", + "type": [ + "object", + "null" + ] + }, + "readOnly": { + "description": "readOnly is Optional: defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "required": [ + "driver" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "flocker": { + "description": "Represents a Flocker volume mounted by the Flocker agent. One and only one of datasetName and datasetUUID should be set. Flocker volumes do not support ownership management or SELinux relabeling.", + "properties": { + "datasetName": { + "description": "datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker should be considered as deprecated", + "type": [ + "string", + "null" + ] + }, + "datasetUUID": { + "description": "datasetUUID is the UUID of the dataset. This is unique identifier of a Flocker dataset", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "gcePersistentDisk": { + "description": "Represents a Persistent Disk resource in Google Compute Engine.\n\nA GCE PD must exist before mounting to a container. The disk must also be in the same GCE project and zone as the kubelet. A GCE PD can only be mounted as read/write once or read-only many times. GCE PDs support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "type": [ + "string", + "null" + ] + }, + "partition": { + "description": "partition is the partition in the volume that you want to mount. If omitted, the default is to mount by volume name. Examples: For volume /dev/sda1, you specify the partition as \"1\". Similarly, the volume partition for /dev/sda is \"0\" (or you can leave the property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "pdName": { + "description": "pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "pdName" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "gitRepo": { + "description": "Represents a volume that is populated with the contents of a git repository. Git repo volumes do not support ownership management. Git repo volumes support SELinux relabeling.\n\nDEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir into the Pod's container.", + "properties": { + "directory": { + "description": "directory is the target directory name. Must not contain or start with '..'. If '.' is supplied, the volume directory will be the git repository. Otherwise, if specified, the volume will contain the git repository in the subdirectory with the given name.", + "type": [ + "string", + "null" + ] + }, + "repository": { + "description": "repository is the URL", + "type": "string" + }, + "revision": { + "description": "revision is the commit hash for the specified revision.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "repository" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "glusterfs": { + "description": "Represents a Glusterfs mount that lasts the lifetime of a pod. Glusterfs volumes do not support ownership management or SELinux relabeling.", + "properties": { + "endpoints": { + "description": "endpoints is the endpoint name that details Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", + "type": "string" + }, + "path": { + "description": "path is the Glusterfs volume path. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the Glusterfs volume to be mounted with read-only permissions. Defaults to false. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "endpoints", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "hostPath": { + "description": "Represents a host path mapped into a pod. Host path volumes do not support ownership management or SELinux relabeling.", + "properties": { + "path": { + "description": "path of the directory on the host. If the path is a symlink, it will follow the link to the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath", + "type": "string" + }, + "type": { + "description": "type for HostPath Volume Defaults to \"\" More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "iscsi": { + "description": "Represents an ISCSI disk. ISCSI volumes can only be mounted as read/write once. ISCSI volumes support ownership management and SELinux relabeling.", + "properties": { + "chapAuthDiscovery": { + "description": "chapAuthDiscovery defines whether support iSCSI Discovery CHAP authentication", + "type": [ + "boolean", + "null" + ] + }, + "chapAuthSession": { + "description": "chapAuthSession defines whether support iSCSI Session CHAP authentication", + "type": [ + "boolean", + "null" + ] + }, + "fsType": { + "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi", + "type": [ + "string", + "null" + ] + }, + "initiatorName": { + "description": "initiatorName is the custom iSCSI Initiator Name. If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface : will be created for the connection.", + "type": [ + "string", + "null" + ] + }, + "iqn": { + "description": "iqn is the target iSCSI Qualified Name.", + "type": "string" + }, + "iscsiInterface": { + "description": "iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp).", + "type": [ + "string", + "null" + ] + }, + "lun": { + "description": "lun represents iSCSI Target Lun number.", + "format": "int32", + "type": "integer" + }, + "portals": { + "description": "portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port is other than default (typically TCP ports 860 and 3260).", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "readOnly": { + "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "targetPortal": { + "description": "targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port is other than default (typically TCP ports 860 and 3260).", + "type": "string" + } + }, + "required": [ + "targetPortal", + "iqn", + "lun" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "name of the volume. Must be a DNS_LABEL and unique within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "nfs": { + "description": "Represents an NFS mount that lasts the lifetime of a pod. NFS volumes do not support ownership management or SELinux relabeling.", + "properties": { + "path": { + "description": "path that is exported by the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", + "type": "string" + }, + "readOnly": { + "description": "readOnly here will force the NFS export to be mounted with read-only permissions. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", + "type": [ + "boolean", + "null" + ] + }, + "server": { + "description": "server is the hostname or IP address of the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs", + "type": "string" + } + }, + "required": [ + "server", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "persistentVolumeClaim": { + "description": "PersistentVolumeClaimVolumeSource references the user's PVC in the same namespace. This volume finds the bound PV and mounts that volume for the pod. A PersistentVolumeClaimVolumeSource is, essentially, a wrapper around another type of volume that is owned by someone else (the system).", + "properties": { + "claimName": { + "description": "claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims", + "type": "string" + }, + "readOnly": { + "description": "readOnly Will force the ReadOnly setting in VolumeMounts. Default false.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "claimName" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "photonPersistentDisk": { + "description": "Represents a Photon Controller persistent disk resource.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "pdID": { + "description": "pdID is the ID that identifies Photon Controller persistent disk", + "type": "string" + } + }, + "required": [ + "pdID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "portworxVolume": { + "description": "PortworxVolumeSource represents a Portworx volume resource.", + "properties": { + "fsType": { + "description": "fSType represents the filesystem type to mount Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "volumeID": { + "description": "volumeID uniquely identifies a Portworx volume", + "type": "string" + } + }, + "required": [ + "volumeID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "projected": { + "description": "Represents a projected volume source", + "properties": { + "defaultMode": { + "description": "defaultMode are the mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "sources": { + "description": "sources is the list of volume projections", + "items": { + "description": "Projection that may be projected along with other supported volume types", + "properties": { + "configMap": { + "description": "Adapts a ConfigMap into a projected volume.\n\nThe contents of the target ConfigMap's Data field will be presented in a projected volume as files using the keys in the Data field as the file names, unless the items element is populated with specific mappings of keys to paths. Note that this is identical to a configmap volume source without the default mode.", + "properties": { + "items": { + "description": "items if unspecified, each key-value pair in the Data field of the referenced ConfigMap will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the ConfigMap, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "description": "Maps a string key to a path within a volume.", + "properties": { + "key": { + "description": "key is the key to project.", + "type": "string" + }, + "mode": { + "description": "mode is Optional: mode bits used to set permissions on this file. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.", + "type": "string" + } + }, + "required": [ + "key", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "optional specify whether the ConfigMap or its keys must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "downwardAPI": { + "description": "Represents downward API info for projecting into a projected volume. Note that this is identical to a downwardAPI volume source without the default mode.", + "properties": { + "items": { + "description": "Items is a list of DownwardAPIVolume file", + "items": { + "description": "DownwardAPIVolumeFile represents information to create the file containing the pod field", + "properties": { + "fieldRef": { + "description": "ObjectFieldSelector selects an APIVersioned field of an object.", + "properties": { + "apiVersion": { + "description": "Version of the schema the FieldPath is written in terms of, defaults to \"v1\".", + "type": [ + "string", + "null" + ] + }, + "fieldPath": { + "description": "Path of the field to select in the specified API version.", + "type": "string" + } + }, + "required": [ + "fieldPath" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "mode": { + "description": "Optional: mode bits used to set permissions on this file, must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "Required: Path is the relative path name of the file to be created. Must not be absolute or contain the '..' path. Must be utf-8 encoded. The first item of the relative path must not start with '..'", + "type": "string" + }, + "resourceFieldRef": { + "description": "ResourceFieldSelector represents container resources (cpu, memory) and their output format", + "properties": { + "containerName": { + "description": "Container name: required for volumes, optional for env vars", + "type": [ + "string", + "null" + ] + }, + "divisor": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "resource": { + "description": "Required: resource to select", + "type": "string" + } + }, + "required": [ + "resource" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + } + }, + "required": [ + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "secret": { + "description": "Adapts a secret into a projected volume.\n\nThe contents of the target Secret's Data field will be presented in a projected volume as files using the keys in the Data field as the file names. Note that this is identical to a secret volume source without the default mode.", + "properties": { + "items": { + "description": "items if unspecified, each key-value pair in the Data field of the referenced Secret will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the Secret, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "description": "Maps a string key to a path within a volume.", + "properties": { + "key": { + "description": "key is the key to project.", + "type": "string" + }, + "mode": { + "description": "mode is Optional: mode bits used to set permissions on this file. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.", + "type": "string" + } + }, + "required": [ + "key", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + }, + "optional": { + "description": "optional field specify whether the Secret or its key must be defined", + "type": [ + "boolean", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "serviceAccountToken": { + "description": "ServiceAccountTokenProjection represents a projected service account token volume. This projection can be used to insert a service account token into the pods runtime filesystem for use against APIs (Kubernetes API Server or otherwise).", + "properties": { + "audience": { + "description": "audience is the intended audience of the token. A recipient of a token must identify itself with an identifier specified in the audience of the token, and otherwise should reject the token. The audience defaults to the identifier of the apiserver.", + "type": [ + "string", + "null" + ] + }, + "expirationSeconds": { + "description": "expirationSeconds is the requested duration of validity of the service account token. As the token approaches expiration, the kubelet volume plugin will proactively rotate the service account token. The kubelet will start trying to rotate the token if the token is older than 80 percent of its time to live or if the token is older than 24 hours.Defaults to 1 hour and must be at least 10 minutes.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "path is the path relative to the mount point of the file to project the token into.", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "quobyte": { + "description": "Represents a Quobyte mount that lasts the lifetime of a pod. Quobyte volumes do not support ownership management or SELinux relabeling.", + "properties": { + "group": { + "description": "group to map volume access to Default is no group", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly here will force the Quobyte volume to be mounted with read-only permissions. Defaults to false.", + "type": [ + "boolean", + "null" + ] + }, + "registry": { + "description": "registry represents a single or multiple Quobyte Registry services specified as a string as host:port pair (multiple entries are separated with commas) which acts as the central registry for volumes", + "type": "string" + }, + "tenant": { + "description": "tenant owning the given Quobyte volume in the Backend Used with dynamically provisioned Quobyte volumes, value is set by the plugin", + "type": [ + "string", + "null" + ] + }, + "user": { + "description": "user to map volume access to Defaults to serivceaccount user", + "type": [ + "string", + "null" + ] + }, + "volume": { + "description": "volume is a string that references an already created Quobyte volume by name.", + "type": "string" + } + }, + "required": [ + "registry", + "volume" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "rbd": { + "description": "Represents a Rados Block Device mount that lasts the lifetime of a pod. RBD volumes support ownership management and SELinux relabeling.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type of the volume that you want to mount. Tip: Ensure that the filesystem type is supported by the host operating system. Examples: \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd", + "type": [ + "string", + "null" + ] + }, + "image": { + "description": "image is the rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": "string" + }, + "keyring": { + "description": "keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": [ + "string", + "null" + ] + }, + "monitors": { + "description": "monitors is a collection of Ceph monitors. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": "array" + }, + "pool": { + "description": "pool is the rados pool name. Default is rbd. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "user": { + "description": "user is the rados user name. Default is admin. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "monitors", + "image" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "scaleIO": { + "description": "ScaleIOVolumeSource represents a persistent ScaleIO volume", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Default is \"xfs\".", + "type": [ + "string", + "null" + ] + }, + "gateway": { + "description": "gateway is the host address of the ScaleIO API Gateway.", + "type": "string" + }, + "protectionDomain": { + "description": "protectionDomain is the name of the ScaleIO Protection Domain for the configured storage.", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": "object", + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "sslEnabled": { + "description": "sslEnabled Flag enable/disable SSL communication with Gateway, default false", + "type": [ + "boolean", + "null" + ] + }, + "storageMode": { + "description": "storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. Default is ThinProvisioned.", + "type": [ + "string", + "null" + ] + }, + "storagePool": { + "description": "storagePool is the ScaleIO Storage Pool associated with the protection domain.", + "type": [ + "string", + "null" + ] + }, + "system": { + "description": "system is the name of the storage system as configured in ScaleIO.", + "type": "string" + }, + "volumeName": { + "description": "volumeName is the name of a volume already created in the ScaleIO system that is associated with this volume source.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "gateway", + "system", + "secretRef" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "secret": { + "description": "Adapts a Secret into a volume.\n\nThe contents of the target Secret's Data field will be presented in a volume as files using the keys in the Data field as the file names. Secret volumes support ownership management and SELinux relabeling.", + "properties": { + "defaultMode": { + "description": "defaultMode is Optional: mode bits used to set permissions on created files by default. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. Defaults to 0644. Directories within the path are not affected by this setting. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "items": { + "description": "items If unspecified, each key-value pair in the Data field of the referenced Secret will be projected into the volume as a file whose name is the key and content is the value. If specified, the listed keys will be projected into the specified paths, and unlisted keys will not be present. If a key is specified which is not present in the Secret, the volume setup will error unless it is marked optional. Paths must be relative and may not contain the '..' path or start with '..'.", + "items": { + "description": "Maps a string key to a path within a volume.", + "properties": { + "key": { + "description": "key is the key to project.", + "type": "string" + }, + "mode": { + "description": "mode is Optional: mode bits used to set permissions on this file. Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. If not specified, the volume defaultMode will be used. This might be in conflict with other options that affect the file mode, like fsGroup, and the result can be other mode bits set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "path": { + "description": "path is the relative path of the file to map the key to. May not be an absolute path. May not contain the path element '..'. May not start with the string '..'.", + "type": "string" + } + }, + "required": [ + "key", + "path" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "optional": { + "description": "optional field specify whether the Secret or its keys must be defined", + "type": [ + "boolean", + "null" + ] + }, + "secretName": { + "description": "secretName is the name of the secret in the pod's namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "storageos": { + "description": "Represents a StorageOS persistent volume resource.", + "properties": { + "fsType": { + "description": "fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "readOnly": { + "description": "readOnly defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", + "type": [ + "boolean", + "null" + ] + }, + "secretRef": { + "description": "LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.", + "properties": { + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "volumeName": { + "description": "volumeName is the human-readable name of the StorageOS volume. Volume names are only unique within a namespace.", + "type": [ + "string", + "null" + ] + }, + "volumeNamespace": { + "description": "volumeNamespace specifies the scope of the volume within StorageOS. If no namespace is specified then the Pod's namespace will be used. This allows the Kubernetes name scoping to be mirrored within StorageOS for tighter integration. Set VolumeName to any name to override the default behaviour. Set to \"default\" if you are not using namespaces within StorageOS. Namespaces that do not pre-exist within StorageOS will be created.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "vsphereVolume": { + "description": "Represents a vSphere volume resource.", + "properties": { + "fsType": { + "description": "fsType is filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\". Implicitly inferred to be \"ext4\" if unspecified.", + "type": [ + "string", + "null" + ] + }, + "storagePolicyID": { + "description": "storagePolicyID is the storage Policy Based Management (SPBM) profile ID associated with the StoragePolicyName.", + "type": [ + "string", + "null" + ] + }, + "storagePolicyName": { + "description": "storagePolicyName is the storage Policy Based Management (SPBM) profile name.", + "type": [ + "string", + "null" + ] + }, + "volumePath": { + "description": "volumePath is the path that identifies vSphere volume vmdk", + "type": "string" + } + }, + "required": [ + "volumePath" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge,retainKeys" + } + }, + "required": [ + "containers" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "status": { + "description": "PodStatus represents information about the status of a pod. Status may trail the actual state of a system, especially if the node that hosts the pod cannot contact the control plane.", + "properties": { + "conditions": { + "description": "Current service state of pod. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#pod-conditions", + "items": { + "description": "PodCondition contains details for the current condition of this pod.", + "properties": { + "lastProbeTime": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "lastTransitionTime": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "message": { + "description": "Human-readable message indicating details about last transition.", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "Unique, one-word, CamelCase reason for the condition's last transition.", + "type": [ + "string", + "null" + ] + }, + "status": { + "description": "Status is the status of the condition. Can be True, False, Unknown. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#pod-conditions", + "type": "string" + }, + "type": { + "description": "Type is the type of the condition. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#pod-conditions", + "type": "string" + } + }, + "required": [ + "type", + "status" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "type", + "x-kubernetes-patch-strategy": "merge" + }, + "containerStatuses": { + "description": "The list has one entry per container in the manifest. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#pod-and-container-status", + "items": { + "description": "ContainerStatus contains details for the current status of this container.", + "properties": { + "allocatedResources": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "AllocatedResources represents the compute resources allocated for this container by the node. Kubelet sets this value to Container.Resources.Requests upon successful pod admission and after successfully admitting desired pod resize.", + "type": [ + "object", + "null" + ] + }, + "containerID": { + "description": "ContainerID is the ID of the container in the format '://'. Where type is a container runtime identifier, returned from Version call of CRI API (for example \"containerd\").", + "type": [ + "string", + "null" + ] + }, + "image": { + "description": "Image is the name of container image that the container is running. The container image may not match the image used in the PodSpec, as it may have been resolved by the runtime. More info: https://kubernetes.io/docs/concepts/containers/images.", + "type": "string" + }, + "imageID": { + "description": "ImageID is the image ID of the container's image. The image ID may not match the image ID of the image used in the PodSpec, as it may have been resolved by the runtime.", + "type": "string" + }, + "lastState": { + "description": "ContainerState holds a possible state of container. Only one of its members may be specified. If none of them is specified, the default one is ContainerStateWaiting.", + "properties": { + "running": { + "description": "ContainerStateRunning is a running state of a container.", + "properties": { + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminated": { + "description": "ContainerStateTerminated is a terminated state of a container.", + "properties": { + "containerID": { + "description": "Container's ID in the format '://'", + "type": [ + "string", + "null" + ] + }, + "exitCode": { + "description": "Exit status from the last termination of the container", + "format": "int32", + "type": "integer" + }, + "finishedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "message": { + "description": "Message regarding the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason from the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "signal": { + "description": "Signal from the last termination of the container", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "exitCode" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "waiting": { + "description": "ContainerStateWaiting is a waiting state of a container.", + "properties": { + "message": { + "description": "Message regarding why the container is not yet running.", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason the container is not yet running.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "Name is a DNS_LABEL representing the unique name of the container. Each container in a pod must have a unique name across all container types. Cannot be updated.", + "type": "string" + }, + "ready": { + "description": "Ready specifies whether the container is currently passing its readiness check. The value will change as readiness probes keep executing. If no readiness probes are specified, this field defaults to true once the container is fully started (see Started field).\n\nThe value is typically used to determine whether a container is ready to accept traffic.", + "type": "boolean" + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "restartCount": { + "description": "RestartCount holds the number of times the container has been restarted. Kubelet makes an effort to always increment the value, but there are cases when the state may be lost due to node restarts and then the value may be reset to 0. The value is never negative.", + "format": "int32", + "type": "integer" + }, + "started": { + "description": "Started indicates whether the container has finished its postStart lifecycle hook and passed its startup probe. Initialized as false, becomes true after startupProbe is considered successful. Resets to false when the container is restarted, or if kubelet loses state temporarily. In both cases, startup probes will run again. Is always true when no startupProbe is defined and container is running and has passed the postStart lifecycle hook. The null value must be treated the same as false.", + "type": [ + "boolean", + "null" + ] + }, + "state": { + "description": "ContainerState holds a possible state of container. Only one of its members may be specified. If none of them is specified, the default one is ContainerStateWaiting.", + "properties": { + "running": { + "description": "ContainerStateRunning is a running state of a container.", + "properties": { + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminated": { + "description": "ContainerStateTerminated is a terminated state of a container.", + "properties": { + "containerID": { + "description": "Container's ID in the format '://'", + "type": [ + "string", + "null" + ] + }, + "exitCode": { + "description": "Exit status from the last termination of the container", + "format": "int32", + "type": "integer" + }, + "finishedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "message": { + "description": "Message regarding the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason from the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "signal": { + "description": "Signal from the last termination of the container", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "exitCode" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "waiting": { + "description": "ContainerStateWaiting is a waiting state of a container.", + "properties": { + "message": { + "description": "Message regarding why the container is not yet running.", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason the container is not yet running.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "ready", + "restartCount", + "image", + "imageID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "ephemeralContainerStatuses": { + "description": "Status for any ephemeral containers that have run in this pod.", + "items": { + "description": "ContainerStatus contains details for the current status of this container.", + "properties": { + "allocatedResources": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "AllocatedResources represents the compute resources allocated for this container by the node. Kubelet sets this value to Container.Resources.Requests upon successful pod admission and after successfully admitting desired pod resize.", + "type": [ + "object", + "null" + ] + }, + "containerID": { + "description": "ContainerID is the ID of the container in the format '://'. Where type is a container runtime identifier, returned from Version call of CRI API (for example \"containerd\").", + "type": [ + "string", + "null" + ] + }, + "image": { + "description": "Image is the name of container image that the container is running. The container image may not match the image used in the PodSpec, as it may have been resolved by the runtime. More info: https://kubernetes.io/docs/concepts/containers/images.", + "type": "string" + }, + "imageID": { + "description": "ImageID is the image ID of the container's image. The image ID may not match the image ID of the image used in the PodSpec, as it may have been resolved by the runtime.", + "type": "string" + }, + "lastState": { + "description": "ContainerState holds a possible state of container. Only one of its members may be specified. If none of them is specified, the default one is ContainerStateWaiting.", + "properties": { + "running": { + "description": "ContainerStateRunning is a running state of a container.", + "properties": { + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminated": { + "description": "ContainerStateTerminated is a terminated state of a container.", + "properties": { + "containerID": { + "description": "Container's ID in the format '://'", + "type": [ + "string", + "null" + ] + }, + "exitCode": { + "description": "Exit status from the last termination of the container", + "format": "int32", + "type": "integer" + }, + "finishedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "message": { + "description": "Message regarding the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason from the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "signal": { + "description": "Signal from the last termination of the container", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "exitCode" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "waiting": { + "description": "ContainerStateWaiting is a waiting state of a container.", + "properties": { + "message": { + "description": "Message regarding why the container is not yet running.", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason the container is not yet running.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "Name is a DNS_LABEL representing the unique name of the container. Each container in a pod must have a unique name across all container types. Cannot be updated.", + "type": "string" + }, + "ready": { + "description": "Ready specifies whether the container is currently passing its readiness check. The value will change as readiness probes keep executing. If no readiness probes are specified, this field defaults to true once the container is fully started (see Started field).\n\nThe value is typically used to determine whether a container is ready to accept traffic.", + "type": "boolean" + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "restartCount": { + "description": "RestartCount holds the number of times the container has been restarted. Kubelet makes an effort to always increment the value, but there are cases when the state may be lost due to node restarts and then the value may be reset to 0. The value is never negative.", + "format": "int32", + "type": "integer" + }, + "started": { + "description": "Started indicates whether the container has finished its postStart lifecycle hook and passed its startup probe. Initialized as false, becomes true after startupProbe is considered successful. Resets to false when the container is restarted, or if kubelet loses state temporarily. In both cases, startup probes will run again. Is always true when no startupProbe is defined and container is running and has passed the postStart lifecycle hook. The null value must be treated the same as false.", + "type": [ + "boolean", + "null" + ] + }, + "state": { + "description": "ContainerState holds a possible state of container. Only one of its members may be specified. If none of them is specified, the default one is ContainerStateWaiting.", + "properties": { + "running": { + "description": "ContainerStateRunning is a running state of a container.", + "properties": { + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminated": { + "description": "ContainerStateTerminated is a terminated state of a container.", + "properties": { + "containerID": { + "description": "Container's ID in the format '://'", + "type": [ + "string", + "null" + ] + }, + "exitCode": { + "description": "Exit status from the last termination of the container", + "format": "int32", + "type": "integer" + }, + "finishedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "message": { + "description": "Message regarding the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason from the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "signal": { + "description": "Signal from the last termination of the container", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "exitCode" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "waiting": { + "description": "ContainerStateWaiting is a waiting state of a container.", + "properties": { + "message": { + "description": "Message regarding why the container is not yet running.", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason the container is not yet running.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "ready", + "restartCount", + "image", + "imageID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "hostIP": { + "description": "hostIP holds the IP address of the host to which the pod is assigned. Empty if the pod has not started yet. A pod can be assigned to a node that has a problem in kubelet which in turns mean that HostIP will not be updated even if there is a node is assigned to pod", + "type": [ + "string", + "null" + ] + }, + "hostIPs": { + "description": "hostIPs holds the IP addresses allocated to the host. If this field is specified, the first entry must match the hostIP field. This list is empty if the pod has not started yet. A pod can be assigned to a node that has a problem in kubelet which in turns means that HostIPs will not be updated even if there is a node is assigned to this pod.", + "items": { + "description": "HostIP represents a single IP address allocated to the host.", + "properties": { + "ip": { + "description": "IP is the IP address assigned to the host", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic", + "x-kubernetes-patch-merge-key": "ip", + "x-kubernetes-patch-strategy": "merge" + }, + "initContainerStatuses": { + "description": "The list has one entry per init container in the manifest. The most recent successful init container will have ready = true, the most recently started container will have startTime set. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#pod-and-container-status", + "items": { + "description": "ContainerStatus contains details for the current status of this container.", + "properties": { + "allocatedResources": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "AllocatedResources represents the compute resources allocated for this container by the node. Kubelet sets this value to Container.Resources.Requests upon successful pod admission and after successfully admitting desired pod resize.", + "type": [ + "object", + "null" + ] + }, + "containerID": { + "description": "ContainerID is the ID of the container in the format '://'. Where type is a container runtime identifier, returned from Version call of CRI API (for example \"containerd\").", + "type": [ + "string", + "null" + ] + }, + "image": { + "description": "Image is the name of container image that the container is running. The container image may not match the image used in the PodSpec, as it may have been resolved by the runtime. More info: https://kubernetes.io/docs/concepts/containers/images.", + "type": "string" + }, + "imageID": { + "description": "ImageID is the image ID of the container's image. The image ID may not match the image ID of the image used in the PodSpec, as it may have been resolved by the runtime.", + "type": "string" + }, + "lastState": { + "description": "ContainerState holds a possible state of container. Only one of its members may be specified. If none of them is specified, the default one is ContainerStateWaiting.", + "properties": { + "running": { + "description": "ContainerStateRunning is a running state of a container.", + "properties": { + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminated": { + "description": "ContainerStateTerminated is a terminated state of a container.", + "properties": { + "containerID": { + "description": "Container's ID in the format '://'", + "type": [ + "string", + "null" + ] + }, + "exitCode": { + "description": "Exit status from the last termination of the container", + "format": "int32", + "type": "integer" + }, + "finishedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "message": { + "description": "Message regarding the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason from the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "signal": { + "description": "Signal from the last termination of the container", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "exitCode" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "waiting": { + "description": "ContainerStateWaiting is a waiting state of a container.", + "properties": { + "message": { + "description": "Message regarding why the container is not yet running.", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason the container is not yet running.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "name": { + "description": "Name is a DNS_LABEL representing the unique name of the container. Each container in a pod must have a unique name across all container types. Cannot be updated.", + "type": "string" + }, + "ready": { + "description": "Ready specifies whether the container is currently passing its readiness check. The value will change as readiness probes keep executing. If no readiness probes are specified, this field defaults to true once the container is fully started (see Started field).\n\nThe value is typically used to determine whether a container is ready to accept traffic.", + "type": "boolean" + }, + "resources": { + "description": "ResourceRequirements describes the compute resource requirements.", + "properties": { + "claims": { + "description": "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container.\n\nThis is an alpha field and requires enabling the DynamicResourceAllocation feature gate.\n\nThis field is immutable. It can only be set for containers.", + "items": { + "description": "ResourceClaim references one entry in PodSpec.ResourceClaims.", + "properties": { + "name": { + "description": "Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "number", + "null" + ] + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": [ + "object", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "restartCount": { + "description": "RestartCount holds the number of times the container has been restarted. Kubelet makes an effort to always increment the value, but there are cases when the state may be lost due to node restarts and then the value may be reset to 0. The value is never negative.", + "format": "int32", + "type": "integer" + }, + "started": { + "description": "Started indicates whether the container has finished its postStart lifecycle hook and passed its startup probe. Initialized as false, becomes true after startupProbe is considered successful. Resets to false when the container is restarted, or if kubelet loses state temporarily. In both cases, startup probes will run again. Is always true when no startupProbe is defined and container is running and has passed the postStart lifecycle hook. The null value must be treated the same as false.", + "type": [ + "boolean", + "null" + ] + }, + "state": { + "description": "ContainerState holds a possible state of container. Only one of its members may be specified. If none of them is specified, the default one is ContainerStateWaiting.", + "properties": { + "running": { + "description": "ContainerStateRunning is a running state of a container.", + "properties": { + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "terminated": { + "description": "ContainerStateTerminated is a terminated state of a container.", + "properties": { + "containerID": { + "description": "Container's ID in the format '://'", + "type": [ + "string", + "null" + ] + }, + "exitCode": { + "description": "Exit status from the last termination of the container", + "format": "int32", + "type": "integer" + }, + "finishedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "message": { + "description": "Message regarding the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason from the last termination of the container", + "type": [ + "string", + "null" + ] + }, + "signal": { + "description": "Signal from the last termination of the container", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "startedAt": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "exitCode" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "waiting": { + "description": "ContainerStateWaiting is a waiting state of a container.", + "properties": { + "message": { + "description": "Message regarding why the container is not yet running.", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "(brief) reason the container is not yet running.", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "required": [ + "name", + "ready", + "restartCount", + "image", + "imageID" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "message": { + "description": "A human readable message indicating details about why the pod is in this condition.", + "type": [ + "string", + "null" + ] + }, + "nominatedNodeName": { + "description": "nominatedNodeName is set only when this pod preempts other pods on the node, but it cannot be scheduled right away as preemption victims receive their graceful termination periods. This field does not guarantee that the pod will be scheduled on this node. Scheduler may decide to place the pod elsewhere if other nodes become available sooner. Scheduler may also decide to give the resources on this node to a higher priority pod that is created after preemption. As a result, this field may be different than PodSpec.nodeName when the pod is scheduled.", + "type": [ + "string", + "null" + ] + }, + "phase": { + "description": "The phase of a Pod is a simple, high-level summary of where the Pod is in its lifecycle. The conditions array, the reason and message fields, and the individual container status arrays contain more detail about the pod's status. There are five possible phase values:\n\nPending: The pod has been accepted by the Kubernetes system, but one or more of the container images has not been created. This includes time before being scheduled as well as time spent downloading images over the network, which could take a while. Running: The pod has been bound to a node, and all of the containers have been created. At least one container is still running, or is in the process of starting or restarting. Succeeded: All containers in the pod have terminated in success, and will not be restarted. Failed: All containers in the pod have terminated, and at least one container has terminated in failure. The container either exited with non-zero status or was terminated by the system. Unknown: For some reason the state of the pod could not be obtained, typically due to an error in communicating with the host of the pod.\n\nMore info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#pod-phase", + "type": [ + "string", + "null" + ] + }, + "podIP": { + "description": "podIP address allocated to the pod. Routable at least within the cluster. Empty if not yet allocated.", + "type": [ + "string", + "null" + ] + }, + "podIPs": { + "description": "podIPs holds the IP addresses allocated to the pod. If this field is specified, the 0th entry must match the podIP field. Pods may be allocated at most 1 value for each of IPv4 and IPv6. This list is empty if no IPs have been allocated yet.", + "items": { + "description": "PodIP represents a single IP address allocated to the pod.", + "properties": { + "ip": { + "description": "IP is the IP address assigned to the pod", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "ip", + "x-kubernetes-patch-strategy": "merge" + }, + "qosClass": { + "description": "The Quality of Service (QOS) classification assigned to the pod based on resource requirements See PodQOSClass type for available QOS classes More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/#quality-of-service-classes", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "A brief CamelCase message indicating details about why the pod is in this state. e.g. 'Evicted'", + "type": [ + "string", + "null" + ] + }, + "resize": { + "description": "Status of resources resize desired for pod's containers. It is empty if no resources resize is pending. Any changes to container resources will automatically set this to \"Proposed\"", + "type": [ + "string", + "null" + ] + }, + "resourceClaimStatuses": { + "description": "Status of resource claims.", + "items": { + "description": "PodResourceClaimStatus is stored in the PodStatus for each PodResourceClaim which references a ResourceClaimTemplate. It stores the generated name for the corresponding ResourceClaim.", + "properties": { + "name": { + "description": "Name uniquely identifies this resource claim inside the pod. This must match the name of an entry in pod.spec.resourceClaims, which implies that the string must be a DNS_LABEL.", + "type": "string" + }, + "resourceClaimName": { + "description": "ResourceClaimName is the name of the ResourceClaim that was generated for the Pod in the namespace of the Pod. It this is unset, then generating a ResourceClaim was not necessary. The pod.spec.resourceClaims entry can be ignored in this case.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge,retainKeys" + }, + "startTime": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "", + "kind": "Pod", + "version": "v1" + } + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/schema#" +} \ No newline at end of file diff --git a/packages/editors/src/schemas/kubernetes/service-v1.json b/packages/editors/src/schemas/kubernetes/service-v1.json new file mode 100644 index 0000000..28ebfcc --- /dev/null +++ b/packages/editors/src/schemas/kubernetes/service-v1.json @@ -0,0 +1,697 @@ +{ + "description": "Service is a named abstraction of software service (for example, mysql) consisting of local port (for example 3306) that the proxy listens on, and the selector that determines which pods will answer requests sent through the proxy.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": [ + "string", + "null" + ], + "enum": [ + "v1" + ] + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": [ + "string", + "null" + ], + "enum": [ + "Service" + ] + }, + "metadata": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "properties": { + "annotations": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": [ + "object", + "null" + ] + }, + "creationTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "deletionTimestamp": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": [ + "string", + "null" + ] + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "labels": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": [ + "object", + "null" + ] + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "items": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": [ + "string", + "null" + ] + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": [ + "string", + "null" + ] + }, + "fieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": [ + "object", + "null" + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": [ + "string", + "null" + ] + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": [ + "string", + "null" + ] + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": [ + "string", + "null" + ] + }, + "time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": [ + "string", + "null" + ] + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": [ + "string", + "null" + ] + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "items": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": [ + "boolean", + "null" + ] + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": [ + "boolean", + "null" + ] + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + }, + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic", + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": [ + "string", + "null" + ] + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": [ + "string", + "null" + ] + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "spec": { + "description": "ServiceSpec describes the attributes that a user creates on a service.", + "properties": { + "allocateLoadBalancerNodePorts": { + "description": "allocateLoadBalancerNodePorts defines if NodePorts will be automatically allocated for services with type LoadBalancer. Default is \"true\". It may be set to \"false\" if the cluster load-balancer does not rely on NodePorts. If the caller requests specific NodePorts (by specifying a value), those requests will be respected, regardless of this field. This field may only be set for services with type LoadBalancer and will be cleared if the type is changed to any other type.", + "type": [ + "boolean", + "null" + ] + }, + "clusterIP": { + "description": "clusterIP is the IP address of the service and is usually assigned randomly. If an address is specified manually, is in-range (as per system configuration), and is not in use, it will be allocated to the service; otherwise creation of the service will fail. This field may not be changed through updates unless the type field is also being changed to ExternalName (which requires this field to be blank) or the type field is being changed from ExternalName (in which case this field may optionally be specified, as describe above). Valid values are \"None\", empty string (\"\"), or a valid IP address. Setting this to \"None\" makes a \"headless service\" (no virtual IP), which is useful when direct endpoint connections are preferred and proxying is not required. Only applies to types ClusterIP, NodePort, and LoadBalancer. If this field is specified when creating a Service of type ExternalName, creation will fail. This field will be wiped when updating a Service to type ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies", + "type": [ + "string", + "null" + ] + }, + "clusterIPs": { + "description": "ClusterIPs is a list of IP addresses assigned to this service, and are usually assigned randomly. If an address is specified manually, is in-range (as per system configuration), and is not in use, it will be allocated to the service; otherwise creation of the service will fail. This field may not be changed through updates unless the type field is also being changed to ExternalName (which requires this field to be empty) or the type field is being changed from ExternalName (in which case this field may optionally be specified, as describe above). Valid values are \"None\", empty string (\"\"), or a valid IP address. Setting this to \"None\" makes a \"headless service\" (no virtual IP), which is useful when direct endpoint connections are preferred and proxying is not required. Only applies to types ClusterIP, NodePort, and LoadBalancer. If this field is specified when creating a Service of type ExternalName, creation will fail. This field will be wiped when updating a Service to type ExternalName. If this field is not specified, it will be initialized from the clusterIP field. If this field is specified, clients must ensure that clusterIPs[0] and clusterIP have the same value.\n\nThis field may hold a maximum of two entries (dual-stack IPs, in either order). These IPs must correspond to the values of the ipFamilies field. Both clusterIPs and ipFamilies are governed by the ipFamilyPolicy field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "externalIPs": { + "description": "externalIPs is a list of IP addresses for which nodes in the cluster will also accept traffic for this service. These IPs are not managed by Kubernetes. The user is responsible for ensuring that traffic arrives at a node with this IP. A common example is external load-balancers that are not part of the Kubernetes system.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "externalName": { + "description": "externalName is the external reference that discovery mechanisms will return as an alias for this service (e.g. a DNS CNAME record). No proxying will be involved. Must be a lowercase RFC-1123 hostname (https://tools.ietf.org/html/rfc1123) and requires `type` to be \"ExternalName\".", + "type": [ + "string", + "null" + ] + }, + "externalTrafficPolicy": { + "description": "externalTrafficPolicy describes how nodes distribute service traffic they receive on one of the Service's \"externally-facing\" addresses (NodePorts, ExternalIPs, and LoadBalancer IPs). If set to \"Local\", the proxy will configure the service in a way that assumes that external load balancers will take care of balancing the service traffic between nodes, and so each node will deliver traffic only to the node-local endpoints of the service, without masquerading the client source IP. (Traffic mistakenly sent to a node with no endpoints will be dropped.) The default value, \"Cluster\", uses the standard behavior of routing to all endpoints evenly (possibly modified by topology and other features). Note that traffic sent to an External IP or LoadBalancer IP from within the cluster will always get \"Cluster\" semantics, but clients sending to a NodePort from within the cluster may need to take traffic policy into account when picking a node.", + "type": [ + "string", + "null" + ] + }, + "healthCheckNodePort": { + "description": "healthCheckNodePort specifies the healthcheck nodePort for the service. This only applies when type is set to LoadBalancer and externalTrafficPolicy is set to Local. If a value is specified, is in-range, and is not in use, it will be used. If not specified, a value will be automatically allocated. External systems (e.g. load-balancers) can use this port to determine if a given node holds endpoints for this service or not. If this field is specified when creating a Service which does not need it, creation will fail. This field will be wiped when updating a Service to no longer need it (e.g. changing type). This field cannot be updated once set.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "internalTrafficPolicy": { + "description": "InternalTrafficPolicy describes how nodes distribute service traffic they receive on the ClusterIP. If set to \"Local\", the proxy will assume that pods only want to talk to endpoints of the service on the same node as the pod, dropping the traffic if there are no local endpoints. The default value, \"Cluster\", uses the standard behavior of routing to all endpoints evenly (possibly modified by topology and other features).", + "type": [ + "string", + "null" + ] + }, + "ipFamilies": { + "description": "IPFamilies is a list of IP families (e.g. IPv4, IPv6) assigned to this service. This field is usually assigned automatically based on cluster configuration and the ipFamilyPolicy field. If this field is specified manually, the requested family is available in the cluster, and ipFamilyPolicy allows it, it will be used; otherwise creation of the service will fail. This field is conditionally mutable: it allows for adding or removing a secondary IP family, but it does not allow changing the primary IP family of the Service. Valid values are \"IPv4\" and \"IPv6\". This field only applies to Services of types ClusterIP, NodePort, and LoadBalancer, and does apply to \"headless\" services. This field will be wiped when updating a Service to type ExternalName.\n\nThis field may hold a maximum of two entries (dual-stack families, in either order). These families must correspond to the values of the clusterIPs field, if specified. Both clusterIPs and ipFamilies are governed by the ipFamilyPolicy field.", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + }, + "ipFamilyPolicy": { + "description": "IPFamilyPolicy represents the dual-stack-ness requested or required by this Service. If there is no value provided, then this field will be set to SingleStack. Services can be \"SingleStack\" (a single IP family), \"PreferDualStack\" (two IP families on dual-stack configured clusters or a single IP family on single-stack clusters), or \"RequireDualStack\" (two IP families on dual-stack configured clusters, otherwise fail). The ipFamilies and clusterIPs fields depend on the value of this field. This field will be wiped when updating a service to type ExternalName.", + "type": [ + "string", + "null" + ] + }, + "loadBalancerClass": { + "description": "loadBalancerClass is the class of the load balancer implementation this Service belongs to. If specified, the value of this field must be a label-style identifier, with an optional prefix, e.g. \"internal-vip\" or \"example.com/internal-vip\". Unprefixed names are reserved for end-users. This field can only be set when the Service type is 'LoadBalancer'. If not set, the default load balancer implementation is used, today this is typically done through the cloud provider integration, but should apply for any default implementation. If set, it is assumed that a load balancer implementation is watching for Services with a matching class. Any default load balancer implementation (e.g. cloud providers) should ignore Services that set this field. This field can only be set when creating or updating a Service to type 'LoadBalancer'. Once set, it can not be changed. This field will be wiped when a service is updated to a non 'LoadBalancer' type.", + "type": [ + "string", + "null" + ] + }, + "loadBalancerIP": { + "description": "Only applies to Service Type: LoadBalancer. This feature depends on whether the underlying cloud-provider supports specifying the loadBalancerIP when a load balancer is created. This field will be ignored if the cloud-provider does not support the feature. Deprecated: This field was under-specified and its meaning varies across implementations. Using it is non-portable and it may not support dual-stack. Users are encouraged to use implementation-specific annotations when available.", + "type": [ + "string", + "null" + ] + }, + "loadBalancerSourceRanges": { + "description": "If specified and supported by the platform, this will restrict traffic through the cloud-provider load-balancer will be restricted to the specified client IPs. This field will be ignored if the cloud-provider does not support the feature.\" More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/", + "items": { + "type": [ + "string", + "null" + ] + }, + "type": [ + "array", + "null" + ] + }, + "ports": { + "description": "The list of ports that are exposed by this service. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies", + "items": { + "description": "ServicePort contains information on service's port.", + "properties": { + "appProtocol": { + "description": "The application protocol for this port. This is used as a hint for implementations to offer richer behavior for protocols that they understand. This field follows standard Kubernetes label syntax. Valid values are either:\n\n* Un-prefixed protocol names - reserved for IANA standard service names (as per RFC-6335 and https://www.iana.org/assignments/service-names).\n\n* Kubernetes-defined prefixed names:\n * 'kubernetes.io/h2c' - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540\n * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455\n * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455\n\n* Other protocols should use implementation-defined prefixed names such as mycompany.com/my-custom-protocol.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The name of this port within the service. This must be a DNS_LABEL. All ports within a ServiceSpec must have unique names. When considering the endpoints for a Service, this must match the 'name' field in the EndpointPort. Optional if only one ServicePort is defined on this service.", + "type": [ + "string", + "null" + ] + }, + "nodePort": { + "description": "The port on each node on which this service is exposed when type is NodePort or LoadBalancer. Usually assigned by the system. If a value is specified, in-range, and not in use it will be used, otherwise the operation will fail. If not specified, a port will be allocated if this Service requires one. If this field is specified when creating a Service which does not need it, creation will fail. This field will be wiped when updating a Service to no longer need it (e.g. changing type from NodePort to ClusterIP). More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "port": { + "description": "The port that will be exposed by this service.", + "format": "int32", + "type": "integer" + }, + "protocol": { + "description": "The IP protocol for this port. Supports \"TCP\", \"UDP\", and \"SCTP\". Default is TCP.", + "type": [ + "string", + "null" + ] + }, + "targetPort": { + "oneOf": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "integer", + "null" + ] + } + ] + } + }, + "required": [ + "port" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "port", + "protocol" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "port", + "x-kubernetes-patch-strategy": "merge" + }, + "publishNotReadyAddresses": { + "description": "publishNotReadyAddresses indicates that any agent which deals with endpoints for this Service should disregard any indications of ready/not-ready. The primary use case for setting this field is for a StatefulSet's Headless Service to propagate SRV DNS records for its Pods for the purpose of peer discovery. The Kubernetes controllers that generate Endpoints and EndpointSlice resources for Services interpret this to mean that all endpoints are considered \"ready\" even if the Pods themselves are not. Agents which consume only Kubernetes generated endpoints through the Endpoints or EndpointSlice resources can safely assume this behavior.", + "type": [ + "boolean", + "null" + ] + }, + "selector": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Route service traffic to pods with label keys and values matching this selector. If empty or not present, the service is assumed to have an external process managing its endpoints, which Kubernetes will not modify. Only applies to types ClusterIP, NodePort, and LoadBalancer. Ignored if type is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/", + "type": [ + "object", + "null" + ], + "x-kubernetes-map-type": "atomic" + }, + "sessionAffinity": { + "description": "Supports \"ClientIP\" and \"None\". Used to maintain session affinity. Enable client IP based session affinity. Must be ClientIP or None. Defaults to None. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies", + "type": [ + "string", + "null" + ] + }, + "sessionAffinityConfig": { + "description": "SessionAffinityConfig represents the configurations of session affinity.", + "properties": { + "clientIP": { + "description": "ClientIPConfig represents the configurations of Client IP based session affinity.", + "properties": { + "timeoutSeconds": { + "description": "timeoutSeconds specifies the seconds of ClientIP type session sticky time. The value must be >0 && <=86400(for 1 day) if ServiceAffinity == \"ClientIP\". Default value is 10800(for 3 hours).", + "format": "int32", + "type": [ + "integer", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": { + "description": "type determines how the Service is exposed. Defaults to ClusterIP. Valid options are ExternalName, ClusterIP, NodePort, and LoadBalancer. \"ClusterIP\" allocates a cluster-internal IP address for load-balancing to endpoints. Endpoints are determined by the selector or if that is not specified, by manual construction of an Endpoints object or EndpointSlice objects. If clusterIP is \"None\", no virtual IP is allocated and the endpoints are published as a set of endpoints rather than a virtual IP. \"NodePort\" builds on ClusterIP and allocates a port on every node which routes to the same endpoints as the clusterIP. \"LoadBalancer\" builds on NodePort and creates an external load-balancer (if supported in the current cloud) which routes to the same endpoints as the clusterIP. \"ExternalName\" aliases this service to the specified externalName. Several other fields do not apply to ExternalName services. More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "status": { + "description": "ServiceStatus represents the current status of a service.", + "properties": { + "conditions": { + "description": "Current service state", + "items": { + "description": "Condition contains details for one aspect of the current state of this API Resource.", + "properties": { + "lastTransitionTime": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "format": "date-time", + "type": "string" + }, + "message": { + "description": "message is a human readable message indicating details about the transition. This may be an empty string.", + "type": "string" + }, + "observedGeneration": { + "description": "observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "reason": { + "description": "reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.", + "type": "string" + }, + "status": { + "description": "status of the condition, one of True, False, Unknown.", + "type": "string" + }, + "type": { + "description": "type of condition in CamelCase or in foo.example.com/CamelCase.", + "type": "string" + } + }, + "required": [ + "type", + "status", + "lastTransitionTime", + "reason", + "message" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-map-keys": [ + "type" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "type", + "x-kubernetes-patch-strategy": "merge" + }, + "loadBalancer": { + "description": "LoadBalancerStatus represents the status of a load-balancer.", + "properties": { + "ingress": { + "description": "Ingress is a list containing ingress points for the load-balancer. Traffic intended for the service should be sent to these ingress points.", + "items": { + "description": "LoadBalancerIngress represents the status of a load-balancer ingress point: traffic intended for the service should be sent to an ingress point.", + "properties": { + "hostname": { + "description": "Hostname is set for load-balancer ingress points that are DNS based (typically AWS load-balancers)", + "type": [ + "string", + "null" + ] + }, + "ip": { + "description": "IP is set for load-balancer ingress points that are IP based (typically GCE or OpenStack load-balancers)", + "type": [ + "string", + "null" + ] + }, + "ports": { + "description": "Ports is a list of records of service ports If used, every port defined in the service should have an entry in it", + "items": { + "properties": { + "error": { + "description": "Error is to record the problem with the service port The format of the error shall comply with the following rules: - built-in error values shall be specified in this file and those shall use\n CamelCase names\n- cloud provider specific error values must have names that comply with the\n format foo.example.com/CamelCase.", + "type": [ + "string", + "null" + ] + }, + "port": { + "description": "Port is the port number of the service port of which status is recorded here", + "format": "int32", + "type": "integer" + }, + "protocol": { + "description": "Protocol is the protocol of the service port of which status is recorded here The supported values are: \"TCP\", \"UDP\", \"SCTP\"", + "type": "string" + } + }, + "required": [ + "port", + "protocol" + ], + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ], + "x-kubernetes-list-type": "atomic" + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + }, + "type": [ + "array", + "null" + ] + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": [ + "object", + "null" + ], + "additionalProperties": false + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "", + "kind": "Service", + "version": "v1" + } + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/schema#" +} \ No newline at end of file diff --git a/packages/editors/src/setup/css.worker.ts b/packages/editors/src/setup/css.worker.ts new file mode 100644 index 0000000..f361e4d --- /dev/null +++ b/packages/editors/src/setup/css.worker.ts @@ -0,0 +1 @@ +import 'monaco-editor/esm/vs/language/css/css.worker.js'; diff --git a/packages/editors/src/setup/editor.worker.ts b/packages/editors/src/setup/editor.worker.ts new file mode 100644 index 0000000..ad51566 --- /dev/null +++ b/packages/editors/src/setup/editor.worker.ts @@ -0,0 +1 @@ +import 'monaco-editor/esm/vs/editor/editor.worker.js'; diff --git a/packages/editors/src/setup/html.worker.ts b/packages/editors/src/setup/html.worker.ts new file mode 100644 index 0000000..3c13091 --- /dev/null +++ b/packages/editors/src/setup/html.worker.ts @@ -0,0 +1 @@ +import 'monaco-editor/esm/vs/language/html/html.worker.js'; diff --git a/packages/editors/src/setup/index.ts b/packages/editors/src/setup/index.ts new file mode 100644 index 0000000..16154e5 --- /dev/null +++ b/packages/editors/src/setup/index.ts @@ -0,0 +1 @@ +export { setupMonacoWorkers } from './setupMonacoWorkers'; diff --git a/packages/editors/src/setup/json.worker.ts b/packages/editors/src/setup/json.worker.ts new file mode 100644 index 0000000..c536d86 --- /dev/null +++ b/packages/editors/src/setup/json.worker.ts @@ -0,0 +1 @@ +import 'monaco-editor/esm/vs/language/json/json.worker.js'; diff --git a/packages/editors/src/setup/setupMonacoWorkers.ts b/packages/editors/src/setup/setupMonacoWorkers.ts new file mode 100644 index 0000000..a01bae7 --- /dev/null +++ b/packages/editors/src/setup/setupMonacoWorkers.ts @@ -0,0 +1,113 @@ +/** + * Configure Monaco web workers for Vite-based environments. + * + * Import this module as a **side-effect** before any Monaco editor mounts. + * It sets up web workers for all language services including + * YAML (via `monaco-yaml`). + * + * @example + * ```ts + * // In your app's entry point or Storybook preview: + * import '@omniview/editors/setup'; + * ``` + */ +import * as monaco from 'monaco-editor'; +import { configureMonacoYaml } from 'monaco-yaml'; +import { editorSchemas } from '../schemas'; + +// Vite `?worker` imports — Vite bundles each worker with its dependencies, +// transforming CJS→ESM so they work correctly in the browser Worker context. +import EditorWorker from './editor.worker?worker'; +import JsonWorker from './json.worker?worker'; +import TsWorker from './ts.worker?worker'; +import CssWorker from './css.worker?worker'; +import HtmlWorker from './html.worker?worker'; +import YamlWorker from './yaml.worker?worker'; + +const DEBUG = false; +function log(...args: unknown[]) { + if (DEBUG) console.log('[monaco-setup]', ...args); +} +function warn(...args: unknown[]) { + if (DEBUG) console.warn('[monaco-setup]', ...args); +} + +log('Module loaded — configuring MonacoEnvironment'); + +// --------------------------------------------------------------------------- +// 1. MonacoEnvironment.getWorker — maps language labels to bundled workers +// --------------------------------------------------------------------------- + +self.MonacoEnvironment = { + getWorker(_workerId: string, label: string) { + log(`getWorker called — label="${label}", workerId="${_workerId}"`); + + let worker: Worker; + try { + switch (label) { + case 'yaml': + worker = new YamlWorker(); + break; + case 'json': + worker = new JsonWorker(); + break; + case 'typescript': + case 'javascript': + worker = new TsWorker(); + break; + case 'css': + case 'scss': + case 'less': + worker = new CssWorker(); + break; + case 'html': + case 'handlebars': + case 'razor': + worker = new HtmlWorker(); + break; + default: + worker = new EditorWorker(); + break; + } + log(`Worker created for "${label}" — instanceof Worker: ${worker instanceof Worker}`); + } catch (err) { + warn(`Failed to create worker for "${label}":`, err); + throw err; + } + + worker.onerror = (e) => { + warn(`Worker "${label}" error:`, e.message, '| filename:', e.filename); + }; + + worker.addEventListener('message', (e) => { + log(`Worker "${label}" first message:`, JSON.stringify(e.data).slice(0, 200)); + }, { once: true }); + + return worker; + }, +}; + +log('MonacoEnvironment set'); + +// --------------------------------------------------------------------------- +// 3. Configure monaco-yaml ONCE at startup with empty schemas +// --------------------------------------------------------------------------- + +try { + const yamlHandle = configureMonacoYaml(monaco, { + enableSchemaRequest: false, + schemas: [], + }); + log('configureMonacoYaml succeeded — handle keys:', Object.keys(yamlHandle)); + editorSchemas._setYamlHandle(yamlHandle as unknown as Parameters[0]); + log('YAML handle injected into EditorSchemaRegistry'); +} catch (err) { + warn('configureMonacoYaml FAILED:', err); +} + +// Re-export for consumers that need the function form +export function setupMonacoWorkers(): void { + // No-op — importing this module is sufficient. The side effects above + // have already configured everything. This export exists so consumers + // can call it explicitly if they prefer that pattern. +} diff --git a/packages/editors/src/setup/ts.worker.ts b/packages/editors/src/setup/ts.worker.ts new file mode 100644 index 0000000..b58a2b2 --- /dev/null +++ b/packages/editors/src/setup/ts.worker.ts @@ -0,0 +1 @@ +import 'monaco-editor/esm/vs/language/typescript/ts.worker.js'; diff --git a/packages/editors/src/setup/yaml.worker.ts b/packages/editors/src/setup/yaml.worker.ts new file mode 100644 index 0000000..3b9edbd --- /dev/null +++ b/packages/editors/src/setup/yaml.worker.ts @@ -0,0 +1,3 @@ +// Vite workaround for monaco-yaml worker loading. +// See: https://github.com/remcohaszing/monaco-yaml#why-doesnt-it-work-with-vite +import 'monaco-yaml/yaml.worker.js'; diff --git a/packages/editors/src/themes/monaco.ts b/packages/editors/src/themes/monaco.ts index fc00dda..b60adfe 100644 --- a/packages/editors/src/themes/monaco.ts +++ b/packages/editors/src/themes/monaco.ts @@ -9,6 +9,37 @@ interface MonacoThemeData { colors: Record; } +/** + * Convert any CSS color value to hex format that Monaco understands. + * Monaco only accepts #RGB, #RRGGBB, or #RRGGBBAA — it renders a bright red + * fallback for rgb(), rgba(), transparent, or any other format. + */ +function toHex(color: string): string { + const trimmed = color.trim(); + if (!trimmed || trimmed === 'transparent') return ''; + + // Already hex — return as-is (with hash) + if (trimmed.startsWith('#')) return trimmed; + + // Match rgb/rgba in both legacy (comma) and modern (space) syntax + // rgb(255, 255, 255) | rgb(255 255 255) | rgba(255, 255, 255, 0.5) | rgb(255 255 255 / 0.5) + const rgbMatch = trimmed.match( + /^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:\s*[/,]\s*([\d.]+))?\s*\)$/, + ); + if (rgbMatch) { + const r = Number(rgbMatch[1]); + const g = Number(rgbMatch[2]); + const b = Number(rgbMatch[3]); + const a = rgbMatch[4] !== undefined ? Number(rgbMatch[4]) : 1; + const hex = (c: number) => c.toString(16).padStart(2, '0'); + if (a >= 1) return `#${hex(r)}${hex(g)}${hex(b)}`; + return `#${hex(r)}${hex(g)}${hex(b)}${hex(Math.round(a * 255))}`; + } + + // Unrecognized format — drop it so Monaco falls back to base theme + return ''; +} + function stripHash(color: string): string { return color.startsWith('#') ? color.slice(1) : color; } @@ -17,6 +48,11 @@ function token(name: string): string { return getComputedToken(name) || ''; } +/** Read a CSS token and convert to hex for Monaco. */ +function hexToken(name: string): string { + return toHex(token(name)); +} + function getBaseTheme(mode: ThemeMode): 'vs' | 'vs-dark' | 'hc-black' | 'hc-light' { switch (mode) { case 'light': @@ -30,89 +66,111 @@ function getBaseTheme(mode: ThemeMode): 'vs' | 'vs-dark' | 'hc-black' | 'hc-ligh } } +/** + * Build a token rule, omitting foreground if the CSS variable is not defined. + * Monaco throws on empty-string foreground values, so we filter them out + * and let `inherit: true` fall back to the base theme defaults. + */ +function tokenRule( + name: string, + fgVar: string, + styleVar?: string, + styleDefault?: string, +): { token: string; foreground?: string; fontStyle?: string } | null { + const fg = toHex(token(fgVar)); + const stripped = fg ? stripHash(fg) : ''; + if (!stripped) return null; + const entry: { token: string; foreground: string; fontStyle?: string } = { + token: name, + foreground: stripped, + }; + if (styleVar) { + entry.fontStyle = token(styleVar) || styleDefault || ''; + } + return entry; +} + +/** + * Filter a color map to exclude entries with empty-string values. + * Monaco accepts missing keys but throws on empty strings. + */ +function filterColors(colors: Record): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(colors)) { + if (value) result[key] = value; + } + return result; +} + export function buildMonacoTheme(mode: ThemeMode): MonacoThemeData { + const rules = [ + tokenRule('comment', '--ov-syntax-comment', '--ov-syntax-style-comment', 'italic'), + tokenRule('string', '--ov-syntax-string'), + tokenRule('string.escape', '--ov-syntax-string-escape'), + tokenRule('number', '--ov-syntax-number'), + tokenRule('keyword', '--ov-syntax-keyword', '--ov-syntax-style-keyword'), + tokenRule('keyword.control', '--ov-syntax-keyword-control'), + tokenRule('keyword.operator', '--ov-syntax-keyword-operator'), + tokenRule('type', '--ov-syntax-type', '--ov-syntax-style-type'), + tokenRule('type.identifier', '--ov-syntax-type'), + tokenRule('class', '--ov-syntax-class'), + tokenRule('interface', '--ov-syntax-interface'), + tokenRule('function', '--ov-syntax-function', '--ov-syntax-style-function'), + tokenRule('function.declaration', '--ov-syntax-function'), + tokenRule('method', '--ov-syntax-method'), + tokenRule('variable', '--ov-syntax-variable'), + tokenRule('parameter', '--ov-syntax-parameter'), + tokenRule('property', '--ov-syntax-property'), + tokenRule('namespace', '--ov-syntax-namespace'), + tokenRule('decorator', '--ov-syntax-decorator'), + tokenRule('regexp', '--ov-syntax-regexp'), + tokenRule('operator', '--ov-syntax-operator'), + tokenRule('delimiter', '--ov-syntax-punctuation'), + tokenRule('delimiter.bracket', '--ov-syntax-punctuation'), + ].filter((r): r is NonNullable => r !== null); + return { base: getBaseTheme(mode), inherit: true, - rules: [ - { - token: 'comment', - foreground: stripHash(token('--ov-syntax-comment')), - fontStyle: token('--ov-syntax-style-comment') || 'italic', - }, - { token: 'string', foreground: stripHash(token('--ov-syntax-string')) }, - { token: 'string.escape', foreground: stripHash(token('--ov-syntax-string-escape')) }, - { token: 'number', foreground: stripHash(token('--ov-syntax-number')) }, - { - token: 'keyword', - foreground: stripHash(token('--ov-syntax-keyword')), - fontStyle: token('--ov-syntax-style-keyword') || '', - }, - { token: 'keyword.control', foreground: stripHash(token('--ov-syntax-keyword-control')) }, - { token: 'keyword.operator', foreground: stripHash(token('--ov-syntax-keyword-operator')) }, - { - token: 'type', - foreground: stripHash(token('--ov-syntax-type')), - fontStyle: token('--ov-syntax-style-type') || '', - }, - { token: 'type.identifier', foreground: stripHash(token('--ov-syntax-type')) }, - { token: 'class', foreground: stripHash(token('--ov-syntax-class')) }, - { token: 'interface', foreground: stripHash(token('--ov-syntax-interface')) }, - { - token: 'function', - foreground: stripHash(token('--ov-syntax-function')), - fontStyle: token('--ov-syntax-style-function') || '', - }, - { token: 'function.declaration', foreground: stripHash(token('--ov-syntax-function')) }, - { token: 'method', foreground: stripHash(token('--ov-syntax-method')) }, - { token: 'variable', foreground: stripHash(token('--ov-syntax-variable')) }, - { token: 'parameter', foreground: stripHash(token('--ov-syntax-parameter')) }, - { token: 'property', foreground: stripHash(token('--ov-syntax-property')) }, - { token: 'namespace', foreground: stripHash(token('--ov-syntax-namespace')) }, - { token: 'decorator', foreground: stripHash(token('--ov-syntax-decorator')) }, - { token: 'regexp', foreground: stripHash(token('--ov-syntax-regexp')) }, - { token: 'operator', foreground: stripHash(token('--ov-syntax-operator')) }, - { token: 'delimiter', foreground: stripHash(token('--ov-syntax-punctuation')) }, - { token: 'delimiter.bracket', foreground: stripHash(token('--ov-syntax-punctuation')) }, - ], - colors: { - 'editor.background': token('--ov-color-editor-bg'), - 'editor.foreground': token('--ov-color-editor-fg'), - 'editorCursor.foreground': token('--ov-color-editor-cursor'), - 'editor.selectionBackground': token('--ov-color-editor-selection-bg'), - 'editor.inactiveSelectionBackground': token('--ov-color-editor-selection-inactive-bg'), - 'editor.lineHighlightBackground': token('--ov-color-editor-line-highlight-bg'), - 'editor.lineHighlightBorder': token('--ov-color-editor-line-highlight-border'), - 'editorLineNumber.foreground': token('--ov-color-editor-line-number'), - 'editorLineNumber.activeForeground': token('--ov-color-editor-line-number-active'), - 'editorWhitespace.foreground': token('--ov-color-editor-whitespace'), - 'editorIndentGuide.background': token('--ov-color-editor-indent-guide'), - 'editorIndentGuide.activeBackground': token('--ov-color-editor-indent-guide-active'), - 'editorRuler.foreground': token('--ov-color-editor-ruler'), - 'editor.findMatchBackground': token('--ov-color-editor-find-match-bg'), - 'editor.findMatchBorder': token('--ov-color-editor-find-match-border'), - 'editor.findMatchHighlightBackground': token('--ov-color-editor-find-range-bg'), - 'editorLink.activeForeground': token('--ov-color-editor-link'), - 'editorBracketMatch.background': token('--ov-color-editor-bracket-match-bg'), - 'editorBracketMatch.border': token('--ov-color-editor-bracket-match-border'), - 'editorBracketHighlight.foreground1': token('--ov-color-editor-bracket-1'), - 'editorBracketHighlight.foreground2': token('--ov-color-editor-bracket-2'), - 'editorBracketHighlight.foreground3': token('--ov-color-editor-bracket-3'), - 'editorBracketHighlight.foreground4': token('--ov-color-editor-bracket-4'), - 'editorBracketHighlight.foreground5': token('--ov-color-editor-bracket-5'), - 'editorBracketHighlight.foreground6': token('--ov-color-editor-bracket-6'), - 'editorGutter.background': token('--ov-color-gutter-bg'), - 'editorGutter.addedBackground': token('--ov-color-gutter-added'), - 'editorGutter.modifiedBackground': token('--ov-color-gutter-modified'), - 'editorGutter.deletedBackground': token('--ov-color-gutter-deleted'), - 'diffEditor.insertedTextBackground': token('--ov-color-diff-insert-bg'), - 'diffEditor.removedTextBackground': token('--ov-color-diff-remove-bg'), - 'minimap.background': token('--ov-color-minimap-bg'), - 'minimap.selectionHighlight': token('--ov-color-minimap-selection'), - 'minimap.errorHighlight': token('--ov-color-minimap-error'), - 'minimap.warningHighlight': token('--ov-color-minimap-warning'), - 'minimap.findMatchHighlight': token('--ov-color-minimap-find-match'), - }, + rules, + colors: filterColors({ + 'editor.background': hexToken('--ov-color-editor-bg'), + 'editor.foreground': hexToken('--ov-color-editor-fg'), + 'editorCursor.foreground': hexToken('--ov-color-editor-cursor'), + 'editor.selectionBackground': hexToken('--ov-color-editor-selection-bg'), + 'editor.inactiveSelectionBackground': hexToken('--ov-color-editor-selection-inactive-bg'), + 'editor.lineHighlightBackground': hexToken('--ov-color-editor-line-highlight-bg'), + 'editor.lineHighlightBorder': hexToken('--ov-color-editor-line-highlight-border'), + 'editorLineNumber.foreground': hexToken('--ov-color-editor-line-number'), + 'editorLineNumber.activeForeground': hexToken('--ov-color-editor-line-number-active'), + 'editorWhitespace.foreground': hexToken('--ov-color-editor-whitespace'), + 'editorIndentGuide.background': hexToken('--ov-color-editor-indent-guide'), + 'editorIndentGuide.activeBackground': hexToken('--ov-color-editor-indent-guide-active'), + 'editorRuler.foreground': hexToken('--ov-color-editor-ruler'), + 'editor.findMatchBackground': hexToken('--ov-color-editor-find-match-bg'), + 'editor.findMatchBorder': hexToken('--ov-color-editor-find-match-border'), + 'editor.findMatchHighlightBackground': hexToken('--ov-color-editor-find-range-bg'), + 'editorLink.activeForeground': hexToken('--ov-color-editor-link'), + 'editorBracketMatch.background': hexToken('--ov-color-editor-bracket-match-bg'), + 'editorBracketMatch.border': hexToken('--ov-color-editor-bracket-match-border'), + 'editorBracketHighlight.foreground1': hexToken('--ov-color-editor-bracket-1'), + 'editorBracketHighlight.foreground2': hexToken('--ov-color-editor-bracket-2'), + 'editorBracketHighlight.foreground3': hexToken('--ov-color-editor-bracket-3'), + 'editorBracketHighlight.foreground4': hexToken('--ov-color-editor-bracket-4'), + 'editorBracketHighlight.foreground5': hexToken('--ov-color-editor-bracket-5'), + 'editorBracketHighlight.foreground6': hexToken('--ov-color-editor-bracket-6'), + 'editorGutter.background': hexToken('--ov-color-gutter-bg'), + 'editorGutter.addedBackground': hexToken('--ov-color-gutter-added'), + 'editorGutter.modifiedBackground': hexToken('--ov-color-gutter-modified'), + 'editorGutter.deletedBackground': hexToken('--ov-color-gutter-deleted'), + 'diffEditor.insertedTextBackground': hexToken('--ov-color-diff-insert-bg'), + 'diffEditor.removedTextBackground': hexToken('--ov-color-diff-remove-bg'), + 'minimap.background': hexToken('--ov-color-minimap-bg'), + 'minimap.selectionHighlight': hexToken('--ov-color-minimap-selection'), + 'minimap.errorHighlight': hexToken('--ov-color-minimap-error'), + 'minimap.warningHighlight': hexToken('--ov-color-minimap-warning'), + 'minimap.findMatchHighlight': hexToken('--ov-color-minimap-find-match'), + }), }; } diff --git a/packages/editors/tsconfig.json b/packages/editors/tsconfig.json index d0fade3..c874c1e 100644 --- a/packages/editors/tsconfig.json +++ b/packages/editors/tsconfig.json @@ -5,7 +5,7 @@ "declaration": true, "declarationMap": true, "outDir": "./dist", - "types": ["vitest/globals", "@testing-library/jest-dom"] + "types": ["vitest/globals", "@testing-library/jest-dom", "vite/client"] }, - "include": ["src", "vite.config.ts", "vitest.setup.ts"] + "include": ["src", "src/**/*.json", "vite.config.ts", "vitest.setup.ts"] } diff --git a/packages/editors/vite.config.ts b/packages/editors/vite.config.ts index a922aa8..4d88426 100644 --- a/packages/editors/vite.config.ts +++ b/packages/editors/vite.config.ts @@ -16,7 +16,6 @@ export default defineConfig({ 'react-dom', /^@omniview\/base-ui/, /^monaco-editor/, - /^@monaco-editor/, /^@xterm/, /^react-markdown/, /^remark-/, @@ -25,6 +24,14 @@ export default defineConfig({ }, sourcemap: true, }, + resolve: { + alias: { + '@omniview/base-ui': new URL('../base-ui/src/index.ts', import.meta.url).pathname, + // monaco-editor@0.52 only has "module" (no "main"/"exports"), which + // Vite 5's resolver can't find. Point directly to the ESM entry. + 'monaco-editor': 'monaco-editor/esm/vs/editor/editor.main.js', + }, + }, test: { environment: 'jsdom', globals: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40751d3..d232a73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,9 +129,6 @@ importers: packages/editors: dependencies: - '@monaco-editor/react': - specifier: ^4.7.0 - version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@omniview/base-ui': specifier: workspace:* version: link:../base-ui @@ -156,6 +153,9 @@ importers: monaco-editor: specifier: ^0.52.2 version: 0.52.2 + monaco-yaml: + specifier: ^5.4.1 + version: 5.4.1(monaco-editor@0.52.2) react: specifier: ^19.0.0 version: 19.2.4 @@ -165,16 +165,28 @@ importers: react-markdown: specifier: ^9.0.3 version: 9.1.0(@types/react@19.2.14)(react@19.2.4) - rehype-highlight: - specifier: ^7.0.2 - version: 7.0.2 + rehype-raw: + specifier: ^7.0.0 + version: 7.0.0 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 remark-gfm: specifier: ^4.0.0 version: 4.0.1 devDependencies: + '@storybook/addon-a11y': + specifier: ^10.2.15 + version: 10.2.16(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@storybook/addon-docs': + specifier: ^10.2.15 + version: 10.2.16(@types/react@19.2.14)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@5.4.21(@types/node@22.19.15)) '@storybook/react': specifier: ^10.2.15 version: 10.2.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@storybook/react-vite': + specifier: ^10.2.15 + version: 10.2.16(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.15)) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.9.1 @@ -791,16 +803,6 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} - '@monaco-editor/loader@1.7.0': - resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} - - '@monaco-editor/react@4.7.0': - resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} - peerDependencies: - monaco-editor: '>= 0.25.0 < 1' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1969,17 +1971,23 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hast-util-is-element@3.0.0: - resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} - hast-util-to-text@4.0.2: - resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -1994,10 +2002,6 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - highlight.js@11.11.1: - resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} - engines: {node: '>=12.0.0'} - highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} @@ -2008,6 +2012,9 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2231,6 +2238,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} @@ -2275,9 +2285,6 @@ packages: lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} - lowlight@3.3.0: - resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2475,6 +2482,25 @@ packages: monaco-editor@0.52.2: resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + monaco-languageserver-types@0.4.0: + resolution: {integrity: sha512-QQ3BZiU5LYkJElGncSNb5AKoJ/LCs6YBMCJMAz9EA7v+JaOdn3kx2cXpPTcZfKA5AEsR0vc97sAw+5mdNhVBmw==} + + monaco-marker-data-provider@1.2.5: + resolution: {integrity: sha512-5ZdcYukhPwgYMCvlZ9H5uWs5jc23BQ8fFF5AhSIdrz5mvYLsqGZ58ZLxTv8rCX6+AxdJ8+vxg1HVSk+F2bLosg==} + + monaco-types@0.1.2: + resolution: {integrity: sha512-8LwfrlWXsedHwAL41xhXyqzPibS8IqPuIXr9NdORhonS495c2/wky+sI1PRLvMCuiI0nqC2NH1six9hdiRY4Xg==} + + monaco-worker-manager@2.0.1: + resolution: {integrity: sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==} + peerDependencies: + monaco-editor: '>=0.30.0' + + monaco-yaml@5.4.1: + resolution: {integrity: sha512-YQ6d/Ei98Uk073SJLFbwuSi95qhnl8F8NNmIUqN2XhDt9psZN2LqQ1T7pPQ866NJb2wFj44IrjnANgpa2jTfag==} + peerDependencies: + monaco-editor: '>=0.36' + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2700,8 +2726,11 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - rehype-highlight@7.0.2: - resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==} + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} @@ -2843,9 +2872,6 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - state-local@1.0.7: - resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} - std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -3049,9 +3075,6 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unist-util-find-after@5.0.0: - resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} - unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -3089,6 +3112,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -3165,6 +3191,19 @@ packages: jsdom: optional: true + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -3172,6 +3211,9 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -3251,6 +3293,11 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3750,17 +3797,6 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} - '@monaco-editor/loader@1.7.0': - dependencies: - state-local: 1.0.7 - - '@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@monaco-editor/loader': 1.7.0 - monaco-editor: 0.52.2 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/pluginutils@5.3.0(rollup@4.59.0)': @@ -5100,14 +5136,43 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-is-element@3.0.0: + hast-util-from-parse5@8.0.3: dependencies: '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + unist-util-position: 5.0.0 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.8 @@ -5128,12 +5193,15 @@ snapshots: transitivePeerDependencies: - supports-color - hast-util-to-text@4.0.2: + hast-util-to-parse5@8.0.1: dependencies: '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - hast-util-is-element: 3.0.0 - unist-util-find-after: 5.0.0 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 hast-util-whitespace@3.0.0: dependencies: @@ -5151,8 +5219,6 @@ snapshots: highlight.js@10.7.3: {} - highlight.js@11.11.1: {} - highlightjs-vue@1.0.0: {} html-encoding-sniffer@4.0.0: @@ -5161,6 +5227,8 @@ snapshots: html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -5394,6 +5462,8 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsonfile@6.2.0: dependencies: universalify: 2.0.1 @@ -5445,12 +5515,6 @@ snapshots: fault: 1.0.4 highlight.js: 10.7.3 - lowlight@3.3.0: - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - highlight.js: 11.11.1 - lru-cache@10.4.3: {} lru-cache@11.2.6: {} @@ -5854,6 +5918,37 @@ snapshots: monaco-editor@0.52.2: {} + monaco-languageserver-types@0.4.0: + dependencies: + monaco-types: 0.1.2 + vscode-languageserver-protocol: 3.17.5 + vscode-uri: 3.1.0 + + monaco-marker-data-provider@1.2.5: + dependencies: + monaco-types: 0.1.2 + + monaco-types@0.1.2: {} + + monaco-worker-manager@2.0.1(monaco-editor@0.52.2): + dependencies: + monaco-editor: 0.52.2 + + monaco-yaml@5.4.1(monaco-editor@0.52.2): + dependencies: + jsonc-parser: 3.3.1 + monaco-editor: 0.52.2 + monaco-languageserver-types: 0.4.0 + monaco-marker-data-provider: 1.2.5 + monaco-types: 0.1.2 + monaco-worker-manager: 2.0.1(monaco-editor@0.52.2) + path-browserify: 1.0.1 + prettier: 3.8.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.1.0 + yaml: 2.8.2 + ms@2.1.3: {} muggle-string@0.4.1: {} @@ -6128,14 +6223,17 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 - rehype-highlight@7.0.2: + rehype-raw@7.0.0: dependencies: '@types/hast': 3.0.4 - hast-util-to-text: 4.0.2 - lowlight: 3.3.0 - unist-util-visit: 5.1.0 + hast-util-raw: 9.1.0 vfile: 6.0.3 + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -6331,8 +6429,6 @@ snapshots: stackback@0.0.2: {} - state-local@1.0.7: {} - std-env@3.10.0: {} stop-iteration-iterator@1.1.0: @@ -6572,11 +6668,6 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unist-util-find-after@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -6623,6 +6714,11 @@ snapshots: dependencies: react: 19.2.4 + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -6715,12 +6811,25 @@ snapshots: - supports-color - terser + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + vscode-uri@3.1.0: {} w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 + web-namespaces@2.0.1: {} + webidl-conversions@7.0.0: {} webpack-virtual-modules@0.6.2: {} @@ -6802,6 +6911,8 @@ snapshots: yallist@4.0.0: {} + yaml@2.8.2: {} + yocto-queue@0.1.0: {} zwitch@2.0.4: {} From 13620fb03995e5d84de17f90365a0a906ecdb7ca Mon Sep 17 00:00:00 2001 From: Joshua Pare Date: Mon, 9 Mar 2026 17:36:36 -0500 Subject: [PATCH 04/11] feat(editors): Improve CommandPalette styling and test coverage --- .../command-palette/CommandPalette.module.css | 114 +------ .../CommandPalette.stories.tsx | 177 +++++++++- .../command-palette/CommandPalette.test.tsx | 319 +++++++++++++++++- .../command-palette/CommandPalette.tsx | 209 +++--------- 4 files changed, 539 insertions(+), 280 deletions(-) diff --git a/packages/editors/src/components/command-palette/CommandPalette.module.css b/packages/editors/src/components/command-palette/CommandPalette.module.css index 0d75a58..331cb5a 100644 --- a/packages/editors/src/components/command-palette/CommandPalette.module.css +++ b/packages/editors/src/components/command-palette/CommandPalette.module.css @@ -20,114 +20,12 @@ overflow: hidden; } -.InputWrapper { - padding: var(--ov-space-2, 8px); - border-bottom: 1px solid var(--ov-color-border-default); +/* CommandList fills the palette; its results scroll within the palette's max-height. */ +.CommandList { + min-height: 0; + flex: 1 1 auto; } -.Input { - width: 100%; - padding: var(--ov-space-2, 8px) var(--ov-space-3, 12px); - background: var(--ov-color-input-bg, transparent); - color: var(--ov-color-input-fg, var(--ov-color-fg-default)); - border: 1px solid var(--ov-color-input-border, var(--ov-color-border-default)); - border-radius: var(--ov-radius-sm, 4px); - font-family: var(--ov-font-family-sans, sans-serif); - font-size: var(--ov-font-size-md, 14px); - outline: none; -} - -.Input::placeholder { - color: var(--ov-color-input-placeholder, var(--ov-color-fg-muted)); -} - -.Input:focus { - border-color: var(--ov-color-fg-accent); -} - -.List { - flex: 1; - overflow-y: auto; - padding: var(--ov-space-1, 4px); -} - -.Group { - margin-bottom: var(--ov-space-1, 4px); -} - -.GroupLabel { - padding: var(--ov-space-1, 4px) var(--ov-space-2, 8px); - font-size: var(--ov-font-size-xs, 11px); - font-weight: var(--ov-font-weight-semibold, 500); - color: var(--ov-color-fg-muted); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.Item { - display: flex; - align-items: center; - gap: var(--ov-space-2, 8px); - padding: var(--ov-space-2, 8px) var(--ov-space-3, 12px); - border-radius: var(--ov-radius-sm, 4px); - cursor: pointer; - user-select: none; -} - -.Item:hover, -.ItemActive { - background: var(--ov-color-list-hover-bg, var(--ov-color-bg-subtle)); - color: var(--ov-color-list-hover-fg, var(--ov-color-fg-default)); -} - -.Icon { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - color: var(--ov-color-fg-muted); -} - -.Content { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 1px; -} - -.Label { - font-size: var(--ov-font-size-sm, 13px); - color: var(--ov-color-fg-default); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.Description { - font-size: var(--ov-font-size-xs, 11px); - color: var(--ov-color-fg-muted); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.Shortcut { - flex-shrink: 0; - padding: var(--ov-space-0, 2px) var(--ov-space-1, 4px); - font-family: var(--ov-font-family-sans, sans-serif); - font-size: var(--ov-font-size-xs, 11px); - color: var(--ov-color-fg-muted); - background: var(--ov-color-bg-subtle); - border: 1px solid var(--ov-color-border-default); - border-radius: var(--ov-radius-xs, 3px); -} - -.Empty { - padding: var(--ov-space-4, 16px); - text-align: center; - color: var(--ov-color-fg-muted); - font-size: var(--ov-font-size-sm, 13px); +.Results { + max-block-size: none; } diff --git a/packages/editors/src/components/command-palette/CommandPalette.stories.tsx b/packages/editors/src/components/command-palette/CommandPalette.stories.tsx index ed8e422..1d3d4c3 100644 --- a/packages/editors/src/components/command-palette/CommandPalette.stories.tsx +++ b/packages/editors/src/components/command-palette/CommandPalette.stories.tsx @@ -7,8 +7,11 @@ const sampleCommands: CommandItem[] = [ { id: 'open-file', label: 'Open File', shortcut: 'Ctrl+O', group: 'File' }, { id: 'save', label: 'Save', shortcut: 'Ctrl+S', group: 'File' }, { id: 'save-as', label: 'Save As…', shortcut: 'Ctrl+Shift+S', group: 'File' }, + { id: 'close-tab', label: 'Close Tab', shortcut: 'Ctrl+W', group: 'File' }, { id: 'find', label: 'Find', shortcut: 'Ctrl+F', group: 'Edit' }, { id: 'replace', label: 'Replace', shortcut: 'Ctrl+H', group: 'Edit' }, + { id: 'undo', label: 'Undo', shortcut: 'Ctrl+Z', group: 'Edit' }, + { id: 'redo', label: 'Redo', shortcut: 'Ctrl+Shift+Z', group: 'Edit' }, { id: 'find-files', label: 'Find in Files', shortcut: 'Ctrl+Shift+F', group: 'Search' }, { id: 'toggle-terminal', label: 'Toggle Terminal', shortcut: 'Ctrl+`', group: 'View' }, { id: 'toggle-sidebar', label: 'Toggle Sidebar', shortcut: 'Ctrl+B', group: 'View' }, @@ -16,6 +19,8 @@ const sampleCommands: CommandItem[] = [ { id: 'zoom-out', label: 'Zoom Out', shortcut: 'Ctrl+-', group: 'View' }, { id: 'git-commit', label: 'Git: Commit', group: 'Source Control' }, { id: 'git-push', label: 'Git: Push', group: 'Source Control' }, + { id: 'git-pull', label: 'Git: Pull', group: 'Source Control' }, + { id: 'git-stash', label: 'Git: Stash Changes', group: 'Source Control' }, ]; const meta: Meta = { @@ -29,6 +34,10 @@ const meta: Meta = { commands: sampleCommands, placeholder: 'Type a command…', }, + argTypes: { + open: { control: 'boolean' }, + placeholder: { control: 'text' }, + }, }; export default meta; @@ -36,14 +45,21 @@ type Story = StoryObj; function PlaygroundStory(args: CommandPaletteProps) { const [open, setOpen] = useState(true); + const [lastSelected, setLastSelected] = useState(null); return (
+ {lastSelected && ( +

Last selected: {lastSelected}

+ )} setOpen(false)} - onSelect={() => setOpen(false)} + onSelect={(cmd) => { + setLastSelected(cmd.label); + setOpen(false); + }} />
); @@ -55,7 +71,7 @@ export const Playground: Story = { function ManyCommandsStory(args: CommandPaletteProps) { const [open, setOpen] = useState(true); - const manyCommands = Array.from({ length: 50 }, (_, i) => ({ + const manyCommands = Array.from({ length: 100 }, (_, i) => ({ id: `cmd-${i}`, label: `Command ${i + 1}`, description: `Description for command ${i + 1}`, @@ -63,7 +79,7 @@ function ManyCommandsStory(args: CommandPaletteProps) { })); return (
- + , }; + +/** Commands with descriptions showing additional context. */ +function WithDescriptionsStory(args: CommandPaletteProps) { + const [open, setOpen] = useState(true); + const commands: CommandItem[] = [ + { + id: 'k8s-pods', + label: 'Kubernetes: List Pods', + description: 'Show all pods in the current namespace', + group: 'Kubernetes', + }, + { + id: 'k8s-deploy', + label: 'Kubernetes: Create Deployment', + description: 'Deploy a new application to the cluster', + group: 'Kubernetes', + }, + { + id: 'k8s-logs', + label: 'Kubernetes: View Logs', + description: 'Stream logs from a running pod', + group: 'Kubernetes', + }, + { + id: 'docker-build', + label: 'Docker: Build Image', + description: 'Build a container image from Dockerfile', + group: 'Docker', + }, + { + id: 'docker-push', + label: 'Docker: Push Image', + description: 'Push image to container registry', + group: 'Docker', + }, + ]; + return ( +
+ + setOpen(false)} + onSelect={() => setOpen(false)} + /> +
+ ); +} + +export const WithDescriptions: Story = { + render: (args) => , +}; + +/** Demonstrates disabled commands being filtered out. */ +function WithDisabledStory(args: CommandPaletteProps) { + const [open, setOpen] = useState(true); + const commands: CommandItem[] = [ + { id: 'save', label: 'Save', shortcut: 'Ctrl+S', group: 'File' }, + { id: 'save-as', label: 'Save As…', shortcut: 'Ctrl+Shift+S', group: 'File' }, + { id: 'revert', label: 'Revert File', group: 'File', disabled: true }, + { id: 'close', label: 'Close', shortcut: 'Ctrl+W', group: 'File' }, + { id: 'deploy', label: 'Deploy to Staging', group: 'Actions', disabled: true }, + { id: 'publish', label: 'Publish Package', group: 'Actions', disabled: true }, + { id: 'test', label: 'Run Tests', shortcut: 'Ctrl+Shift+T', group: 'Actions' }, + ]; + return ( +
+ + setOpen(false)} + onSelect={() => setOpen(false)} + /> +
+ ); +} + +export const WithDisabledCommands: Story = { + render: (args) => , +}; + +/** Empty command list edge case. */ +function EmptyStory(args: CommandPaletteProps) { + const [open, setOpen] = useState(true); + return ( +
+ + setOpen(false)} + onSelect={() => setOpen(false)} + /> +
+ ); +} + +export const EmptyCommands: Story = { + render: (args) => , +}; + +/** Commands without groups — flat list. */ +function UngroupedStory(args: CommandPaletteProps) { + const [open, setOpen] = useState(true); + const commands: CommandItem[] = [ + { id: 'action-1', label: 'Format Document', shortcut: 'Shift+Alt+F' }, + { id: 'action-2', label: 'Toggle Word Wrap', shortcut: 'Alt+Z' }, + { id: 'action-3', label: 'Sort Lines Ascending' }, + { id: 'action-4', label: 'Sort Lines Descending' }, + { id: 'action-5', label: 'Transform to Uppercase' }, + { id: 'action-6', label: 'Transform to Lowercase' }, + { id: 'action-7', label: 'Trim Trailing Whitespace' }, + ]; + return ( +
+ + setOpen(false)} + onSelect={() => setOpen(false)} + /> +
+ ); +} + +export const UngroupedCommands: Story = { + render: (args) => , +}; + +/** Custom placeholder text. */ +function CustomPlaceholderStory(args: CommandPaletteProps) { + const [open, setOpen] = useState(true); + return ( +
+ + setOpen(false)} + onSelect={() => setOpen(false)} + /> +
+ ); +} + +export const CustomPlaceholder: Story = { + render: (args) => , +}; diff --git a/packages/editors/src/components/command-palette/CommandPalette.test.tsx b/packages/editors/src/components/command-palette/CommandPalette.test.tsx index 7373d0c..0b7cab4 100644 --- a/packages/editors/src/components/command-palette/CommandPalette.test.tsx +++ b/packages/editors/src/components/command-palette/CommandPalette.test.tsx @@ -3,14 +3,239 @@ import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } from 'vitest'; import { CommandPalette, type CommandItem } from './CommandPalette'; +// --------------------------------------------------------------------------- +// Mock @omniview/base-ui — CommandList compound component +// --------------------------------------------------------------------------- + +vi.mock('@omniview/base-ui', () => { + const React = require('react'); + + // Simple CommandList mock that renders items directly and supports filtering + function MockRoot({ + items, + itemKey, + renderItem, + filterFn, + groupBy, + onAction, + onDismiss, + placeholder, + children, + }: { + items: CommandItem[]; + itemKey: (item: CommandItem) => string; + renderItem: (item: CommandItem, meta: { key: string; isActive: boolean; isDisabled: boolean }) => React.ReactNode; + filterFn?: (item: CommandItem, query: string) => boolean; + groupBy?: (item: CommandItem) => string; + onAction?: (key: string, item: CommandItem) => void; + onDismiss?: () => void; + placeholder?: string; + children?: React.ReactNode; + }) { + const [query, setQuery] = React.useState(''); + const [activeIndex, setActiveIndex] = React.useState(0); + + const filtered = React.useMemo(() => { + if (!query || !filterFn) return items; + return items.filter((item) => filterFn(item, query)); + }, [items, query, filterFn]); + + // Group items + const groups = React.useMemo(() => { + if (!groupBy) return new Map([['', filtered]]); + const map = new Map(); + for (const item of filtered) { + const group = groupBy(item); + const list = map.get(group); + if (list) list.push(item); + else map.set(group, [item]); + } + return map; + }, [filtered, groupBy]); + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setActiveIndex((i: number) => (i + 1) % Math.max(filtered.length, 1)); + break; + case 'ArrowUp': + e.preventDefault(); + setActiveIndex((i: number) => (i - 1 + filtered.length) % Math.max(filtered.length, 1)); + break; + case 'Enter': { + e.preventDefault(); + const selected = filtered[activeIndex]; + if (selected && onAction) onAction(itemKey(selected), selected); + break; + } + case 'Escape': + e.preventDefault(); + onDismiss?.(); + break; + } + }, + [filtered, activeIndex, onAction, onDismiss, itemKey], + ); + + // Provide context to children via data attributes on root + const contextValue = React.useMemo( + () => ({ query, setQuery, setActiveIndex, handleKeyDown, placeholder }), + [query, handleKeyDown, placeholder], + ); + + let flatIndex = 0; + + // Build results content + const resultsContent = filtered.length > 0 ? ( + Array.from(groups.entries()).map(([group, groupItems]: [string, CommandItem[]]) => ( +
+ {group &&
{group}
} + {groupItems.map((item: CommandItem) => { + const idx = flatIndex++; + return renderItem(item, { + key: itemKey(item), + isActive: idx === activeIndex, + isDisabled: false, + }); + })} +
+ )) + ) : null; + + return ( + +
+ {React.Children.map(children, (child: React.ReactNode) => { + if (!React.isValidElement(child)) return child; + const displayName = (child.type as { displayName?: string })?.displayName; + if (displayName === 'CommandList.Input') return child; + if (displayName === 'CommandList.Results') { + const childProps = child.props as Record; + return resultsContent ? ( +
{resultsContent}
+ ) : null; + } + if (displayName === 'CommandList.Empty') { + return filtered.length === 0 ? child : null; + } + return child; + })} +
+
+ ); + } + MockRoot.displayName = 'CommandList.Root'; + + const MockRootContext = React.createContext<{ + query: string; + setQuery: (q: string) => void; + setActiveIndex: (i: number) => void; + handleKeyDown: (e: React.KeyboardEvent) => void; + placeholder?: string; + } | null>(null); + + function MockInput(props: Record) { + const ctx = React.useContext(MockRootContext); + return ( + ) => { + ctx?.setQuery(e.target.value); + ctx?.setActiveIndex(0); + }} + onKeyDown={ctx?.handleKeyDown} + placeholder={ctx?.placeholder} + {...props} + /> + ); + } + MockInput.displayName = 'CommandList.Input'; + + function MockResults(props: Record) { + return
; + } + MockResults.displayName = 'CommandList.Results'; + + function MockItem({ children, itemKey: _ik, ...props }: Record) { + return ( +
+ {children as React.ReactNode} +
+ ); + } + MockItem.displayName = 'CommandList.Item'; + + function MockItemIcon({ children, ...props }: Record) { + return {children as React.ReactNode}; + } + MockItemIcon.displayName = 'CommandList.ItemIcon'; + + function MockItemLabel({ children, ...props }: Record) { + return {children as React.ReactNode}; + } + MockItemLabel.displayName = 'CommandList.ItemLabel'; + + function MockItemDescription({ children, ...props }: Record) { + return {children as React.ReactNode}; + } + MockItemDescription.displayName = 'CommandList.ItemDescription'; + + function MockItemShortcut({ children, ...props }: Record) { + return {children as React.ReactNode}; + } + MockItemShortcut.displayName = 'CommandList.ItemShortcut'; + + function MockEmpty({ children, ...props }: Record) { + return
{children as React.ReactNode}
; + } + MockEmpty.displayName = 'CommandList.Empty'; + + function MockLoading({ children, ...props }: Record) { + return
{children as React.ReactNode}
; + } + MockLoading.displayName = 'CommandList.Loading'; + + const CommandList = Object.assign(MockRoot, { + Root: MockRoot, + Input: MockInput, + Results: MockResults, + Item: MockItem, + ItemIcon: MockItemIcon, + ItemLabel: MockItemLabel, + ItemDescription: MockItemDescription, + ItemShortcut: MockItemShortcut, + Empty: MockEmpty, + Loading: MockLoading, + }); + + return { CommandList }; +}); + +// --------------------------------------------------------------------------- +// Test data +// --------------------------------------------------------------------------- + const commands: CommandItem[] = [ { id: 'open', label: 'Open File', shortcut: 'Ctrl+O', group: 'File' }, { id: 'save', label: 'Save File', shortcut: 'Ctrl+S', group: 'File' }, { id: 'find', label: 'Find in Files', shortcut: 'Ctrl+Shift+F', group: 'Search' }, - { id: 'terminal', label: 'Toggle Terminal', shortcut: 'Ctrl+`', group: 'View' }, + { + id: 'terminal', + label: 'Toggle Terminal', + shortcut: 'Ctrl+`', + group: 'View', + description: 'Show or hide the integrated terminal', + }, { id: 'disabled', label: 'Disabled Command', disabled: true }, ]; +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + describe('CommandPalette', () => { const defaultProps = { open: true, @@ -50,6 +275,20 @@ describe('CommandPalette', () => { expect(screen.queryByTestId('command-item-open')).not.toBeInTheDocument(); }); + it('filters by description', async () => { + const user = userEvent.setup(); + render(); + await user.type(screen.getByTestId('command-palette-input'), 'integrated'); + expect(screen.getByTestId('command-item-terminal')).toBeInTheDocument(); + }); + + it('filters by group name', async () => { + const user = userEvent.setup(); + render(); + await user.type(screen.getByTestId('command-palette-input'), 'search'); + expect(screen.getByTestId('command-item-find')).toBeInTheDocument(); + }); + it('shows empty state when no matches', async () => { const user = userEvent.setup(); render(); @@ -57,12 +296,14 @@ describe('CommandPalette', () => { expect(screen.getByTestId('command-palette-empty')).toBeInTheDocument(); }); - it('selects command on click', async () => { + it('selects command on Enter after arrow navigation', () => { const onSelect = vi.fn(); const onClose = vi.fn(); render(); - fireEvent.click(screen.getByTestId('command-item-open')); - expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'open' })); + const input = screen.getByTestId('command-palette-input'); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + fireEvent.keyDown(input, { key: 'Enter' }); + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'save' })); expect(onClose).toHaveBeenCalled(); }); @@ -73,16 +314,6 @@ describe('CommandPalette', () => { expect(onClose).toHaveBeenCalled(); }); - it('navigates with arrow keys and selects with Enter', () => { - const onSelect = vi.fn(); - const onClose = vi.fn(); - render(); - const input = screen.getByTestId('command-palette-input'); - fireEvent.keyDown(input, { key: 'ArrowDown' }); - fireEvent.keyDown(input, { key: 'Enter' }); - expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'save' })); - }); - it('closes on overlay click', () => { const onClose = vi.fn(); render(); @@ -90,6 +321,13 @@ describe('CommandPalette', () => { expect(onClose).toHaveBeenCalled(); }); + it('does not propagate click from dialog to overlay', () => { + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByTestId('command-palette')); + expect(onClose).not.toHaveBeenCalled(); + }); + it('renders group labels', () => { render(); expect(screen.getByText('File')).toBeInTheDocument(); @@ -101,4 +339,57 @@ describe('CommandPalette', () => { render(); expect(screen.getByText('Ctrl+O')).toBeInTheDocument(); }); + + it('shows description text for items that have one', () => { + render(); + expect(screen.getByText('Show or hide the integrated terminal')).toBeInTheDocument(); + }); + + it('uses custom placeholder text', () => { + render(); + expect(screen.getByPlaceholderText('Search resources…')).toBeInTheDocument(); + }); + + it('has proper dialog accessibility attributes', () => { + render(); + const dialog = screen.getByTestId('command-palette'); + expect(dialog).toHaveAttribute('role', 'dialog'); + expect(dialog).toHaveAttribute('aria-modal', 'true'); + expect(dialog).toHaveAttribute('aria-label', 'Command palette'); + }); + + it('handles empty commands array', () => { + render(); + expect(screen.getByTestId('command-palette-empty')).toBeInTheDocument(); + }); + + it('wraps ArrowDown from last to first item', () => { + const onSelect = vi.fn(); + render(); + const input = screen.getByTestId('command-palette-input'); + // 4 non-disabled commands, press down 4 times to wrap + for (let i = 0; i < 4; i++) { + fireEvent.keyDown(input, { key: 'ArrowDown' }); + } + fireEvent.keyDown(input, { key: 'Enter' }); + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'open' })); + }); + + it('wraps ArrowUp from first to last item', () => { + const onSelect = vi.fn(); + render(); + const input = screen.getByTestId('command-palette-input'); + fireEvent.keyDown(input, { key: 'ArrowUp' }); + fireEvent.keyDown(input, { key: 'Enter' }); + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'terminal' })); + }); + + it('does not call onSelect on Enter with empty results', async () => { + const user = userEvent.setup(); + const onSelect = vi.fn(); + render(); + await user.type(screen.getByTestId('command-palette-input'), 'zzzzz'); + fireEvent.keyDown(screen.getByTestId('command-palette-input'), { key: 'Enter' }); + expect(onSelect).not.toHaveBeenCalled(); + }); }); diff --git a/packages/editors/src/components/command-palette/CommandPalette.tsx b/packages/editors/src/components/command-palette/CommandPalette.tsx index f40e78e..212b0f4 100644 --- a/packages/editors/src/components/command-palette/CommandPalette.tsx +++ b/packages/editors/src/components/command-palette/CommandPalette.tsx @@ -1,13 +1,5 @@ -import { - forwardRef, - useCallback, - useEffect, - useMemo, - useRef, - useState, - type KeyboardEvent, - type ReactNode, -} from 'react'; +import { forwardRef, useMemo, type ReactNode } from 'react'; +import { CommandList } from '@omniview/base-ui'; import styles from './CommandPalette.module.css'; export interface CommandItem { @@ -28,10 +20,6 @@ export interface CommandPaletteProps { placeholder?: string; } -function cn(...parts: Array): string { - return parts.filter(Boolean).join(' '); -} - function fuzzyMatch(text: string, query: string): boolean { const lower = text.toLowerCase(); const q = query.toLowerCase(); @@ -47,162 +35,73 @@ export const CommandPalette = forwardRef( { open, onClose, commands, onSelect, placeholder = 'Type a command…' }, ref, ) { - const [search, setSearch] = useState(''); - const [activeIndex, setActiveIndex] = useState(0); - const inputRef = useRef(null); - const listRef = useRef(null); - - const filtered = useMemo(() => { - if (!search) return commands.filter((c) => !c.disabled); - return commands.filter( - (c) => - !c.disabled && - (fuzzyMatch(c.label, search) || - (c.description && fuzzyMatch(c.description, search)) || - (c.group && fuzzyMatch(c.group, search))), - ); - }, [commands, search]); - - // Group commands - const groups = useMemo(() => { - const map = new Map(); - for (const cmd of filtered) { - const group = cmd.group || ''; - const list = map.get(group); - if (list) { - list.push(cmd); - } else { - map.set(group, [cmd]); - } - } - return map; - }, [filtered]); - - // Reset state when opened/closed - useEffect(() => { - if (open) { - setSearch(''); - setActiveIndex(0); - // Focus input after render - requestAnimationFrame(() => inputRef.current?.focus()); - } - }, [open]); - - // Scroll active item into view - useEffect(() => { - if (!listRef.current) return; - const active = listRef.current.querySelector('[data-active="true"]'); - if (active && typeof active.scrollIntoView === 'function') { - active.scrollIntoView({ block: 'nearest' }); - } - }, [activeIndex]); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - switch (e.key) { - case 'ArrowDown': - e.preventDefault(); - setActiveIndex((i) => (i + 1) % Math.max(filtered.length, 1)); - break; - case 'ArrowUp': - e.preventDefault(); - setActiveIndex((i) => (i - 1 + filtered.length) % Math.max(filtered.length, 1)); - break; - case 'Enter': { - e.preventDefault(); - const selected = filtered[activeIndex]; - if (selected) { - onSelect(selected); - onClose(); - } - break; - } - case 'Escape': - e.preventDefault(); - onClose(); - break; - } - }, - [filtered, activeIndex, onSelect, onClose], + const enabledCommands = useMemo( + () => commands.filter((c) => !c.disabled), + [commands], ); if (!open) return null; - let flatIndex = 0; - return ( -
+
e.stopPropagation()} - onKeyDown={handleKeyDown} role="dialog" aria-modal="true" aria-label="Command palette" data-testid="command-palette" > -
- { - setSearch(e.target.value); - setActiveIndex(0); - }} - placeholder={placeholder} - aria-label="Search commands" - data-testid="command-palette-input" - /> -
-
- {filtered.length === 0 ? ( -
- No matching commands -
- ) : ( - Array.from(groups.entries()).map(([group, items]) => ( -
- {group &&
{group}
} - {items.map((cmd) => { - const index = flatIndex++; - const isActive = index === activeIndex; - return ( -
{ - onSelect(cmd); - onClose(); - }} - onMouseEnter={() => setActiveIndex(index)} - > - {cmd.icon && {cmd.icon}} -
- {cmd.label} - {cmd.description && ( - {cmd.description} - )} -
- {cmd.shortcut && {cmd.shortcut}} -
- ); - })} -
- )) + cmd.id} + renderItem={(cmd, meta) => ( + + {cmd.icon && ( + {cmd.icon} + )} + {cmd.label} + {cmd.description && ( + + {cmd.description} + + )} + {cmd.shortcut && ( + + {cmd.shortcut} + + )} + )} -
+ filterFn={(cmd, query) => + fuzzyMatch(cmd.label, query) || + (cmd.description ? fuzzyMatch(cmd.description, query) : false) || + (cmd.group ? fuzzyMatch(cmd.group, query) : false) + } + groupBy={(cmd) => cmd.group || ''} + onAction={(_key, cmd) => { + onSelect(cmd); + onClose(); + }} + onDismiss={onClose} + placeholder={placeholder} + density="compact" + > + + + + No matching commands + +
); From 5957b57ef554b131fe1bed2135b654a520cb23a0 Mon Sep 17 00:00:00 2001 From: Joshua Pare Date: Mon, 9 Mar 2026 17:36:44 -0500 Subject: [PATCH 05/11] fix(base-ui): Update CommandList, Separator, and theme styles --- .../command-list/CommandList.module.css | 1 + .../components/separator/Separator.module.css | 2 +- packages/base-ui/src/theme/styles.css | 124 ++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/packages/base-ui/src/components/command-list/CommandList.module.css b/packages/base-ui/src/components/command-list/CommandList.module.css index 5dc53ed..93ecf2c 100644 --- a/packages/base-ui/src/components/command-list/CommandList.module.css +++ b/packages/base-ui/src/components/command-list/CommandList.module.css @@ -45,6 +45,7 @@ * --------------------------------------------------------------------------- */ .Input { + padding: var(--ov-space-inline-xs, 2px); border-bottom: 1px solid var(--ov-color-border-muted); } diff --git a/packages/base-ui/src/components/separator/Separator.module.css b/packages/base-ui/src/components/separator/Separator.module.css index e971ed5..2a16dc0 100644 --- a/packages/base-ui/src/components/separator/Separator.module.css +++ b/packages/base-ui/src/components/separator/Separator.module.css @@ -1,5 +1,5 @@ .Root { - --_ov-separator-color: color-mix(in srgb, var(--ov-color-fg-muted) 56%, transparent 44%); + --_ov-separator-color: var(--ov-color-border-default); --_ov-separator-thickness: var(--ov-size-separator-thickness-md, 2px); --_ov-separator-inset: var(--ov-size-separator-inset-md, 12px); --_ov-separator-label-color: var(--ov-color-fg-muted); diff --git a/packages/base-ui/src/theme/styles.css b/packages/base-ui/src/theme/styles.css index 1a17114..44b1ae6 100644 --- a/packages/base-ui/src/theme/styles.css +++ b/packages/base-ui/src/theme/styles.css @@ -27,10 +27,46 @@ --ov-primitive-amber-500: #cf9030; --ov-primitive-amber-600: #b17727; + --ov-primitive-red-300: #e89a9a; --ov-primitive-red-400: #d97777; --ov-primitive-red-500: #c45f5f; --ov-primitive-red-600: #ad4f4f; + --ov-primitive-purple-300: #b89aee; + --ov-primitive-purple-400: #a67de8; + --ov-primitive-purple-500: #9363db; + --ov-primitive-purple-600: #7c4ec4; + + --ov-primitive-cyan-300: #7ad4e8; + --ov-primitive-cyan-400: #5ac4db; + --ov-primitive-cyan-500: #3fb0c9; + --ov-primitive-cyan-600: #2e97ae; + + --ov-primitive-blue-200: #a4bdf3; + + --ov-primitive-coral-400: #ff7b72; + --ov-primitive-orange-300: #ffa657; + --ov-primitive-sky-200: #a5d6ff; + --ov-primitive-sky-300: #79c0ff; + --ov-primitive-lavender-300: #d2a8ff; + + --ov-primitive-ansi-black: #3b4252; + --ov-primitive-ansi-red: #bf616a; + --ov-primitive-ansi-green: #a3be8c; + --ov-primitive-ansi-yellow: #ebcb8b; + --ov-primitive-ansi-blue: #81a1c1; + --ov-primitive-ansi-magenta: #b48ead; + --ov-primitive-ansi-cyan: #88c0d0; + --ov-primitive-ansi-white: #e5e9f0; + --ov-primitive-ansi-bright-black: #4c566a; + --ov-primitive-ansi-bright-red: #d08770; + --ov-primitive-ansi-bright-green: #a3be8c; + --ov-primitive-ansi-bright-yellow: #ebcb8b; + --ov-primitive-ansi-bright-blue: #81a1c1; + --ov-primitive-ansi-bright-magenta: #b48ead; + --ov-primitive-ansi-bright-cyan: #8fbcbb; + --ov-primitive-ansi-bright-white: #eceff4; + --ov-primitive-font-sans: 'Inter Variable', 'SF Pro Text', 'Segoe UI', -apple-system, sans-serif; --ov-primitive-font-mono: 'JetBrains Mono Variable', 'SF Mono', 'Cascadia Mono', monospace; --ov-primitive-font-size-11: 0.6875rem; @@ -385,6 +421,94 @@ --ov-color-editor-group-header-bg: var(--ov-color-bg-surface-raised); --ov-color-editor-group-drop-border: var(--ov-color-brand-400); --ov-size-tab-height: 34px; + + /* Editor tokens */ + --ov-color-editor-bg: var(--ov-color-bg-base); + --ov-color-editor-fg: var(--ov-color-fg-default); + --ov-color-editor-cursor: var(--ov-color-accent-strong); + --ov-color-editor-selection-bg: var(--ov-color-accent-soft); + --ov-color-editor-selection-inactive-bg: rgb(161 169 182 / 0.1); + --ov-color-editor-line-highlight-bg: rgb(255 255 255 / 0.04); + --ov-color-editor-line-highlight-border: transparent; + --ov-color-editor-line-number: var(--ov-color-fg-subtle); + --ov-color-editor-line-number-active: var(--ov-color-fg-muted); + --ov-color-editor-whitespace: var(--ov-primitive-gray-400); + --ov-color-editor-indent-guide: var(--ov-primitive-gray-300); + --ov-color-editor-indent-guide-active: var(--ov-primitive-gray-500); + --ov-color-editor-ruler: var(--ov-primitive-gray-300); + --ov-color-editor-find-match-bg: rgb(207 144 48 / 0.35); + --ov-color-editor-find-match-border: var(--ov-color-warning); + --ov-color-editor-find-range-bg: rgb(207 144 48 / 0.15); + --ov-color-editor-link: var(--ov-color-brand-300); + --ov-color-editor-bracket-match-bg: rgb(90 127 226 / 0.2); + --ov-color-editor-bracket-match-border: var(--ov-color-brand-400); + --ov-color-editor-bracket-1: #ffd700; + --ov-color-editor-bracket-2: #da70d6; + --ov-color-editor-bracket-3: #179fff; + --ov-color-editor-bracket-4: #ffd700; + --ov-color-editor-bracket-5: #da70d6; + --ov-color-editor-bracket-6: #179fff; + --ov-color-gutter-bg: var(--ov-color-editor-bg); + --ov-color-gutter-added: var(--ov-color-success); + --ov-color-gutter-modified: var(--ov-color-info); + --ov-color-gutter-deleted: var(--ov-color-danger); + --ov-color-diff-insert-bg: var(--ov-color-success-soft); + --ov-color-diff-remove-bg: var(--ov-color-danger-soft); + --ov-color-minimap-bg: var(--ov-color-bg-surface); + --ov-color-minimap-selection: var(--ov-color-accent-soft); + --ov-color-minimap-error: var(--ov-color-danger); + --ov-color-minimap-warning: var(--ov-color-warning); + --ov-color-minimap-find-match: var(--ov-color-warning); + + /* Terminal tokens */ + --ov-color-terminal-bg: var(--ov-color-bg-inset); + --ov-color-terminal-fg: var(--ov-color-fg-default); + --ov-color-terminal-cursor: var(--ov-color-accent-strong); + --ov-color-terminal-selection-bg: var(--ov-color-accent-soft); + --ov-color-terminal-selection-fg: var(--ov-color-fg-default); + --ov-color-terminal-border: var(--ov-color-border-default); + --ov-color-ansi-black: var(--ov-primitive-ansi-black); + --ov-color-ansi-red: var(--ov-primitive-ansi-red); + --ov-color-ansi-green: var(--ov-primitive-ansi-green); + --ov-color-ansi-yellow: var(--ov-primitive-ansi-yellow); + --ov-color-ansi-blue: var(--ov-primitive-ansi-blue); + --ov-color-ansi-magenta: var(--ov-primitive-ansi-magenta); + --ov-color-ansi-cyan: var(--ov-primitive-ansi-cyan); + --ov-color-ansi-white: var(--ov-primitive-ansi-white); + --ov-color-ansi-bright-black: var(--ov-primitive-ansi-bright-black); + --ov-color-ansi-bright-red: var(--ov-primitive-ansi-bright-red); + --ov-color-ansi-bright-green: var(--ov-primitive-ansi-bright-green); + --ov-color-ansi-bright-yellow: var(--ov-primitive-ansi-bright-yellow); + --ov-color-ansi-bright-blue: var(--ov-primitive-ansi-bright-blue); + --ov-color-ansi-bright-magenta: var(--ov-primitive-ansi-bright-magenta); + --ov-color-ansi-bright-cyan: var(--ov-primitive-ansi-bright-cyan); + --ov-color-ansi-bright-white: var(--ov-primitive-ansi-bright-white); + + /* Syntax highlighting tokens */ + --ov-syntax-comment: #8b949e; + --ov-syntax-string: var(--ov-primitive-sky-200); + --ov-syntax-string-escape: var(--ov-primitive-sky-300); + --ov-syntax-number: var(--ov-primitive-sky-300); + --ov-syntax-keyword: var(--ov-primitive-coral-400); + --ov-syntax-keyword-control: var(--ov-primitive-coral-400); + --ov-syntax-keyword-operator: var(--ov-primitive-coral-400); + --ov-syntax-type: var(--ov-primitive-orange-300); + --ov-syntax-class: var(--ov-primitive-orange-300); + --ov-syntax-interface: var(--ov-primitive-orange-300); + --ov-syntax-function: var(--ov-primitive-lavender-300); + --ov-syntax-method: var(--ov-primitive-lavender-300); + --ov-syntax-variable: var(--ov-color-fg-default); + --ov-syntax-parameter: var(--ov-primitive-orange-300); + --ov-syntax-property: var(--ov-primitive-sky-300); + --ov-syntax-namespace: var(--ov-primitive-orange-300); + --ov-syntax-decorator: var(--ov-primitive-lavender-300); + --ov-syntax-regexp: var(--ov-primitive-sky-200); + --ov-syntax-operator: var(--ov-primitive-coral-400); + --ov-syntax-punctuation: #e6edf3; + --ov-syntax-style-comment: italic; + --ov-syntax-style-keyword: normal; + --ov-syntax-style-function: normal; + --ov-syntax-style-type: normal; } :root[data-ov-theme='light'] { From 0d25cd7dac4ccc590d358922f793ec257d42dcbd Mon Sep 17 00:00:00 2001 From: Joshua Pare Date: Mon, 9 Mar 2026 18:22:29 -0500 Subject: [PATCH 06/11] feat(editors): Production-grade Terminal upgrade with xterm 6, full IDE API surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade all xterm dependencies to latest (xterm 6.0, addons 0.11-0.19) - Remove @xterm/addon-canvas (dropped in v6), simplify renderer to WebGL → DOM - Fix CSS rendering bug: import xterm.css, set monospace font-family on container - Add onReady callback for session attach timing, autoFocus prop - Add fontWeight/fontWeightBold, drawBoldTextInBrightColors, onWriteParsed props - Expand handle: blur(), search (findNext/findPrevious/clearSearch), selection, scroll (scrollToLine/scrollUp/scrollDown), paste, getBufferContent via SerializeAddon - Load Unicode11Addon for proper emoji/CJK width, await document.fonts.ready - Export TerminalErrorInfo type matching Go SDK StreamError for ERROR signal - Wire all xterm events (onBell, onTitleChange, onKey, onSelectionChange, etc.) - Add renderer prop ('auto'|'webgl'|'dom'), disableStdin, customKeyEventHandler - Dynamic option updates: fontSize, cursorBlink, cursorStyle, scrollback, lineHeight, letterSpacing - New stories: Search, ReadOnly, RendererComparison, Serialization, CustomKeyHandler - 79 terminal tests covering lifecycle, resize debounce, cleanup ordering, race conditions --- packages/editors/package.json | 13 +- .../components/terminal/Terminal.module.css | 2 + .../components/terminal/Terminal.stories.tsx | 224 +++++++ .../src/components/terminal/Terminal.test.tsx | 619 +++++++++++++++++- .../src/components/terminal/Terminal.tsx | 450 +++++++++++-- .../editors/src/components/terminal/index.ts | 8 +- pnpm-lock.yaml | 90 ++- 7 files changed, 1292 insertions(+), 114 deletions(-) diff --git a/packages/editors/package.json b/packages/editors/package.json index d78061b..277d760 100644 --- a/packages/editors/package.json +++ b/packages/editors/package.json @@ -35,12 +35,13 @@ "react-dom": "^19.0.0" }, "dependencies": { - "@xterm/addon-canvas": "^0.7.0", - "@xterm/addon-fit": "^0.10.0", - "@xterm/addon-search": "^0.15.0", - "@xterm/addon-web-links": "^0.11.0", - "@xterm/addon-webgl": "^0.18.0", - "@xterm/xterm": "^5.5.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-search": "^0.16.0", + "@xterm/addon-serialize": "^0.14.0", + "@xterm/addon-unicode11": "^0.9.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/addon-webgl": "^0.19.0", + "@xterm/xterm": "^6.0.0", "monaco-editor": "^0.52.2", "monaco-yaml": "^5.4.1", "react-markdown": "^9.0.3", diff --git a/packages/editors/src/components/terminal/Terminal.module.css b/packages/editors/src/components/terminal/Terminal.module.css index 452444d..d4f2fbf 100644 --- a/packages/editors/src/components/terminal/Terminal.module.css +++ b/packages/editors/src/components/terminal/Terminal.module.css @@ -6,4 +6,6 @@ border: 1px solid var(--ov-color-terminal-border, var(--ov-color-border-default)); border-radius: var(--ov-radius-sm, 4px); padding: var(--ov-space-1, 4px); + /* Prevent parent sans-serif font from leaking into xterm's rendering */ + font-family: var(--ov-font-family-mono, monospace); } diff --git a/packages/editors/src/components/terminal/Terminal.stories.tsx b/packages/editors/src/components/terminal/Terminal.stories.tsx index 05c07b4..94894f6 100644 --- a/packages/editors/src/components/terminal/Terminal.stories.tsx +++ b/packages/editors/src/components/terminal/Terminal.stories.tsx @@ -13,11 +13,18 @@ const meta: Meta = { fontSize: { control: { type: 'range', min: 10, max: 24, step: 1 } }, scrollback: { control: { type: 'number', min: 0, max: 50000, step: 500 } }, cursorBlink: { control: 'boolean' }, + cursorStyle: { control: 'select', options: ['block', 'underline', 'bar'] }, convertEol: { control: 'boolean' }, macOptionIsMeta: { control: 'boolean' }, macOptionClickForcesSelection: { control: 'boolean' }, linkHandling: { control: 'boolean' }, allowTransparency: { control: 'boolean' }, + disableStdin: { control: 'boolean' }, + renderer: { control: 'select', options: ['auto', 'webgl', 'dom'] }, + lineHeight: { control: { type: 'range', min: 1.0, max: 2.0, step: 0.1 } }, + letterSpacing: { control: { type: 'range', min: -2, max: 5, step: 0.5 } }, + screenReaderMode: { control: 'boolean' }, + minimumContrastRatio: { control: { type: 'range', min: 1, max: 21, step: 0.5 } }, }, decorators: [ (Story) => ( @@ -287,3 +294,220 @@ function ClickableLinksStory(args: TerminalProps) { export const ClickableLinks: Story = { render: (args) => , }; + +/** Search story — 100 lines of log output with search bar UI. */ +function SearchStory(args: TerminalProps) { + const ref = useRef(null); + const [searchTerm, setSearchTerm] = useState('ERROR'); + + useEffect(() => { + const timeout = setTimeout(() => { + if (!ref.current) return; + for (let i = 0; i < 100; i++) { + const level = + i % 10 === 0 + ? '\x1b[31mERROR\x1b[0m' + : i % 5 === 0 + ? '\x1b[33mWARN\x1b[0m' + : '\x1b[32mINFO\x1b[0m'; + ref.current.writeln( + `[${String(i).padStart(3, '0')}] ${level} Processing request #${i + 1000}`, + ); + } + }, 300); + return () => clearTimeout(timeout); + }, []); + + return ( +
+
+ setSearchTerm(e.target.value)} + placeholder="Search..." + style={{ padding: '4px 8px', background: '#2d2d2d', color: '#fff', border: '1px solid #555', borderRadius: 4, fontSize: 13 }} + /> + + + +
+
+ +
+
+ ); +} + +export const Search: Story = { + render: (args) => , +}; + +/** ReadOnly terminal that streams simulated log output. */ +function ReadOnlyStory(args: TerminalProps) { + const ref = useRef(null); + + useEffect(() => { + let i = 0; + const interval = setInterval(() => { + if (!ref.current) return; + const level = i % 7 === 0 ? '\x1b[31mERROR\x1b[0m' : i % 3 === 0 ? '\x1b[33mWARN\x1b[0m' : '\x1b[32mINFO\x1b[0m'; + const ts = new Date().toISOString(); + ref.current.writeln(`${ts} ${level} app.server Event #${i + 1}`); + i++; + }, 500); + + return () => clearInterval(interval); + }, []); + + return ; +} + +export const ReadOnly: Story = { + render: (args) => , +}; + +/** Three terminals side-by-side comparing renderers. */ +function RendererComparisonStory() { + return ( +
+ {(['webgl', 'dom'] as const).map((r) => ( + + ))} +
+ ); +} + +function RendererPanel({ renderer }: { renderer: 'webgl' | 'dom' }) { + const ref = useRef(null); + useEffect(() => { + const t = setTimeout(() => { + if (!ref.current) return; + ref.current.writeln(`\x1b[1mRenderer: ${renderer}\x1b[0m`); + ref.current.writeln(''); + ref.current.writeln('The quick brown fox jumps'); + ref.current.writeln('over the lazy dog.'); + ref.current.writeln(''); + ref.current.writeln('\x1b[31mRed\x1b[0m \x1b[32mGreen\x1b[0m \x1b[34mBlue\x1b[0m'); + }, 400); + return () => clearTimeout(t); + }, [renderer]); + + return ( +
+
+ {renderer.toUpperCase()} +
+
+ +
+
+ ); +} + +export const RendererComparison: Story = { + render: () => , + decorators: [], +}; + +/** Serialization — save the buffer content. */ +function SerializationStory(args: TerminalProps) { + const ref = useRef(null); + const [buffer, setBuffer] = useState(''); + + useEffect(() => { + const timeout = setTimeout(() => { + if (!ref.current) return; + ref.current.writeln('$ echo "Hello from the terminal"'); + ref.current.writeln('Hello from the terminal'); + ref.current.writeln('$ date'); + ref.current.writeln('Mon Jan 15 10:30:00 UTC 2024'); + ref.current.writeln('$ whoami'); + ref.current.writeln('developer'); + ref.current.write('$ '); + }, 300); + return () => clearTimeout(timeout); + }, []); + + return ( +
+
+ +
+
+ +
+ {buffer && ( +
+          {buffer}
+        
+ )} +
+ ); +} + +export const Serialization: Story = { + render: (args) => , +}; + +/** Custom key handler — intercepts Ctrl+C to show custom behavior. */ +function CustomKeyHandlerStory(args: TerminalProps) { + const ref = useRef(null); + const [intercepted, setIntercepted] = useState(false); + + const handleKey = (event: KeyboardEvent) => { + if (event.ctrlKey && event.key === 'c') { + setIntercepted(true); + ref.current?.writeln('\r\n\x1b[33m[Ctrl+C intercepted by custom handler]\x1b[0m'); + ref.current?.write('$ '); + setTimeout(() => setIntercepted(false), 1500); + return false; // Prevent default xterm handling + } + return true; + }; + + useEffect(() => { + const timeout = setTimeout(() => { + ref.current?.writeln('Type Ctrl+C to see the custom key handler intercept it.'); + ref.current?.write('$ '); + }, 300); + return () => clearTimeout(timeout); + }, []); + + return ( +
+ {intercepted && ( +
+ Ctrl+C was intercepted! +
+ )} +
+ +
+
+ ); +} + +export const CustomKeyHandler: Story = { + render: (args) => , +}; diff --git a/packages/editors/src/components/terminal/Terminal.test.tsx b/packages/editors/src/components/terminal/Terminal.test.tsx index 6f887c8..f5ffd79 100644 --- a/packages/editors/src/components/terminal/Terminal.test.tsx +++ b/packages/editors/src/components/terminal/Terminal.test.tsx @@ -7,11 +7,19 @@ import { Terminal, type TerminalHandle } from './Terminal'; const mockOnData = vi.fn(); const mockOnBinary = vi.fn(); const mockOnResize = vi.fn(); +const mockOnBell = vi.fn(); +const mockOnTitleChange = vi.fn(); +const mockOnKey = vi.fn(); +const mockOnSelectionChange = vi.fn(); +const mockOnLineFeed = vi.fn(); +const mockOnScroll = vi.fn(); +const mockOnWriteParsed = vi.fn(); const mockTerminal = { open: vi.fn(), dispose: vi.fn(), loadAddon: vi.fn(), + attachCustomKeyEventHandler: vi.fn(), onData: vi.fn((cb) => { mockOnData.mockImplementation(cb); }), @@ -21,15 +29,45 @@ const mockTerminal = { onResize: vi.fn((cb) => { mockOnResize.mockImplementation(cb); }), + onBell: vi.fn((cb) => { + mockOnBell.mockImplementation(cb); + }), + onTitleChange: vi.fn((cb) => { + mockOnTitleChange.mockImplementation(cb); + }), + onKey: vi.fn((cb) => { + mockOnKey.mockImplementation(cb); + }), + onSelectionChange: vi.fn((cb) => { + mockOnSelectionChange.mockImplementation(cb); + }), + onLineFeed: vi.fn((cb) => { + mockOnLineFeed.mockImplementation(cb); + }), + onScroll: vi.fn((cb) => { + mockOnScroll.mockImplementation(cb); + }), + onWriteParsed: vi.fn((cb) => { + mockOnWriteParsed.mockImplementation(cb); + }), write: vi.fn(), writeln: vi.fn(), clear: vi.fn(), focus: vi.fn(), + blur: vi.fn(), reset: vi.fn(), scrollToBottom: vi.fn(), + getSelection: vi.fn(() => 'selected-text'), + hasSelection: vi.fn(() => true), + selectAll: vi.fn(), + clearSelection: vi.fn(), + scrollToLine: vi.fn(), + scrollLines: vi.fn(), + paste: vi.fn(), cols: 80, rows: 24, options: {} as Record, + unicode: { activeVersion: '6' }, }; const mockFitAddon = { @@ -38,15 +76,23 @@ const mockFitAddon = { }; const mockSearchAddon = { + findNext: vi.fn(() => true), + findPrevious: vi.fn(() => true), + clearDecorations: vi.fn(), dispose: vi.fn(), }; -const mockWebglAddon = { - onContextLoss: vi.fn(), +const mockSerializeAddon = { + serialize: vi.fn(() => 'buffer-content'), + dispose: vi.fn(), +}; + +const mockUnicode11Addon = { dispose: vi.fn(), }; -const mockCanvasAddon = { +const mockWebglAddon = { + onContextLoss: vi.fn(), dispose: vi.fn(), }; @@ -66,18 +112,28 @@ vi.mock('@xterm/addon-search', () => ({ SearchAddon: vi.fn(() => mockSearchAddon), })); -vi.mock('@xterm/addon-webgl', () => ({ - WebglAddon: vi.fn(() => mockWebglAddon), +vi.mock('@xterm/addon-serialize', () => ({ + SerializeAddon: vi.fn(() => mockSerializeAddon), })); -vi.mock('@xterm/addon-canvas', () => ({ - CanvasAddon: vi.fn(() => mockCanvasAddon), +vi.mock('@xterm/addon-unicode11', () => ({ + Unicode11Addon: vi.fn(() => mockUnicode11Addon), +})); + +vi.mock('@xterm/addon-webgl', () => ({ + WebglAddon: vi.fn(() => mockWebglAddon), })); vi.mock('@xterm/addon-web-links', () => ({ WebLinksAddon: vi.fn(() => mockWebLinksAddon), })); +// Mock document.fonts.ready +Object.defineProperty(document, 'fonts', { + value: { ready: Promise.resolve() }, + writable: true, +}); + // Mock ResizeObserver let resizeCallback: (() => void) | null = null; const mockObserve = vi.fn(); @@ -86,6 +142,7 @@ const mockDisconnect = vi.fn(); beforeEach(() => { vi.clearAllMocks(); resizeCallback = null; + mockTerminal.unicode.activeVersion = '6'; global.ResizeObserver = vi.fn().mockImplementation((cb) => { resizeCallback = cb; return { @@ -118,11 +175,19 @@ describe('Terminal', () => { expect(mockTerminal.open).toHaveBeenCalled(); }); - it('loads fit and search addons', async () => { + it('loads fit, search, serialize, and unicode11 addons', async () => { render(); await waitForInit(); expect(mockTerminal.loadAddon).toHaveBeenCalledWith(mockFitAddon); expect(mockTerminal.loadAddon).toHaveBeenCalledWith(mockSearchAddon); + expect(mockTerminal.loadAddon).toHaveBeenCalledWith(mockSerializeAddon); + expect(mockTerminal.loadAddon).toHaveBeenCalledWith(mockUnicode11Addon); + }); + + it('activates unicode 11', async () => { + render(); + await waitForInit(); + expect(mockTerminal.unicode.activeVersion).toBe('11'); }); it('loads web links addon by default', async () => { @@ -153,7 +218,6 @@ describe('Terminal', () => { const onData = vi.fn(); render(); await waitForInit(); - // Simulate terminal data input expect(mockTerminal.onData).toHaveBeenCalled(); mockOnData('test-input'); expect(onData).toHaveBeenCalledWith('test-input'); @@ -177,16 +241,111 @@ describe('Terminal', () => { expect(onResize).toHaveBeenCalledWith(120, 40); }); + it('calls onBell through stable ref', async () => { + const onBell = vi.fn(); + render(); + await waitForInit(); + expect(mockTerminal.onBell).toHaveBeenCalled(); + mockOnBell(); + expect(onBell).toHaveBeenCalled(); + }); + + it('calls onTitleChange through stable ref', async () => { + const onTitleChange = vi.fn(); + render(); + await waitForInit(); + expect(mockTerminal.onTitleChange).toHaveBeenCalled(); + mockOnTitleChange('new-title'); + expect(onTitleChange).toHaveBeenCalledWith('new-title'); + }); + + it('calls onKey through stable ref', async () => { + const onKey = vi.fn(); + render(); + await waitForInit(); + expect(mockTerminal.onKey).toHaveBeenCalled(); + const event = { key: 'a', domEvent: new KeyboardEvent('keydown') }; + mockOnKey(event); + expect(onKey).toHaveBeenCalledWith(event); + }); + + it('calls onSelectionChange through stable ref', async () => { + const onSelectionChange = vi.fn(); + render(); + await waitForInit(); + expect(mockTerminal.onSelectionChange).toHaveBeenCalled(); + mockOnSelectionChange(); + expect(onSelectionChange).toHaveBeenCalled(); + }); + + it('calls onLineFeed through stable ref', async () => { + const onLineFeed = vi.fn(); + render(); + await waitForInit(); + expect(mockTerminal.onLineFeed).toHaveBeenCalled(); + mockOnLineFeed(); + expect(onLineFeed).toHaveBeenCalled(); + }); + + it('calls onScroll through stable ref', async () => { + const onScroll = vi.fn(); + render(); + await waitForInit(); + expect(mockTerminal.onScroll).toHaveBeenCalled(); + mockOnScroll(42); + expect(onScroll).toHaveBeenCalledWith(42); + }); + it('handles WebGL context loss gracefully', async () => { render(); await waitForInit(); expect(mockWebglAddon.onContextLoss).toHaveBeenCalled(); - // Simulate context loss const contextLossHandler = mockWebglAddon.onContextLoss.mock.calls[0]?.[0] as () => void; contextLossHandler(); expect(mockWebglAddon.dispose).toHaveBeenCalled(); }); + it('passes disableStdin to xterm constructor', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ disableStdin: true }), + ); + }); + + it('passes cursorStyle to xterm constructor', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ cursorStyle: 'underline' }), + ); + }); + + it('attaches customKeyEventHandler', async () => { + const handler = vi.fn(() => true); + render(); + await waitForInit(); + expect(mockTerminal.attachCustomKeyEventHandler).toHaveBeenCalledWith(handler); + }); + + describe('renderer prop', () => { + it('loads WebGL by default (auto)', async () => { + render(); + await waitForInit(); + expect(mockTerminal.loadAddon).toHaveBeenCalledWith(mockWebglAddon); + }); + + it('renderer="dom" skips WebGL', async () => { + const { WebglAddon } = await import('@xterm/addon-webgl'); + (WebglAddon as unknown as ReturnType).mockClear(); + render(); + await waitForInit(); + expect(WebglAddon).not.toHaveBeenCalled(); + }); + }); + describe('imperative handle', () => { it('write accepts string data', async () => { const ref = createRef(); @@ -261,6 +420,102 @@ describe('Terminal', () => { expect(mockTerminal.scrollToBottom).toHaveBeenCalled(); }); + it('findNext proxies to SearchAddon', async () => { + const ref = createRef(); + render(); + await waitForInit(); + const result = ref.current?.findNext('test', { caseSensitive: true }); + expect(mockSearchAddon.findNext).toHaveBeenCalledWith('test', { caseSensitive: true }); + expect(result).toBe(true); + }); + + it('findPrevious proxies to SearchAddon', async () => { + const ref = createRef(); + render(); + await waitForInit(); + const result = ref.current?.findPrevious('test'); + expect(mockSearchAddon.findPrevious).toHaveBeenCalledWith('test', undefined); + expect(result).toBe(true); + }); + + it('clearSearch proxies to SearchAddon.clearDecorations', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.clearSearch(); + expect(mockSearchAddon.clearDecorations).toHaveBeenCalled(); + }); + + it('getSelection returns selected text', async () => { + const ref = createRef(); + render(); + await waitForInit(); + expect(ref.current?.getSelection()).toBe('selected-text'); + }); + + it('hasSelection returns true when selection exists', async () => { + const ref = createRef(); + render(); + await waitForInit(); + expect(ref.current?.hasSelection()).toBe(true); + }); + + it('selectAll selects all content', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.selectAll(); + expect(mockTerminal.selectAll).toHaveBeenCalled(); + }); + + it('clearSelection clears selection', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.clearSelection(); + expect(mockTerminal.clearSelection).toHaveBeenCalled(); + }); + + it('scrollToLine scrolls to specific line', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.scrollToLine(50); + expect(mockTerminal.scrollToLine).toHaveBeenCalledWith(50); + }); + + it('scrollUp scrolls up by N lines', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.scrollUp(5); + expect(mockTerminal.scrollLines).toHaveBeenCalledWith(-5); + }); + + it('scrollDown scrolls down by N lines', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.scrollDown(3); + expect(mockTerminal.scrollLines).toHaveBeenCalledWith(3); + }); + + it('paste pastes data into terminal', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.paste('pasted text'); + expect(mockTerminal.paste).toHaveBeenCalledWith('pasted text'); + }); + + it('getBufferContent returns serialized buffer', async () => { + const ref = createRef(); + render(); + await waitForInit(); + expect(ref.current?.getBufferContent()).toBe('buffer-content'); + expect(mockSerializeAddon.serialize).toHaveBeenCalled(); + }); + it('handles methods called before init gracefully', () => { const ref = createRef(); render(); @@ -268,7 +523,20 @@ describe('Terminal', () => { expect(() => ref.current?.write('test')).not.toThrow(); expect(() => ref.current?.clear()).not.toThrow(); expect(() => ref.current?.focus()).not.toThrow(); + expect(() => ref.current?.blur()).not.toThrow(); expect(ref.current?.getDimensions()).toBeNull(); + expect(ref.current?.findNext('test')).toBe(false); + expect(ref.current?.findPrevious('test')).toBe(false); + expect(() => ref.current?.clearSearch()).not.toThrow(); + expect(ref.current?.getSelection()).toBe(''); + expect(ref.current?.hasSelection()).toBe(false); + expect(() => ref.current?.selectAll()).not.toThrow(); + expect(() => ref.current?.clearSelection()).not.toThrow(); + expect(() => ref.current?.scrollToLine(0)).not.toThrow(); + expect(() => ref.current?.scrollUp(1)).not.toThrow(); + expect(() => ref.current?.scrollDown(1)).not.toThrow(); + expect(() => ref.current?.paste('text')).not.toThrow(); + expect(ref.current?.getBufferContent()).toBe(''); }); }); @@ -334,4 +602,335 @@ describe('Terminal', () => { }), ); }); + + it('calls onReady with dimensions after init', async () => { + const onReady = vi.fn(); + render(); + await waitForInit(); + expect(onReady).toHaveBeenCalledWith({ cols: 80, rows: 24 }); + }); + + it('auto-focuses when autoFocus is true', async () => { + mockTerminal.focus.mockClear(); + render(); + await waitForInit(); + expect(mockTerminal.focus).toHaveBeenCalled(); + }); + + it('does not auto-focus by default', async () => { + mockTerminal.focus.mockClear(); + render(); + await waitForInit(); + expect(mockTerminal.focus).not.toHaveBeenCalled(); + }); + + it('passes fontWeight to xterm constructor', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ fontWeight: 'normal' }), + ); + }); + + it('passes fontWeightBold to xterm constructor', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ fontWeightBold: 'bold' }), + ); + }); + + it('calls onWriteParsed through stable ref', async () => { + const onWriteParsed = vi.fn(); + render(); + await waitForInit(); + expect(mockTerminal.onWriteParsed).toHaveBeenCalled(); + mockOnWriteParsed(); + expect(onWriteParsed).toHaveBeenCalled(); + }); + + it('blur handle method blurs the terminal', async () => { + const ref = createRef(); + render(); + await waitForInit(); + ref.current?.blur(); + expect(mockTerminal.blur).toHaveBeenCalled(); + }); + + describe('dynamic option updates', () => { + it('updates fontSize dynamically and triggers fit', async () => { + vi.useFakeTimers(); + const { rerender } = render(); + vi.useRealTimers(); + await waitForInit(); + vi.useFakeTimers(); + + mockTerminal.options = {} as Record; + mockFitAddon.fit.mockClear(); + + rerender(); + + expect(mockTerminal.options.fontSize).toBe(16); + // Fit is debounced — advance timer + vi.advanceTimersByTime(15); + expect(mockFitAddon.fit).toHaveBeenCalled(); + vi.useRealTimers(); + }); + + it('updates cursorBlink dynamically', async () => { + const { rerender } = render(); + await waitForInit(); + mockTerminal.options = {} as Record; + + rerender(); + expect(mockTerminal.options.cursorBlink).toBe(false); + }); + + it('updates cursorStyle dynamically', async () => { + const { rerender } = render(); + await waitForInit(); + mockTerminal.options = {} as Record; + + rerender(); + expect(mockTerminal.options.cursorStyle).toBe('bar'); + }); + + it('updates scrollback dynamically', async () => { + const { rerender } = render(); + await waitForInit(); + mockTerminal.options = {} as Record; + + rerender(); + expect(mockTerminal.options.scrollback).toBe(10000); + }); + + it('updates lineHeight dynamically and triggers fit', async () => { + vi.useFakeTimers(); + const { rerender } = render(); + vi.useRealTimers(); + await waitForInit(); + vi.useFakeTimers(); + + mockTerminal.options = {} as Record; + mockFitAddon.fit.mockClear(); + + rerender(); + expect(mockTerminal.options.lineHeight).toBe(1.5); + vi.advanceTimersByTime(15); + expect(mockFitAddon.fit).toHaveBeenCalled(); + vi.useRealTimers(); + }); + + it('updates letterSpacing dynamically and triggers fit', async () => { + vi.useFakeTimers(); + const { rerender } = render(); + vi.useRealTimers(); + await waitForInit(); + vi.useFakeTimers(); + + mockTerminal.options = {} as Record; + mockFitAddon.fit.mockClear(); + + rerender(); + expect(mockTerminal.options.letterSpacing).toBe(2); + vi.advanceTimersByTime(15); + expect(mockFitAddon.fit).toHaveBeenCalled(); + vi.useRealTimers(); + }); + }); + + describe('constructor options passthrough', () => { + it('passes lineHeight to constructor', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ lineHeight: 1.2 }), + ); + }); + + it('passes letterSpacing to constructor', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ letterSpacing: 1 }), + ); + }); + + it('passes rows and cols to constructor', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ rows: 30, cols: 120 }), + ); + }); + + it('passes screenReaderMode to constructor', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ screenReaderMode: true }), + ); + }); + + it('passes minimumContrastRatio to constructor', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ minimumContrastRatio: 4.5 }), + ); + }); + + it('passes drawBoldTextInBrightColors to constructor', async () => { + const { Terminal: MockTerminal } = await import('@xterm/xterm'); + render(); + await waitForInit(); + expect(MockTerminal).toHaveBeenCalledWith( + expect.objectContaining({ drawBoldTextInBrightColors: false }), + ); + }); + }); + + describe('renderer prop (extended)', () => { + it('renderer="webgl" explicitly loads WebGL', async () => { + render(); + await waitForInit(); + expect(mockTerminal.loadAddon).toHaveBeenCalledWith(mockWebglAddon); + }); + }); + + describe('cleanup and lifecycle', () => { + it('cancels debounced fit on unmount', async () => { + vi.useFakeTimers(); + const { unmount } = render(); + vi.useRealTimers(); + await waitForInit(); + vi.useFakeTimers(); + + mockFitAddon.fit.mockClear(); + + // Trigger a resize but unmount before debounce fires + resizeCallback?.(); + unmount(); + + // Advance timer — fit should NOT fire because debounce was cancelled + vi.advanceTimersByTime(50); + expect(mockFitAddon.fit).not.toHaveBeenCalled(); + vi.useRealTimers(); + }); + + it('disposes addons before terminal', async () => { + const disposeOrder: string[] = []; + mockFitAddon.dispose.mockImplementation(() => disposeOrder.push('fitAddon')); + mockSearchAddon.dispose.mockImplementation(() => disposeOrder.push('searchAddon')); + mockSerializeAddon.dispose.mockImplementation(() => disposeOrder.push('serializeAddon')); + mockUnicode11Addon.dispose.mockImplementation(() => disposeOrder.push('unicode11Addon')); + mockWebLinksAddon.dispose.mockImplementation(() => disposeOrder.push('webLinksAddon')); + mockWebglAddon.dispose.mockImplementation(() => disposeOrder.push('webglAddon')); + mockTerminal.dispose.mockImplementation(() => disposeOrder.push('terminal')); + + const { unmount } = render(); + await waitForInit(); + unmount(); + + // Terminal should be last + expect(disposeOrder[disposeOrder.length - 1]).toBe('terminal'); + // All addons should come before terminal + expect(disposeOrder.indexOf('terminal')).toBeGreaterThan(disposeOrder.indexOf('fitAddon')); + expect(disposeOrder.indexOf('terminal')).toBeGreaterThan(disposeOrder.indexOf('searchAddon')); + }); + + it('disconnects ResizeObserver on unmount', async () => { + const { unmount } = render(); + await waitForInit(); + unmount(); + expect(mockDisconnect).toHaveBeenCalledTimes(1); + }); + + it('does not call onReady if unmounted during init', async () => { + const onReady = vi.fn(); + const { unmount } = render(); + // Unmount immediately before init completes + unmount(); + await waitForInit(); + expect(onReady).not.toHaveBeenCalled(); + }); + + it('calls onError when init fails', async () => { + // Make the xterm import fail + const { Terminal: OrigTerminal } = await import('@xterm/xterm'); + (OrigTerminal as unknown as ReturnType).mockImplementationOnce(() => { + throw new Error('WebGL context unavailable'); + }); + + const onError = vi.fn(); + render(); + await waitForInit(); + expect(onError).toHaveBeenCalledWith(expect.any(Error)); + }); + }); + + describe('resize handling', () => { + it('handles window resize events', async () => { + render(); + await waitForInit(); + mockFitAddon.fit.mockClear(); + + // Simulate window resize + vi.useFakeTimers(); + window.dispatchEvent(new Event('resize')); + vi.advanceTimersByTime(15); + expect(mockFitAddon.fit).toHaveBeenCalled(); + vi.useRealTimers(); + }); + + it('fit handle method does not throw when addon is not loaded', () => { + const ref = createRef(); + render(); + // fit before init — should not throw + expect(() => ref.current?.fit()).not.toThrow(); + }); + + it('debounce coalesces rapid resize events into one fit call', async () => { + vi.useFakeTimers(); + render(); + vi.useRealTimers(); + await waitForInit(); + vi.useFakeTimers(); + + mockFitAddon.fit.mockClear(); + + // Fire many rapid resizes + for (let i = 0; i < 20; i++) { + resizeCallback?.(); + } + + vi.advanceTimersByTime(15); + // Should coalesce to exactly 1 call + expect(mockFitAddon.fit).toHaveBeenCalledTimes(1); + vi.useRealTimers(); + }); + }); + + describe('onReady ordering', () => { + it('onReady fires after fitAddon.fit()', async () => { + const callOrder: string[] = []; + mockFitAddon.fit.mockImplementation(() => callOrder.push('fit')); + const onReady = vi.fn(() => callOrder.push('onReady')); + + render(); + await waitForInit(); + + const fitIndex = callOrder.indexOf('fit'); + const readyIndex = callOrder.indexOf('onReady'); + expect(fitIndex).toBeGreaterThanOrEqual(0); + expect(readyIndex).toBeGreaterThan(fitIndex); + }); + }); }); diff --git a/packages/editors/src/components/terminal/Terminal.tsx b/packages/editors/src/components/terminal/Terminal.tsx index ee5f633..cc5fa1d 100644 --- a/packages/editors/src/components/terminal/Terminal.tsx +++ b/packages/editors/src/components/terminal/Terminal.tsx @@ -1,9 +1,14 @@ +import '@xterm/xterm/css/xterm.css'; + import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react'; import { useEditorTheme } from '../../themes/useEditorTheme'; import { buildXtermTheme } from '../../themes/xterm'; import styles from './Terminal.module.css'; -/** Unix signal types that exec plugins may emit. */ +/** + * Signal types matching the Omniview plugin SDK's StreamSignal enum. + * These map 1:1 to `core/exec/signal/{SIGNAL}/{sessionId}` Wails events. + */ export type TerminalSignal = | 'ERROR' | 'CLOSE' @@ -16,28 +21,105 @@ export type TerminalSignal = | 'SIGUSR2' | 'SIGWINCH'; +/** + * Structured error payload from the plugin SDK's StreamError. + * Received with the 'ERROR' signal when a session encounters an error. + */ +export interface TerminalErrorInfo { + /** Short error title for display. */ + title: string; + /** Suggested resolution or next step. */ + suggestion: string; + /** Raw error message for debugging. */ + raw: string; + /** Whether the session can be retried. */ + retryable?: boolean; + /** Alternative commands to try (e.g., different shells). */ + retryCommands?: string[]; +} + +/** Options for terminal search operations. */ +export interface TerminalSearchOptions { + /** Whether the search should be case-sensitive. */ + caseSensitive?: boolean; + /** Whether the search should match whole words only. */ + wholeWord?: boolean; + /** Whether the search term is a regex. */ + regex?: boolean; + /** Whether to search incrementally (highlight as you type). */ + incremental?: boolean; +} + export interface TerminalProps { - /** Called when user types input. */ + // ── Session / data callbacks ────────────────────────────────────────── + + /** Called when user types input. Wire to `ExecClient.WriteSession(sessionId, data)`. */ onData?: (data: string) => void; /** Called when user pastes binary data. */ onBinaryData?: (data: string) => void; - /** Called on terminal resize with new dimensions. */ + /** Called on terminal resize with new dimensions. Wire to `ExecClient.ResizeSession(sessionId, rows, cols)`. */ onResize?: (cols: number, rows: number) => void; /** Called when a signal is received (for exec plugin integration). */ onSignal?: (signal: TerminalSignal, payload?: unknown) => void; - /** Called when the terminal session encounters an error. */ + /** Called when the terminal init encounters an error (e.g., failed to load xterm). */ onError?: (error: Error) => void; /** Called when the terminal session closes. */ onClose?: (code?: number) => void; + /** + * Called once when the terminal is fully initialized and ready to accept writes. + * This is the right time to attach to a session and start writing data. + * Receives the current dimensions so the consumer can send an initial resize. + */ + onReady?: (dimensions: { cols: number; rows: number }) => void; + + // ── xterm event callbacks ───────────────────────────────────────────── + + /** Called when the terminal bell is triggered. */ + onBell?: () => void; + /** Called when the terminal title changes (via escape sequence). */ + onTitleChange?: (title: string) => void; + /** Called on each keypress with the key and DOM event. */ + onKey?: (event: { key: string; domEvent: KeyboardEvent }) => void; + /** Called when the text selection changes. */ + onSelectionChange?: () => void; + /** Called on each line feed. */ + onLineFeed?: () => void; + /** Called when the viewport scrolls. */ + onScroll?: (newPosition: number) => void; + /** + * Called after write data has been parsed (at most once per animation frame). + * Useful as a "render complete" signal for high-throughput output. + */ + onWriteParsed?: () => void; + + // ── Appearance ──────────────────────────────────────────────────────── + /** Font size in pixels. */ fontSize?: number; /** Font family override. */ fontFamily?: string; - /** Maximum scrollback buffer lines. 0 = unlimited. */ - scrollback?: number; + /** Font weight ('normal', 'bold', '100'-'900'). */ + fontWeight?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; + /** Font weight for bold text. */ + fontWeightBold?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; /** Enable cursor blink. */ cursorBlink?: boolean; - /** Convert \\n to \\r\\n for proper line endings. */ + /** Cursor style. */ + cursorStyle?: 'block' | 'underline' | 'bar'; + /** Width of the cursor in CSS pixels (only applies to bar cursor). */ + cursorWidth?: number; + /** Line height multiplier. */ + lineHeight?: number; + /** Letter spacing in pixels. */ + letterSpacing?: number; + /** Whether to draw bold text in bright colors. */ + drawBoldTextInBrightColors?: boolean; + + // ── Behavior ────────────────────────────────────────────────────────── + + /** Maximum scrollback buffer lines. 0 = unlimited. */ + scrollback?: number; + /** Convert \n to \r\n for proper line endings. */ convertEol?: boolean; /** Allow transparent background. */ allowTransparency?: boolean; @@ -47,27 +129,98 @@ export interface TerminalProps { macOptionClickForcesSelection?: boolean; /** Enable clickable URLs in terminal output. */ linkHandling?: boolean; + /** Disable user input (read-only / log-viewer mode). */ + disableStdin?: boolean; + /** Intercept key events before xterm processes them. Return false to prevent default handling. */ + customKeyEventHandler?: (event: KeyboardEvent) => boolean; + /** Auto-focus the terminal after initialization. */ + autoFocus?: boolean; + + // ── Rendering ───────────────────────────────────────────────────────── + + /** Renderer to use. 'auto' tries WebGL then falls back to DOM. */ + renderer?: 'auto' | 'webgl' | 'dom'; + /** Initial number of rows before fit. */ + rows?: number; + /** Initial number of columns before fit. */ + cols?: number; + + // ── Accessibility ───────────────────────────────────────────────────── + + /** Enable screen reader mode for accessibility. */ + screenReaderMode?: boolean; + /** Minimum contrast ratio for text (1-21). */ + minimumContrastRatio?: number; + + // ── Layout ──────────────────────────────────────────────────────────── + /** Custom CSS class. */ className?: string; } export interface TerminalHandle { - /** Write a string to the terminal. */ + // ── Write ───────────────────────────────────────────────────────────── + + /** Write a string or Uint8Array to the terminal. */ write: (data: string | Uint8Array) => void; /** Write a string followed by a newline. */ writeln: (data: string) => void; + + // ── Terminal state ──────────────────────────────────────────────────── + /** Clear the terminal viewport and scrollback. */ clear: () => void; /** Focus the terminal. */ focus: () => void; + /** Blur (unfocus) the terminal. */ + blur: () => void; /** Re-fit the terminal to its container. */ fit: () => void; - /** Get the current terminal dimensions. */ + /** Get the current terminal dimensions, or null if not initialized. */ getDimensions: () => { cols: number; rows: number } | null; /** Reset the terminal (clear + reset state). */ reset: () => void; + + // ── Search ──────────────────────────────────────────────────────────── + + /** Find the next match for a search term. */ + findNext: (term: string, options?: TerminalSearchOptions) => boolean; + /** Find the previous match for a search term. */ + findPrevious: (term: string, options?: TerminalSearchOptions) => boolean; + /** Clear search highlights. */ + clearSearch: () => void; + + // ── Selection ───────────────────────────────────────────────────────── + + /** Get the current text selection. */ + getSelection: () => string; + /** Whether text is currently selected. */ + hasSelection: () => boolean; + /** Select all terminal content. */ + selectAll: () => void; + /** Clear the current selection. */ + clearSelection: () => void; + + // ── Scrolling ───────────────────────────────────────────────────────── + /** Scroll to the bottom of the terminal. */ scrollToBottom: () => void; + /** Scroll to a specific line. */ + scrollToLine: (line: number) => void; + /** Scroll up by N lines. */ + scrollUp: (lines: number) => void; + /** Scroll down by N lines. */ + scrollDown: (lines: number) => void; + + // ── Clipboard ───────────────────────────────────────────────────────── + + /** Paste data into the terminal. */ + paste: (data: string) => void; + + // ── Buffer ──────────────────────────────────────────────────────────── + + /** Get the full buffer content as a string (via SerializeAddon). */ + getBufferContent: () => string; } function cn(...parts: Array): string { @@ -100,6 +253,29 @@ function debounce void>( }; } +/** + * Production-grade terminal component built on xterm.js 6. + * + * Designed for the Omniview IDE bottom-drawer terminal, but usable anywhere. + * Wire `onData` → `ExecClient.WriteSession` and `onResize` → `ExecClient.ResizeSession`, + * then write session stdout/stderr to the handle's `write()` method. + * + * @example + * ```tsx + * const ref = useRef(null); + * + * { + * attachSession(sessionId); + * resizeSession(sessionId, rows, cols); + * }} + * onData={(data) => writeSession(sessionId, data)} + * onResize={(cols, rows) => resizeSession(sessionId, rows, cols)} + * autoFocus + * /> + * ``` + */ export const Terminal = forwardRef(function Terminal( { onData, @@ -108,15 +284,38 @@ export const Terminal = forwardRef(function Termi onSignal, onError, onClose, + onReady, + onBell, + onTitleChange, + onKey, + onSelectionChange, + onLineFeed, + onScroll, + onWriteParsed, fontSize = 13, fontFamily, + fontWeight, + fontWeightBold, scrollback = 5000, cursorBlink = true, + cursorStyle, + cursorWidth, + lineHeight, + letterSpacing, + drawBoldTextInBrightColors = true, convertEol = true, allowTransparency = true, macOptionIsMeta = true, macOptionClickForcesSelection = true, linkHandling = true, + disableStdin, + customKeyEventHandler, + autoFocus = false, + renderer = 'auto', + rows: initialRows, + cols: initialCols, + screenReaderMode, + minimumContrastRatio, className, }, ref, @@ -124,14 +323,22 @@ export const Terminal = forwardRef(function Termi const containerRef = useRef(null); const termRef = useRef(null); const fitAddonRef = useRef(null); + const searchAddonRef = useRef(null); + const serializeAddonRef = useRef(null); const addonsRef = useRef void }>>([]); const observerRef = useRef(null); const debouncedFitRef = useRef<{ run: () => void; cancel: () => void } | null>(null); const theme = useEditorTheme(); // Stable refs for callbacks to avoid re-initializing terminal - const callbacksRef = useRef({ onData, onBinaryData, onResize, onSignal, onError, onClose }); - callbacksRef.current = { onData, onBinaryData, onResize, onSignal, onError, onClose }; + const callbacksRef = useRef({ + onData, onBinaryData, onResize, onSignal, onError, onClose, onReady, + onBell, onTitleChange, onKey, onSelectionChange, onLineFeed, onScroll, onWriteParsed, + }); + callbacksRef.current = { + onData, onBinaryData, onResize, onSignal, onError, onClose, onReady, + onBell, onTitleChange, onKey, onSelectionChange, onLineFeed, onScroll, onWriteParsed, + }; const fit = useCallback(() => { try { @@ -159,6 +366,10 @@ export const Terminal = forwardRef(function Termi const t = termRef.current as { focus?: () => void } | null; t?.focus?.(); }, + blur: () => { + const t = termRef.current as { blur?: () => void } | null; + t?.blur?.(); + }, fit, getDimensions: () => { const t = termRef.current as { cols?: number; rows?: number } | null; @@ -175,6 +386,54 @@ export const Terminal = forwardRef(function Termi const t = termRef.current as { scrollToBottom?: () => void } | null; t?.scrollToBottom?.(); }, + findNext: (term: string, options?: TerminalSearchOptions): boolean => { + const s = searchAddonRef.current as { findNext?: (t: string, o?: TerminalSearchOptions) => boolean } | null; + return s?.findNext?.(term, options) ?? false; + }, + findPrevious: (term: string, options?: TerminalSearchOptions): boolean => { + const s = searchAddonRef.current as { findPrevious?: (t: string, o?: TerminalSearchOptions) => boolean } | null; + return s?.findPrevious?.(term, options) ?? false; + }, + clearSearch: () => { + const s = searchAddonRef.current as { clearDecorations?: () => void } | null; + s?.clearDecorations?.(); + }, + getSelection: (): string => { + const t = termRef.current as { getSelection?: () => string } | null; + return t?.getSelection?.() ?? ''; + }, + hasSelection: (): boolean => { + const t = termRef.current as { hasSelection?: () => boolean } | null; + return t?.hasSelection?.() ?? false; + }, + selectAll: () => { + const t = termRef.current as { selectAll?: () => void } | null; + t?.selectAll?.(); + }, + clearSelection: () => { + const t = termRef.current as { clearSelection?: () => void } | null; + t?.clearSelection?.(); + }, + scrollToLine: (line: number) => { + const t = termRef.current as { scrollToLine?: (l: number) => void } | null; + t?.scrollToLine?.(line); + }, + scrollUp: (lines: number) => { + const t = termRef.current as { scrollLines?: (l: number) => void } | null; + t?.scrollLines?.(-lines); + }, + scrollDown: (lines: number) => { + const t = termRef.current as { scrollLines?: (l: number) => void } | null; + t?.scrollLines?.(lines); + }, + paste: (data: string) => { + const t = termRef.current as { paste?: (d: string) => void } | null; + t?.paste?.(data); + }, + getBufferContent: (): string => { + const s = serializeAddonRef.current as { serialize?: () => string } | null; + return s?.serialize?.() ?? ''; + }, })); // Initialize xterm lazily @@ -185,18 +444,24 @@ export const Terminal = forwardRef(function Termi let disposed = false; async function init() { - const [{ Terminal: XTerminal }, { FitAddon }, { SearchAddon }] = await Promise.all([ - import('@xterm/xterm'), - import('@xterm/addon-fit'), - import('@xterm/addon-search'), - ]); + // Wait for fonts to be ready to prevent wrong char width measurements + await document.fonts.ready; + + const [{ Terminal: XTerminal }, { FitAddon }, { SearchAddon }, { SerializeAddon }, { Unicode11Addon }] = + await Promise.all([ + import('@xterm/xterm'), + import('@xterm/addon-fit'), + import('@xterm/addon-search'), + import('@xterm/addon-serialize'), + import('@xterm/addon-unicode11'), + ]); if (disposed) return; const xtermTheme = buildXtermTheme(); - const term = new XTerminal({ + const termOptions: Record = { fontSize, - fontFamily: fontFamily || 'var(--ov-font-family-mono, monospace)', + fontFamily: fontFamily || "'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace", theme: xtermTheme, allowTransparency, cursorBlink, @@ -205,16 +470,42 @@ export const Terminal = forwardRef(function Termi macOptionIsMeta, macOptionClickForcesSelection, allowProposedApi: true, - drawBoldTextInBrightColors: true, - }); + drawBoldTextInBrightColors, + }; + + if (fontWeight !== undefined) termOptions.fontWeight = fontWeight; + if (fontWeightBold !== undefined) termOptions.fontWeightBold = fontWeightBold; + if (disableStdin !== undefined) termOptions.disableStdin = disableStdin; + if (cursorStyle !== undefined) termOptions.cursorStyle = cursorStyle; + if (cursorWidth !== undefined) termOptions.cursorWidth = cursorWidth; + if (lineHeight !== undefined) termOptions.lineHeight = lineHeight; + if (letterSpacing !== undefined) termOptions.letterSpacing = letterSpacing; + if (initialRows !== undefined) termOptions.rows = initialRows; + if (initialCols !== undefined) termOptions.cols = initialCols; + if (screenReaderMode !== undefined) termOptions.screenReaderMode = screenReaderMode; + if (minimumContrastRatio !== undefined) termOptions.minimumContrastRatio = minimumContrastRatio; + + const term = new XTerminal(termOptions); + + if (customKeyEventHandler) { + (term as unknown as { attachCustomKeyEventHandler: (h: (e: KeyboardEvent) => boolean) => void }) + .attachCustomKeyEventHandler(customKeyEventHandler); + } const loadedAddons: Array<{ dispose: () => void }> = []; const fitAddon = new FitAddon(); const searchAddon = new SearchAddon(); + const serializeAddon = new SerializeAddon(); + const unicode11Addon = new Unicode11Addon(); term.loadAddon(fitAddon); term.loadAddon(searchAddon); - loadedAddons.push(fitAddon, searchAddon); + term.loadAddon(serializeAddon); + term.loadAddon(unicode11Addon); + loadedAddons.push(fitAddon, searchAddon, serializeAddon, unicode11Addon); + + // Activate unicode11 for proper emoji/CJK width + (term as unknown as { unicode: { activeVersion: string } }).unicode.activeVersion = '11'; // Load web links addon for clickable URLs if (linkHandling) { @@ -238,36 +529,23 @@ export const Terminal = forwardRef(function Termi // Open terminal in container term.open(container!); - // Try WebGL renderer first, then Canvas fallback - let rendererLoaded = false; - try { - const { WebglAddon } = await import('@xterm/addon-webgl'); - if (!disposed) { - const webglAddon = new WebglAddon(); - // Handle WebGL context loss gracefully - webglAddon.onContextLoss(() => { - webglAddon.dispose(); - }); - term.loadAddon(webglAddon); - loadedAddons.push(webglAddon); - rendererLoaded = true; - } - } catch { - // WebGL not available - } - - if (!rendererLoaded && !disposed) { + // Load renderer based on prop + if (renderer === 'auto' || renderer === 'webgl') { try { - const { CanvasAddon } = await import('@xterm/addon-canvas'); + const { WebglAddon } = await import('@xterm/addon-webgl'); if (!disposed) { - const canvasAddon = new CanvasAddon(); - term.loadAddon(canvasAddon); - loadedAddons.push(canvasAddon); + const webglAddon = new WebglAddon(); + webglAddon.onContextLoss(() => { + webglAddon.dispose(); + }); + term.loadAddon(webglAddon); + loadedAddons.push(webglAddon); } } catch { - // Canvas addon not available, DOM renderer is used as final fallback + // WebGL not available, DOM renderer is used as fallback } } + // renderer === 'dom' — no extra addon needed if (disposed) { for (const addon of loadedAddons) { @@ -287,6 +565,8 @@ export const Terminal = forwardRef(function Termi // Store refs termRef.current = term; fitAddonRef.current = fitAddon; + searchAddonRef.current = searchAddon; + serializeAddonRef.current = serializeAddon; addonsRef.current = loadedAddons; // Wire up callbacks via stable refs @@ -302,6 +582,38 @@ export const Terminal = forwardRef(function Termi callbacksRef.current.onResize?.(cols, rows); }); + (term as unknown as { onBell: (cb: () => void) => void }).onBell(() => { + callbacksRef.current.onBell?.(); + }); + + (term as unknown as { onTitleChange: (cb: (t: string) => void) => void }).onTitleChange( + (title: string) => { + callbacksRef.current.onTitleChange?.(title); + }, + ); + + (term as unknown as { onKey: (cb: (e: { key: string; domEvent: KeyboardEvent }) => void) => void }).onKey( + (event: { key: string; domEvent: KeyboardEvent }) => { + callbacksRef.current.onKey?.(event); + }, + ); + + (term as unknown as { onSelectionChange: (cb: () => void) => void }).onSelectionChange(() => { + callbacksRef.current.onSelectionChange?.(); + }); + + (term as unknown as { onLineFeed: (cb: () => void) => void }).onLineFeed(() => { + callbacksRef.current.onLineFeed?.(); + }); + + (term as unknown as { onScroll: (cb: (p: number) => void) => void }).onScroll((pos: number) => { + callbacksRef.current.onScroll?.(pos); + }); + + (term as unknown as { onWriteParsed: (cb: () => void) => void }).onWriteParsed(() => { + callbacksRef.current.onWriteParsed?.(); + }); + // Debounced fit for resize handling (10ms debounce like legacy impl) const debouncedFit = debounce(() => { try { @@ -330,6 +642,14 @@ export const Terminal = forwardRef(function Termi () => { window.removeEventListener('resize', handleWindowResize); }; + + // Auto-focus if requested + if (autoFocus) { + term.focus(); + } + + // Notify consumer that the terminal is ready + callbacksRef.current.onReady?.({ cols: term.cols, rows: term.rows }); } init().catch((err) => { @@ -372,6 +692,8 @@ export const Terminal = forwardRef(function Termi termRef.current = null; fitAddonRef.current = null; + searchAddonRef.current = null; + serializeAddonRef.current = null; }; }, []); // eslint-disable-line react-hooks/exhaustive-deps @@ -388,11 +710,47 @@ export const Terminal = forwardRef(function Termi const term = termRef.current as { options?: { fontSize: number } } | null; if (term?.options) { term.options.fontSize = fontSize; - // Re-fit after font size change debouncedFitRef.current?.run(); } }, [fontSize]); + // Update dynamically updatable options + useEffect(() => { + const term = termRef.current as { options?: Record } | null; + if (!term?.options) return; + if (cursorBlink !== undefined) term.options.cursorBlink = cursorBlink; + }, [cursorBlink]); + + useEffect(() => { + const term = termRef.current as { options?: Record } | null; + if (!term?.options) return; + if (cursorStyle !== undefined) term.options.cursorStyle = cursorStyle; + }, [cursorStyle]); + + useEffect(() => { + const term = termRef.current as { options?: Record } | null; + if (!term?.options) return; + if (scrollback !== undefined) term.options.scrollback = scrollback; + }, [scrollback]); + + useEffect(() => { + const term = termRef.current as { options?: Record } | null; + if (!term?.options) return; + if (lineHeight !== undefined) { + term.options.lineHeight = lineHeight; + debouncedFitRef.current?.run(); + } + }, [lineHeight]); + + useEffect(() => { + const term = termRef.current as { options?: Record } | null; + if (!term?.options) return; + if (letterSpacing !== undefined) { + term.options.letterSpacing = letterSpacing; + debouncedFitRef.current?.run(); + } + }, [letterSpacing]); + return
; }); diff --git a/packages/editors/src/components/terminal/index.ts b/packages/editors/src/components/terminal/index.ts index f6059c7..e89e652 100644 --- a/packages/editors/src/components/terminal/index.ts +++ b/packages/editors/src/components/terminal/index.ts @@ -1,2 +1,8 @@ export { Terminal } from './Terminal'; -export type { TerminalProps, TerminalHandle, TerminalSignal } from './Terminal'; +export type { + TerminalProps, + TerminalHandle, + TerminalSignal, + TerminalSearchOptions, + TerminalErrorInfo, +} from './Terminal'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d232a73..9fe10e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,24 +132,27 @@ importers: '@omniview/base-ui': specifier: workspace:* version: link:../base-ui - '@xterm/addon-canvas': - specifier: ^0.7.0 - version: 0.7.0(@xterm/xterm@5.5.0) '@xterm/addon-fit': - specifier: ^0.10.0 - version: 0.10.0(@xterm/xterm@5.5.0) + specifier: ^0.11.0 + version: 0.11.0 '@xterm/addon-search': - specifier: ^0.15.0 - version: 0.15.0(@xterm/xterm@5.5.0) + specifier: ^0.16.0 + version: 0.16.0 + '@xterm/addon-serialize': + specifier: ^0.14.0 + version: 0.14.0 + '@xterm/addon-unicode11': + specifier: ^0.9.0 + version: 0.9.0 '@xterm/addon-web-links': - specifier: ^0.11.0 - version: 0.11.0(@xterm/xterm@5.5.0) + specifier: ^0.12.0 + version: 0.12.0 '@xterm/addon-webgl': - specifier: ^0.18.0 - version: 0.18.0(@xterm/xterm@5.5.0) + specifier: ^0.19.0 + version: 0.19.0 '@xterm/xterm': - specifier: ^5.5.0 - version: 5.5.0 + specifier: ^6.0.0 + version: 6.0.0 monaco-editor: specifier: ^0.52.2 version: 0.52.2 @@ -1303,33 +1306,26 @@ packages: '@vue/shared@3.5.29': resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} - '@xterm/addon-canvas@0.7.0': - resolution: {integrity: sha512-LF5LYcfvefJuJ7QotNRdRSPc9YASAVDeoT5uyXS/nZshZXjYplGXRECBGiznwvhNL2I8bq1Lf5MzRwstsYQ2Iw==} - peerDependencies: - '@xterm/xterm': ^5.0.0 + '@xterm/addon-fit@0.11.0': + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} - '@xterm/addon-fit@0.10.0': - resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} - peerDependencies: - '@xterm/xterm': ^5.0.0 + '@xterm/addon-search@0.16.0': + resolution: {integrity: sha512-9OeuBFu0/uZJPu+9AHKY6g/w0Czyb/Ut0A5t79I4ULoU4IfU5BEpPFVGQxP4zTTMdfZEYkVIRYbHBX1xWwjeSA==} - '@xterm/addon-search@0.15.0': - resolution: {integrity: sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg==} - peerDependencies: - '@xterm/xterm': ^5.0.0 + '@xterm/addon-serialize@0.14.0': + resolution: {integrity: sha512-uteyTU1EkrQa2Ux6P/uFl2fzmXI46jy5uoQMKEOM0fKTyiW7cSn0WrFenHm5vO5uEXX/GpwW/FgILvv3r0WbkA==} - '@xterm/addon-web-links@0.11.0': - resolution: {integrity: sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==} - peerDependencies: - '@xterm/xterm': ^5.0.0 + '@xterm/addon-unicode11@0.9.0': + resolution: {integrity: sha512-FxDnYcyuXhNl+XSqGZL/t0U9eiNb/q3EWT5rYkQT/zuig8Gz/VagnQANKHdDWFM2lTMk9ly0EFQxxxtZUoRetw==} - '@xterm/addon-webgl@0.18.0': - resolution: {integrity: sha512-xCnfMBTI+/HKPdRnSOHaJDRqEpq2Ugy8LEj9GiY4J3zJObo3joylIFaMvzBwbYRg8zLtkO0KQaStCeSfoaI2/w==} - peerDependencies: - '@xterm/xterm': ^5.0.0 + '@xterm/addon-web-links@0.12.0': + resolution: {integrity: sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==} + + '@xterm/addon-webgl@0.19.0': + resolution: {integrity: sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==} - '@xterm/xterm@5.5.0': - resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} + '@xterm/xterm@6.0.0': + resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -4356,27 +4352,19 @@ snapshots: '@vue/shared@3.5.29': {} - '@xterm/addon-canvas@0.7.0(@xterm/xterm@5.5.0)': - dependencies: - '@xterm/xterm': 5.5.0 + '@xterm/addon-fit@0.11.0': {} - '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': - dependencies: - '@xterm/xterm': 5.5.0 + '@xterm/addon-search@0.16.0': {} - '@xterm/addon-search@0.15.0(@xterm/xterm@5.5.0)': - dependencies: - '@xterm/xterm': 5.5.0 + '@xterm/addon-serialize@0.14.0': {} - '@xterm/addon-web-links@0.11.0(@xterm/xterm@5.5.0)': - dependencies: - '@xterm/xterm': 5.5.0 + '@xterm/addon-unicode11@0.9.0': {} - '@xterm/addon-webgl@0.18.0(@xterm/xterm@5.5.0)': - dependencies: - '@xterm/xterm': 5.5.0 + '@xterm/addon-web-links@0.12.0': {} + + '@xterm/addon-webgl@0.19.0': {} - '@xterm/xterm@5.5.0': {} + '@xterm/xterm@6.0.0': {} acorn-jsx@5.3.2(acorn@8.16.0): dependencies: From f880317eefc3bc8b0498111573bce7a00be933d6 Mon Sep 17 00:00:00 2001 From: Joshua Pare Date: Mon, 9 Mar 2026 18:48:45 -0500 Subject: [PATCH 07/11] fix(editors): Address PR review feedback across all components - Add theme token overrides for editor/terminal/syntax in base-ui styles - Extract shared ThemeMode type, add xterm theme fallbacks - Add idempotency guard and error surfacing in setupMonacoWorkers - Replace wildcard exports with explicit named exports - Improve accessibility, keyboard handling, and CSS variable usage - Fix DiffViewer language reset, CodeEditor model disposal - Add button type attributes, fix story code fence escaping - Harden ObjectInspector with keyboard nav and circular ref safety - Use fileURLToPath in vite config, surface schema registry errors --- packages/base-ui/src/theme/styles.css | 179 ++++++++++++++++++ packages/editors/docs/MONACO_YAML_COMPAT.md | 6 +- .../code-editor/CodeEditor.stories.tsx | 13 +- .../code-editor/CodeEditor.test.tsx | 15 +- .../src/components/code-editor/CodeEditor.tsx | 6 +- .../command-palette/CommandPalette.module.css | 2 +- .../CommandPalette.stories.tsx | 14 +- .../command-palette/CommandPalette.test.tsx | 8 +- .../command-palette/CommandPalette.tsx | 5 + .../diff-viewer/DiffViewer.test.tsx | 7 +- .../src/components/diff-viewer/DiffViewer.tsx | 7 +- packages/editors/src/components/index.ts | 36 +++- .../MarkdownPreview.stories.tsx | 2 +- .../markdown-preview/MarkdownPreview.tsx | 5 +- .../ObjectInspector.module.css | 8 +- .../object-inspector/ObjectInspector.tsx | 25 ++- .../components/terminal/Terminal.stories.tsx | 4 + .../src/components/terminal/Terminal.tsx | 22 ++- .../src/schemas/EditorSchemaRegistry.ts | 4 +- .../editors/src/setup/setupMonacoWorkers.ts | 16 +- packages/editors/src/themes/index.ts | 1 + packages/editors/src/themes/monaco.ts | 3 +- packages/editors/src/themes/types.ts | 1 + packages/editors/src/themes/useEditorTheme.ts | 3 +- packages/editors/src/themes/xterm.ts | 44 ++--- packages/editors/vite.config.ts | 3 +- 26 files changed, 358 insertions(+), 81 deletions(-) create mode 100644 packages/editors/src/themes/types.ts diff --git a/packages/base-ui/src/theme/styles.css b/packages/base-ui/src/theme/styles.css index 44b1ae6..e600edb 100644 --- a/packages/base-ui/src/theme/styles.css +++ b/packages/base-ui/src/theme/styles.css @@ -550,6 +550,71 @@ --ov-color-state-pressed: rgb(75 85 100 / 0.14); --ov-color-state-selected: rgb(71 109 206 / 0.14); --ov-color-state-focus-ring: #476dce; + + /* Editor tokens — light */ + --ov-color-editor-bg: var(--ov-color-bg-base); + --ov-color-editor-fg: var(--ov-color-fg-default); + --ov-color-editor-cursor: var(--ov-color-accent-strong); + --ov-color-editor-selection-bg: var(--ov-color-accent-soft); + --ov-color-editor-selection-inactive-bg: rgb(75 85 100 / 0.1); + --ov-color-editor-line-highlight-bg: rgb(0 0 0 / 0.04); + --ov-color-editor-line-highlight-border: transparent; + --ov-color-editor-line-number: var(--ov-color-fg-subtle); + --ov-color-editor-line-number-active: var(--ov-color-fg-muted); + --ov-color-editor-whitespace: #c8ced8; + --ov-color-editor-indent-guide: #d4d8de; + --ov-color-editor-indent-guide-active: #9ea6b3; + --ov-color-editor-ruler: #d4d8de; + --ov-color-editor-find-match-bg: rgb(148 98 33 / 0.3); + --ov-color-editor-find-match-border: var(--ov-color-warning); + --ov-color-editor-find-range-bg: rgb(148 98 33 / 0.12); + --ov-color-editor-link: var(--ov-color-brand-500); + + /* Terminal tokens — light */ + --ov-color-terminal-bg: var(--ov-color-bg-inset); + --ov-color-terminal-fg: var(--ov-color-fg-default); + --ov-color-terminal-cursor: var(--ov-color-accent-strong); + --ov-color-terminal-selection-bg: var(--ov-color-accent-soft); + --ov-color-terminal-selection-fg: var(--ov-color-fg-default); + --ov-color-terminal-border: var(--ov-color-border-default); + --ov-color-ansi-black: #3b4252; + --ov-color-ansi-red: #a3171a; + --ov-color-ansi-green: #2f7d4f; + --ov-color-ansi-yellow: #946221; + --ov-color-ansi-blue: #2560a8; + --ov-color-ansi-magenta: #7c3e8e; + --ov-color-ansi-cyan: #1a7d8d; + --ov-color-ansi-white: #4b5564; + --ov-color-ansi-bright-black: #6b7481; + --ov-color-ansi-bright-red: #c4554d; + --ov-color-ansi-bright-green: #2f9161; + --ov-color-ansi-bright-yellow: #b17727; + --ov-color-ansi-bright-blue: #476dce; + --ov-color-ansi-bright-magenta: #9363db; + --ov-color-ansi-bright-cyan: #2e97ae; + --ov-color-ansi-bright-white: #1b1d22; + + /* Syntax highlighting tokens — light */ + --ov-syntax-comment: #6a737d; + --ov-syntax-string: #032f62; + --ov-syntax-string-escape: #22863a; + --ov-syntax-number: #005cc5; + --ov-syntax-keyword: #d73a49; + --ov-syntax-keyword-control: #d73a49; + --ov-syntax-keyword-operator: #d73a49; + --ov-syntax-type: #e36209; + --ov-syntax-class: #e36209; + --ov-syntax-interface: #e36209; + --ov-syntax-function: #6f42c1; + --ov-syntax-method: #6f42c1; + --ov-syntax-variable: #1b1d22; + --ov-syntax-parameter: #e36209; + --ov-syntax-property: #005cc5; + --ov-syntax-namespace: #e36209; + --ov-syntax-decorator: #6f42c1; + --ov-syntax-regexp: #032f62; + --ov-syntax-operator: #d73a49; + --ov-syntax-punctuation: #24292e; } :root[data-ov-theme='high-contrast-dark'] { @@ -590,6 +655,63 @@ --ov-color-state-pressed: rgb(255 255 255 / 0.24); --ov-color-state-selected: rgb(127 179 255 / 0.34); --ov-color-state-focus-ring: #ffffff; + + /* Editor tokens — high-contrast dark */ + --ov-color-editor-bg: #000000; + --ov-color-editor-fg: #ffffff; + --ov-color-editor-cursor: #ffffff; + --ov-color-editor-selection-bg: rgb(127 179 255 / 0.4); + --ov-color-editor-selection-inactive-bg: rgb(255 255 255 / 0.15); + --ov-color-editor-line-highlight-bg: rgb(255 255 255 / 0.08); + --ov-color-editor-line-highlight-border: #f0f0f0; + --ov-color-editor-line-number: #d7d7d7; + --ov-color-editor-line-number-active: #ffffff; + + /* Terminal tokens — high-contrast dark */ + --ov-color-terminal-bg: #000000; + --ov-color-terminal-fg: #ffffff; + --ov-color-terminal-cursor: #ffffff; + --ov-color-terminal-selection-bg: rgb(127 179 255 / 0.4); + --ov-color-terminal-selection-fg: #ffffff; + --ov-color-terminal-border: #f0f0f0; + --ov-color-ansi-black: #000000; + --ov-color-ansi-red: #ff9f9f; + --ov-color-ansi-green: #73ffa2; + --ov-color-ansi-yellow: #ffd27d; + --ov-color-ansi-blue: #a6c8ff; + --ov-color-ansi-magenta: #d2a8ff; + --ov-color-ansi-cyan: #7ad4e8; + --ov-color-ansi-white: #ffffff; + --ov-color-ansi-bright-black: #8d8d8d; + --ov-color-ansi-bright-red: #ffb3b3; + --ov-color-ansi-bright-green: #9dffbd; + --ov-color-ansi-bright-yellow: #ffe2a3; + --ov-color-ansi-bright-blue: #c8deff; + --ov-color-ansi-bright-magenta: #e3c8ff; + --ov-color-ansi-bright-cyan: #a4e5f2; + --ov-color-ansi-bright-white: #ffffff; + + /* Syntax highlighting tokens — high-contrast dark */ + --ov-syntax-comment: #bcbcbc; + --ov-syntax-string: #a5d6ff; + --ov-syntax-string-escape: #79c0ff; + --ov-syntax-number: #79c0ff; + --ov-syntax-keyword: #ff9f9f; + --ov-syntax-keyword-control: #ff9f9f; + --ov-syntax-keyword-operator: #ff9f9f; + --ov-syntax-type: #ffa657; + --ov-syntax-class: #ffa657; + --ov-syntax-interface: #ffa657; + --ov-syntax-function: #d2a8ff; + --ov-syntax-method: #d2a8ff; + --ov-syntax-variable: #ffffff; + --ov-syntax-parameter: #ffa657; + --ov-syntax-property: #79c0ff; + --ov-syntax-namespace: #ffa657; + --ov-syntax-decorator: #d2a8ff; + --ov-syntax-regexp: #a5d6ff; + --ov-syntax-operator: #ff9f9f; + --ov-syntax-punctuation: #ffffff; } :root[data-ov-theme='high-contrast-light'] { @@ -630,6 +752,63 @@ --ov-color-state-pressed: rgb(0 0 0 / 0.14); --ov-color-state-selected: rgb(0 74 215 / 0.2); --ov-color-state-focus-ring: #000000; + + /* Editor tokens — high-contrast light */ + --ov-color-editor-bg: #ffffff; + --ov-color-editor-fg: #000000; + --ov-color-editor-cursor: #000000; + --ov-color-editor-selection-bg: rgb(0 74 215 / 0.25); + --ov-color-editor-selection-inactive-bg: rgb(0 0 0 / 0.1); + --ov-color-editor-line-highlight-bg: rgb(0 0 0 / 0.06); + --ov-color-editor-line-highlight-border: #000000; + --ov-color-editor-line-number: #2d2d2d; + --ov-color-editor-line-number-active: #000000; + + /* Terminal tokens — high-contrast light */ + --ov-color-terminal-bg: #f4f4f4; + --ov-color-terminal-fg: #000000; + --ov-color-terminal-cursor: #000000; + --ov-color-terminal-selection-bg: rgb(0 74 215 / 0.25); + --ov-color-terminal-selection-fg: #000000; + --ov-color-terminal-border: #000000; + --ov-color-ansi-black: #000000; + --ov-color-ansi-red: #8f0000; + --ov-color-ansi-green: #006b2f; + --ov-color-ansi-yellow: #8a3f00; + --ov-color-ansi-blue: #004ad7; + --ov-color-ansi-magenta: #6b20a8; + --ov-color-ansi-cyan: #006d7d; + --ov-color-ansi-white: #2d2d2d; + --ov-color-ansi-bright-black: #5c5c5c; + --ov-color-ansi-bright-red: #b30000; + --ov-color-ansi-bright-green: #008a3e; + --ov-color-ansi-bright-yellow: #a85400; + --ov-color-ansi-bright-blue: #1d4ed8; + --ov-color-ansi-bright-magenta: #7c3eb8; + --ov-color-ansi-bright-cyan: #0088a0; + --ov-color-ansi-bright-white: #000000; + + /* Syntax highlighting tokens — high-contrast light */ + --ov-syntax-comment: #5c5c5c; + --ov-syntax-string: #032f62; + --ov-syntax-string-escape: #006b2f; + --ov-syntax-number: #004ad7; + --ov-syntax-keyword: #8f0000; + --ov-syntax-keyword-control: #8f0000; + --ov-syntax-keyword-operator: #8f0000; + --ov-syntax-type: #8a3f00; + --ov-syntax-class: #8a3f00; + --ov-syntax-interface: #8a3f00; + --ov-syntax-function: #6b20a8; + --ov-syntax-method: #6b20a8; + --ov-syntax-variable: #000000; + --ov-syntax-parameter: #8a3f00; + --ov-syntax-property: #004ad7; + --ov-syntax-namespace: #8a3f00; + --ov-syntax-decorator: #6b20a8; + --ov-syntax-regexp: #032f62; + --ov-syntax-operator: #8f0000; + --ov-syntax-punctuation: #000000; } :root[data-ov-density='compact'] { diff --git a/packages/editors/docs/MONACO_YAML_COMPAT.md b/packages/editors/docs/MONACO_YAML_COMPAT.md index d7c7746..5f03262 100644 --- a/packages/editors/docs/MONACO_YAML_COMPAT.md +++ b/packages/editors/docs/MONACO_YAML_COMPAT.md @@ -26,7 +26,7 @@ tries `'then' in descriptorOrWorker` (line ~110). When `descriptorOrWorker` is `undefined` — which happens because `monaco-worker-manager` doesn't pass `opts.worker` — this crashes with: -``` +```text TypeError: Cannot use 'in' operator to search for 'then' in undefined ``` @@ -53,7 +53,7 @@ structurally incompatible with how `monaco-worker-manager` bootstraps workers. ### Summary -``` +```text monaco-editor@0.55 + monaco-yaml@5.4.1 = broken (hangs on "Loading...") monaco-editor@0.52 + monaco-yaml@5.4.1 = working (completions + validation) ``` @@ -97,7 +97,7 @@ whenever schemas change. ### Schema URI Bug — YAML(768) "No schema request service" -``` +```text Unable to load schema from '...'. No schema request service available YAML(768) ``` diff --git a/packages/editors/src/components/code-editor/CodeEditor.stories.tsx b/packages/editors/src/components/code-editor/CodeEditor.stories.tsx index ce3198f..beb07c9 100644 --- a/packages/editors/src/components/code-editor/CodeEditor.stories.tsx +++ b/packages/editors/src/components/code-editor/CodeEditor.stories.tsx @@ -413,6 +413,13 @@ function RuntimeSchemaStory(args: CodeEditorProps) { const loadedCount = Object.values(loaded).filter(Boolean).length; + function schemaStatusLabel(): string { + if (loadedCount === 0) return 'none — no completions available'; + const count = `${loadedCount} schema(s)`; + if (loaded[activeGvr]) return `${count} — active file has schema`; + return `${count} — active file has NO schema loaded`; + } + return (
@@ -427,6 +434,7 @@ function RuntimeSchemaStory(args: CodeEditorProps) {
{k8sSchemas.map((entry) => (
- Loaded: {loadedCount === 0 ? 'none — no completions available' : `${loadedCount} schema(s)`} - {loaded[activeGvr] ? ' — active file has schema' : loaded[activeGvr] === undefined && loadedCount > 0 ? ' — active file has NO schema loaded' : ''} + Loaded: {schemaStatusLabel()}
; +let mockModel: Record; const mockCreate = vi.fn(); const mockGetModel = vi.fn(); @@ -23,14 +24,15 @@ vi.mock('monaco-editor', () => { editor: { create: (...args: unknown[]) => { mockCreate(...args); + mockModel = { + getFullModelRange: vi.fn(() => ({})), + uri: { toString: () => 'test', path: 'test' }, + dispose: vi.fn(), + }; mockEditorInstance = { getValue: vi.fn(() => ''), setValue: vi.fn(), - getModel: vi.fn(() => ({ - getFullModelRange: vi.fn(() => ({})), - uri: { toString: () => 'test', path: 'test' }, - dispose: vi.fn(), - })), + getModel: vi.fn(() => mockModel), updateOptions: vi.fn(), executeEdits: vi.fn(), pushUndoStop: vi.fn(), @@ -202,9 +204,10 @@ describe('CodeEditor', () => { expect(mockCreate).toHaveBeenCalled(); }); - it('disposes editor on unmount', () => { + it('disposes editor and model on unmount', () => { const { unmount } = render(); unmount(); + expect(mockModel.dispose).toHaveBeenCalled(); expect(mockEditorInstance.dispose).toHaveBeenCalled(); }); }); diff --git a/packages/editors/src/components/code-editor/CodeEditor.tsx b/packages/editors/src/components/code-editor/CodeEditor.tsx index bfe9bf8..7584393 100644 --- a/packages/editors/src/components/code-editor/CodeEditor.tsx +++ b/packages/editors/src/components/code-editor/CodeEditor.tsx @@ -389,6 +389,8 @@ export const CodeEditor = forwardRef( editorRef.current = null; setIsReady(false); }; + // Intentional mount-once effect — props like value, language, readOnly, + // theme, filename, and callbacks are synced via dedicated effects below. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -403,10 +405,12 @@ export const CodeEditor = forwardRef( if (editor.getOption(monaco.editor.EditorOption.readOnly)) { editor.setValue(displayValue); } else if (displayValue !== editor.getValue()) { + const model = editor.getModel(); + if (!model) return; preventTriggerChangeEvent.current = true; editor.executeEdits('', [ { - range: editor.getModel()!.getFullModelRange(), + range: model.getFullModelRange(), text: displayValue, forceMoveMarkers: true, }, diff --git a/packages/editors/src/components/command-palette/CommandPalette.module.css b/packages/editors/src/components/command-palette/CommandPalette.module.css index 331cb5a..d8765ed 100644 --- a/packages/editors/src/components/command-palette/CommandPalette.module.css +++ b/packages/editors/src/components/command-palette/CommandPalette.module.css @@ -5,7 +5,7 @@ display: flex; justify-content: center; padding-top: 20vh; - background: rgba(0, 0, 0, 0.4); + background: var(--ov-color-overlay-bg, rgba(0, 0, 0, 0.4)); } .Root { diff --git a/packages/editors/src/components/command-palette/CommandPalette.stories.tsx b/packages/editors/src/components/command-palette/CommandPalette.stories.tsx index 1d3d4c3..82e707a 100644 --- a/packages/editors/src/components/command-palette/CommandPalette.stories.tsx +++ b/packages/editors/src/components/command-palette/CommandPalette.stories.tsx @@ -48,7 +48,7 @@ function PlaygroundStory(args: CommandPaletteProps) { const [lastSelected, setLastSelected] = useState(null); return (
- + {lastSelected && (

Last selected: {lastSelected}

)} @@ -79,7 +79,7 @@ function ManyCommandsStory(args: CommandPaletteProps) { })); return (
- + - + - + - + - + - + { - const React = require('react'); +vi.mock('@omniview/base-ui', async () => { + const React = await vi.importActual('react'); // Simple CommandList mock that renders items directly and supports filtering function MockRoot({ @@ -159,9 +159,9 @@ vi.mock('@omniview/base-ui', () => { } MockResults.displayName = 'CommandList.Results'; - function MockItem({ children, itemKey: _ik, ...props }: Record) { + function MockItem({ children, itemKey: _itemKey, ...props }: Record) { return ( -
+
{children as React.ReactNode}
); diff --git a/packages/editors/src/components/command-palette/CommandPalette.tsx b/packages/editors/src/components/command-palette/CommandPalette.tsx index 212b0f4..9ce9d61 100644 --- a/packages/editors/src/components/command-palette/CommandPalette.tsx +++ b/packages/editors/src/components/command-palette/CommandPalette.tsx @@ -43,15 +43,20 @@ export const CommandPalette = forwardRef( if (!open) return null; return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions
{ + if (e.key === 'Escape') onClose(); + }} data-testid="command-palette-overlay" >
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Command palette" diff --git a/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx b/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx index ea04d25..688d725 100644 --- a/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx +++ b/packages/editors/src/components/diff-viewer/DiffViewer.test.tsx @@ -35,7 +35,7 @@ vi.mock('monaco-editor', () => ({ }; return mockDiffEditorInstance; }, - createModel: vi.fn((_value: string, _lang?: string) => ({ + createModel: vi.fn(() => ({ setValue: vi.fn(), getValue: vi.fn(() => ''), dispose: vi.fn(), @@ -139,9 +139,12 @@ describe('DiffViewer', () => { expect(ref.current).toBeInstanceOf(HTMLDivElement); }); - it('disposes editor on unmount', () => { + it('disposes editor and models on unmount', () => { const { unmount } = render(); + const models = (mockDiffEditorInstance.getModel as ReturnType)(); unmount(); + expect(models.original.dispose).toHaveBeenCalled(); + expect(models.modified.dispose).toHaveBeenCalled(); expect(mockDiffEditorInstance.dispose).toHaveBeenCalled(); }); }); diff --git a/packages/editors/src/components/diff-viewer/DiffViewer.tsx b/packages/editors/src/components/diff-viewer/DiffViewer.tsx index 52641f8..7d66a93 100644 --- a/packages/editors/src/components/diff-viewer/DiffViewer.tsx +++ b/packages/editors/src/components/diff-viewer/DiffViewer.tsx @@ -102,10 +102,11 @@ export const DiffViewer = forwardRef(function D // Update language useEffect(() => { - if (!editorRef.current || !isReady || !language) return; + if (!editorRef.current || !isReady) return; + const lang = language ?? 'plaintext'; const models = editorRef.current.getModel(); - if (models?.original) monaco.editor.setModelLanguage(models.original, language); - if (models?.modified) monaco.editor.setModelLanguage(models.modified, language); + if (models?.original) monaco.editor.setModelLanguage(models.original, lang); + if (models?.modified) monaco.editor.setModelLanguage(models.modified, lang); }, [language, isReady]); // Update options diff --git a/packages/editors/src/components/index.ts b/packages/editors/src/components/index.ts index e42c26a..c843fac 100644 --- a/packages/editors/src/components/index.ts +++ b/packages/editors/src/components/index.ts @@ -1,6 +1,30 @@ -export * from './code-editor'; -export * from './diff-viewer'; -export * from './terminal'; -export * from './markdown-preview'; -export * from './command-palette'; -export * from './object-inspector'; +export { CodeEditor } from './code-editor'; +export type { + CodeEditorProps, + CodeEditorHandle, + EditorDiagnostic, + DiagnosticSeverity, + CursorPosition, + EditorDebugState, +} from './code-editor'; + +export { DiffViewer } from './diff-viewer'; +export type { DiffViewerProps, DiffMode } from './diff-viewer'; + +export { Terminal } from './terminal'; +export type { + TerminalProps, + TerminalHandle, + TerminalSignal, + TerminalSearchOptions, + TerminalErrorInfo, +} from './terminal'; + +export { MarkdownPreview } from './markdown-preview'; +export type { MarkdownPreviewProps } from './markdown-preview'; + +export { CommandPalette } from './command-palette'; +export type { CommandPaletteProps, CommandItem } from './command-palette'; + +export { ObjectInspector } from './object-inspector'; +export type { ObjectInspectorProps, InspectorFormat } from './object-inspector'; diff --git a/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx b/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx index 3ce922c..b70e0f5 100644 --- a/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx +++ b/packages/editors/src/components/markdown-preview/MarkdownPreview.stories.tsx @@ -68,7 +68,7 @@ interface PodListResponse { export async function fetchPods(namespace: string): Promise { const { data } = await client.get( - \\\`/api/v1/namespaces/\\\${namespace}/pods\\\`, + \`/api/v1/namespaces/\${namespace}/pods\`, ); return data; } diff --git a/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx b/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx index 0b2753e..51752c8 100644 --- a/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx +++ b/packages/editors/src/components/markdown-preview/MarkdownPreview.tsx @@ -34,6 +34,9 @@ function hastText(node: any): string { return ''; } +/** Counter for generating unique IDs for details/summary accordion items. */ +let detailsCounter = 0; + /** Stable component overrides for react-markdown */ // eslint-disable-next-line @typescript-eslint/no-explicit-any type MdProps = Record; @@ -106,7 +109,7 @@ const markdownComponents: Record> = { body.push(child); }); - const itemId = `md-details-${summaryText.replace(/\s+/g, '-').toLowerCase().slice(0, 32)}`; + const itemId = `md-details-${summaryText.replace(/\s+/g, '-').toLowerCase().slice(0, 32)}-${detailsCounter++}`; return ( diff --git a/packages/editors/src/components/object-inspector/ObjectInspector.module.css b/packages/editors/src/components/object-inspector/ObjectInspector.module.css index d25299b..8885a35 100644 --- a/packages/editors/src/components/object-inspector/ObjectInspector.module.css +++ b/packages/editors/src/components/object-inspector/ObjectInspector.module.css @@ -103,20 +103,20 @@ } .Value[data-type='string'] { - color: var(--ov-syntax-string, #ce9178); + color: var(--ov-syntax-string, var(--ov-color-fg-accent, #ce9178)); } .Value[data-type='number'] { - color: var(--ov-syntax-number, #b5cea8); + color: var(--ov-syntax-number, var(--ov-color-fg-success, #b5cea8)); } .Value[data-type='boolean'] { - color: var(--ov-syntax-keyword, #569cd6); + color: var(--ov-syntax-keyword, var(--ov-color-fg-info, #569cd6)); } .Value[data-type='null'], .Value[data-type='undefined'] { - color: var(--ov-syntax-keyword, #569cd6); + color: var(--ov-syntax-keyword, var(--ov-color-fg-info, #569cd6)); font-style: italic; } diff --git a/packages/editors/src/components/object-inspector/ObjectInspector.tsx b/packages/editors/src/components/object-inspector/ObjectInspector.tsx index f87e1ea..47fcba9 100644 --- a/packages/editors/src/components/object-inspector/ObjectInspector.tsx +++ b/packages/editors/src/components/object-inspector/ObjectInspector.tsx @@ -115,6 +115,13 @@ function TreeNode({ className={cn(styles.Row, isHighlighted && styles.RowHighlight)} style={{ paddingLeft: depth * 16 }} onClick={toggle} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggle(); + } + }} + tabIndex={0} role="treeitem" aria-expanded={isExpandable && !isCircular ? expanded : undefined} data-testid={`inspector-node-${nodeKey}`} @@ -176,7 +183,7 @@ export const ObjectInspector = forwardRef( if (format === 'yaml') { text = toYaml(data); } else { - text = JSON.stringify(data, null, 2); + text = safeJsonStringify(data); } await navigator.clipboard.writeText(text); } catch { @@ -239,6 +246,22 @@ export const ObjectInspector = forwardRef( ObjectInspector.displayName = 'ObjectInspector'; +/** JSON.stringify with circular reference safety. */ +function safeJsonStringify(value: unknown): string { + const seen = new WeakSet(); + return JSON.stringify( + value, + (_key, val) => { + if (typeof val === 'object' && val !== null) { + if (seen.has(val)) return '[Circular]'; + seen.add(val); + } + return val; + }, + 2, + ); +} + /** Simple YAML serializer (no dependency) */ function toYaml(value: unknown, indent: number = 0, seen?: WeakSet): string { const prefix = ' '.repeat(indent); diff --git a/packages/editors/src/components/terminal/Terminal.stories.tsx b/packages/editors/src/components/terminal/Terminal.stories.tsx index 94894f6..0ab4c7d 100644 --- a/packages/editors/src/components/terminal/Terminal.stories.tsx +++ b/packages/editors/src/components/terminal/Terminal.stories.tsx @@ -329,18 +329,21 @@ function SearchStory(args: TerminalProps) { style={{ padding: '4px 8px', background: '#2d2d2d', color: '#fff', border: '1px solid #555', borderRadius: 4, fontSize: 13 }} />