diff --git a/.gitleaks.toml b/.gitleaks.toml index 74154ec467..a600df76c7 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -4,5 +4,5 @@ title = "Gitleaks title" [allowlist] id = "skip class_generator/schema" description = "ignore class_generator/schema" -paths = ['''class_generator/schema/'''] +paths = ['''class_generator/schema/''', '''docs/'''] regexex = ['''^.*\.(yaml|yml)$'''] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8425ba6646..844921c7ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,6 +42,7 @@ repos: "--exclude-files=class_generator/schema/*", "--exclude-files=class_generator/__k8s-openapi-.*.json", "--exclude-files=fake_kubernetes_client/__resources-mappings.json", + "--exclude-files=docs/.*", ] - repo: https://github.com/astral-sh/ruff-pre-commit diff --git a/README.md b/README.md index 5ed4123381..68ea01712b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # openshift-python-wrapper (`wrapper`) +📖 **[Full Documentation](https://redhatqe.github.io/openshift-python-wrapper/)** + Pypi: [openshift-python-wrapper](https://pypi.org/project/openshift-python-wrapper) A python wrapper for [kubernetes-python-client](https://github.com/kubernetes-client/python) with support for [RedHat Container Virtualization](https://www.openshift.com/learn/topics/virtualization) Docs: [openshift-python-wrapper docs](https://openshift-python-wrapper.readthedocs.io/en/latest/) diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/assets/callouts.js b/docs/assets/callouts.js new file mode 100644 index 0000000000..af82afc794 --- /dev/null +++ b/docs/assets/callouts.js @@ -0,0 +1,26 @@ +(function() { + var blockquotes = document.querySelectorAll('blockquote'); + blockquotes.forEach(function(bq) { + var firstStrong = bq.querySelector('strong'); + if (!firstStrong) return; + + var text = firstStrong.textContent.toLowerCase().replace(':', '').trim(); + var type = null; + + if (text === 'note' || text === 'info') { + type = 'note'; + } else if (text === 'warning' || text === 'caution') { + type = 'warning'; + } else if (text === 'tip' || text === 'hint') { + type = 'tip'; + } else if (text === 'danger' || text === 'error') { + type = 'danger'; + } else if (text === 'important') { + type = 'important'; + } + + if (type) { + bq.classList.add('callout', 'callout-' + type); + } + }); +})(); diff --git a/docs/assets/codelabels.js b/docs/assets/codelabels.js new file mode 100644 index 0000000000..34467b3035 --- /dev/null +++ b/docs/assets/codelabels.js @@ -0,0 +1,75 @@ +(function() { + var blocks = document.querySelectorAll('pre code'); + blocks.forEach(function(code) { + var classes = code.className || ''; + var match = classes.match(/language-(\w+)/); + if (!match) return; + + var lang = match[1]; + var labelMap = { + 'python': 'Python', + 'py': 'Python', + 'javascript': 'JavaScript', + 'js': 'JavaScript', + 'typescript': 'TypeScript', + 'ts': 'TypeScript', + 'bash': 'Bash', + 'sh': 'Shell', + 'shell': 'Shell', + 'json': 'JSON', + 'yaml': 'YAML', + 'yml': 'YAML', + 'html': 'HTML', + 'css': 'CSS', + 'go': 'Go', + 'rust': 'Rust', + 'java': 'Java', + 'ruby': 'Ruby', + 'rb': 'Ruby', + 'sql': 'SQL', + 'dockerfile': 'Dockerfile', + 'docker': 'Dockerfile', + 'toml': 'TOML', + 'xml': 'XML', + 'c': 'C', + 'cpp': 'C++', + 'csharp': 'C#', + 'cs': 'C#', + 'php': 'PHP', + 'swift': 'Swift', + 'kotlin': 'Kotlin', + 'scala': 'Scala', + 'r': 'R', + 'lua': 'Lua', + 'perl': 'Perl', + 'makefile': 'Makefile', + 'graphql': 'GraphQL', + 'markdown': 'Markdown', + 'md': 'Markdown', + 'plaintext': 'Text', + 'text': 'Text', + 'ini': 'INI', + 'env': '.env', + }; + + var displayName = labelMap[lang.toLowerCase()] || lang; + + var pre = code.parentElement; + if (!pre || pre.tagName !== 'PRE') return; + + var label = document.createElement('span'); + label.className = 'code-label'; + label.textContent = displayName; + + var wrapper = pre.closest('.code-block-wrapper'); + if (wrapper) { + wrapper.insertBefore(label, wrapper.firstChild); + } else { + pre.style.position = 'relative'; + label.style.position = 'absolute'; + label.style.top = '0.5rem'; + label.style.right = '0.5rem'; + pre.insertBefore(label, pre.firstChild); + } + }); +})(); diff --git a/docs/assets/copy.js b/docs/assets/copy.js new file mode 100644 index 0000000000..c7e2a7a04c --- /dev/null +++ b/docs/assets/copy.js @@ -0,0 +1,41 @@ +(function() { + document.querySelectorAll('pre').forEach(function(pre) { + var btn = document.createElement('button'); + btn.className = 'copy-btn'; + btn.textContent = 'Copy'; + btn.addEventListener('click', function() { + var code = pre.querySelector('code'); + var text = code ? code.textContent : pre.textContent; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function() { + btn.textContent = 'Copied!'; + setTimeout(function() { btn.textContent = 'Copy'; }, 2000); + }).catch(function() { + fallbackCopy(text, btn); + }); + } else { + fallbackCopy(text, btn); + } + }); + pre.style.position = 'relative'; + pre.appendChild(btn); + }); + + function fallbackCopy(text, btn) { + var textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + btn.textContent = 'Copied!'; + setTimeout(function() { btn.textContent = 'Copy'; }, 2000); + } catch (e) { + btn.textContent = 'Failed'; + setTimeout(function() { btn.textContent = 'Copy'; }, 2000); + } + document.body.removeChild(textarea); + } +})(); diff --git a/docs/assets/github.js b/docs/assets/github.js new file mode 100644 index 0000000000..bd57b2f0c4 --- /dev/null +++ b/docs/assets/github.js @@ -0,0 +1,38 @@ +(function() { + var link = document.getElementById('github-link'); + if (!link) return; + + var repoUrl = link.getAttribute('data-repo-url'); + if (!repoUrl) return; + + // Extract owner/repo from GitHub URL + var match = repoUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/); + if (!match) return; + + var owner = match[1]; + var repo = match[2]; + + var starsEl = document.getElementById('github-stars'); + if (!starsEl) return; + + fetch('https://api.github.com/repos/' + owner + '/' + repo) + .then(function(response) { + if (!response.ok) return null; + return response.json(); + }) + .then(function(data) { + if (!data || typeof data.stargazers_count === 'undefined') return; + var count = data.stargazers_count; + var display; + if (count >= 1000) { + display = (count / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; + } else { + display = count.toString(); + } + starsEl.textContent = '★ ' + display; + starsEl.title = count.toLocaleString() + ' stars'; + }) + .catch(function() { + // Silently fail - star count is a nice-to-have + }); +})(); diff --git a/docs/assets/scrollspy.js b/docs/assets/scrollspy.js new file mode 100644 index 0000000000..6ab088703d --- /dev/null +++ b/docs/assets/scrollspy.js @@ -0,0 +1,49 @@ +(function() { + var tocLinks = document.querySelectorAll('.toc-container a'); + if (tocLinks.length === 0) return; + + var headings = []; + tocLinks.forEach(function(link) { + var href = link.getAttribute('href'); + if (href && href.startsWith('#')) { + var target = document.getElementById(href.substring(1)); + if (target) { + headings.push({ element: target, link: link }); + } + } + }); + + if (headings.length === 0) return; + + function updateActive() { + var scrollPos = window.scrollY + 100; + var current = null; + + for (var i = 0; i < headings.length; i++) { + if (headings[i].element.offsetTop <= scrollPos) { + current = headings[i]; + } + } + + tocLinks.forEach(function(link) { + link.classList.remove('active'); + }); + + if (current) { + current.link.classList.add('active'); + } + } + + var ticking = false; + window.addEventListener('scroll', function() { + if (!ticking) { + window.requestAnimationFrame(function() { + updateActive(); + ticking = false; + }); + ticking = true; + } + }); + + updateActive(); +})(); diff --git a/docs/assets/search.js b/docs/assets/search.js new file mode 100644 index 0000000000..a8fc6de6dc --- /dev/null +++ b/docs/assets/search.js @@ -0,0 +1,125 @@ +(function() { + // Create modal HTML + var overlay = document.createElement('div'); + overlay.className = 'search-modal-overlay'; + overlay.innerHTML = '
' + + '' + + '
' + + '' + + '
'; + document.body.appendChild(overlay); + + var input = overlay.querySelector('.search-modal-input'); + var results = overlay.querySelector('.search-modal-results'); + var index = []; + var selectedIdx = -1; + + // Load index + fetch('search-index.json').then(function(r) { return r.json(); }) + .then(function(data) { index = data; }).catch(function() {}); + + // Open/close + function openModal() { + overlay.classList.add('active'); + input.value = ''; + results.innerHTML = ''; + selectedIdx = -1; + setTimeout(function() { input.focus(); }, 50); + } + + function closeModal() { + overlay.classList.remove('active'); + } + + // Keyboard shortcut + document.addEventListener('keydown', function(e) { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + openModal(); + } + if (e.key === 'Escape') closeModal(); + }); + + // Click overlay to close + overlay.addEventListener('click', function(e) { + if (e.target === overlay) closeModal(); + }); + + // Search trigger button in sidebar + var sidebarSearch = document.getElementById('search-input'); + if (sidebarSearch) { + sidebarSearch.addEventListener('focus', function(e) { + e.preventDefault(); + this.blur(); + openModal(); + }); + } + + // Search trigger button in top bar + var topBarSearch = document.getElementById('search-trigger'); + if (topBarSearch) { + topBarSearch.addEventListener('click', function(e) { + e.preventDefault(); + openModal(); + }); + } + + // Search logic + input.addEventListener('input', function() { + var q = this.value.toLowerCase().trim(); + results.innerHTML = ''; + selectedIdx = -1; + if (!q) return; + + var matches = index.filter(function(item) { + return item.title.toLowerCase().includes(q) || item.content.toLowerCase().includes(q); + }).slice(0, 10); + + matches.forEach(function(m, i) { + var div = document.createElement('a'); + div.href = m.slug + '.html'; + div.className = 'search-result-item'; + + var title = document.createElement('span'); + title.className = 'search-result-title'; + title.textContent = m.title; + div.appendChild(title); + + // Content preview + var preview = document.createElement('span'); + preview.className = 'search-result-preview'; + var contentIdx = m.content.toLowerCase().indexOf(q); + if (contentIdx >= 0) { + var start = Math.max(0, contentIdx - 40); + var end = Math.min(m.content.length, contentIdx + q.length + 60); + var snippet = (start > 0 ? '...' : '') + m.content.substring(start, end) + (end < m.content.length ? '...' : ''); + preview.textContent = snippet; + } + div.appendChild(preview); + results.appendChild(div); + }); + + if (matches.length === 0) { + var empty = document.createElement('div'); + empty.className = 'search-no-results'; + empty.textContent = 'No results found'; + results.appendChild(empty); + } + }); + + // Keyboard navigation + input.addEventListener('keydown', function(e) { + var items = results.querySelectorAll('.search-result-item'); + if (e.key === 'ArrowDown') { + e.preventDefault(); + selectedIdx = Math.min(selectedIdx + 1, items.length - 1); + items.forEach(function(item, i) { item.classList.toggle('selected', i === selectedIdx); }); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + selectedIdx = Math.max(selectedIdx - 1, 0); + items.forEach(function(item, i) { item.classList.toggle('selected', i === selectedIdx); }); + } else if (e.key === 'Enter' && selectedIdx >= 0 && items[selectedIdx]) { + window.location.href = items[selectedIdx].href; + } + }); +})(); diff --git a/docs/assets/style.css b/docs/assets/style.css new file mode 100644 index 0000000000..82aa789bf9 --- /dev/null +++ b/docs/assets/style.css @@ -0,0 +1,1545 @@ +/* ========================================================================== + Docsfy - Documentation Theme (Warm Stone + Orange) + ========================================================================== */ + +/* -------------------------------------------------------------------------- + CSS Custom Properties (Light Theme - Default) + -------------------------------------------------------------------------- */ +:root { + /* Backgrounds */ + --bg-primary: #ffffff; + --bg-secondary: #fafaf9; + --bg-tertiary: #f5f5f4; + --bg-sidebar: #fafaf9; + --bg-code: #fef3c7; + --bg-code-block: #1c1917; + --bg-card: #ffffff; + --bg-search-result: #ffffff; + + /* Text */ + --text-primary: #1c1917; + --text-secondary: #57534e; + --text-tertiary: #78716c; + --text-inverse: #ffffff; + --text-link: #c2410c; + --text-link-hover: #9a3412; + --text-code: #92400e; + --text-code-block: #d4d4d8; + + /* Borders */ + --border-primary: #e7e5e4; + --border-secondary: #f5f5f4; + --border-focus: #ea580c; + + /* Accent */ + --accent: #c2410c; + --accent-light: #fff7ed; + --accent-hover: #9a3412; + --accent-contrast: #ffffff; + --accent-focus-ring: rgba(194, 65, 12, 0.15); + --accent-underline: rgba(194, 65, 12, 0.3); + + /* Callouts */ + --callout-note-bg: #eff6ff; + --callout-note-border: #3b82f6; + --callout-note-text: #1e40af; + --callout-warning-bg: #fffbeb; + --callout-warning-border: #f59e0b; + --callout-warning-text: #92400e; + --callout-tip-bg: #ecfdf5; + --callout-tip-border: #10b981; + --callout-tip-text: #065f46; + --callout-danger-bg: #fef2f2; + --callout-danger-border: #ef4444; + --callout-danger-text: #991b1b; + --callout-important-bg: #faf5ff; + --callout-important-border: #8b5cf6; + --callout-important-text: #5b21b6; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.06), 0 2px 4px -2px rgba(0, 0, 0, 0.04); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.04); + + /* Layout */ + --sidebar-width: 280px; + --content-max-width: 720px; + --header-height: 56px; + + /* Typography */ + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + --font-mono: "SF Mono", SFMono-Regular, ui-monospace, "DejaVu Sans Mono", Menlo, Consolas, monospace; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; + + /* Table */ + --table-row-alt: #fafaf9; + --table-border: #e7e5e4; +} + +/* -------------------------------------------------------------------------- + Dark Theme + -------------------------------------------------------------------------- */ +[data-theme="dark"] { + --bg-primary: #1c1917; + --bg-secondary: #292524; + --bg-tertiary: #44403c; + --bg-sidebar: #292524; + --bg-code: rgba(251, 191, 36, 0.15); + --bg-code-block: #1c1917; + --bg-card: #292524; + --bg-search-result: #292524; + + --text-primary: #e7e5e4; + --text-secondary: #a8a29e; + --text-tertiary: #78716c; + --text-inverse: #ffffff; + --text-link: #fb923c; + --text-link-hover: #fdba74; + --text-code: #fbbf24; + --text-code-block: #d4d4d8; + + --border-primary: #44403c; + --border-secondary: #292524; + --border-focus: #fb923c; + + --accent: #fb923c; + --accent-light: rgba(251, 146, 60, 0.1); + --accent-hover: #fdba74; + --accent-contrast: #1c1917; + --accent-focus-ring: rgba(251, 146, 60, 0.15); + --accent-underline: rgba(251, 146, 60, 0.3); + + --callout-note-bg: #172554; + --callout-note-border: #3b82f6; + --callout-note-text: #93c5fd; + --callout-warning-bg: #451a03; + --callout-warning-border: #f59e0b; + --callout-warning-text: #fcd34d; + --callout-tip-bg: #052e16; + --callout-tip-border: #10b981; + --callout-tip-text: #6ee7b7; + --callout-danger-bg: #450a0a; + --callout-danger-border: #ef4444; + --callout-danger-text: #fca5a5; + --callout-important-bg: #2e1065; + --callout-important-border: #8b5cf6; + --callout-important-text: #c4b5fd; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.3); + + --table-row-alt: #292524; + --table-border: #44403c; +} + +/* -------------------------------------------------------------------------- + Reset & Base + -------------------------------------------------------------------------- */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + scroll-behavior: smooth; + scroll-padding-top: calc(var(--header-height, 60px) + 20px); + -webkit-text-size-adjust: 100%; +} + +body { + font-family: var(--font-sans); + color: var(--text-primary); + background-color: var(--bg-primary); + line-height: 1.7; + transition: background-color var(--transition-normal), color var(--transition-normal); + display: flex; + min-height: 100vh; +} + +a { + color: var(--text-link); + text-decoration: none; + transition: color var(--transition-fast); +} + +a:hover { + color: var(--text-link-hover); + text-decoration: underline; +} + +/* -------------------------------------------------------------------------- + Sidebar + -------------------------------------------------------------------------- */ +.sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: var(--sidebar-width); + background: var(--bg-sidebar); + border-right: 1px solid var(--border-primary); + overflow-y: auto; + z-index: 100; + display: flex; + flex-direction: column; + transition: background-color var(--transition-normal), border-color var(--transition-normal); +} + +.sidebar-header { + padding: 24px 20px 16px; + border-bottom: 1px solid var(--border-secondary); +} + +.sidebar-logo { + display: block; + font-size: 1.125rem; + font-weight: 700; + color: var(--text-primary); + text-decoration: none; + letter-spacing: -0.01em; +} + +.sidebar-logo:hover { + color: var(--accent); + text-decoration: none; +} + +.sidebar-tagline { + font-size: 0.8125rem; + color: var(--text-tertiary); + margin-top: 4px; + line-height: 1.4; +} + +/* Sidebar Search */ +.sidebar-search { + padding: 12px 20px; + position: relative; +} + +#search-input { + width: 100%; + padding: 8px 12px; + font-size: 0.875rem; + font-family: var(--font-sans); + border: 1px solid var(--border-primary); + border-radius: 8px; + background: var(--bg-primary); + color: var(--text-primary); + outline: none; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast), + background-color var(--transition-normal); +} + +#search-input::placeholder { + color: var(--text-tertiary); +} + +#search-input:focus { + border-color: var(--border-focus); + box-shadow: 0 0 0 3px var(--accent-focus-ring); +} + +.search-results { + display: none; + position: absolute; + top: 100%; + left: 20px; + right: 20px; + background: var(--bg-search-result); + border: 1px solid var(--border-primary); + border-radius: 8px; + box-shadow: var(--shadow-lg); + z-index: 200; + max-height: 300px; + overflow-y: auto; +} + +.search-result-item { + display: block; + padding: 10px 14px; + font-size: 0.875rem; + color: var(--text-primary); + border-bottom: 1px solid var(--border-secondary); + text-decoration: none; + transition: background-color var(--transition-fast); +} + +.search-result-item:last-child { + border-bottom: none; +} + +.search-result-item:hover { + background: var(--accent-light); + color: var(--accent); + text-decoration: none; +} + +/* Sidebar Navigation */ +.sidebar-nav { + flex: 1; + padding: 8px 0 24px; + overflow-y: auto; +} + +.nav-group { + padding: 0 12px; + margin-bottom: 8px; +} + +.nav-group-title { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-tertiary); + padding: 12px 8px 6px; +} + +.nav-group-pages { + list-style: none; +} + +.nav-link { + display: block; + padding: 6px 12px; + font-size: 0.875rem; + color: var(--text-secondary); + border-radius: 6px; + text-decoration: none; + transition: color var(--transition-fast), background-color var(--transition-fast); + line-height: 1.5; +} + +.nav-link:hover { + color: var(--text-primary); + background: var(--bg-tertiary); + text-decoration: none; +} + +.nav-link.active { + color: var(--accent); + background: var(--accent-light); + font-weight: 500; +} + +/* -------------------------------------------------------------------------- + Main Wrapper + -------------------------------------------------------------------------- */ +.main-wrapper { + margin-left: var(--sidebar-width); + flex: 1; + display: flex; + flex-direction: column; + min-height: 100vh; + transition: margin-left var(--transition-normal); +} + +/* -------------------------------------------------------------------------- + Top Bar + -------------------------------------------------------------------------- */ +.top-bar { + position: sticky; + top: 0; + height: var(--header-height); + display: flex; + align-items: center; + padding: 0 24px; + background: var(--bg-primary); + border-bottom: 1px solid var(--border-primary); + z-index: 50; + transition: background-color var(--transition-normal), border-color var(--transition-normal); +} + +.top-bar-spacer { + flex: 1; +} + +.sidebar-toggle { + display: none; + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 6px; + border-radius: 6px; + transition: color var(--transition-fast), background-color var(--transition-fast); +} + +.sidebar-toggle:hover { + color: var(--text-primary); + background: var(--bg-tertiary); +} + +.theme-toggle { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 6px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: color var(--transition-fast), background-color var(--transition-fast); +} + +.theme-toggle:hover { + color: var(--text-primary); + background: var(--bg-tertiary); +} + +/* Show sun in dark mode, moon in light mode */ +.icon-sun { + display: none; +} + +.icon-moon { + display: block; +} + +[data-theme="dark"] .icon-sun { + display: block; +} + +[data-theme="dark"] .icon-moon { + display: none; +} + +/* -------------------------------------------------------------------------- + Content + -------------------------------------------------------------------------- */ +.content { + flex: 1; + padding: calc(var(--header-height, 60px) + 20px) 48px 40px; + max-width: calc(var(--content-max-width) + 96px); +} + +.article { + max-width: var(--content-max-width); +} + +.article-title { + font-size: 2rem; + font-weight: 700; + letter-spacing: -0.025em; + color: var(--text-primary); + margin-bottom: 24px; + line-height: 1.25; +} + +.article-body { + font-size: 1rem; + line-height: 1.8; + color: var(--text-secondary); +} + +/* Typography in article body */ +.article-body h1 { + font-size: 1.875rem; + font-weight: 700; + margin-top: 56px; + margin-bottom: 20px; + color: var(--text-primary); + letter-spacing: -0.02em; + line-height: 1.3; +} + +.article-body h2 { + font-size: 1.5rem; + font-weight: 600; + margin-top: 48px; + margin-bottom: 16px; + color: var(--text-primary); + letter-spacing: -0.015em; + line-height: 1.35; + padding-bottom: 10px; + border-bottom: 1px solid var(--border-secondary); +} + +.article-body h3 { + font-size: 1.25rem; + font-weight: 600; + margin-top: 40px; + margin-bottom: 12px; + color: var(--text-primary); + line-height: 1.4; +} + +.article-body h4 { + font-size: 1.0625rem; + font-weight: 600; + margin-top: 32px; + margin-bottom: 10px; + color: var(--text-primary); +} + +.article-body p { + margin-bottom: 18px; +} + +.article-body ul, +.article-body ol { + margin-bottom: 18px; + padding-left: 24px; +} + +.article-body li { + margin-bottom: 8px; +} + +.article-body li > ul, +.article-body li > ol { + margin-top: 6px; + margin-bottom: 0; +} + +/* Inline code */ +.article-body code { + font-family: var(--font-mono); + font-size: 0.875em; + background: var(--bg-code); + color: var(--text-code); + padding: 2px 6px; + border-radius: 4px; +} + +/* Code blocks (Pygments / codehilite) */ +.article-body pre { + margin-bottom: 24px; + border-radius: 8px; + overflow-x: auto; + font-size: 0.875rem; + line-height: 1.6; +} + +.article-body .highlight { + background: var(--bg-code-block); + border-radius: 8px; + padding: 16px 20px; + overflow-x: auto; + margin-bottom: 24px; +} + +.article-body .highlight pre { + margin: 0; + background: transparent; + padding: 0; + color: var(--text-code-block); +} + +.article-body .highlight code { + background: transparent; + color: inherit; + padding: 0; + font-size: 0.875rem; +} + +/* Pygments syntax highlighting tokens */ +.highlight .hll { background-color: #3d405b; } +.highlight .c { color: #6a737d; font-style: italic; } /* Comment */ +.highlight .k { color: #c678dd; font-weight: bold; } /* Keyword */ +.highlight .o { color: #c678dd; } /* Operator */ +.highlight .cm { color: #6a737d; font-style: italic; } /* Comment.Multiline */ +.highlight .cp { color: #6a737d; font-weight: bold; } /* Comment.Preproc */ +.highlight .c1 { color: #6a737d; font-style: italic; } /* Comment.Single */ +.highlight .cs { color: #6a737d; font-style: italic; } /* Comment.Special */ +.highlight .gd { color: #e06c75; } /* Generic.Deleted */ +.highlight .gi { color: #98c379; } /* Generic.Inserted */ +.highlight .ge { font-style: italic; } /* Generic.Emph */ +.highlight .gs { font-weight: bold; } /* Generic.Strong */ +.highlight .gu { color: #56b6c2; font-weight: bold; } /* Generic.Subheading */ +.highlight .kc { color: #c678dd; font-weight: bold; } /* Keyword.Constant */ +.highlight .kd { color: #c678dd; font-weight: bold; } /* Keyword.Declaration */ +.highlight .kn { color: #c678dd; } /* Keyword.Namespace */ +.highlight .kp { color: #c678dd; } /* Keyword.Pseudo */ +.highlight .kr { color: #c678dd; font-weight: bold; } /* Keyword.Reserved */ +.highlight .kt { color: #e5c07b; } /* Keyword.Type */ +.highlight .m { color: #d19a66; } /* Literal.Number */ +.highlight .s { color: #98c379; } /* Literal.String */ +.highlight .na { color: #e06c75; } /* Name.Attribute */ +.highlight .nb { color: #56b6c2; } /* Name.Builtin */ +.highlight .nc { color: #e5c07b; font-weight: bold; } /* Name.Class */ +.highlight .no { color: #e06c75; } /* Name.Constant */ +.highlight .nd { color: #61afef; } /* Name.Decorator */ +.highlight .nf { color: #61afef; } /* Name.Function */ +.highlight .nn { color: #e5c07b; } /* Name.Namespace */ +.highlight .nt { color: #e06c75; } /* Name.Tag */ +.highlight .nv { color: #e06c75; } /* Name.Variable */ +.highlight .ow { color: #c678dd; font-weight: bold; } /* Operator.Word */ +.highlight .w { color: #abb2bf; } /* Text.Whitespace */ +.highlight .mb { color: #d19a66; } /* Literal.Number.Bin */ +.highlight .mf { color: #d19a66; } /* Literal.Number.Float */ +.highlight .mh { color: #d19a66; } /* Literal.Number.Hex */ +.highlight .mi { color: #d19a66; } /* Literal.Number.Integer */ +.highlight .mo { color: #d19a66; } /* Literal.Number.Oct */ +.highlight .sa { color: #98c379; } /* Literal.String.Affix */ +.highlight .sb { color: #98c379; } /* Literal.String.Backtick */ +.highlight .sc { color: #98c379; } /* Literal.String.Char */ +.highlight .dl { color: #98c379; } /* Literal.String.Delimiter */ +.highlight .sd { color: #98c379; font-style: italic; } /* Literal.String.Doc */ +.highlight .s2 { color: #98c379; } /* Literal.String.Double */ +.highlight .se { color: #d19a66; } /* Literal.String.Escape */ +.highlight .sh { color: #98c379; } /* Literal.String.Heredoc */ +.highlight .si { color: #98c379; } /* Literal.String.Interpol */ +.highlight .sx { color: #98c379; } /* Literal.String.Other */ +.highlight .sr { color: #56b6c2; } /* Literal.String.Regex */ +.highlight .s1 { color: #98c379; } /* Literal.String.Single */ +.highlight .ss { color: #98c379; } /* Literal.String.Symbol */ +.highlight .bp { color: #56b6c2; } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #61afef; } /* Name.Function.Magic */ +.highlight .vc { color: #e06c75; } /* Name.Variable.Class */ +.highlight .vg { color: #e06c75; } /* Name.Variable.Global */ +.highlight .vi { color: #e06c75; } /* Name.Variable.Instance */ +.highlight .vm { color: #e06c75; } /* Name.Variable.Magic */ +.highlight .il { color: #d19a66; } /* Literal.Number.Integer.Long */ + +/* -------------------------------------------------------------------------- + Blockquotes / Callouts + -------------------------------------------------------------------------- */ +.article-body blockquote p { + margin-bottom: 8px; +} + +.article-body blockquote p:last-child { + margin-bottom: 0; +} + +/* Callout boxes */ +blockquote.callout-note, +blockquote.callout-warning, +blockquote.callout-tip, +blockquote.callout-danger, +blockquote.callout-important { + border-left-width: 4px; + border-left-style: solid; + padding: 1rem 1.25rem; + border-radius: 0 8px 8px 0; + margin: 1.5rem 0; +} + +blockquote.callout-note { + border-left-color: var(--callout-note-border); + background: var(--callout-note-bg); + color: var(--callout-note-text); +} + +blockquote.callout-warning { + border-left-color: var(--callout-warning-border); + background: var(--callout-warning-bg); + color: var(--callout-warning-text); +} + +blockquote.callout-tip { + border-left-color: var(--callout-tip-border); + background: var(--callout-tip-bg); + color: var(--callout-tip-text); +} + +blockquote.callout-danger { + border-left-color: var(--callout-danger-border); + background: var(--callout-danger-bg); + color: var(--callout-danger-text); +} + +blockquote.callout-important { + border-left-color: var(--callout-important-border); + background: var(--callout-important-bg); + color: var(--callout-important-text); +} + +/* Chained :not() for broader browser compatibility */ +/* Default blockquote styling (not a callout) */ +blockquote:not(.callout-note):not(.callout-warning):not(.callout-tip):not(.callout-danger):not(.callout-important) { + border-left: 4px solid var(--border-primary); + padding: 1rem 1.25rem; + margin: 1.5rem 0; + color: var(--text-secondary); +} + +/* -------------------------------------------------------------------------- + Tables + -------------------------------------------------------------------------- */ +.article-body table { + width: 100%; + border-collapse: collapse; + margin-bottom: 24px; + font-size: 0.9375rem; + border: 1px solid var(--table-border); + border-radius: 8px; + overflow: hidden; +} + +.article-body thead { + background: var(--bg-tertiary); +} + +.article-body th { + padding: 10px 16px; + text-align: left; + font-weight: 600; + color: var(--text-primary); + border-bottom: 2px solid var(--table-border); + font-size: 0.8125rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.article-body td { + padding: 10px 16px; + border-bottom: 1px solid var(--table-border); + color: var(--text-secondary); +} + +.article-body tbody tr:nth-child(even) { + background: var(--table-row-alt); +} + +.article-body tbody tr:hover { + background: var(--accent-light); +} + +/* -------------------------------------------------------------------------- + Links in article + -------------------------------------------------------------------------- */ +.article-body a { + color: var(--text-link); + text-decoration: underline; + text-decoration-color: var(--accent-underline); + text-underline-offset: 2px; + transition: color var(--transition-fast), text-decoration-color var(--transition-fast); +} + +.article-body a:hover { + color: var(--text-link-hover); + text-decoration-color: var(--text-link-hover); +} + +/* -------------------------------------------------------------------------- + Images + -------------------------------------------------------------------------- */ +.article-body img { + max-width: 100%; + height: auto; + border-radius: 8px; + margin: 16px 0; +} + +/* -------------------------------------------------------------------------- + Horizontal Rule + -------------------------------------------------------------------------- */ +.article-body hr { + border: none; + border-top: 1px solid var(--border-primary); + margin: 40px 0; +} + +/* -------------------------------------------------------------------------- + Details / Summary (Collapsible Sections) + -------------------------------------------------------------------------- */ +details { + margin: 1.5rem 0; + border: 1px solid var(--border-primary); + border-radius: 8px; + overflow: hidden; +} + +details summary { + padding: 12px 16px; + font-weight: 600; + cursor: pointer; + background: var(--bg-secondary); + color: var(--text-primary); + list-style: none; + display: flex; + align-items: center; + gap: 8px; +} + +details summary::before { + content: '▶'; + font-size: 0.75rem; + transition: transform 0.2s ease; + color: var(--text-tertiary); +} + +details[open] summary::before { + transform: rotate(90deg); +} + +details summary::-webkit-details-marker { + display: none; +} + +details > *:not(summary) { + padding: 0 16px; +} + +details > *:not(summary):first-of-type { + padding-top: 16px; +} + +details > p:last-child, +details > ul:last-child, +details > ol:last-child { + padding-bottom: 16px; +} + +/* -------------------------------------------------------------------------- + Index Header (Landing Page) + -------------------------------------------------------------------------- */ +.index-header { + padding: 48px 0 40px; + border-bottom: 1px solid var(--border-primary); + margin-bottom: 40px; +} + +.index-title { + font-size: 2.25rem; + font-weight: 800; + letter-spacing: -0.03em; + color: var(--text-primary); + margin-bottom: 8px; + line-height: 1.2; +} + +.index-tagline { + font-size: 1.125rem; + color: var(--text-secondary); + margin-bottom: 24px; + line-height: 1.5; + max-width: 560px; +} + +.index-cta { + display: inline-block; + padding: 10px 24px; + background: var(--accent); + color: var(--accent-contrast); + border-radius: 6px; + font-size: 0.9375rem; + font-weight: 600; + text-decoration: none; + transition: background var(--transition-fast); +} + +.index-cta:hover { + background: var(--accent-hover); + color: var(--accent-contrast); + text-decoration: none; +} + +/* -------------------------------------------------------------------------- + Card Grid (Index Page) + -------------------------------------------------------------------------- */ +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; + margin-bottom: 48px; +} + +.card { + background: var(--bg-card); + border: 1px solid var(--border-primary); + border-radius: 12px; + padding: 24px; + align-self: start; + transition: border-color var(--transition-fast), + background-color var(--transition-normal); +} + +.card:hover { + border-color: var(--accent); +} + +.card-title { + font-size: 1.0625rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 12px; +} + +.card-pages { + list-style: none; + margin-bottom: 16px; +} + +.card-pages li { + margin-bottom: 8px; +} + +.card-pages a { + font-size: 0.9375rem; + color: var(--text-link); + text-decoration: none; +} + +.card-pages a:hover { + text-decoration: underline; +} + +.card-page-desc { + font-size: 0.8125rem; + color: var(--text-tertiary); + margin-top: 2px; + line-height: 1.4; +} + +.card-link { + display: inline-block; + font-size: 0.875rem; + font-weight: 500; + color: var(--accent); + text-decoration: none; + transition: color var(--transition-fast); +} + +.card-link:hover { + color: var(--accent-hover); + text-decoration: none; +} + +/* -------------------------------------------------------------------------- + Table of Contents - Right Sidebar + -------------------------------------------------------------------------- */ +.toc-sidebar { + position: fixed; + top: var(--header-height); + right: 20px; + width: 200px; + max-height: calc(100vh - var(--header-height) - 20px); + overflow-y: auto; + font-size: 0.8125rem; + display: none; +} + +.toc-container h3 { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin-bottom: 0.75rem; + position: sticky; + top: 0; + background: var(--bg-primary); + padding: 12px 0 8px; + z-index: 1; +} + +.toc-container ul { + list-style: none; + padding: 0; + margin: 0; +} + +.toc-container li { + margin-bottom: 0.4rem; +} + +.toc-container a { + color: var(--text-secondary); + text-decoration: none; + display: block; + padding: 0.2rem 0; + padding-left: 0.75rem; + border-left: 2px solid var(--border-primary); + transition: all 0.15s ease; +} + +.toc-container a:hover { + color: var(--accent); + border-left-color: var(--accent); +} + +/* Hide h3+ nested items to limit TOC depth to h2 only */ +.toc-container ul ul { + display: none; +} + +/* Show TOC and adjust main content on wide screens only */ +@media (min-width: 1280px) { + .toc-sidebar { + display: block; + } + + .content { + margin-right: 220px; + } +} + +/* TOC is hidden by default (display: none above), no extra rule needed */ + +/* -------------------------------------------------------------------------- + Footer + -------------------------------------------------------------------------- */ +.footer { + padding: 24px 48px; + border-top: 1px solid var(--border-primary); + color: var(--text-tertiary); + font-size: 0.8125rem; + transition: border-color var(--transition-normal); +} + +.footer a { + color: var(--text-link); + text-decoration: none; +} + +.footer a:hover { + text-decoration: underline; +} + +/* -------------------------------------------------------------------------- + Sidebar Overlay (Mobile) + -------------------------------------------------------------------------- */ +.sidebar-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 90; +} + +.sidebar-overlay.open { + display: block; +} + +/* -------------------------------------------------------------------------- + Responsive Design + -------------------------------------------------------------------------- */ +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + transition: transform var(--transition-normal); + } + + .sidebar.open { + transform: translateX(0); + } + + .main-wrapper { + margin-left: 0; + } + + .sidebar-toggle { + display: flex; + } + + .content { + padding: 24px 20px; + } + + .footer { + padding: 20px; + } + + .index-header { + padding: 32px 0 28px; + } + + .index-title { + font-size: 1.75rem; + } + + .index-tagline { + font-size: 1rem; + } + + .article-title { + font-size: 1.625rem; + } + + .card-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 480px) { + .index-title { + font-size: 1.5rem; + } + + .article-body h1 { + font-size: 1.5rem; + } + + .article-body h2 { + font-size: 1.25rem; + } + + .article-body h3 { + font-size: 1.125rem; + } +} + +/* -------------------------------------------------------------------------- + Scrollbar Styling + -------------------------------------------------------------------------- */ +.sidebar::-webkit-scrollbar { + width: 4px; +} + +.sidebar::-webkit-scrollbar-track { + background: transparent; +} + +.sidebar::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 4px; +} + +.sidebar::-webkit-scrollbar-thumb:hover { + background: var(--text-tertiary); +} + +/* -------------------------------------------------------------------------- + Print Styles + -------------------------------------------------------------------------- */ +@media print { + .sidebar, + .top-bar, + .sidebar-overlay { + display: none !important; + } + + .main-wrapper { + margin-left: 0 !important; + } + + .content { + padding: 0; + max-width: 100%; + } +} + +/* -------------------------------------------------------------------------- + Code Copy Button + -------------------------------------------------------------------------- */ +.copy-btn { + position: absolute; + top: 8px; + right: 8px; + padding: 4px 10px; + font-size: 0.75rem; + background: var(--bg-secondary); + color: var(--text-secondary); + border: 1px solid var(--border-primary); + border-radius: 4px; + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease; +} + +pre:hover .copy-btn { + opacity: 1; +} + +/* Make copy button always visible on touch devices */ +@media (hover: none) { + .copy-btn { + opacity: 0.7; + } +} + +.copy-btn:hover { + background: var(--accent); + color: var(--accent-contrast); + border-color: var(--accent); +} + +/* -------------------------------------------------------------------------- + Prev/Next Page Navigation + -------------------------------------------------------------------------- */ +.page-nav { + display: flex; + justify-content: space-between; + gap: 1rem; + margin-top: 3rem; + padding-top: 2rem; + border-top: 1px solid var(--border-primary); +} + +.page-nav-link { + display: flex; + flex-direction: column; + padding: 1rem 1.25rem; + border: 1px solid var(--border-primary); + border-radius: 8px; + text-decoration: none; + transition: all 0.15s ease; + max-width: 50%; +} + +.page-nav-link:hover { + border-color: var(--accent); + text-decoration: none; +} + +.page-nav-next { + text-align: right; + margin-left: auto; +} + +.page-nav-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin-bottom: 0.25rem; +} + +.page-nav-title { + font-size: 0.95rem; + font-weight: 600; + color: var(--accent); +} + +/* -------------------------------------------------------------------------- + Search Modal (Cmd+K) + -------------------------------------------------------------------------- */ +.search-modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + justify-content: center; + padding-top: 15vh; +} + +.search-modal-overlay.active { + display: flex; +} + +.search-modal { + background: var(--bg-primary); + border: 1px solid var(--border-primary); + border-radius: 12px; + width: 560px; + max-height: 480px; + display: flex; + flex-direction: column; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2); + overflow: hidden; +} + +.search-modal-input { + width: 100%; + padding: 16px 20px; + border: none; + border-bottom: 1px solid var(--border-primary); + background: transparent; + color: var(--text-primary); + font-size: 1rem; + outline: none; + box-sizing: border-box; +} + +.search-modal-results { + overflow-y: auto; + flex: 1; +} + +.search-modal .search-result-item { + display: flex; + flex-direction: column; + padding: 12px 20px; + text-decoration: none; + border-bottom: 1px solid var(--border-primary); + transition: background 0.1s ease; +} + +.search-modal .search-result-item:hover, +.search-modal .search-result-item.selected { + background: var(--bg-secondary); +} + +.search-result-title { + font-weight: 600; + color: var(--text-primary); + font-size: 0.9rem; +} + +.search-result-preview { + font-size: 0.8rem; + color: var(--text-secondary); + margin-top: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.search-no-results { + padding: 20px; + text-align: center; + color: var(--text-secondary); +} + +.search-modal-footer { + padding: 8px 20px; + font-size: 0.75rem; + color: var(--text-secondary); + border-top: 1px solid var(--border-primary); + text-align: center; +} + +.search-modal-footer kbd { + display: inline-block; + padding: 2px 6px; + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 4px; + font-family: inherit; + font-size: 0.7rem; +} + +/* -------------------------------------------------------------------------- + Scroll Spy - TOC Active State + -------------------------------------------------------------------------- */ +.toc-container a.toc-active { + color: var(--accent); + border-left-color: var(--accent); + font-weight: 500; +} + +/* -------------------------------------------------------------------------- + Top Bar - Desktop Visibility & Layout + -------------------------------------------------------------------------- */ +.top-bar-left { + display: flex; + align-items: center; + gap: 8px; +} + +.top-bar-title { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + text-decoration: none; + letter-spacing: -0.01em; +} + +.top-bar-title:hover { + color: var(--accent); + text-decoration: none; +} + +.top-bar-right { + display: flex; + align-items: center; + gap: 8px; +} + +.top-bar-search-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 8px; + color: var(--text-tertiary); + font-size: 0.8125rem; + cursor: pointer; + transition: color var(--transition-fast), background-color var(--transition-fast), + border-color var(--transition-fast); +} + +.top-bar-search-btn:hover { + color: var(--text-primary); + background: var(--bg-tertiary); + border-color: var(--border-primary); +} + +.top-bar-search-btn kbd { + display: inline-block; + padding: 1px 5px; + background: var(--bg-primary); + border: 1px solid var(--border-primary); + border-radius: 4px; + font-family: inherit; + font-size: 0.7rem; + color: var(--text-tertiary); +} + +.top-bar-github { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1px solid var(--border-primary); + border-radius: 6px; + color: var(--text-secondary); + text-decoration: none; + font-size: 0.85rem; + transition: color var(--transition-fast), background-color var(--transition-fast), + border-color var(--transition-fast); +} + +.top-bar-github:hover { + color: var(--text-primary); + background: var(--bg-tertiary); + border-color: var(--accent); + text-decoration: none; +} + +.top-bar-github svg { + flex-shrink: 0; +} + +.github-stars { + font-weight: 600; + font-size: 0.8rem; +} + +.github-stars:empty { + display: none; +} + +/* -------------------------------------------------------------------------- + Code Language Labels + -------------------------------------------------------------------------- */ +.code-label { + position: absolute; + top: 8px; + right: 60px; + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.6; +} + +/* -------------------------------------------------------------------------- + docsfy Branding Badge + -------------------------------------------------------------------------- */ +.docsfy-badge { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + font-size: 0.75rem; + font-weight: 600; + color: var(--accent); + text-decoration: none; + border: 1px solid var(--border-primary); + border-radius: 4px; + transition: all 0.15s ease; + white-space: nowrap; +} + +.docsfy-badge:hover { + border-color: var(--accent); + background: var(--accent-light); +} + +.brand-accent { + color: var(--accent); +} + +/* -------------------------------------------------------------------------- + Page Footer + -------------------------------------------------------------------------- */ +.page-footer { + margin-top: 3rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border-primary); + text-align: center; + font-size: 0.8rem; + color: var(--text-tertiary); +} + +.page-footer a { + color: var(--accent); + text-decoration: none; + font-weight: 600; +} + +.page-footer a:hover { + text-decoration: underline; +} + +/* -------------------------------------------------------------------------- + LLM Docs Banner + -------------------------------------------------------------------------- */ +.llm-docs-banner { + display: flex; + align-items: flex-start; + gap: 1rem; + padding: 1.25rem 1.5rem; + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 8px; + margin-bottom: 2rem; +} + +.llm-docs-icon { + font-size: 1.5rem; + line-height: 1; +} + +.llm-docs-content { + flex: 1; +} + +.llm-docs-content strong { + display: block; + margin-bottom: 0.25rem; + color: var(--text-primary); +} + +.llm-docs-content p { + margin: 0 0 0.5rem; + font-size: 0.85rem; + color: var(--text-secondary); +} + +.llm-docs-links { + font-size: 0.85rem; +} + +.llm-docs-links a { + color: var(--accent); + text-decoration: none; + font-weight: 500; +} + +.llm-docs-links a:hover { + text-decoration: underline; +} + +.llm-docs-sep { + margin: 0 0.5rem; + color: var(--text-tertiary); +} + +.footer-sep { + margin: 0 0.5rem; + color: var(--text-tertiary); +} + +.footer-llm { + font-size: 0.8rem; +} + +.footer-llm a { + color: var(--accent); + text-decoration: none; +} + +.footer-llm a:hover { + text-decoration: underline; +} diff --git a/docs/assets/theme.js b/docs/assets/theme.js new file mode 100644 index 0000000000..9d51df31bc --- /dev/null +++ b/docs/assets/theme.js @@ -0,0 +1,22 @@ +(function() { + function getTheme() { + try { return localStorage.getItem('theme'); } catch(e) { return null; } + } + function setTheme(theme) { + try { localStorage.setItem('theme', theme); } catch(e) {} + } + + var toggle = document.getElementById('theme-toggle'); + var stored = getTheme(); + if (stored === 'dark' || stored === 'light') { + document.documentElement.setAttribute('data-theme', stored); + } else { + document.documentElement.setAttribute('data-theme', 'light'); + } + if (toggle) toggle.addEventListener('click', function() { + var current = document.documentElement.getAttribute('data-theme'); + var next = current === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + setTheme(next); + }); +})(); diff --git a/docs/class-generator-cli.html b/docs/class-generator-cli.html new file mode 100644 index 0000000000..5dec5c84e7 --- /dev/null +++ b/docs/class-generator-cli.html @@ -0,0 +1,1537 @@ + + + + + + class-generator CLI Reference - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

I'll start by reading the knowledge graph and page manifest, then explore the class-generator source code in detail.

+

Now let me check the entry point configuration and any additional schema-related functions:

+

Now let me check the test files for CLI tests and the entry point:

+

Now let me check the parsers and any additional CLI-relevant components:

+

Now I have all the information I need. Let me compile the comprehensive CLI reference page.

+

class-generator CLI Reference

+

The class-generator command generates Python wrapper classes for Kubernetes and OpenShift resources. It is installed as a console script entry point from the openshift-python-wrapper package.

+
class-generator = "class_generator.cli:main"
+
+ +
pip install openshift-python-wrapper
+# or
+uv tool install openshift-python-wrapper
+
+ +
+

Tip: Enable shell completion by adding to ~/.bashrc or ~/.zshrc: +bash +if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=zsh_source class-generator)"; fi

+
+

Synopsis

+
class-generator [OPTIONS]
+
+ +

At least one action option must be specified: --kind, --update-schema, --update-schema-for, --discover-missing, --coverage-report, --generate-missing, or --regenerate-all.

+
+

Options Reference

+

Kind Generation

+

-k, --kind

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
TypeSTRING
DefaultNone
RequiredNo (but at least one action must be specified)
RequiresConnected cluster with admin privileges
+

Generate Python wrapper classes for the specified Kubernetes resource Kind(s). Multiple kinds can be comma-separated (no spaces).

+

When a kind is not found in the local schema mapping file, the CLI interactively prompts to run --update-schema (only in interactive CLI mode).

+
# Single kind
+class-generator -k Pod
+
+# Multiple kinds (processed in parallel)
+class-generator -k Deployment,Pod,ConfigMap
+
+ +

When multiple kinds share the same Kind name but belong to different API groups (e.g., DNS from config.openshift.io and operator.openshift.io), separate files are generated with API group suffixes:

+
dns_config_openshift_io.py
+dns_operator_openshift_io.py
+
+ +
+

-o, --output-file

+ + + + + + + + + + + + + + + + + + + + + +
PropertyValue
TypePATH
Defaultocp_resources/<snake_case_kind>.py
RequiredNo
+

Override the output file path for the generated Python module. If not provided, the filename is derived from the Kind using convert_camel_case_to_snake_case.

+
class-generator -k Pod -o my_custom_pod.py
+
+ +
+

Note: When generating multiple comma-separated kinds, the --output-file value applies to all kinds. For independent output paths, run separate commands.

+
+
+

--overwrite

+ + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
+

Overwrite an existing output file. Without this flag, if the target file already exists, a _TEMP.py suffixed file is created instead.

+

When overwriting, any user-added code blocks (code after the # End of generated code marker) and user imports are preserved in the regenerated file.

+
class-generator -k Pod --overwrite
+
+ +
+

--dry-run

+ + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
+

Preview the generated output without writing any files. The generated Python code is printed to the console with syntax highlighting and line numbers using Rich.

+
class-generator -k Pod --dry-run
+
+ +
+

--backup

+ + + + + + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
Requires--regenerate-all or --overwrite
+

Create a timestamped backup of existing files before overwriting or regenerating. Backups are stored in .backups/backup-YYYYMMDD-HHMMSS/ preserving the original directory structure.

+
class-generator -k Pod --overwrite --backup
+
+ +
+

Note: Using --backup without either --regenerate-all or --overwrite results in a constraint error.

+
+
+

Test Generation

+

--add-tests

+ + + + + + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
Requires-k/--kind
+

Generate test files for the specified Kind and run them. This performs two actions:

+
    +
  1. Generates a test manifest in class_generator/tests/manifests/<Kind>/ and regenerates class_generator/tests/test_class_generator.py from the Jinja2 template.
  2. +
  3. Runs the generated test file using uv run --group tests pytest class_generator/tests/test_class_generator.py.
  4. +
+
class-generator -k Pod --add-tests
+
+ +
+

Warning: --add-tests cannot be used without -k/--kind. Running class-generator --add-tests alone exits with a non-zero status.

+
+
+

Schema Management

+

--update-schema

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
RequiresConnected cluster; oc or kubectl in PATH
Mutually exclusive with--update-schema-for
+

Fetch all resource schemas from the connected cluster's OpenAPI v3 endpoints and update the local schema files:

+
    +
  • class_generator/schema/__resources-mappings.json (compressed as .json.gz)
  • +
  • class_generator/schema/_definitions.json
  • +
+

The update strategy depends on the cluster version:

+ + + + + + + + + + + + + + + + + +
Cluster VersionBehavior
Same or newer than last updateFull update — fetches all schemas, updates existing resources
Older than last updateIncremental — only adds missing resources, preserves existing schemas
+

When used alone, exits after updating. When combined with --generate-missing, continues to resource generation after the update.

+
# Update schema only
+class-generator --update-schema
+
+# Update schema then generate missing resources
+class-generator --update-schema --generate-missing
+
+ +
+

Note: When --update-schema is used without --generate-missing, it cannot be combined with -k, --discover-missing, --coverage-report, --dry-run, --overwrite, -o, --add-tests, or --regenerate-all.

+
+
+

--update-schema-for

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
TypeSTRING
DefaultNone
RequiresConnected cluster; oc or kubectl in PATH
Mutually exclusive with--update-schema
+

Update the schema for a single resource Kind without affecting other resources. The Kind name is case-sensitive.

+

This fetches only the API paths relevant to the specified Kind, updates (or adds) its schema in the mapping file, and exits.

+
class-generator --update-schema-for LlamaStackDistribution
+
+ +

Use cases:

+
    +
  • Connected to an older cluster but need to update a specific CRD
  • +
  • A new operator was installed and you need its resource schema
  • +
  • Refreshing just one resource without a full schema update
  • +
+

After updating, regenerate the class:

+
class-generator --update-schema-for LlamaStackDistribution
+class-generator -k LlamaStackDistribution --overwrite
+
+ +

Errors:

+ + + + + + + + + + + + + + + + + +
ErrorCause
ResourceNotFoundErrorThe Kind is not found on the cluster (CRD not installed or name misspelled)
RuntimeErrorAPI paths not found or schema extraction failed
+
+

Note: Cannot be combined with -k, --discover-missing, --coverage-report, --generate-missing, or --regenerate-all.

+
+
+

Coverage and Discovery

+

--discover-missing

+ + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
+

Analyze resource coverage by comparing schema-mapped resources against implemented wrapper classes in ocp_resources/. Generates a console report showing coverage statistics and missing resources.

+
class-generator --discover-missing
+
+ +
+

--coverage-report

+ + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
+

Generate a detailed coverage report showing:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MetricDescription
Total Resources in SchemaNumber of resource Kinds in the schema mapping
Auto-Generated ResourcesWrapper classes with the generated marker
CoveragePercentage of mapped resources with generated classes
Missing (Not Generated)Resources in schema but without generated classes
Manual ImplementationsResource classes without the generated marker
+
# Console output (default)
+class-generator --coverage-report
+
+# JSON output
+class-generator --coverage-report --json
+
+ +
+

--json

+ + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
+

Output reports in JSON format instead of Rich console tables. Applies to --coverage-report, --discover-missing, and --generate-missing.

+

The JSON output structure:

+
{
+  "generated_resources": ["ConfigMap", "Deployment", "Pod"],
+  "manual_resources": ["VirtualMachine"],
+  "missing_resources": [{"kind": "Binding"}, {"kind": "ComponentStatus"}],
+  "coverage_stats": {
+    "total_in_mapping": 400,
+    "total_generated": 197,
+    "total_manual": 25,
+    "coverage_percentage": 49.25,
+    "missing_count": 203
+  }
+}
+
+ +
class-generator --coverage-report --json
+
+ +
+

--generate-missing

+ + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
+

Generate wrapper classes for all resources found in the schema mapping that do not yet have generated files. Each missing resource Kind is passed to class_generator() individually.

+

Can be combined with --update-schema to first refresh the schema, then generate all missing classes.

+
# Generate missing resources
+class-generator --generate-missing
+
+# Update schema first, then generate missing
+class-generator --update-schema --generate-missing
+
+# Dry run to preview
+class-generator --generate-missing --dry-run
+
+ +
+

Batch Regeneration

+

--regenerate-all

+ + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
+

Regenerate all existing generated resource classes using the latest schemas. Only files containing the # Generated using marker in ocp_resources/ are processed.

+

Regeneration runs in parallel (up to 10 workers). User-added code (below # End of generated code) is preserved during regeneration.

+
# Regenerate all resources
+class-generator --regenerate-all
+
+# With backup
+class-generator --regenerate-all --backup
+
+# Dry run
+class-generator --regenerate-all --dry-run
+
+# Filter to specific resources
+class-generator --regenerate-all --filter "Pod*"
+
+ +

Output summary:

+
Regeneration complete: 195 succeeded, 2 failed
+Backup files stored in: .backups/backup-20260705-143022
+
+ +
+

--filter

+ + + + + + + + + + + + + + + + + + + + + +
PropertyValue
TypeSTRING
DefaultNone
Requires--regenerate-all
+

Filter which resources to regenerate using a glob pattern matched against the resource Kind name. Uses fnmatch semantics.

+
# Regenerate only Pod-related resources
+class-generator --regenerate-all --filter "Pod*"
+
+# Regenerate resources ending in "Service"
+class-generator --regenerate-all --filter "*Service"
+
+# Regenerate a specific resource
+class-generator --regenerate-all --filter "VirtualMachine"
+
+ +
+

Logging

+

-v, --verbose

+ + + + + + + + + + + + + + + + + +
PropertyValue
TypeFlag
DefaultFalse
+

Enable verbose output with debug-level logs. Sets DEBUG level on the following loggers:

+
    +
  • class_generator.core.schema
  • +
  • class_generator.core.generator
  • +
  • class_generator.core.coverage
  • +
  • class_generator.core.discovery
  • +
  • class_generator.cli
  • +
  • class_generator.utils
  • +
  • ocp_resources
  • +
+
class-generator -k Pod -v
+
+ +
+

Constraint Rules

+

The CLI enforces the following option constraints:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConstraintRule
--update-schema--update-schema-forMutually exclusive
--update-schema (alone)Cannot combine with -k, --discover-missing, --coverage-report, --dry-run, --overwrite, -o, --add-tests, --regenerate-all
--update-schema-forCannot combine with -k, --discover-missing, --coverage-report, --generate-missing, --regenerate-all
--backupRequires --regenerate-all or --overwrite
--filterRequires --regenerate-all
No optionsExits with error — at least one action required
+
+

Execution Order

+

When multiple compatible options are specified together, the CLI processes them in this fixed order:

+
    +
  1. --update-schema-for (exits after completion)
  2. +
  3. --update-schema (exits unless --generate-missing is also set)
  4. +
  5. --coverage-report / --discover-missing / --generate-missing (coverage analysis and reporting)
  6. +
  7. --generate-missing (generates classes for missing resources)
  8. +
  9. --regenerate-all (batch regeneration, exits after completion)
  10. +
  11. -k/--kind (normal kind generation)
  12. +
  13. --add-tests (test generation and execution)
  14. +
+
+

Exit Codes

+ + + + + + + + + + + + + + + + + + + + + +
CodeMeaning
0Success
1Generation failure, schema update failure, resource not found, or any kind in a batch failed
2Invalid CLI arguments or constraint violation
+
+

Programmatic API

+

The generation logic can be invoked directly from Python. See Generating New Resource Classes with class-generator for usage examples.

+

class_generator.core.generator.class_generator()

+
from class_generator.core.generator import class_generator
+
+generated_files: list[str] = class_generator(
+    kind="Pod",
+    overwrite=False,
+    dry_run=False,
+    output_file="",
+    output_dir="",
+    add_tests=False,
+    called_from_cli=True,
+    update_schema_executed=False,
+)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
kindstr(required)Kubernetes resource Kind
overwriteboolFalseOverwrite existing files
dry_runboolFalsePreview output without writing files
output_filestr""Specific output file path
output_dirstr""Output directory (defaults to ocp_resources)
add_testsboolFalseGenerate test files
called_from_cliboolTrueEnables interactive prompts when True
update_schema_executedboolFalseWhether schema update was already performed
+

Returns: list[str] — List of generated file paths. Empty list if the kind is not found in the schema mapping (when called_from_cli=False).

+

Raises:

+
    +
  • RuntimeError — Kind not found after schema update, or user declined schema update
  • +
  • ValueError — Generated filename contains invalid patterns (single-letter segments)
  • +
+
+

class_generator.core.generator.generate_resource_file_from_dict()

+
from class_generator.core.generator import generate_resource_file_from_dict
+
+orig_filename, generated_filename = generate_resource_file_from_dict(
+    resource_dict={"kind": "Pod", ...},
+    overwrite=False,
+    dry_run=False,
+    output_file="",
+    add_tests=False,
+    output_file_suffix="",
+    output_dir="",
+)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
resource_dictdict[str, Any](required)Dictionary containing parsed resource information
overwriteboolFalseOverwrite existing files
dry_runboolFalsePreview without writing
output_filestr""Specific output file path
add_testsboolFalseGenerate test files under class_generator/tests/manifests/
output_file_suffixstr""Suffix appended to filename (for API group disambiguation)
output_dirstr""Output directory (defaults to ocp_resources)
+

Returns: tuple[str, str](original_filename, generated_filename). These differ when a _TEMP.py file is created.

+
+

class_generator.core.schema.update_kind_schema()

+
from class_generator.core.schema import update_kind_schema
+
+update_kind_schema(client=None)
+
+ + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
clientstr \| NoneNonePath to oc/kubectl binary. Auto-detected if None.
+

Raises:

+
    +
  • ClusterVersionError — Cannot determine cluster version
  • +
  • RuntimeError — Failed to fetch OpenAPI v3 index
  • +
  • OSError — Failed to write schema files
  • +
+
+

class_generator.core.schema.update_single_resource_schema()

+
from class_generator.core.schema import update_single_resource_schema
+
+update_single_resource_schema(kind="LlamaStackDistribution", client=None)
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
kindstr(required)Resource Kind name (case-sensitive)
clientstr \| NoneNonePath to oc/kubectl binary. Auto-detected if None.
+

Raises:

+
    +
  • ResourceNotFoundError — Kind not found on the cluster
  • +
  • RuntimeError — Schema extraction or save failed
  • +
+
+

class_generator.core.coverage.analyze_coverage()

+
from class_generator.core.coverage import analyze_coverage
+
+result: dict[str, Any] = analyze_coverage(resources_dir="ocp_resources")
+
+ + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
resources_dirstr"ocp_resources"Directory to scan for wrapper classes
+

Returns: dict[str, Any] with keys:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDescription
generated_resourceslist[str]Sorted list of auto-generated resource class names
manual_resourceslist[str]Sorted list of manually written resource class names
missing_resourceslist[dict]List of {"kind": "..."} for resources in schema but not generated
coverage_statsdictStatistics including total_in_mapping, total_generated, total_manual, coverage_percentage, missing_count
+
+

class_generator.core.discovery.discover_generated_resources()

+
from class_generator.core.discovery import discover_generated_resources
+
+resources: list[dict[str, Any]] = discover_generated_resources()
+
+ +

Returns: list[dict[str, Any]] — Each dict contains:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDescription
pathstrFull path to the resource file
kindstrResource class name
filenamestrFile name without extension
has_user_codeboolWhether file contains user modifications below # End of generated code
+
+

class_generator.core.discovery.discover_cluster_resources()

+
from class_generator.core.discovery import discover_cluster_resources
+
+resources: dict[str, list[dict[str, Any]]] = discover_cluster_resources(
+    client=None,
+    api_group_filter=None,
+)
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
clientDynamicClient \| NoneNoneKubernetes dynamic client. Creates one if None.
api_group_filterstr \| NoneNoneFilter by API group name
+

Returns: dict[str, list[dict[str, Any]]] — Mapping of API version to list of resource dicts (name, kind, namespaced).

+

Raises: ValueError — If client is not a DynamicClient instance.

+
+

Exceptions

+

class_generator.exceptions.ResourceNotFoundError

+
from class_generator.exceptions import ResourceNotFoundError
+
+ +

Raised when a resource Kind is not found in the schema mapping or on the cluster.

+ + + + + + + + + + + + + + + +
AttributeTypeDescription
kindstrThe Kind that was not found
+

class_generator.core.schema.ClusterVersionError

+
from class_generator.core.schema import ClusterVersionError
+
+ +

Raised when the cluster version cannot be determined (client binary missing, cluster unreachable, or authentication failure).

+
+

Schema Files

+

The CLI manages two schema files under class_generator/schema/:

+ + + + + + + + + + + + + + + + + + + + + +
FilePurpose
__resources-mappings.json.gzCompressed JSON mapping of lowercase Kind → list of schemas with GVK metadata and namespaced status
_definitions.jsonJSON Schema definitions for $ref resolution during validation
__cluster_version__.txtLast cluster version used for schema generation
+

See Schema Validation and Code Generation Architecture for details on how these files are structured and consumed.

+
+

Common Workflows

+

Generate a new resource class

+
class-generator -k MyCustomResource
+
+ +

Update schema and regenerate all classes

+
class-generator --update-schema
+class-generator --regenerate-all --backup
+
+ +

Add a new CRD to an older cluster

+
class-generator --update-schema-for MyNewCRD
+class-generator -k MyNewCRD
+
+ +

CI/CD coverage check

+
class-generator --coverage-report --json > coverage.json
+
+ +

Preview changes before writing

+
class-generator -k Pod --dry-run
+class-generator --regenerate-all --dry-run
+
+ +
+ + + + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/class-generator-cli.md b/docs/class-generator-cli.md new file mode 100644 index 0000000000..85c50bc015 --- /dev/null +++ b/docs/class-generator-cli.md @@ -0,0 +1,726 @@ +I'll start by reading the knowledge graph and page manifest, then explore the class-generator source code in detail. + +Now let me check the entry point configuration and any additional schema-related functions: + +Now let me check the test files for CLI tests and the entry point: + +Now let me check the parsers and any additional CLI-relevant components: + +Now I have all the information I need. Let me compile the comprehensive CLI reference page. + +# class-generator CLI Reference + +The `class-generator` command generates Python wrapper classes for Kubernetes and OpenShift resources. It is installed as a console script entry point from the `openshift-python-wrapper` package. + +``` +class-generator = "class_generator.cli:main" +``` + +```bash +pip install openshift-python-wrapper +# or +uv tool install openshift-python-wrapper +``` + +> **Tip:** Enable shell completion by adding to `~/.bashrc` or `~/.zshrc`: +> ```bash +> if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=zsh_source class-generator)"; fi +> ``` + +## Synopsis + +``` +class-generator [OPTIONS] +``` + +At least one action option must be specified: `--kind`, `--update-schema`, `--update-schema-for`, `--discover-missing`, `--coverage-report`, `--generate-missing`, or `--regenerate-all`. + +--- + +## Options Reference + +### Kind Generation + +#### `-k`, `--kind` + +| Property | Value | +|----------|-------| +| **Type** | `STRING` | +| **Default** | `None` | +| **Required** | No (but at least one action must be specified) | +| **Requires** | Connected cluster with admin privileges | + +Generate Python wrapper classes for the specified Kubernetes resource Kind(s). Multiple kinds can be comma-separated (no spaces). + +When a kind is not found in the local schema mapping file, the CLI interactively prompts to run `--update-schema` (only in interactive CLI mode). + +```bash +# Single kind +class-generator -k Pod + +# Multiple kinds (processed in parallel) +class-generator -k Deployment,Pod,ConfigMap +``` + +When multiple kinds share the same Kind name but belong to different API groups (e.g., `DNS` from `config.openshift.io` and `operator.openshift.io`), separate files are generated with API group suffixes: + +``` +dns_config_openshift_io.py +dns_operator_openshift_io.py +``` + +--- + +#### `-o`, `--output-file` + +| Property | Value | +|----------|-------| +| **Type** | `PATH` | +| **Default** | `ocp_resources/.py` | +| **Required** | No | + +Override the output file path for the generated Python module. If not provided, the filename is derived from the Kind using `convert_camel_case_to_snake_case`. + +```bash +class-generator -k Pod -o my_custom_pod.py +``` + +> **Note:** When generating multiple comma-separated kinds, the `--output-file` value applies to all kinds. For independent output paths, run separate commands. + +--- + +#### `--overwrite` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Overwrite an existing output file. Without this flag, if the target file already exists, a `_TEMP.py` suffixed file is created instead. + +When overwriting, any user-added code blocks (code after the `# End of generated code` marker) and user imports are preserved in the regenerated file. + +```bash +class-generator -k Pod --overwrite +``` + +--- + +#### `--dry-run` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Preview the generated output without writing any files. The generated Python code is printed to the console with syntax highlighting and line numbers using Rich. + +```bash +class-generator -k Pod --dry-run +``` + +--- + +#### `--backup` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | +| **Requires** | `--regenerate-all` or `--overwrite` | + +Create a timestamped backup of existing files before overwriting or regenerating. Backups are stored in `.backups/backup-YYYYMMDD-HHMMSS/` preserving the original directory structure. + +```bash +class-generator -k Pod --overwrite --backup +``` + +> **Note:** Using `--backup` without either `--regenerate-all` or `--overwrite` results in a constraint error. + +--- + +### Test Generation + +#### `--add-tests` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | +| **Requires** | `-k`/`--kind` | + +Generate test files for the specified Kind and run them. This performs two actions: + +1. Generates a test manifest in `class_generator/tests/manifests//` and regenerates `class_generator/tests/test_class_generator.py` from the Jinja2 template. +2. Runs the generated test file using `uv run --group tests pytest class_generator/tests/test_class_generator.py`. + +```bash +class-generator -k Pod --add-tests +``` + +> **Warning:** `--add-tests` cannot be used without `-k`/`--kind`. Running `class-generator --add-tests` alone exits with a non-zero status. + +--- + +### Schema Management + +#### `--update-schema` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | +| **Requires** | Connected cluster; `oc` or `kubectl` in PATH | +| **Mutually exclusive with** | `--update-schema-for` | + +Fetch all resource schemas from the connected cluster's OpenAPI v3 endpoints and update the local schema files: + +- `class_generator/schema/__resources-mappings.json` (compressed as `.json.gz`) +- `class_generator/schema/_definitions.json` + +The update strategy depends on the cluster version: + +| Cluster Version | Behavior | +|-----------------|----------| +| Same or newer than last update | Full update — fetches all schemas, updates existing resources | +| Older than last update | Incremental — only adds missing resources, preserves existing schemas | + +When used alone, exits after updating. When combined with `--generate-missing`, continues to resource generation after the update. + +```bash +# Update schema only +class-generator --update-schema + +# Update schema then generate missing resources +class-generator --update-schema --generate-missing +``` + +> **Note:** When `--update-schema` is used without `--generate-missing`, it cannot be combined with `-k`, `--discover-missing`, `--coverage-report`, `--dry-run`, `--overwrite`, `-o`, `--add-tests`, or `--regenerate-all`. + +--- + +#### `--update-schema-for` + +| Property | Value | +|----------|-------| +| **Type** | `STRING` | +| **Default** | `None` | +| **Requires** | Connected cluster; `oc` or `kubectl` in PATH | +| **Mutually exclusive with** | `--update-schema` | + +Update the schema for a single resource Kind without affecting other resources. The Kind name is **case-sensitive**. + +This fetches only the API paths relevant to the specified Kind, updates (or adds) its schema in the mapping file, and exits. + +```bash +class-generator --update-schema-for LlamaStackDistribution +``` + +Use cases: +- Connected to an older cluster but need to update a specific CRD +- A new operator was installed and you need its resource schema +- Refreshing just one resource without a full schema update + +After updating, regenerate the class: + +```bash +class-generator --update-schema-for LlamaStackDistribution +class-generator -k LlamaStackDistribution --overwrite +``` + +**Errors:** + +| Error | Cause | +|-------|-------| +| `ResourceNotFoundError` | The Kind is not found on the cluster (CRD not installed or name misspelled) | +| `RuntimeError` | API paths not found or schema extraction failed | + +> **Note:** Cannot be combined with `-k`, `--discover-missing`, `--coverage-report`, `--generate-missing`, or `--regenerate-all`. + +--- + +### Coverage and Discovery + +#### `--discover-missing` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Analyze resource coverage by comparing schema-mapped resources against implemented wrapper classes in `ocp_resources/`. Generates a console report showing coverage statistics and missing resources. + +```bash +class-generator --discover-missing +``` + +--- + +#### `--coverage-report` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Generate a detailed coverage report showing: + +| Metric | Description | +|--------|-------------| +| Total Resources in Schema | Number of resource Kinds in the schema mapping | +| Auto-Generated Resources | Wrapper classes with the generated marker | +| Coverage | Percentage of mapped resources with generated classes | +| Missing (Not Generated) | Resources in schema but without generated classes | +| Manual Implementations | Resource classes without the generated marker | + +```bash +# Console output (default) +class-generator --coverage-report + +# JSON output +class-generator --coverage-report --json +``` + +--- + +#### `--json` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Output reports in JSON format instead of Rich console tables. Applies to `--coverage-report`, `--discover-missing`, and `--generate-missing`. + +The JSON output structure: + +```json +{ + "generated_resources": ["ConfigMap", "Deployment", "Pod"], + "manual_resources": ["VirtualMachine"], + "missing_resources": [{"kind": "Binding"}, {"kind": "ComponentStatus"}], + "coverage_stats": { + "total_in_mapping": 400, + "total_generated": 197, + "total_manual": 25, + "coverage_percentage": 49.25, + "missing_count": 203 + } +} +``` + +```bash +class-generator --coverage-report --json +``` + +--- + +#### `--generate-missing` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Generate wrapper classes for all resources found in the schema mapping that do not yet have generated files. Each missing resource Kind is passed to `class_generator()` individually. + +Can be combined with `--update-schema` to first refresh the schema, then generate all missing classes. + +```bash +# Generate missing resources +class-generator --generate-missing + +# Update schema first, then generate missing +class-generator --update-schema --generate-missing + +# Dry run to preview +class-generator --generate-missing --dry-run +``` + +--- + +### Batch Regeneration + +#### `--regenerate-all` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Regenerate all existing generated resource classes using the latest schemas. Only files containing the `# Generated using` marker in `ocp_resources/` are processed. + +Regeneration runs in parallel (up to 10 workers). User-added code (below `# End of generated code`) is preserved during regeneration. + +```bash +# Regenerate all resources +class-generator --regenerate-all + +# With backup +class-generator --regenerate-all --backup + +# Dry run +class-generator --regenerate-all --dry-run + +# Filter to specific resources +class-generator --regenerate-all --filter "Pod*" +``` + +Output summary: + +``` +Regeneration complete: 195 succeeded, 2 failed +Backup files stored in: .backups/backup-20260705-143022 +``` + +--- + +#### `--filter` + +| Property | Value | +|----------|-------| +| **Type** | `STRING` | +| **Default** | `None` | +| **Requires** | `--regenerate-all` | + +Filter which resources to regenerate using a glob pattern matched against the resource Kind name. Uses `fnmatch` semantics. + +```bash +# Regenerate only Pod-related resources +class-generator --regenerate-all --filter "Pod*" + +# Regenerate resources ending in "Service" +class-generator --regenerate-all --filter "*Service" + +# Regenerate a specific resource +class-generator --regenerate-all --filter "VirtualMachine" +``` + +--- + +### Logging + +#### `-v`, `--verbose` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Enable verbose output with debug-level logs. Sets `DEBUG` level on the following loggers: + +- `class_generator.core.schema` +- `class_generator.core.generator` +- `class_generator.core.coverage` +- `class_generator.core.discovery` +- `class_generator.cli` +- `class_generator.utils` +- `ocp_resources` + +```bash +class-generator -k Pod -v +``` + +--- + +## Constraint Rules + +The CLI enforces the following option constraints: + +| Constraint | Rule | +|------------|------| +| `--update-schema` ↔ `--update-schema-for` | Mutually exclusive | +| `--update-schema` (alone) | Cannot combine with `-k`, `--discover-missing`, `--coverage-report`, `--dry-run`, `--overwrite`, `-o`, `--add-tests`, `--regenerate-all` | +| `--update-schema-for` | Cannot combine with `-k`, `--discover-missing`, `--coverage-report`, `--generate-missing`, `--regenerate-all` | +| `--backup` | Requires `--regenerate-all` or `--overwrite` | +| `--filter` | Requires `--regenerate-all` | +| No options | Exits with error — at least one action required | + +--- + +## Execution Order + +When multiple compatible options are specified together, the CLI processes them in this fixed order: + +1. `--update-schema-for` (exits after completion) +2. `--update-schema` (exits unless `--generate-missing` is also set) +3. `--coverage-report` / `--discover-missing` / `--generate-missing` (coverage analysis and reporting) +4. `--generate-missing` (generates classes for missing resources) +5. `--regenerate-all` (batch regeneration, exits after completion) +6. `-k`/`--kind` (normal kind generation) +7. `--add-tests` (test generation and execution) + +--- + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | Generation failure, schema update failure, resource not found, or any kind in a batch failed | +| `2` | Invalid CLI arguments or constraint violation | + +--- + +## Programmatic API + +The generation logic can be invoked directly from Python. See [Generating New Resource Classes with class-generator](generating-resource-classes.html) for usage examples. + +### `class_generator.core.generator.class_generator()` + +```python +from class_generator.core.generator import class_generator + +generated_files: list[str] = class_generator( + kind="Pod", + overwrite=False, + dry_run=False, + output_file="", + output_dir="", + add_tests=False, + called_from_cli=True, + update_schema_executed=False, +) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `kind` | `str` | *(required)* | Kubernetes resource Kind | +| `overwrite` | `bool` | `False` | Overwrite existing files | +| `dry_run` | `bool` | `False` | Preview output without writing files | +| `output_file` | `str` | `""` | Specific output file path | +| `output_dir` | `str` | `""` | Output directory (defaults to `ocp_resources`) | +| `add_tests` | `bool` | `False` | Generate test files | +| `called_from_cli` | `bool` | `True` | Enables interactive prompts when `True` | +| `update_schema_executed` | `bool` | `False` | Whether schema update was already performed | + +**Returns:** `list[str]` — List of generated file paths. Empty list if the kind is not found in the schema mapping (when `called_from_cli=False`). + +**Raises:** +- `RuntimeError` — Kind not found after schema update, or user declined schema update +- `ValueError` — Generated filename contains invalid patterns (single-letter segments) + +--- + +### `class_generator.core.generator.generate_resource_file_from_dict()` + +```python +from class_generator.core.generator import generate_resource_file_from_dict + +orig_filename, generated_filename = generate_resource_file_from_dict( + resource_dict={"kind": "Pod", ...}, + overwrite=False, + dry_run=False, + output_file="", + add_tests=False, + output_file_suffix="", + output_dir="", +) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `resource_dict` | `dict[str, Any]` | *(required)* | Dictionary containing parsed resource information | +| `overwrite` | `bool` | `False` | Overwrite existing files | +| `dry_run` | `bool` | `False` | Preview without writing | +| `output_file` | `str` | `""` | Specific output file path | +| `add_tests` | `bool` | `False` | Generate test files under `class_generator/tests/manifests/` | +| `output_file_suffix` | `str` | `""` | Suffix appended to filename (for API group disambiguation) | +| `output_dir` | `str` | `""` | Output directory (defaults to `ocp_resources`) | + +**Returns:** `tuple[str, str]` — `(original_filename, generated_filename)`. These differ when a `_TEMP.py` file is created. + +--- + +### `class_generator.core.schema.update_kind_schema()` + +```python +from class_generator.core.schema import update_kind_schema + +update_kind_schema(client=None) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `client` | `str \| None` | `None` | Path to `oc`/`kubectl` binary. Auto-detected if `None`. | + +**Raises:** +- `ClusterVersionError` — Cannot determine cluster version +- `RuntimeError` — Failed to fetch OpenAPI v3 index +- `OSError` — Failed to write schema files + +--- + +### `class_generator.core.schema.update_single_resource_schema()` + +```python +from class_generator.core.schema import update_single_resource_schema + +update_single_resource_schema(kind="LlamaStackDistribution", client=None) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `kind` | `str` | *(required)* | Resource Kind name (case-sensitive) | +| `client` | `str \| None` | `None` | Path to `oc`/`kubectl` binary. Auto-detected if `None`. | + +**Raises:** +- `ResourceNotFoundError` — Kind not found on the cluster +- `RuntimeError` — Schema extraction or save failed + +--- + +### `class_generator.core.coverage.analyze_coverage()` + +```python +from class_generator.core.coverage import analyze_coverage + +result: dict[str, Any] = analyze_coverage(resources_dir="ocp_resources") +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `resources_dir` | `str` | `"ocp_resources"` | Directory to scan for wrapper classes | + +**Returns:** `dict[str, Any]` with keys: + +| Key | Type | Description | +|-----|------|-------------| +| `generated_resources` | `list[str]` | Sorted list of auto-generated resource class names | +| `manual_resources` | `list[str]` | Sorted list of manually written resource class names | +| `missing_resources` | `list[dict]` | List of `{"kind": "..."}` for resources in schema but not generated | +| `coverage_stats` | `dict` | Statistics including `total_in_mapping`, `total_generated`, `total_manual`, `coverage_percentage`, `missing_count` | + +--- + +### `class_generator.core.discovery.discover_generated_resources()` + +```python +from class_generator.core.discovery import discover_generated_resources + +resources: list[dict[str, Any]] = discover_generated_resources() +``` + +**Returns:** `list[dict[str, Any]]` — Each dict contains: + +| Key | Type | Description | +|-----|------|-------------| +| `path` | `str` | Full path to the resource file | +| `kind` | `str` | Resource class name | +| `filename` | `str` | File name without extension | +| `has_user_code` | `bool` | Whether file contains user modifications below `# End of generated code` | + +--- + +### `class_generator.core.discovery.discover_cluster_resources()` + +```python +from class_generator.core.discovery import discover_cluster_resources + +resources: dict[str, list[dict[str, Any]]] = discover_cluster_resources( + client=None, + api_group_filter=None, +) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `client` | `DynamicClient \| None` | `None` | Kubernetes dynamic client. Creates one if `None`. | +| `api_group_filter` | `str \| None` | `None` | Filter by API group name | + +**Returns:** `dict[str, list[dict[str, Any]]]` — Mapping of API version to list of resource dicts (`name`, `kind`, `namespaced`). + +**Raises:** `ValueError` — If client is not a `DynamicClient` instance. + +--- + +## Exceptions + +### `class_generator.exceptions.ResourceNotFoundError` + +```python +from class_generator.exceptions import ResourceNotFoundError +``` + +Raised when a resource Kind is not found in the schema mapping or on the cluster. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `kind` | `str` | The Kind that was not found | + +### `class_generator.core.schema.ClusterVersionError` + +```python +from class_generator.core.schema import ClusterVersionError +``` + +Raised when the cluster version cannot be determined (client binary missing, cluster unreachable, or authentication failure). + +--- + +## Schema Files + +The CLI manages two schema files under `class_generator/schema/`: + +| File | Purpose | +|------|---------| +| `__resources-mappings.json.gz` | Compressed JSON mapping of lowercase Kind → list of schemas with GVK metadata and namespaced status | +| `_definitions.json` | JSON Schema definitions for `$ref` resolution during validation | +| `__cluster_version__.txt` | Last cluster version used for schema generation | + +See [Schema Validation and Code Generation Architecture](schema-validation-internals.html) for details on how these files are structured and consumed. + +--- + +## Common Workflows + +### Generate a new resource class + +```bash +class-generator -k MyCustomResource +``` + +### Update schema and regenerate all classes + +```bash +class-generator --update-schema +class-generator --regenerate-all --backup +``` + +### Add a new CRD to an older cluster + +```bash +class-generator --update-schema-for MyNewCRD +class-generator -k MyNewCRD +``` + +### CI/CD coverage check + +```bash +class-generator --coverage-report --json > coverage.json +``` + +### Preview changes before writing + +```bash +class-generator -k Pod --dry-run +class-generator --regenerate-all --dry-run +``` + +--- + +## Related Pages + +- [Generating New Resource Classes with class-generator](generating-resource-classes.html) — Step-by-step guide for scaffolding resource classes +- [Resource and NamespacedResource API](resource-api.html) — API reference for the base classes that generated code extends +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) — How generated subclasses fit into the class hierarchy +- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) — Internals of schema fetching, caching, and code generation +- [Environment Variables and Configuration](environment-variables.html) — Environment variables that affect runtime behavior + +## Related Pages + +- [Generating New Resource Classes with class-generator](generating-resource-classes.html) +- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) +- [Resource and NamespacedResource API](resource-api.html) +- [Environment Variables and Configuration](environment-variables.html) diff --git a/docs/common-patterns.html b/docs/common-patterns.html new file mode 100644 index 0000000000..f1343ff791 --- /dev/null +++ b/docs/common-patterns.html @@ -0,0 +1,1213 @@ + + + + + + Common Resource Patterns - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by reading the knowledge graph and the pages manifest, then explore the codebase.

+

Now let me look at more resource files for networking, RBAC, and DataVolume:

+

Now let me look at the secret, network policy, network attachment definition, and the duplicate API groups handling:

+

Now let me look at the duplicate API groups handling:

+

Now let me look at some files with duplicate kinds to understand the pattern:

+

Now let me look at how to exec in pods and also look at the user_defined_network and apply_yaml approach:

+

Now let me look at how the yaml_file parameter works and look at the event-related code:

+

Now let me check some gateway files to see the pattern for duplicate API groups:

+

Now let me look at the user_defined_network.py for newer networking resources:

+

Now I have enough information. Let me write the documentation page:

+

Common Resource Patterns

+

Copy-paste recipes for the most common Kubernetes and OpenShift resource operations using openshift-python-wrapper.

+
+

Note: All recipes assume you already have a connected client. See Connecting to Clusters for setup instructions.

+
+
# Preamble used by all recipes below
+from ocp_resources.resource import get_client
+client = get_client()
+
+ +
+

Create and Run a Pod

+

Create a pod with a single container and wait for it to start.

+
from ocp_resources.pod import Pod
+
+pod = Pod(
+    client=client,
+    name="my-nginx",
+    namespace="default",
+    containers=[{
+        "name": "nginx",
+        "image": "nginx:1.25",
+        "ports": [{"containerPort": 80}],
+    }],
+    restart_policy="Never",
+)
+pod.deploy()
+pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120)
+
+ +

The deploy() method creates the pod on the cluster. wait_for_status polls until the pod reaches Running. Use restart_policy="Always" for long-running pods.

+
+

Create a Pod as a Context Manager

+

Automatically clean up a pod when the block exits — ideal for tests.

+
from ocp_resources.pod import Pod
+
+with Pod(
+    client=client,
+    name="test-curl",
+    namespace="default",
+    containers=[{
+        "name": "curl",
+        "image": "curlimages/curl:8.5.0",
+        "command": ["sleep", "3600"],
+    }],
+) as pod:
+    pod.wait_for_status(status=Pod.Status.RUNNING, timeout=60)
+    output = pod.execute(command=["curl", "-s", "http://httpbin.org/get"])
+    print(output)
+# Pod is automatically deleted here
+
+ +

The context manager calls deploy() on enter and clean_up() on exit. Set teardown=False to skip automatic deletion.

+
+

Tip: See Creating and Managing Resources for all lifecycle management patterns.

+
+
+

Execute a Command Inside a Pod

+

Run a shell command inside an already-running pod and capture the output.

+
from ocp_resources.pod import Pod
+
+pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True)
+output = pod.execute(command=["cat", "/etc/nginx/nginx.conf"], timeout=30)
+print(output)
+
+ +

execute() streams output via the Kubernetes exec API. Use container="sidecar" to target a specific container in multi-container pods. Set ignore_rc=True to suppress errors from non-zero exit codes.

+
+

Get Pod Logs

+

Retrieve logs from a running or completed pod.

+
from ocp_resources.pod import Pod
+
+pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True)
+
+# Full logs
+print(pod.log())
+
+# Last 50 lines
+print(pod.log(tail_lines=50))
+
+# Logs from a specific container
+print(pod.log(container="sidecar"))
+
+ +

The log() method passes kwargs through to the Kubernetes read_namespaced_pod_log API, so all standard parameters like tail_lines, since_seconds, and container are supported.

+
+

Create a Deployment and Wait for Replicas

+

Deploy an application with multiple replicas and wait until they are all ready.

+
from ocp_resources.deployment import Deployment
+
+dep = Deployment(
+    client=client,
+    name="web-app",
+    namespace="default",
+    replicas=3,
+    selector={"matchLabels": {"app": "web-app"}},
+    template={
+        "metadata": {"labels": {"app": "web-app"}},
+        "spec": {
+            "containers": [{
+                "name": "app",
+                "image": "nginx:1.25",
+                "ports": [{"containerPort": 80}],
+            }]
+        },
+    },
+)
+dep.deploy()
+dep.wait_for_replicas(deployed=True, timeout=300)
+
+ +

wait_for_replicas polls until availableReplicas == readyReplicas == updatedReplicas == spec.replicas. Pass deployed=False to wait for all replicas to be scaled down.

+
+

Scale a Deployment

+

Change the replica count of an existing deployment.

+
from ocp_resources.deployment import Deployment
+
+dep = Deployment(client=client, name="web-app", namespace="default", ensure_exists=True)
+
+# Scale up
+dep.scale_replicas(replica_count=5)
+dep.wait_for_replicas(deployed=True, timeout=300)
+
+# Scale down
+dep.scale_replicas(replica_count=1)
+dep.wait_for_replicas(deployed=True, timeout=300)
+
+ +

scale_replicas patches the deployment's spec.replicas field. Always follow with wait_for_replicas to confirm the rollout completes.

+
+

Create a Service

+

Expose a deployment via a ClusterIP service.

+
from ocp_resources.service import Service
+
+svc = Service(
+    client=client,
+    name="web-app-svc",
+    namespace="default",
+    selector={"app": "web-app"},
+    ports=[{
+        "protocol": "TCP",
+        "port": 80,
+        "targetPort": 80,
+    }],
+    type="ClusterIP",
+)
+svc.deploy()
+
+ +

Change type to "NodePort" or "LoadBalancer" as needed. The selector must match labels on your target pods.

+
+

Create an OpenShift Route

+

Expose a service externally via an OpenShift Route.

+
from ocp_resources.route import Route
+
+route = Route(
+    client=client,
+    name="web-app-route",
+    namespace="default",
+    service="web-app-svc",
+)
+route.deploy()
+
+# Get the assigned hostname
+print(route.host)
+
+ +

For TLS re-encrypt routes, pass destination_ca_cert="<PEM certificate string>". Access the exposed service name with route.exposed_service and the TLS termination type with route.termination.

+
+

Create a Namespace

+

Create a namespace (or use as a context manager for automatic cleanup).

+
from ocp_resources.namespace import Namespace
+
+# Simple creation
+ns = Namespace(client=client, name="test-ns")
+ns.deploy()
+
+# As a context manager (deleted on exit)
+with Namespace(client=client, name="ephemeral-ns") as ns:
+    # ... do work in the namespace ...
+    pass
+
+ +

Namespace extends Resource (cluster-scoped), so no namespace parameter is needed.

+
+

Create a ConfigMap

+

Store configuration data for pods to consume.

+
from ocp_resources.config_map import ConfigMap
+
+cm = ConfigMap(
+    client=client,
+    name="app-config",
+    namespace="default",
+    data={
+        "DATABASE_URL": "postgres://db:5432/myapp",
+        "LOG_LEVEL": "info",
+    },
+)
+cm.deploy()
+
+ +

Use binary_data for non-UTF-8 content. Set immutable=True to prevent changes after creation.

+
+

Create a Secret

+

Store sensitive data such as credentials.

+
from ocp_resources.secret import Secret
+
+secret = Secret(
+    client=client,
+    name="db-credentials",
+    namespace="default",
+    string_data={
+        "username": "admin",
+        "password": "s3cur3-pa$$word",
+    },
+    type="Opaque",
+)
+secret.deploy()
+
+ +

Use data_dict instead of string_data if your values are already base64-encoded. Secret data is automatically hashed in log output for security.

+
+

Create a Job

+

Run a one-off task to completion.

+
from ocp_resources.job import Job
+
+job = Job(
+    client=client,
+    name="db-migration",
+    namespace="default",
+    backoff_limit=3,
+    restart_policy="Never",
+    containers=[{
+        "name": "migrate",
+        "image": "my-app:latest",
+        "command": ["python", "manage.py", "migrate"],
+    }],
+)
+job.deploy()
+job.wait_for_condition(
+    condition=Job.Condition.COMPLETE,
+    status=Job.Condition.Status.TRUE,
+    timeout=300,
+)
+
+ +

Set background_propagation_policy="Background" to delete leftover pods when the job is cleaned up. The backoff_limit controls how many times the job retries on failure.

+
+

Create a PersistentVolumeClaim

+

Request persistent storage for your workloads.

+
from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+
+pvc = PersistentVolumeClaim(
+    client=client,
+    name="app-data",
+    namespace="default",
+    accessmodes=PersistentVolumeClaim.AccessMode.RWO,
+    size="10Gi",
+    storage_class="gp3-csi",
+    volume_mode=PersistentVolumeClaim.VolumeMode.FILE,
+)
+pvc.deploy()
+pvc.wait_for_status(status=PersistentVolumeClaim.Status.BOUND, timeout=120)
+
+ +

Use the AccessMode and VolumeMode constants instead of raw strings. The storage_class must match an available StorageClass on your cluster.

+
+

Create a DataVolume (KubeVirt / CDI)

+

Import a VM disk image into a PVC using the Containerized Data Importer.

+
from ocp_resources.datavolume import DataVolume
+
+dv = DataVolume(
+    client=client,
+    name="fedora-disk",
+    namespace="default",
+    source_dict={"http": {"url": "https://download.fedoraproject.org/pub/fedora/linux/releases/40/Cloud/x86_64/images/Fedora-Cloud-Base-40-1.14.x86_64.qcow2"}},
+    size="30Gi",
+    storage_class="ocs-storagecluster-ceph-rbd-virtualization",
+    access_modes=DataVolume.AccessMode.RWX,
+    volume_mode=DataVolume.VolumeMode.BLOCK,
+    api_name="storage",
+)
+dv.deploy()
+dv.wait_for_dv_success(timeout=600)
+
+ +

Always pass api_name="storage" explicitly — the default will change in a future release. Use source_dict={"blank": {}} for empty disks, or source_dict={"pvc": {"name": "source-pvc", "namespace": "default"}} for cloning.

+
    +
  • Clone a DataVolume: + python + dv_clone = DataVolume( + client=client, + name="fedora-disk-clone", + namespace="default", + source_dict={"pvc": {"name": "fedora-disk", "namespace": "default"}}, + size="30Gi", + storage_class="ocs-storagecluster-ceph-rbd-virtualization", + access_modes=DataVolume.AccessMode.RWX, + volume_mode=DataVolume.VolumeMode.BLOCK, + api_name="storage", + )
  • +
+
+

Tip: See Working with Virtual Machines (KubeVirt) for full VM lifecycle recipes.

+
+
+

Create a NetworkPolicy

+

Restrict traffic to pods matching a label selector.

+
from ocp_resources.network_policy import NetworkPolicy
+
+netpol = NetworkPolicy(
+    client=client,
+    name="allow-http-only",
+    namespace="default",
+    pod_selector={"matchLabels": {"app": "web-app"}},
+    policy_types=["Ingress"],
+    ingress=[{
+        "ports": [{"protocol": "TCP", "port": 80}],
+        "from": [
+            {"namespaceSelector": {"matchLabels": {"env": "production"}}},
+        ],
+    }],
+)
+netpol.deploy()
+
+ +

This allows TCP port 80 ingress only from namespaces labeled env=production. Omit ingress and set policy_types=["Ingress"] to deny all inbound traffic.

+
+

Create a NetworkAttachmentDefinition

+

Attach pods to a secondary network using Multus.

+
from ocp_resources.network_attachment_definition import LinuxBridgeNetworkAttachmentDefinition
+
+nad = LinuxBridgeNetworkAttachmentDefinition(
+    client=client,
+    name="br-100",
+    namespace="default",
+    bridge_name="br-100",
+    cni_type="cnv-bridge",
+    vlan=100,
+    mtu=1500,
+)
+nad.deploy()
+
+ +

Variant classes are available for different bridge types:

+
    +
  • LinuxBridgeNetworkAttachmentDefinition — Linux bridge (cnv-bridge)
  • +
  • OvsBridgeNetworkAttachmentDefinition — OVS bridge
  • +
  • OVNOverlayNetworkAttachmentDefinition — OVN overlay (layer 2/3)
  • +
+
+

Create an RBAC Role and RoleBinding

+

Grant specific permissions to a service account within a namespace.

+
from ocp_resources.role import Role
+from ocp_resources.role_binding import RoleBinding
+from ocp_resources.service_account import ServiceAccount
+
+sa = ServiceAccount(
+    client=client,
+    name="app-sa",
+    namespace="default",
+)
+sa.deploy()
+
+role = Role(
+    client=client,
+    name="pod-reader",
+    namespace="default",
+    rules=[{
+        "apiGroups": [""],
+        "resources": ["pods", "pods/log"],
+        "verbs": ["get", "list", "watch"],
+    }],
+)
+role.deploy()
+
+binding = RoleBinding(
+    client=client,
+    name="app-sa-pod-reader",
+    namespace="default",
+    subjects_kind="ServiceAccount",
+    subjects_name="app-sa",
+    subjects_namespace="default",
+    role_ref_kind="Role",
+    role_ref_name="pod-reader",
+)
+binding.deploy()
+
+ +

This grants the app-sa service account read-only access to pods in the default namespace.

+
+

Create a ClusterRole and ClusterRoleBinding

+

Grant cluster-wide permissions to a service account.

+
from ocp_resources.cluster_role import ClusterRole
+from ocp_resources.cluster_role_binding import ClusterRoleBinding
+
+cr = ClusterRole(
+    client=client,
+    name="node-viewer",
+    rules=[{
+        "apiGroups": [""],
+        "resources": ["nodes"],
+        "verbs": ["get", "list", "watch"],
+    }],
+)
+cr.deploy()
+
+crb = ClusterRoleBinding(
+    client=client,
+    name="app-sa-node-viewer",
+    cluster_role="node-viewer",
+    subjects=[{
+        "kind": "ServiceAccount",
+        "name": "app-sa",
+        "namespace": "default",
+    }],
+)
+crb.deploy()
+
+ +

ClusterRole and ClusterRoleBinding are cluster-scoped (Resource subclasses), so no namespace parameter is needed on the role or binding itself.

+
+

List and Filter Resources

+

Query existing resources using label and field selectors.

+
from ocp_resources.pod import Pod
+from ocp_resources.namespace import Namespace
+
+# List all pods in a namespace
+for pod in Pod.get(client=client, namespace="default"):
+    print(f"{pod.name}{pod.status}")
+
+# Filter by label
+for pod in Pod.get(client=client, namespace="default", label_selector="app=web-app"):
+    print(pod.name)
+
+# Filter by field
+for pod in Pod.get(client=client, namespace="default", field_selector="status.phase=Running"):
+    print(pod.name)
+
+# List cluster-scoped resources (no namespace)
+for ns in Namespace.get(client=client, label_selector="env=staging"):
+    print(ns.name)
+
+ +

The get() class method returns a generator of resource objects. Pass raw=True to get raw ResourceInstance objects instead.

+
+

Tip: See Querying and Watching Resources for advanced querying patterns.

+
+
+

Create a Resource from a YAML File

+

Load a resource definition from an existing YAML file.

+
from ocp_resources.pod import Pod
+
+pod = Pod(
+    client=client,
+    yaml_file="manifests/my-pod.yaml",
+)
+pod.deploy()
+
+ +

When using yaml_file, the name and namespace are read from the file automatically. You cannot combine yaml_file with kind_dict.

+
+

Create a Resource from a Dictionary

+

Pass a raw Kubernetes resource dict directly.

+
from ocp_resources.deployment import Deployment
+
+dep = Deployment(
+    client=client,
+    kind_dict={
+        "apiVersion": "apps/v1",
+        "kind": "Deployment",
+        "metadata": {
+            "name": "from-dict-app",
+            "namespace": "default",
+        },
+        "spec": {
+            "replicas": 2,
+            "selector": {"matchLabels": {"app": "from-dict"}},
+            "template": {
+                "metadata": {"labels": {"app": "from-dict"}},
+                "spec": {
+                    "containers": [{
+                        "name": "app",
+                        "image": "nginx:1.25",
+                    }],
+                },
+            },
+        },
+    },
+)
+dep.deploy()
+
+ +

Using kind_dict bypasses all constructor logic in to_dict() — the dictionary is sent as-is to the API. This is useful when migrating existing manifests.

+
+

Handle Resources with Duplicate API Groups

+

Some Kubernetes resource kinds (e.g., DNS, Ingress, Gateway) exist in multiple API groups. The wrapper provides separate modules for each variant.

+
# DNS from config.openshift.io (cluster DNS config)
+from ocp_resources.dns_config_openshift_io import DNS as DNSConfig
+
+dns_config = DNSConfig(client=client, name="cluster", ensure_exists=True)
+print(dns_config.instance.spec.baseDomain)
+
+# DNS from operator.openshift.io (CoreDNS operator)
+from ocp_resources.dns_operator_openshift_io import DNS as DNSOperator
+
+dns_operator = DNSOperator(client=client, name="default", ensure_exists=True)
+print(dns_operator.instance.spec.logLevel)
+
+ +
# Ingress from config.openshift.io (cluster ingress config)
+from ocp_resources.ingress_config_openshift_io import Ingress as IngressConfig
+
+ingress = IngressConfig(client=client, name="cluster", ensure_exists=True)
+print(ingress.instance.spec.domain)
+
+# Ingress from networking.k8s.io (K8s Ingress rules)
+from ocp_resources.ingress_networking_k8s_io import Ingress as K8sIngress
+
+k8s_ingress = K8sIngress(
+    client=client,
+    name="my-app-ingress",
+    rules=[{
+        "host": "app.example.com",
+        "http": {
+            "paths": [{
+                "path": "/",
+                "pathType": "Prefix",
+                "backend": {"service": {"name": "web-app-svc", "port": {"number": 80}}},
+            }],
+        },
+    }],
+)
+
+ +

The naming convention for duplicate-kind modules is <kind>_<api_group_snake_case>.py. Use Python import aliases (as) to distinguish them in your code.

+

Common duplicate kinds and their modules:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KindModuleAPI Group
DNSdns_config_openshift_ioconfig.openshift.io
DNSdns_operator_openshift_iooperator.openshift.io
Ingressingress_config_openshift_ioconfig.openshift.io
Ingressingress_networking_k8s_ionetworking.k8s.io
Gatewaygatewaynetworking.istio.io
Gatewaygateway_gateway_networking_k8s_iogateway.networking.k8s.io
Gatewaygateway_networking_istio_ionetworking.istio.io
+
+

Warning: Importing the wrong module will target the wrong API group, causing NotFoundError or unexpected behavior. Always verify the api_group attribute on your resource class.

+
+
+

Manage Multiple Similar Resources with ResourceList

+

Create N copies of a resource with auto-numbered names.

+
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import ResourceList
+
+with ResourceList(
+    resource_class=Namespace,
+    num_resources=3,
+    client=client,
+    name="perf-test-ns",
+) as namespaces:
+    # Creates: perf-test-ns-1, perf-test-ns-2, perf-test-ns-3
+    for ns in namespaces:
+        print(ns.name)
+# All 3 namespaces are deleted on exit
+
+ +
+

Tip: See Managing Bulk Resources with ResourceList for NamespacedResourceList and other bulk patterns.

+
+
+

Deploy One Resource Per Namespace with NamespacedResourceList

+

Create an identical namespaced resource in each of several namespaces.

+
from ocp_resources.namespace import Namespace
+from ocp_resources.config_map import ConfigMap
+from ocp_resources.resource import ResourceList, NamespacedResourceList
+
+with ResourceList(
+    resource_class=Namespace,
+    num_resources=3,
+    client=client,
+    name="team-ns",
+) as namespaces:
+    with NamespacedResourceList(
+        resource_class=ConfigMap,
+        namespaces=namespaces,
+        client=client,
+        name="shared-config",
+        data={"REGION": "us-east-1"},
+    ) as configmaps:
+        # One ConfigMap "shared-config" in each of team-ns-1, team-ns-2, team-ns-3
+        for cm in configmaps:
+            print(f"{cm.namespace}/{cm.name}")
+
+ +

NamespacedResourceList requires a ResourceList of Namespace objects. All resources are cleaned up in reverse order on exit.

+
+

Temporarily Edit a Resource with ResourceEditor

+

Apply temporary patches during a test and automatically restore original values.

+
from ocp_resources.resource import ResourceEditor
+from ocp_resources.config_map import ConfigMap
+
+cm = ConfigMap(client=client, name="app-config", namespace="default", ensure_exists=True)
+
+with ResourceEditor(
+    patches={cm: {"data": {"LOG_LEVEL": "debug"}}},
+    action="update",
+):
+    # LOG_LEVEL is now "debug"
+    print(cm.instance.data.LOG_LEVEL)
+# Original LOG_LEVEL is restored here
+
+ +

ResourceEditor backs up original values on enter and restores them on exit. Use action="replace" when you need to remove fields entirely rather than patch them.

+
+

Tip: See Editing Resources Temporarily with ResourceEditor for complete details.

+
+
+

Validate a Resource Before Creating It

+

Catch schema errors before submitting to the API server.

+
from ocp_resources.pod import Pod
+from ocp_resources.exceptions import ValidationError
+
+pod = Pod(
+    client=client,
+    name="validated-pod",
+    namespace="default",
+    containers=[{
+        "name": "app",
+        "image": "nginx:1.25",
+    }],
+    schema_validation_enabled=True,  # Auto-validate on create()
+)
+
+# Manual validation
+try:
+    pod.validate()
+    print("Resource is valid")
+except ValidationError as e:
+    print(f"Validation failed: {e}")
+
+# Or validate a raw dict without instantiation
+try:
+    Pod.validate_dict({
+        "apiVersion": "v1",
+        "kind": "Pod",
+        "metadata": {"name": "test"},
+        "spec": {"containers": [{"name": "app", "image": "nginx"}]},
+    })
+except ValidationError as e:
+    print(f"Dict validation failed: {e}")
+
+ +

When schema_validation_enabled=True, create() and update_replace() automatically validate before sending to the API. Validation requires OpenAPI schemas to be available.

+
+

Tip: See Validating Resources Against OpenAPI Schemas for schema setup and troubleshooting.

+
+
+

Check If a Resource Exists

+

Verify a resource exists before operating on it.

+
from ocp_resources.deployment import Deployment
+
+dep = Deployment(client=client, name="web-app", namespace="default")
+
+if dep.exists:
+    print(f"Deployment exists, status: {dep.instance.status.availableReplicas} replicas available")
+else:
+    print("Deployment not found")
+
+ +

exists returns the resource instance if found or None if not. For fail-fast behavior, use ensure_exists=True in the constructor — it raises ResourceNotFoundError immediately if the resource is missing.

+
+

Watch Resource Events

+

Stream events related to a specific resource.

+
from ocp_resources.pod import Pod
+
+pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True)
+
+# Watch events for this pod (10 second window)
+for event in pod.events(timeout=10):
+    ev = event["object"]
+    print(f"[{ev.type}] {ev.reason}: {ev.message}")
+
+ +

Use field_selector to narrow results further, for example: field_selector="type==Warning". The events() method automatically filters by involvedObject.name.

+
+

List Recent Events in a Namespace

+

Get existing events (not a watch stream) from the last N seconds.

+
from ocp_resources.event import Event
+
+# Warning events from the last 5 minutes
+events = Event.list(
+    client=client,
+    namespace="default",
+    field_selector="type==Warning",
+    since_seconds=300,
+)
+for ev in events:
+    print(f"[{ev.reason}] {ev.message}")
+
+ +

Event.list() returns a sorted list (most recent first), unlike Event.get() which is a live watch stream.

+
+

Wait for a Resource Condition

+

Block until a resource reaches a specific condition.

+
from ocp_resources.deployment import Deployment
+
+dep = Deployment(client=client, name="web-app", namespace="default", ensure_exists=True)
+
+dep.wait_for_condition(
+    condition="Available",
+    status="True",
+    timeout=300,
+)
+
+ +

Use stop_condition to fail fast if an unrecoverable condition is detected:

+
dep.wait_for_condition(
+    condition="Available",
+    status="True",
+    timeout=300,
+    stop_condition="ReplicaFailure",
+    stop_status="True",
+)
+
+ +
+

Tip: See Waiting for Resource Conditions and Status for advanced waiting patterns.

+
+
+

Get a Resource's YAML Representation

+

Dump the intended resource dict as YAML (useful for debugging).

+
from ocp_resources.pod import Pod
+
+pod = Pod(
+    client=client,
+    name="debug-pod",
+    namespace="default",
+    containers=[{"name": "app", "image": "busybox", "command": ["sleep", "3600"]}],
+)
+print(pod.to_yaml())
+
+ +

to_yaml() calls to_dict() internally and returns the YAML string. This shows what would be sent to the API, not the live state.

+
+

Get the Pod's Node and IP

+

Access commonly needed runtime properties of a pod.

+
from ocp_resources.pod import Pod
+
+pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True)
+
+# Node where the pod is running
+print(f"Node: {pod.node.name}")
+
+# Pod IP address
+print(f"IP: {pod.ip}")
+
+ +

The node property returns a Node resource object. The ip property reads from status.podIP.

+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/common-patterns.md b/docs/common-patterns.md new file mode 100644 index 0000000000..5edb67f376 --- /dev/null +++ b/docs/common-patterns.md @@ -0,0 +1,941 @@ +Let me start by reading the knowledge graph and the pages manifest, then explore the codebase. + +Now let me look at more resource files for networking, RBAC, and DataVolume: + +Now let me look at the secret, network policy, network attachment definition, and the duplicate API groups handling: + +Now let me look at the duplicate API groups handling: + +Now let me look at some files with duplicate kinds to understand the pattern: + +Now let me look at how to exec in pods and also look at the user_defined_network and apply_yaml approach: + +Now let me look at how the `yaml_file` parameter works and look at the event-related code: + +Now let me check some gateway files to see the pattern for duplicate API groups: + +Now let me look at the `user_defined_network.py` for newer networking resources: + +Now I have enough information. Let me write the documentation page: + +# Common Resource Patterns + +Copy-paste recipes for the most common Kubernetes and OpenShift resource operations using openshift-python-wrapper. + +> **Note:** All recipes assume you already have a connected client. See [Connecting to Clusters](connecting-to-clusters.html) for setup instructions. + +```python +# Preamble used by all recipes below +from ocp_resources.resource import get_client +client = get_client() +``` + +--- + +## Create and Run a Pod + +Create a pod with a single container and wait for it to start. + +```python +from ocp_resources.pod import Pod + +pod = Pod( + client=client, + name="my-nginx", + namespace="default", + containers=[{ + "name": "nginx", + "image": "nginx:1.25", + "ports": [{"containerPort": 80}], + }], + restart_policy="Never", +) +pod.deploy() +pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120) +``` + +The `deploy()` method creates the pod on the cluster. `wait_for_status` polls until the pod reaches `Running`. Use `restart_policy="Always"` for long-running pods. + +--- + +## Create a Pod as a Context Manager + +Automatically clean up a pod when the block exits — ideal for tests. + +```python +from ocp_resources.pod import Pod + +with Pod( + client=client, + name="test-curl", + namespace="default", + containers=[{ + "name": "curl", + "image": "curlimages/curl:8.5.0", + "command": ["sleep", "3600"], + }], +) as pod: + pod.wait_for_status(status=Pod.Status.RUNNING, timeout=60) + output = pod.execute(command=["curl", "-s", "http://httpbin.org/get"]) + print(output) +# Pod is automatically deleted here +``` + +The context manager calls `deploy()` on enter and `clean_up()` on exit. Set `teardown=False` to skip automatic deletion. + +> **Tip:** See [Creating and Managing Resources](creating-and-managing-resources.html) for all lifecycle management patterns. + +--- + +## Execute a Command Inside a Pod + +Run a shell command inside an already-running pod and capture the output. + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True) +output = pod.execute(command=["cat", "/etc/nginx/nginx.conf"], timeout=30) +print(output) +``` + +`execute()` streams output via the Kubernetes exec API. Use `container="sidecar"` to target a specific container in multi-container pods. Set `ignore_rc=True` to suppress errors from non-zero exit codes. + +--- + +## Get Pod Logs + +Retrieve logs from a running or completed pod. + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True) + +# Full logs +print(pod.log()) + +# Last 50 lines +print(pod.log(tail_lines=50)) + +# Logs from a specific container +print(pod.log(container="sidecar")) +``` + +The `log()` method passes kwargs through to the Kubernetes `read_namespaced_pod_log` API, so all standard parameters like `tail_lines`, `since_seconds`, and `container` are supported. + +--- + +## Create a Deployment and Wait for Replicas + +Deploy an application with multiple replicas and wait until they are all ready. + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment( + client=client, + name="web-app", + namespace="default", + replicas=3, + selector={"matchLabels": {"app": "web-app"}}, + template={ + "metadata": {"labels": {"app": "web-app"}}, + "spec": { + "containers": [{ + "name": "app", + "image": "nginx:1.25", + "ports": [{"containerPort": 80}], + }] + }, + }, +) +dep.deploy() +dep.wait_for_replicas(deployed=True, timeout=300) +``` + +`wait_for_replicas` polls until `availableReplicas == readyReplicas == updatedReplicas == spec.replicas`. Pass `deployed=False` to wait for all replicas to be scaled down. + +--- + +## Scale a Deployment + +Change the replica count of an existing deployment. + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment(client=client, name="web-app", namespace="default", ensure_exists=True) + +# Scale up +dep.scale_replicas(replica_count=5) +dep.wait_for_replicas(deployed=True, timeout=300) + +# Scale down +dep.scale_replicas(replica_count=1) +dep.wait_for_replicas(deployed=True, timeout=300) +``` + +`scale_replicas` patches the deployment's `spec.replicas` field. Always follow with `wait_for_replicas` to confirm the rollout completes. + +--- + +## Create a Service + +Expose a deployment via a ClusterIP service. + +```python +from ocp_resources.service import Service + +svc = Service( + client=client, + name="web-app-svc", + namespace="default", + selector={"app": "web-app"}, + ports=[{ + "protocol": "TCP", + "port": 80, + "targetPort": 80, + }], + type="ClusterIP", +) +svc.deploy() +``` + +Change `type` to `"NodePort"` or `"LoadBalancer"` as needed. The `selector` must match labels on your target pods. + +--- + +## Create an OpenShift Route + +Expose a service externally via an OpenShift Route. + +```python +from ocp_resources.route import Route + +route = Route( + client=client, + name="web-app-route", + namespace="default", + service="web-app-svc", +) +route.deploy() + +# Get the assigned hostname +print(route.host) +``` + +For TLS re-encrypt routes, pass `destination_ca_cert=""`. Access the exposed service name with `route.exposed_service` and the TLS termination type with `route.termination`. + +--- + +## Create a Namespace + +Create a namespace (or use as a context manager for automatic cleanup). + +```python +from ocp_resources.namespace import Namespace + +# Simple creation +ns = Namespace(client=client, name="test-ns") +ns.deploy() + +# As a context manager (deleted on exit) +with Namespace(client=client, name="ephemeral-ns") as ns: + # ... do work in the namespace ... + pass +``` + +`Namespace` extends `Resource` (cluster-scoped), so no `namespace` parameter is needed. + +--- + +## Create a ConfigMap + +Store configuration data for pods to consume. + +```python +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap( + client=client, + name="app-config", + namespace="default", + data={ + "DATABASE_URL": "postgres://db:5432/myapp", + "LOG_LEVEL": "info", + }, +) +cm.deploy() +``` + +Use `binary_data` for non-UTF-8 content. Set `immutable=True` to prevent changes after creation. + +--- + +## Create a Secret + +Store sensitive data such as credentials. + +```python +from ocp_resources.secret import Secret + +secret = Secret( + client=client, + name="db-credentials", + namespace="default", + string_data={ + "username": "admin", + "password": "s3cur3-pa$$word", + }, + type="Opaque", +) +secret.deploy() +``` + +Use `data_dict` instead of `string_data` if your values are already base64-encoded. Secret data is automatically hashed in log output for security. + +--- + +## Create a Job + +Run a one-off task to completion. + +```python +from ocp_resources.job import Job + +job = Job( + client=client, + name="db-migration", + namespace="default", + backoff_limit=3, + restart_policy="Never", + containers=[{ + "name": "migrate", + "image": "my-app:latest", + "command": ["python", "manage.py", "migrate"], + }], +) +job.deploy() +job.wait_for_condition( + condition=Job.Condition.COMPLETE, + status=Job.Condition.Status.TRUE, + timeout=300, +) +``` + +Set `background_propagation_policy="Background"` to delete leftover pods when the job is cleaned up. The `backoff_limit` controls how many times the job retries on failure. + +--- + +## Create a PersistentVolumeClaim + +Request persistent storage for your workloads. + +```python +from ocp_resources.persistent_volume_claim import PersistentVolumeClaim + +pvc = PersistentVolumeClaim( + client=client, + name="app-data", + namespace="default", + accessmodes=PersistentVolumeClaim.AccessMode.RWO, + size="10Gi", + storage_class="gp3-csi", + volume_mode=PersistentVolumeClaim.VolumeMode.FILE, +) +pvc.deploy() +pvc.wait_for_status(status=PersistentVolumeClaim.Status.BOUND, timeout=120) +``` + +Use the `AccessMode` and `VolumeMode` constants instead of raw strings. The `storage_class` must match an available StorageClass on your cluster. + +--- + +## Create a DataVolume (KubeVirt / CDI) + +Import a VM disk image into a PVC using the Containerized Data Importer. + +```python +from ocp_resources.datavolume import DataVolume + +dv = DataVolume( + client=client, + name="fedora-disk", + namespace="default", + source_dict={"http": {"url": "https://download.fedoraproject.org/pub/fedora/linux/releases/40/Cloud/x86_64/images/Fedora-Cloud-Base-40-1.14.x86_64.qcow2"}}, + size="30Gi", + storage_class="ocs-storagecluster-ceph-rbd-virtualization", + access_modes=DataVolume.AccessMode.RWX, + volume_mode=DataVolume.VolumeMode.BLOCK, + api_name="storage", +) +dv.deploy() +dv.wait_for_dv_success(timeout=600) +``` + +Always pass `api_name="storage"` explicitly — the default will change in a future release. Use `source_dict={"blank": {}}` for empty disks, or `source_dict={"pvc": {"name": "source-pvc", "namespace": "default"}}` for cloning. + +- **Clone a DataVolume:** + ```python + dv_clone = DataVolume( + client=client, + name="fedora-disk-clone", + namespace="default", + source_dict={"pvc": {"name": "fedora-disk", "namespace": "default"}}, + size="30Gi", + storage_class="ocs-storagecluster-ceph-rbd-virtualization", + access_modes=DataVolume.AccessMode.RWX, + volume_mode=DataVolume.VolumeMode.BLOCK, + api_name="storage", + ) + ``` + +> **Tip:** See [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) for full VM lifecycle recipes. + +--- + +## Create a NetworkPolicy + +Restrict traffic to pods matching a label selector. + +```python +from ocp_resources.network_policy import NetworkPolicy + +netpol = NetworkPolicy( + client=client, + name="allow-http-only", + namespace="default", + pod_selector={"matchLabels": {"app": "web-app"}}, + policy_types=["Ingress"], + ingress=[{ + "ports": [{"protocol": "TCP", "port": 80}], + "from": [ + {"namespaceSelector": {"matchLabels": {"env": "production"}}}, + ], + }], +) +netpol.deploy() +``` + +This allows TCP port 80 ingress only from namespaces labeled `env=production`. Omit `ingress` and set `policy_types=["Ingress"]` to deny all inbound traffic. + +--- + +## Create a NetworkAttachmentDefinition + +Attach pods to a secondary network using Multus. + +```python +from ocp_resources.network_attachment_definition import LinuxBridgeNetworkAttachmentDefinition + +nad = LinuxBridgeNetworkAttachmentDefinition( + client=client, + name="br-100", + namespace="default", + bridge_name="br-100", + cni_type="cnv-bridge", + vlan=100, + mtu=1500, +) +nad.deploy() +``` + +Variant classes are available for different bridge types: +- `LinuxBridgeNetworkAttachmentDefinition` — Linux bridge (cnv-bridge) +- `OvsBridgeNetworkAttachmentDefinition` — OVS bridge +- `OVNOverlayNetworkAttachmentDefinition` — OVN overlay (layer 2/3) + +--- + +## Create an RBAC Role and RoleBinding + +Grant specific permissions to a service account within a namespace. + +```python +from ocp_resources.role import Role +from ocp_resources.role_binding import RoleBinding +from ocp_resources.service_account import ServiceAccount + +sa = ServiceAccount( + client=client, + name="app-sa", + namespace="default", +) +sa.deploy() + +role = Role( + client=client, + name="pod-reader", + namespace="default", + rules=[{ + "apiGroups": [""], + "resources": ["pods", "pods/log"], + "verbs": ["get", "list", "watch"], + }], +) +role.deploy() + +binding = RoleBinding( + client=client, + name="app-sa-pod-reader", + namespace="default", + subjects_kind="ServiceAccount", + subjects_name="app-sa", + subjects_namespace="default", + role_ref_kind="Role", + role_ref_name="pod-reader", +) +binding.deploy() +``` + +This grants the `app-sa` service account read-only access to pods in the `default` namespace. + +--- + +## Create a ClusterRole and ClusterRoleBinding + +Grant cluster-wide permissions to a service account. + +```python +from ocp_resources.cluster_role import ClusterRole +from ocp_resources.cluster_role_binding import ClusterRoleBinding + +cr = ClusterRole( + client=client, + name="node-viewer", + rules=[{ + "apiGroups": [""], + "resources": ["nodes"], + "verbs": ["get", "list", "watch"], + }], +) +cr.deploy() + +crb = ClusterRoleBinding( + client=client, + name="app-sa-node-viewer", + cluster_role="node-viewer", + subjects=[{ + "kind": "ServiceAccount", + "name": "app-sa", + "namespace": "default", + }], +) +crb.deploy() +``` + +`ClusterRole` and `ClusterRoleBinding` are cluster-scoped (`Resource` subclasses), so no `namespace` parameter is needed on the role or binding itself. + +--- + +## List and Filter Resources + +Query existing resources using label and field selectors. + +```python +from ocp_resources.pod import Pod +from ocp_resources.namespace import Namespace + +# List all pods in a namespace +for pod in Pod.get(client=client, namespace="default"): + print(f"{pod.name} — {pod.status}") + +# Filter by label +for pod in Pod.get(client=client, namespace="default", label_selector="app=web-app"): + print(pod.name) + +# Filter by field +for pod in Pod.get(client=client, namespace="default", field_selector="status.phase=Running"): + print(pod.name) + +# List cluster-scoped resources (no namespace) +for ns in Namespace.get(client=client, label_selector="env=staging"): + print(ns.name) +``` + +The `get()` class method returns a generator of resource objects. Pass `raw=True` to get raw `ResourceInstance` objects instead. + +> **Tip:** See [Querying and Watching Resources](querying-resources.html) for advanced querying patterns. + +--- + +## Create a Resource from a YAML File + +Load a resource definition from an existing YAML file. + +```python +from ocp_resources.pod import Pod + +pod = Pod( + client=client, + yaml_file="manifests/my-pod.yaml", +) +pod.deploy() +``` + +When using `yaml_file`, the `name` and `namespace` are read from the file automatically. You cannot combine `yaml_file` with `kind_dict`. + +--- + +## Create a Resource from a Dictionary + +Pass a raw Kubernetes resource dict directly. + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment( + client=client, + kind_dict={ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "from-dict-app", + "namespace": "default", + }, + "spec": { + "replicas": 2, + "selector": {"matchLabels": {"app": "from-dict"}}, + "template": { + "metadata": {"labels": {"app": "from-dict"}}, + "spec": { + "containers": [{ + "name": "app", + "image": "nginx:1.25", + }], + }, + }, + }, + }, +) +dep.deploy() +``` + +Using `kind_dict` bypasses all constructor logic in `to_dict()` — the dictionary is sent as-is to the API. This is useful when migrating existing manifests. + +--- + +## Handle Resources with Duplicate API Groups + +Some Kubernetes resource kinds (e.g., `DNS`, `Ingress`, `Gateway`) exist in multiple API groups. The wrapper provides separate modules for each variant. + +```python +# DNS from config.openshift.io (cluster DNS config) +from ocp_resources.dns_config_openshift_io import DNS as DNSConfig + +dns_config = DNSConfig(client=client, name="cluster", ensure_exists=True) +print(dns_config.instance.spec.baseDomain) + +# DNS from operator.openshift.io (CoreDNS operator) +from ocp_resources.dns_operator_openshift_io import DNS as DNSOperator + +dns_operator = DNSOperator(client=client, name="default", ensure_exists=True) +print(dns_operator.instance.spec.logLevel) +``` + +```python +# Ingress from config.openshift.io (cluster ingress config) +from ocp_resources.ingress_config_openshift_io import Ingress as IngressConfig + +ingress = IngressConfig(client=client, name="cluster", ensure_exists=True) +print(ingress.instance.spec.domain) + +# Ingress from networking.k8s.io (K8s Ingress rules) +from ocp_resources.ingress_networking_k8s_io import Ingress as K8sIngress + +k8s_ingress = K8sIngress( + client=client, + name="my-app-ingress", + rules=[{ + "host": "app.example.com", + "http": { + "paths": [{ + "path": "/", + "pathType": "Prefix", + "backend": {"service": {"name": "web-app-svc", "port": {"number": 80}}}, + }], + }, + }], +) +``` + +The naming convention for duplicate-kind modules is `_.py`. Use Python import aliases (`as`) to distinguish them in your code. + +**Common duplicate kinds and their modules:** + +| Kind | Module | API Group | +|------|--------|-----------| +| `DNS` | `dns_config_openshift_io` | `config.openshift.io` | +| `DNS` | `dns_operator_openshift_io` | `operator.openshift.io` | +| `Ingress` | `ingress_config_openshift_io` | `config.openshift.io` | +| `Ingress` | `ingress_networking_k8s_io` | `networking.k8s.io` | +| `Gateway` | `gateway` | `networking.istio.io` | +| `Gateway` | `gateway_gateway_networking_k8s_io` | `gateway.networking.k8s.io` | +| `Gateway` | `gateway_networking_istio_io` | `networking.istio.io` | + +> **Warning:** Importing the wrong module will target the wrong API group, causing `NotFoundError` or unexpected behavior. Always verify the `api_group` attribute on your resource class. + +--- + +## Manage Multiple Similar Resources with ResourceList + +Create N copies of a resource with auto-numbered names. + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import ResourceList + +with ResourceList( + resource_class=Namespace, + num_resources=3, + client=client, + name="perf-test-ns", +) as namespaces: + # Creates: perf-test-ns-1, perf-test-ns-2, perf-test-ns-3 + for ns in namespaces: + print(ns.name) +# All 3 namespaces are deleted on exit +``` + +> **Tip:** See [Managing Bulk Resources with ResourceList](managing-resource-lists.html) for `NamespacedResourceList` and other bulk patterns. + +--- + +## Deploy One Resource Per Namespace with NamespacedResourceList + +Create an identical namespaced resource in each of several namespaces. + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.config_map import ConfigMap +from ocp_resources.resource import ResourceList, NamespacedResourceList + +with ResourceList( + resource_class=Namespace, + num_resources=3, + client=client, + name="team-ns", +) as namespaces: + with NamespacedResourceList( + resource_class=ConfigMap, + namespaces=namespaces, + client=client, + name="shared-config", + data={"REGION": "us-east-1"}, + ) as configmaps: + # One ConfigMap "shared-config" in each of team-ns-1, team-ns-2, team-ns-3 + for cm in configmaps: + print(f"{cm.namespace}/{cm.name}") +``` + +`NamespacedResourceList` requires a `ResourceList` of `Namespace` objects. All resources are cleaned up in reverse order on exit. + +--- + +## Temporarily Edit a Resource with ResourceEditor + +Apply temporary patches during a test and automatically restore original values. + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap(client=client, name="app-config", namespace="default", ensure_exists=True) + +with ResourceEditor( + patches={cm: {"data": {"LOG_LEVEL": "debug"}}}, + action="update", +): + # LOG_LEVEL is now "debug" + print(cm.instance.data.LOG_LEVEL) +# Original LOG_LEVEL is restored here +``` + +`ResourceEditor` backs up original values on enter and restores them on exit. Use `action="replace"` when you need to remove fields entirely rather than patch them. + +> **Tip:** See [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html) for complete details. + +--- + +## Validate a Resource Before Creating It + +Catch schema errors before submitting to the API server. + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ValidationError + +pod = Pod( + client=client, + name="validated-pod", + namespace="default", + containers=[{ + "name": "app", + "image": "nginx:1.25", + }], + schema_validation_enabled=True, # Auto-validate on create() +) + +# Manual validation +try: + pod.validate() + print("Resource is valid") +except ValidationError as e: + print(f"Validation failed: {e}") + +# Or validate a raw dict without instantiation +try: + Pod.validate_dict({ + "apiVersion": "v1", + "kind": "Pod", + "metadata": {"name": "test"}, + "spec": {"containers": [{"name": "app", "image": "nginx"}]}, + }) +except ValidationError as e: + print(f"Dict validation failed: {e}") +``` + +When `schema_validation_enabled=True`, `create()` and `update_replace()` automatically validate before sending to the API. Validation requires OpenAPI schemas to be available. + +> **Tip:** See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for schema setup and troubleshooting. + +--- + +## Check If a Resource Exists + +Verify a resource exists before operating on it. + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment(client=client, name="web-app", namespace="default") + +if dep.exists: + print(f"Deployment exists, status: {dep.instance.status.availableReplicas} replicas available") +else: + print("Deployment not found") +``` + +`exists` returns the resource instance if found or `None` if not. For fail-fast behavior, use `ensure_exists=True` in the constructor — it raises `ResourceNotFoundError` immediately if the resource is missing. + +--- + +## Watch Resource Events + +Stream events related to a specific resource. + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True) + +# Watch events for this pod (10 second window) +for event in pod.events(timeout=10): + ev = event["object"] + print(f"[{ev.type}] {ev.reason}: {ev.message}") +``` + +Use `field_selector` to narrow results further, for example: `field_selector="type==Warning"`. The `events()` method automatically filters by `involvedObject.name`. + +--- + +## List Recent Events in a Namespace + +Get existing events (not a watch stream) from the last N seconds. + +```python +from ocp_resources.event import Event + +# Warning events from the last 5 minutes +events = Event.list( + client=client, + namespace="default", + field_selector="type==Warning", + since_seconds=300, +) +for ev in events: + print(f"[{ev.reason}] {ev.message}") +``` + +`Event.list()` returns a sorted list (most recent first), unlike `Event.get()` which is a live watch stream. + +--- + +## Wait for a Resource Condition + +Block until a resource reaches a specific condition. + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment(client=client, name="web-app", namespace="default", ensure_exists=True) + +dep.wait_for_condition( + condition="Available", + status="True", + timeout=300, +) +``` + +Use `stop_condition` to fail fast if an unrecoverable condition is detected: + +```python +dep.wait_for_condition( + condition="Available", + status="True", + timeout=300, + stop_condition="ReplicaFailure", + stop_status="True", +) +``` + +> **Tip:** See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for advanced waiting patterns. + +--- + +## Get a Resource's YAML Representation + +Dump the intended resource dict as YAML (useful for debugging). + +```python +from ocp_resources.pod import Pod + +pod = Pod( + client=client, + name="debug-pod", + namespace="default", + containers=[{"name": "app", "image": "busybox", "command": ["sleep", "3600"]}], +) +print(pod.to_yaml()) +``` + +`to_yaml()` calls `to_dict()` internally and returns the YAML string. This shows what would be sent to the API, not the live state. + +--- + +## Get the Pod's Node and IP + +Access commonly needed runtime properties of a pod. + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True) + +# Node where the pod is running +print(f"Node: {pod.node.name}") + +# Pod IP address +print(f"IP: {pod.ip}") +``` + +The `node` property returns a `Node` resource object. The `ip` property reads from `status.podIP`. + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Querying and Watching Resources](querying-resources.html) +- [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) +- [Working with Kubernetes Events](working-with-events.html) diff --git a/docs/connecting-to-clusters.html b/docs/connecting-to-clusters.html new file mode 100644 index 0000000000..8bef134d0d --- /dev/null +++ b/docs/connecting-to-clusters.html @@ -0,0 +1,760 @@ + + + + + + Connecting to Clusters - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by exploring the repository structure and the knowledge graph.

+

Now let me explore the client configuration and authentication code:

+

Now let me look at how the client parameter is used in the Resource class:

+

Now let me look at how resources accept the client parameter:

+

Now let me look at the NamespacedResource and how it handles client:

+

Now let me look at the tests for the client configuration:

+

Let me also check how environment variables relate to client configuration:

+

Now let me check how get_client is used with generate_kubeconfig:

+

Now let me look at the conftest.py for the tests to understand how fake_client is set up:

+

Let me also check the test file for more patterns of client usage:

+

Now let me check what environment variables are documented:

+

Now let me check the generate_kubeconfig related tests more carefully and how DynamicClientWithKubeconfig is used:

+

Now let me also look at how get_client is imported/used in other contexts to understand its full interface:

+

Let me look at some example files to see real usage patterns:

+

Now let me check the get classmethod for how client is used:

+

Now let me check for the client_configuration parameter and how verify_ssl is used:

+

Now I have all the information I need. Let me write the documentation page.

+

Connecting to Clusters

+

Establish authenticated connections to Kubernetes and OpenShift clusters so you can create, query, and manage resources with openshift-python-wrapper.

+

Prerequisites

+ +

Quick Example

+
from ocp_resources.resource import get_client
+from ocp_resources.namespace import Namespace
+
+# Connect using default kubeconfig (~/.kube/config or $KUBECONFIG)
+client = get_client()
+
+# Use the client with any resource
+for ns in Namespace.get(client=client):
+    print(ns.name)
+
+ +

Creating a Client

+

All cluster connections go through get_client(), which returns a Kubernetes DynamicClient. Pass this client to every resource you create or query.

+

Using the Default Kubeconfig

+

If you have a kubeconfig at ~/.kube/config or the KUBECONFIG environment variable is set, no arguments are needed:

+
from ocp_resources.resource import get_client
+
+client = get_client()
+
+ +

The lookup order is:

+
    +
  1. The config_file parameter (if provided)
  2. +
  3. The KUBECONFIG environment variable
  4. +
  5. ~/.kube/config
  6. +
+

Using a Specific Kubeconfig File

+
client = get_client(config_file="/path/to/my/kubeconfig")
+
+ +

Selecting a Context

+

If your kubeconfig has multiple contexts, select one by name:

+
client = get_client(config_file="/path/to/kubeconfig", context="my-staging-cluster")
+
+ +

Authenticating with a Bearer Token

+

Connect to a cluster API server directly using a host URL and token:

+
client = get_client(
+    host="https://api.my-cluster.example.com:6443",
+    token="sha256~my-bearer-token",
+)
+
+ +

Authenticating with Username and Password (Basic Auth)

+

For OpenShift clusters that support OAuth-based basic authentication:

+
client = get_client(
+    host="https://api.my-cluster.example.com:6443",
+    username="my-user",
+    password="my-password",
+)
+
+ +
+

Note: Basic auth requires all three parameters: host, username, and password. This uses OpenShift's OAuth flow internally — it is not plain HTTP Basic Authentication.

+
+

Using a Kubeconfig Dictionary

+

Pass a kubeconfig as a Python dictionary instead of a file path:

+
config_dict = {
+    "apiVersion": "v1",
+    "kind": "Config",
+    "clusters": [{"name": "my-cluster", "cluster": {"server": "https://api.example.com:6443"}}],
+    "users": [{"name": "my-user", "user": {"token": "my-token"}}],
+    "contexts": [{"name": "my-context", "context": {"cluster": "my-cluster", "user": "my-user"}}],
+    "current-context": "my-context",
+}
+
+client = get_client(config_dict=config_dict)
+
+ +

Passing the Client to Resources

+

Once you have a client, pass it to resource constructors and class methods:

+
from ocp_resources.resource import get_client
+from ocp_resources.namespace import Namespace
+from ocp_resources.pod import Pod
+
+client = get_client()
+
+# Creating a resource
+ns = Namespace(client=client, name="my-namespace")
+ns.deploy()
+
+# Querying resources
+for pod in Pod.get(client=client, namespace="my-namespace"):
+    print(pod.name)
+
+# Using context managers
+with Namespace(client=client, name="temp-namespace") as ns:
+    ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
+
+ +
+

Warning: The client parameter will become mandatory in the next major release. Always pass it explicitly. If omitted, the library creates a client automatically using the default kubeconfig, but this triggers a FutureWarning.

+
+

Environment Variables

+

get_client() respects several environment variables automatically:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
VariablePurposeDefault
KUBECONFIGPath to the kubeconfig file~/.kube/config
HTTPS_PROXYHTTPS proxy for cluster connectionsNone
HTTP_PROXYHTTP proxy (used if HTTPS_PROXY is not set)None
+
+

Tip: HTTPS_PROXY takes precedence over HTTP_PROXY when both are set.

+
+

For other environment variables that control logging, resource reuse, and teardown behavior, see Environment Variables and Configuration.

+

Advanced Usage

+

Disabling TLS Verification

+

For development clusters with self-signed certificates:

+
client = get_client(
+    host="https://api.dev-cluster.example.com:6443",
+    token="sha256~my-token",
+    verify_ssl=False,
+)
+
+ +
+

Warning: Never disable TLS verification in production environments.

+
+

In-Cluster Configuration

+

When your code runs inside a pod on the cluster (for example, in a CI/CD pipeline or operator), get_client() automatically falls back to in-cluster configuration if the kubeconfig-based connection fails:

+
# Inside a pod — no config_file or host needed
+client = get_client()
+
+ +

The library first attempts to connect using the kubeconfig. If that fails with a connection error, it loads credentials from the pod's service account token mounted at /var/run/secrets/kubernetes.io/serviceaccount/.

+

Generating a Kubeconfig File from a Token Connection

+

When you connect with host and token, you might need a kubeconfig file on disk (for example, to pass to external tools). Use generate_kubeconfig=True:

+
client = get_client(
+    host="https://api.my-cluster.example.com:6443",
+    token="sha256~my-token",
+    generate_kubeconfig=True,
+)
+
+# Access the generated kubeconfig path
+print(client.kubeconfig)
+
+ +

The generated kubeconfig file:

+
    +
  • Is written to a temporary file with 0o600 permissions
  • +
  • Is automatically cleaned up when the process exits
  • +
+

If you already passed a config_file, the generate_kubeconfig flag reuses that file path instead of creating a new one.

+

Passing a Custom Client Configuration

+

For fine-grained control over TLS settings, timeouts, or proxy configuration, pass a kubernetes.client.Configuration object:

+
import kubernetes
+from ocp_resources.resource import get_client
+
+config = kubernetes.client.Configuration()
+config.verify_ssl = False
+config.proxy = "http://my-proxy:8080"
+
+client = get_client(
+    host="https://api.my-cluster.example.com:6443",
+    token="sha256~my-token",
+    client_configuration=config,
+)
+
+ +

Connecting to Multiple Clusters

+

Create separate clients for each cluster and pass them to different resources:

+
from ocp_resources.resource import get_client
+from ocp_resources.namespace import Namespace
+
+prod_client = get_client(config_file="/path/to/prod-kubeconfig")
+staging_client = get_client(config_file="/path/to/staging-kubeconfig")
+
+# Query namespaces on both clusters
+prod_namespaces = list(Namespace.get(client=prod_client))
+staging_namespaces = list(Namespace.get(client=staging_client))
+
+ +

Using the Fake Client for Testing

+

For unit tests that don't need a real cluster, pass fake=True:

+
client = get_client(fake=True)
+
+ +

This returns a fake client that simulates Kubernetes API operations in memory. See Testing Without a Cluster Using the Fake Client for full details.

+

get_client() API Reference

+
from ocp_resources.resource import get_client
+
+get_client(
+    config_file: str | None = None,
+    config_dict: dict | None = None,
+    context: str | None = None,
+    client_configuration: kubernetes.client.Configuration | None = None,
+    persist_config: bool = True,
+    temp_file_path: str | None = None,
+    try_refresh_token: bool = True,
+    username: str | None = None,
+    password: str | None = None,
+    host: str | None = None,
+    verify_ssl: bool | None = None,
+    token: str | None = None,
+    fake: bool = False,
+    generate_kubeconfig: bool = False,
+) -> DynamicClient
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
config_filestr \| NonePath to a kubeconfig file
config_dictdict \| NoneKubeconfig as a Python dictionary
contextstr \| NoneName of the kubeconfig context to use
client_configurationConfiguration \| NoneCustom kubernetes.client.Configuration object
persist_configboolWhether to persist config changes back to the kubeconfig file
temp_file_pathstr \| NonePath for temporary kubeconfig file (used with config_dict)
try_refresh_tokenboolAttempt to refresh the authentication token (for in-cluster config)
usernamestr \| NoneUsername for basic authentication
passwordstr \| NonePassword for basic authentication
hoststr \| NoneCluster API server URL (e.g., https://api.example.com:6443)
verify_sslbool \| NoneSet to False to skip TLS certificate verification
tokenstr \| NoneBearer token for authentication
fakeboolReturn a fake in-memory client for testing
generate_kubeconfigboolSave connection info to a temporary kubeconfig file accessible via client.kubeconfig
+

Returns: DynamicClient — a Kubernetes dynamic client ready for resource operations.

+

Raises:

+
    +
  • kubernetes.config.ConfigException — if no valid kubeconfig is found and in-cluster config is unavailable
  • +
  • urllib3.exceptions.MaxRetryError — if the cluster is unreachable (before falling back to in-cluster config)
  • +
  • ClientWithBasicAuthError — if basic auth (username/password) fails
  • +
+

Authentication Method Priority

+

When multiple authentication parameters are provided, get_client() uses this priority order:

+
    +
  1. Basic auth — if username, password, and host are all provided
  2. +
  3. Token auth — if host and token are provided
  4. +
  5. Config dict — if config_dict is provided
  6. +
  7. Kubeconfig file — uses config_file, KUBECONFIG env var, or ~/.kube/config
  8. +
  9. In-cluster config — automatic fallback if the kubeconfig connection fails
  10. +
+

Troubleshooting

+

"FutureWarning: 'client' arg will be mandatory" +You are creating a resource without passing a client. Always call get_client() first and pass the result:

+
client = get_client()
+ns = Namespace(client=client, name="my-ns")  # Pass client explicitly
+
+ +

Connection refused or timeout errors +Verify your cluster is reachable and the kubeconfig or token is correct. Check that HTTPS_PROXY / HTTP_PROXY environment variables are set correctly if you are behind a proxy.

+

"KUBECONFIG environment variable set in code is not detected" +The Kubernetes Python client reads KUBECONFIG at module import time. If you set it dynamically in your code, pass the path directly instead:

+
import os
+# Don't rely on: os.environ["KUBECONFIG"] = "/path/to/config"
+# Instead, pass it explicitly:
+client = get_client(config_file=os.environ.get("KUBECONFIG", "/path/to/config"))
+
+ +

Basic auth fails with ClientWithBasicAuthError +Basic auth uses the OpenShift OAuth flow. Ensure your cluster has a well-known OAuth endpoint at /.well-known/oauth-authorization-server. This method does not work with vanilla Kubernetes clusters.

+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/connecting-to-clusters.md b/docs/connecting-to-clusters.md new file mode 100644 index 0000000000..30198ef277 --- /dev/null +++ b/docs/connecting-to-clusters.md @@ -0,0 +1,356 @@ +Let me start by exploring the repository structure and the knowledge graph. + +Now let me explore the client configuration and authentication code: + +Now let me look at how the `client` parameter is used in the Resource class: + +Now let me look at how resources accept the client parameter: + +Now let me look at the NamespacedResource and how it handles client: + +Now let me look at the tests for the client configuration: + +Let me also check how environment variables relate to client configuration: + +Now let me check how `get_client` is used with `generate_kubeconfig`: + +Now let me look at the `conftest.py` for the tests to understand how fake_client is set up: + +Let me also check the test file for more patterns of client usage: + +Now let me check what environment variables are documented: + +Now let me check the `generate_kubeconfig` related tests more carefully and how `DynamicClientWithKubeconfig` is used: + +Now let me also look at how `get_client` is imported/used in other contexts to understand its full interface: + +Let me look at some example files to see real usage patterns: + +Now let me check the `get` classmethod for how client is used: + +Now let me check for the `client_configuration` parameter and how `verify_ssl` is used: + +Now I have all the information I need. Let me write the documentation page. + +# Connecting to Clusters + +Establish authenticated connections to Kubernetes and OpenShift clusters so you can create, query, and manage resources with openshift-python-wrapper. + +## Prerequisites + +- openshift-python-wrapper installed (see [Installing and Creating Your First Resource](quickstart.html)) +- Access to a Kubernetes or OpenShift cluster (kubeconfig file, token, or in-cluster credentials) + +## Quick Example + +```python +from ocp_resources.resource import get_client +from ocp_resources.namespace import Namespace + +# Connect using default kubeconfig (~/.kube/config or $KUBECONFIG) +client = get_client() + +# Use the client with any resource +for ns in Namespace.get(client=client): + print(ns.name) +``` + +## Creating a Client + +All cluster connections go through `get_client()`, which returns a Kubernetes `DynamicClient`. Pass this client to every resource you create or query. + +### Using the Default Kubeconfig + +If you have a kubeconfig at `~/.kube/config` or the `KUBECONFIG` environment variable is set, no arguments are needed: + +```python +from ocp_resources.resource import get_client + +client = get_client() +``` + +The lookup order is: + +1. The `config_file` parameter (if provided) +2. The `KUBECONFIG` environment variable +3. `~/.kube/config` + +### Using a Specific Kubeconfig File + +```python +client = get_client(config_file="/path/to/my/kubeconfig") +``` + +### Selecting a Context + +If your kubeconfig has multiple contexts, select one by name: + +```python +client = get_client(config_file="/path/to/kubeconfig", context="my-staging-cluster") +``` + +### Authenticating with a Bearer Token + +Connect to a cluster API server directly using a host URL and token: + +```python +client = get_client( + host="https://api.my-cluster.example.com:6443", + token="sha256~my-bearer-token", +) +``` + +### Authenticating with Username and Password (Basic Auth) + +For OpenShift clusters that support OAuth-based basic authentication: + +```python +client = get_client( + host="https://api.my-cluster.example.com:6443", + username="my-user", + password="my-password", +) +``` + +> **Note:** Basic auth requires all three parameters: `host`, `username`, and `password`. This uses OpenShift's OAuth flow internally — it is not plain HTTP Basic Authentication. + +### Using a Kubeconfig Dictionary + +Pass a kubeconfig as a Python dictionary instead of a file path: + +```python +config_dict = { + "apiVersion": "v1", + "kind": "Config", + "clusters": [{"name": "my-cluster", "cluster": {"server": "https://api.example.com:6443"}}], + "users": [{"name": "my-user", "user": {"token": "my-token"}}], + "contexts": [{"name": "my-context", "context": {"cluster": "my-cluster", "user": "my-user"}}], + "current-context": "my-context", +} + +client = get_client(config_dict=config_dict) +``` + +## Passing the Client to Resources + +Once you have a client, pass it to resource constructors and class methods: + +```python +from ocp_resources.resource import get_client +from ocp_resources.namespace import Namespace +from ocp_resources.pod import Pod + +client = get_client() + +# Creating a resource +ns = Namespace(client=client, name="my-namespace") +ns.deploy() + +# Querying resources +for pod in Pod.get(client=client, namespace="my-namespace"): + print(pod.name) + +# Using context managers +with Namespace(client=client, name="temp-namespace") as ns: + ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) +``` + +> **Warning:** The `client` parameter will become mandatory in the next major release. Always pass it explicitly. If omitted, the library creates a client automatically using the default kubeconfig, but this triggers a `FutureWarning`. + +## Environment Variables + +`get_client()` respects several environment variables automatically: + +| Variable | Purpose | Default | +|---|---|---| +| `KUBECONFIG` | Path to the kubeconfig file | `~/.kube/config` | +| `HTTPS_PROXY` | HTTPS proxy for cluster connections | None | +| `HTTP_PROXY` | HTTP proxy (used if `HTTPS_PROXY` is not set) | None | + +> **Tip:** `HTTPS_PROXY` takes precedence over `HTTP_PROXY` when both are set. + +For other environment variables that control logging, resource reuse, and teardown behavior, see [Environment Variables and Configuration](environment-variables.html). + +## Advanced Usage + +### Disabling TLS Verification + +For development clusters with self-signed certificates: + +```python +client = get_client( + host="https://api.dev-cluster.example.com:6443", + token="sha256~my-token", + verify_ssl=False, +) +``` + +> **Warning:** Never disable TLS verification in production environments. + +### In-Cluster Configuration + +When your code runs inside a pod on the cluster (for example, in a CI/CD pipeline or operator), `get_client()` automatically falls back to in-cluster configuration if the kubeconfig-based connection fails: + +```python +# Inside a pod — no config_file or host needed +client = get_client() +``` + +The library first attempts to connect using the kubeconfig. If that fails with a connection error, it loads credentials from the pod's service account token mounted at `/var/run/secrets/kubernetes.io/serviceaccount/`. + +### Generating a Kubeconfig File from a Token Connection + +When you connect with `host` and `token`, you might need a kubeconfig file on disk (for example, to pass to external tools). Use `generate_kubeconfig=True`: + +```python +client = get_client( + host="https://api.my-cluster.example.com:6443", + token="sha256~my-token", + generate_kubeconfig=True, +) + +# Access the generated kubeconfig path +print(client.kubeconfig) +``` + +The generated kubeconfig file: +- Is written to a temporary file with `0o600` permissions +- Is automatically cleaned up when the process exits + +If you already passed a `config_file`, the `generate_kubeconfig` flag reuses that file path instead of creating a new one. + +### Passing a Custom Client Configuration + +For fine-grained control over TLS settings, timeouts, or proxy configuration, pass a `kubernetes.client.Configuration` object: + +```python +import kubernetes +from ocp_resources.resource import get_client + +config = kubernetes.client.Configuration() +config.verify_ssl = False +config.proxy = "http://my-proxy:8080" + +client = get_client( + host="https://api.my-cluster.example.com:6443", + token="sha256~my-token", + client_configuration=config, +) +``` + +### Connecting to Multiple Clusters + +Create separate clients for each cluster and pass them to different resources: + +```python +from ocp_resources.resource import get_client +from ocp_resources.namespace import Namespace + +prod_client = get_client(config_file="/path/to/prod-kubeconfig") +staging_client = get_client(config_file="/path/to/staging-kubeconfig") + +# Query namespaces on both clusters +prod_namespaces = list(Namespace.get(client=prod_client)) +staging_namespaces = list(Namespace.get(client=staging_client)) +``` + +### Using the Fake Client for Testing + +For unit tests that don't need a real cluster, pass `fake=True`: + +```python +client = get_client(fake=True) +``` + +This returns a fake client that simulates Kubernetes API operations in memory. See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for full details. + +## `get_client()` API Reference + +```python +from ocp_resources.resource import get_client + +get_client( + config_file: str | None = None, + config_dict: dict | None = None, + context: str | None = None, + client_configuration: kubernetes.client.Configuration | None = None, + persist_config: bool = True, + temp_file_path: str | None = None, + try_refresh_token: bool = True, + username: str | None = None, + password: str | None = None, + host: str | None = None, + verify_ssl: bool | None = None, + token: str | None = None, + fake: bool = False, + generate_kubeconfig: bool = False, +) -> DynamicClient +``` + +| Parameter | Type | Description | +|---|---|---| +| `config_file` | `str \| None` | Path to a kubeconfig file | +| `config_dict` | `dict \| None` | Kubeconfig as a Python dictionary | +| `context` | `str \| None` | Name of the kubeconfig context to use | +| `client_configuration` | `Configuration \| None` | Custom `kubernetes.client.Configuration` object | +| `persist_config` | `bool` | Whether to persist config changes back to the kubeconfig file | +| `temp_file_path` | `str \| None` | Path for temporary kubeconfig file (used with `config_dict`) | +| `try_refresh_token` | `bool` | Attempt to refresh the authentication token (for in-cluster config) | +| `username` | `str \| None` | Username for basic authentication | +| `password` | `str \| None` | Password for basic authentication | +| `host` | `str \| None` | Cluster API server URL (e.g., `https://api.example.com:6443`) | +| `verify_ssl` | `bool \| None` | Set to `False` to skip TLS certificate verification | +| `token` | `str \| None` | Bearer token for authentication | +| `fake` | `bool` | Return a fake in-memory client for testing | +| `generate_kubeconfig` | `bool` | Save connection info to a temporary kubeconfig file accessible via `client.kubeconfig` | + +**Returns:** `DynamicClient` — a Kubernetes dynamic client ready for resource operations. + +**Raises:** +- `kubernetes.config.ConfigException` — if no valid kubeconfig is found and in-cluster config is unavailable +- `urllib3.exceptions.MaxRetryError` — if the cluster is unreachable (before falling back to in-cluster config) +- `ClientWithBasicAuthError` — if basic auth (username/password) fails + +## Authentication Method Priority + +When multiple authentication parameters are provided, `get_client()` uses this priority order: + +1. **Basic auth** — if `username`, `password`, and `host` are all provided +2. **Token auth** — if `host` and `token` are provided +3. **Config dict** — if `config_dict` is provided +4. **Kubeconfig file** — uses `config_file`, `KUBECONFIG` env var, or `~/.kube/config` +5. **In-cluster config** — automatic fallback if the kubeconfig connection fails + +## Troubleshooting + +**"FutureWarning: 'client' arg will be mandatory"** +You are creating a resource without passing a `client`. Always call `get_client()` first and pass the result: + +```python +client = get_client() +ns = Namespace(client=client, name="my-ns") # Pass client explicitly +``` + +**Connection refused or timeout errors** +Verify your cluster is reachable and the kubeconfig or token is correct. Check that `HTTPS_PROXY` / `HTTP_PROXY` environment variables are set correctly if you are behind a proxy. + +**"KUBECONFIG environment variable set in code is not detected"** +The Kubernetes Python client reads `KUBECONFIG` at module import time. If you set it dynamically in your code, pass the path directly instead: + +```python +import os +# Don't rely on: os.environ["KUBECONFIG"] = "/path/to/config" +# Instead, pass it explicitly: +client = get_client(config_file=os.environ.get("KUBECONFIG", "/path/to/config")) +``` + +**Basic auth fails with `ClientWithBasicAuthError`** +Basic auth uses the OpenShift OAuth flow. Ensure your cluster has a well-known OAuth endpoint at `/.well-known/oauth-authorization-server`. This method does not work with vanilla Kubernetes clusters. + +## Related Pages + +- [Installing and Creating Your First Resource](quickstart.html) +- [Environment Variables and Configuration](environment-variables.html) +- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Error Handling and Exception Reference](error-handling.html) diff --git a/docs/creating-and-managing-resources.html b/docs/creating-and-managing-resources.html new file mode 100644 index 0000000000..ba3b43db68 --- /dev/null +++ b/docs/creating-and-managing-resources.html @@ -0,0 +1,924 @@ + + + + + + Creating and Managing Resources - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by exploring the repository structure and the knowledge graph.

+

Now let me look at the YAML-related functions and some example resource files:

+

Now let me look at some concrete resource examples and tests:

+

Now let me look at tests for create, update, delete operations and the exceptions:

+

Now let me look at the exceptions module and the project resource:

+

Now let me look at how to_yaml, yaml_file, and kind_dict usage works, and the dry_run option:

+

Now let me check how ensure_exists and wait_for_resource work:

+

Now let me look at how the service.py and route.py look to understand namespaced resources better:

+

Let me also look at the node_network_configuration_policy.py for a more complex deploy/clean_up example:

+

Now let me look at the to_yaml method and some environment variable usage:

+

Let me also check the dry_run behavior and check for how label and annotations work:

+

Now I have a thorough understanding of the codebase. Let me write the documentation.

+

Creating and Managing Resources

+

Deploy Kubernetes and OpenShift resources to your cluster, update them in place, and clean them up reliably using the openshift-python-wrapper's resource lifecycle methods.

+

Prerequisites

+ +

Quick Example

+
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+# Create, use, and automatically delete a Namespace
+with Namespace(client=client, name="my-app") as ns:
+    print(f"{ns.name} is ready")
+# Namespace is deleted when the block exits
+
+ +

Creating Resources

+

Every resource provides three ways to create it: calling deploy() directly, using a context manager, or loading from a YAML file.

+

Using deploy() and clean_up()

+
from ocp_resources.namespace import Namespace
+
+ns = Namespace(client=client, name="my-namespace")
+ns.deploy()
+
+# ... do work ...
+
+ns.clean_up()  # Deletes the resource and waits for deletion
+
+ +

deploy() returns the resource instance, so you can chain:

+
ns = Namespace(client=client, name="my-namespace").deploy()
+
+ +

Pass wait=True to deploy() to block until the resource reaches its ready state:

+
ns = Namespace(client=client, name="my-namespace")
+ns.deploy(wait=True)
+
+ +

Using a Context Manager

+

The context manager calls deploy() on entry and clean_up() on exit, guaranteeing cleanup even if an exception occurs:

+
from ocp_resources.config_map import ConfigMap
+
+with ConfigMap(
+    client=client,
+    name="app-config",
+    namespace="my-namespace",
+    data={"database_url": "postgres://db:5432/myapp"},
+) as cm:
+    print(cm.instance.data)
+# ConfigMap is automatically deleted here
+
+ +
+

Note: If cleanup fails during context manager exit, a ResourceTeardownError is raised so failures are never silently ignored.

+
+

Namespaced vs. Cluster-Scoped Resources

+

Resources that live within a namespace inherit from NamespacedResource and require both name and namespace:

+
from ocp_resources.pod import Pod
+
+pod = Pod(
+    client=client,
+    name="nginx",
+    namespace="my-namespace",
+    containers=[{"name": "nginx", "image": "nginx:latest"}],
+)
+pod.deploy()
+
+ +

Cluster-scoped resources (like Namespace) inherit from Resource and need only name:

+
from ocp_resources.namespace import Namespace
+
+ns = Namespace(client=client, name="my-namespace")
+ns.deploy()
+
+ +

Creating Resources from a YAML File

+

Pass yaml_file to create any resource directly from a YAML manifest. The resource name and namespace are read from the file:

+
from ocp_resources.config_map import ConfigMap
+
+cm = ConfigMap(client=client, yaml_file="manifests/configmap.yaml")
+cm.deploy()
+
+ +

yaml_file also accepts a StringIO object for in-memory YAML:

+
from io import StringIO
+from ocp_resources.config_map import ConfigMap
+
+yaml_content = StringIO("""
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: dynamic-config
+  namespace: my-namespace
+data:
+  setting: "enabled"
+""")
+
+cm = ConfigMap(client=client, yaml_file=yaml_content)
+cm.deploy()
+
+ +

Creating Resources from a Dictionary

+

Use kind_dict to pass a pre-built dictionary. When kind_dict is provided, the resource class's to_dict() logic is bypassed entirely:

+
from ocp_resources.config_map import ConfigMap
+
+resource_dict = {
+    "apiVersion": "v1",
+    "kind": "ConfigMap",
+    "metadata": {
+        "name": "from-dict",
+        "namespace": "my-namespace",
+    },
+    "data": {"key": "value"},
+}
+
+cm = ConfigMap(client=client, kind_dict=resource_dict)
+cm.deploy()
+
+ +
+

Warning: yaml_file and kind_dict are mutually exclusive. Passing both raises a ValueError.

+
+

Adding Labels and Annotations

+

Pass label and annotations at construction time:

+
from ocp_resources.namespace import Namespace
+
+ns = Namespace(
+    client=client,
+    name="labeled-ns",
+    label={"team": "platform", "env": "staging"},
+    annotations={"description": "Staging environment namespace"},
+)
+ns.deploy()
+
+ +

Updating Resources

+

Patching with update()

+

update() sends a merge patch — it adds or modifies fields without removing existing ones:

+
cm = ConfigMap(
+    client=client,
+    name="app-config",
+    namespace="my-namespace",
+    ensure_exists=True,
+)
+
+cm.update(resource_dict={
+    "data": {"new_key": "new_value"},
+})
+
+ +

Replacing with update_replace()

+

update_replace() performs a full replacement of the resource. Use this when you need to remove fields that update() would leave untouched:

+
# Get the current state
+current = cm.instance.to_dict()
+
+# Remove a key
+current["data"].pop("old_key", None)
+
+# Replace the entire resource
+cm.update_replace(resource_dict=current)
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
MethodHTTP VerbBehaviorUse When
update()PATCHMerges fields into existing resourceAdding or changing fields
update_replace()PUTReplaces entire resourceRemoving fields or doing full replacements
+

Deleting Resources

+

Using clean_up()

+
ns.clean_up()  # Deletes and waits for removal (default: wait=True)
+
+ +

Control the deletion behavior:

+
# Delete without waiting
+ns.clean_up(wait=False)
+
+# Delete with a custom timeout (in seconds)
+ns.clean_up(timeout=120)
+
+ +

clean_up() returns True if the resource was successfully deleted, False otherwise.

+

Using delete()

+

delete() is the lower-level method called by clean_up():

+
ns.delete(wait=True, timeout=240)
+
+ +

You can also pass a custom body for delete options:

+
ns.delete(body={"propagationPolicy": "Background"})
+
+ +
+

Tip: If you delete a resource that doesn't exist, delete() logs a warning and returns True instead of raising an error.

+
+

Controlling Teardown

+

Set teardown=False at construction to prevent automatic deletion in context managers:

+
with Namespace(client=client, name="persistent-ns", teardown=False) as ns:
+    print("This namespace will NOT be deleted on exit")
+
+ +

Checking Resource State

+

Check If a Resource Exists

+
ns = Namespace(client=client, name="my-namespace")
+if ns.exists:
+    print("Namespace exists")
+
+ +

Ensure a Resource Already Exists

+

Use ensure_exists=True to raise ResourceNotFoundError if the resource is not present on the cluster:

+
from ocp_resources.config_map import ConfigMap
+
+# Raises ResourceNotFoundError if the ConfigMap doesn't exist
+cm = ConfigMap(
+    client=client,
+    name="must-exist",
+    namespace="default",
+    ensure_exists=True,
+)
+
+ +

Get the Live Instance

+

The instance property fetches the current state from the API server:

+
ns = Namespace(client=client, name="my-namespace", ensure_exists=True)
+print(ns.instance.metadata.labels)
+print(ns.status)
+
+ +

Export as YAML

+
ns = Namespace(client=client, name="my-namespace")
+print(ns.to_yaml())
+
+ +

For more on querying, watching, and status checks, see Querying and Watching Resources.

+

Dry Run Mode

+

Validate a resource against the API server without actually creating it:

+
from ocp_resources.namespace import Namespace
+
+ns = Namespace(client=client, name="dry-run-ns", dry_run=True)
+ns.deploy()  # Sends the request with ?dryRun=All — no resource is created
+
+ +

Advanced Usage

+

Constructor Parameters Reference

+

All resource classes accept these common parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
clientDynamicClientNoneCluster client connection (will be required in next major release)
namestrNoneResource name
namespacestrNoneNamespace (namespaced resources only)
teardownboolTrueWhether clean_up() deletes the resource
yaml_filestrNonePath to a YAML manifest file
kind_dictdictNoneDictionary representation of the resource
delete_timeoutint240Timeout in seconds for delete operations
dry_runboolFalseServer-side dry run without persisting
labeldictNoneLabels to set on the resource
annotationsdictNoneAnnotations to set on the resource
ensure_existsboolFalseRaise if resource doesn't already exist
wait_for_resourceboolFalseWait for resource creation in context manager
schema_validation_enabledboolFalseValidate against OpenAPI schema on create/replace
hash_log_databoolTrueMask sensitive data in logs
+

For the full API reference, see Resource and NamespacedResource API.

+

Schema Validation on Create and Replace

+

Enable automatic schema validation so create() and update_replace() check your resource against its OpenAPI schema before sending it to the API server:

+
from ocp_resources.config_map import ConfigMap
+
+cm = ConfigMap(
+    client=client,
+    name="validated-cm",
+    namespace="default",
+    data={"key": "value"},
+    schema_validation_enabled=True,  # Validates before create()
+)
+cm.deploy()
+
+ +

You can also validate on demand or validate a dictionary directly:

+
# Validate an existing resource instance
+cm.validate()
+
+# Validate a dictionary without creating a resource
+ConfigMap.validate_dict({
+    "apiVersion": "v1",
+    "kind": "ConfigMap",
+    "metadata": {"name": "test"},
+    "data": {"key": "value"},
+})
+
+ +

Both methods raise ValidationError if validation fails. For more details, see Validating Resources Against OpenAPI Schemas.

+

Temporary Edits with ResourceEditor

+

ResourceEditor applies patches to existing resources and automatically restores original values when used as a context manager:

+
from ocp_resources.resource import ResourceEditor
+
+ns = Namespace(client=client, name="my-namespace", ensure_exists=True)
+
+with ResourceEditor(
+    patches={ns: {"metadata": {"labels": {"temporary-label": "true"}}}}
+):
+    # Label is applied
+    assert ns.labels["temporary-label"] == "true"
+# Label is automatically removed
+
+ +

ResourceEditor supports both update (merge patch) and replace actions:

+
with ResourceEditor(
+    patches={ns: {"metadata": {"labels": {"keep-only-this": "true"}}}},
+    action="replace",
+):
+    pass  # Full replacement applied, then restored on exit
+
+ +

For complete coverage, see Editing Resources Temporarily with ResourceEditor.

+

Managing Multiple Resources

+

ResourceList — Create N Copies

+

Create multiple instances of the same resource type with indexed names:

+
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import ResourceList
+
+with ResourceList(
+    client=client,
+    resource_class=Namespace,
+    name="test-ns",
+    num_resources=3,
+) as namespaces:
+    # Creates test-ns-1, test-ns-2, test-ns-3
+    for ns in namespaces:
+        print(ns.name)
+# All three namespaces are deleted on exit
+
+ +

NamespacedResourceList — One Per Namespace

+

Create one resource in each namespace from a ResourceList:

+
from ocp_resources.pod import Pod
+from ocp_resources.resource import NamespacedResourceList, ResourceList
+
+namespaces = ResourceList(
+    client=client,
+    resource_class=Namespace,
+    name="env",
+    num_resources=3,
+)
+
+with NamespacedResourceList(
+    client=client,
+    resource_class=Pod,
+    namespaces=namespaces,
+    name="worker",
+    containers=[{"name": "app", "image": "myapp:latest"}],
+) as pods:
+    for pod in pods:
+        print(f"{pod.name} in {pod.namespace}")
+
+ +

For detailed patterns, see Managing Bulk Resources with ResourceList.

+

Debugging with Environment Variables

+

These environment variables control resource lifecycle behavior at runtime without code changes:

+ + + + + + + + + + + + + + + + + +
VariableEffect
REUSE_IF_RESOURCE_EXISTSSkip deploy() if the resource already exists
SKIP_RESOURCE_TEARDOWNSkip clean_up() to keep resources for debugging
+

Both accept a YAML-formatted dictionary. Spaces are significant:

+
# Skip creation of all Pods
+export REUSE_IF_RESOURCE_EXISTS="{Pod: {}}"
+
+# Skip creation of a specific Pod in a specific namespace
+export REUSE_IF_RESOURCE_EXISTS="{Pod: {my-pod: my-namespace}}"
+
+# Skip teardown for a Namespace and a Pod
+export SKIP_RESOURCE_TEARDOWN="{Namespace: {my-ns:}, Pod: {my-pod: my-namespace}}"
+
+ +

For the full list of environment variables, see Environment Variables and Configuration.

+

Troubleshooting

+

MissingRequiredArgumentError on construction +You must provide either name, yaml_file, or kind_dict. For namespaced resources, both name and namespace are required unless using yaml_file or kind_dict.

+

ResourceTeardownError in context manager +The resource's clean_up() returned False, meaning deletion failed. Check cluster permissions and resource finalizers.

+

ValueError: yaml_file and resource_dict are mutually exclusive +You passed both yaml_file and kind_dict to a constructor. Use only one.

+

ResourceNotFoundError with ensure_exists=True +The resource doesn't exist on the cluster. Verify the name (and namespace for namespaced resources) are correct.

+

Resource not deleted after context manager exits +Check if teardown=False was passed. Also verify the SKIP_RESOURCE_TEARDOWN environment variable is not set.

+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/creating-and-managing-resources.md b/docs/creating-and-managing-resources.md new file mode 100644 index 0000000000..b1fa30eedd --- /dev/null +++ b/docs/creating-and-managing-resources.md @@ -0,0 +1,517 @@ +Let me start by exploring the repository structure and the knowledge graph. + +Now let me look at the YAML-related functions and some example resource files: + +Now let me look at some concrete resource examples and tests: + +Now let me look at tests for create, update, delete operations and the exceptions: + +Now let me look at the exceptions module and the project resource: + +Now let me look at how `to_yaml`, `yaml_file`, and `kind_dict` usage works, and the `dry_run` option: + +Now let me check how `ensure_exists` and `wait_for_resource` work: + +Now let me look at how the `service.py` and `route.py` look to understand namespaced resources better: + +Let me also look at the `node_network_configuration_policy.py` for a more complex `deploy/clean_up` example: + +Now let me look at the `to_yaml` method and some environment variable usage: + +Let me also check the `dry_run` behavior and check for how `label` and `annotations` work: + +Now I have a thorough understanding of the codebase. Let me write the documentation. + +# Creating and Managing Resources + +Deploy Kubernetes and OpenShift resources to your cluster, update them in place, and clean them up reliably using the openshift-python-wrapper's resource lifecycle methods. + +## Prerequisites + +- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html)) +- A connected client — either a real cluster connection or a fake client for testing (see [Connecting to Clusters](connecting-to-clusters.html)) + +## Quick Example + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +# Create, use, and automatically delete a Namespace +with Namespace(client=client, name="my-app") as ns: + print(f"{ns.name} is ready") +# Namespace is deleted when the block exits +``` + +## Creating Resources + +Every resource provides three ways to create it: calling `deploy()` directly, using a context manager, or loading from a YAML file. + +### Using `deploy()` and `clean_up()` + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-namespace") +ns.deploy() + +# ... do work ... + +ns.clean_up() # Deletes the resource and waits for deletion +``` + +`deploy()` returns the resource instance, so you can chain: + +```python +ns = Namespace(client=client, name="my-namespace").deploy() +``` + +Pass `wait=True` to `deploy()` to block until the resource reaches its ready state: + +```python +ns = Namespace(client=client, name="my-namespace") +ns.deploy(wait=True) +``` + +### Using a Context Manager + +The context manager calls `deploy()` on entry and `clean_up()` on exit, guaranteeing cleanup even if an exception occurs: + +```python +from ocp_resources.config_map import ConfigMap + +with ConfigMap( + client=client, + name="app-config", + namespace="my-namespace", + data={"database_url": "postgres://db:5432/myapp"}, +) as cm: + print(cm.instance.data) +# ConfigMap is automatically deleted here +``` + +> **Note:** If cleanup fails during context manager exit, a `ResourceTeardownError` is raised so failures are never silently ignored. + +### Namespaced vs. Cluster-Scoped Resources + +Resources that live within a namespace inherit from `NamespacedResource` and require both `name` and `namespace`: + +```python +from ocp_resources.pod import Pod + +pod = Pod( + client=client, + name="nginx", + namespace="my-namespace", + containers=[{"name": "nginx", "image": "nginx:latest"}], +) +pod.deploy() +``` + +Cluster-scoped resources (like `Namespace`) inherit from `Resource` and need only `name`: + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-namespace") +ns.deploy() +``` + +### Creating Resources from a YAML File + +Pass `yaml_file` to create any resource directly from a YAML manifest. The resource name and namespace are read from the file: + +```python +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap(client=client, yaml_file="manifests/configmap.yaml") +cm.deploy() +``` + +`yaml_file` also accepts a `StringIO` object for in-memory YAML: + +```python +from io import StringIO +from ocp_resources.config_map import ConfigMap + +yaml_content = StringIO(""" +apiVersion: v1 +kind: ConfigMap +metadata: + name: dynamic-config + namespace: my-namespace +data: + setting: "enabled" +""") + +cm = ConfigMap(client=client, yaml_file=yaml_content) +cm.deploy() +``` + +### Creating Resources from a Dictionary + +Use `kind_dict` to pass a pre-built dictionary. When `kind_dict` is provided, the resource class's `to_dict()` logic is bypassed entirely: + +```python +from ocp_resources.config_map import ConfigMap + +resource_dict = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "from-dict", + "namespace": "my-namespace", + }, + "data": {"key": "value"}, +} + +cm = ConfigMap(client=client, kind_dict=resource_dict) +cm.deploy() +``` + +> **Warning:** `yaml_file` and `kind_dict` are mutually exclusive. Passing both raises a `ValueError`. + +### Adding Labels and Annotations + +Pass `label` and `annotations` at construction time: + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace( + client=client, + name="labeled-ns", + label={"team": "platform", "env": "staging"}, + annotations={"description": "Staging environment namespace"}, +) +ns.deploy() +``` + +## Updating Resources + +### Patching with `update()` + +`update()` sends a merge patch — it adds or modifies fields without removing existing ones: + +```python +cm = ConfigMap( + client=client, + name="app-config", + namespace="my-namespace", + ensure_exists=True, +) + +cm.update(resource_dict={ + "data": {"new_key": "new_value"}, +}) +``` + +### Replacing with `update_replace()` + +`update_replace()` performs a full replacement of the resource. Use this when you need to **remove** fields that `update()` would leave untouched: + +```python +# Get the current state +current = cm.instance.to_dict() + +# Remove a key +current["data"].pop("old_key", None) + +# Replace the entire resource +cm.update_replace(resource_dict=current) +``` + +| Method | HTTP Verb | Behavior | Use When | +|--------|-----------|----------|----------| +| `update()` | PATCH | Merges fields into existing resource | Adding or changing fields | +| `update_replace()` | PUT | Replaces entire resource | Removing fields or doing full replacements | + +## Deleting Resources + +### Using `clean_up()` + +```python +ns.clean_up() # Deletes and waits for removal (default: wait=True) +``` + +Control the deletion behavior: + +```python +# Delete without waiting +ns.clean_up(wait=False) + +# Delete with a custom timeout (in seconds) +ns.clean_up(timeout=120) +``` + +`clean_up()` returns `True` if the resource was successfully deleted, `False` otherwise. + +### Using `delete()` + +`delete()` is the lower-level method called by `clean_up()`: + +```python +ns.delete(wait=True, timeout=240) +``` + +You can also pass a custom body for delete options: + +```python +ns.delete(body={"propagationPolicy": "Background"}) +``` + +> **Tip:** If you delete a resource that doesn't exist, `delete()` logs a warning and returns `True` instead of raising an error. + +### Controlling Teardown + +Set `teardown=False` at construction to prevent automatic deletion in context managers: + +```python +with Namespace(client=client, name="persistent-ns", teardown=False) as ns: + print("This namespace will NOT be deleted on exit") +``` + +## Checking Resource State + +### Check If a Resource Exists + +```python +ns = Namespace(client=client, name="my-namespace") +if ns.exists: + print("Namespace exists") +``` + +### Ensure a Resource Already Exists + +Use `ensure_exists=True` to raise `ResourceNotFoundError` if the resource is not present on the cluster: + +```python +from ocp_resources.config_map import ConfigMap + +# Raises ResourceNotFoundError if the ConfigMap doesn't exist +cm = ConfigMap( + client=client, + name="must-exist", + namespace="default", + ensure_exists=True, +) +``` + +### Get the Live Instance + +The `instance` property fetches the current state from the API server: + +```python +ns = Namespace(client=client, name="my-namespace", ensure_exists=True) +print(ns.instance.metadata.labels) +print(ns.status) +``` + +### Export as YAML + +```python +ns = Namespace(client=client, name="my-namespace") +print(ns.to_yaml()) +``` + +For more on querying, watching, and status checks, see [Querying and Watching Resources](querying-resources.html). + +## Dry Run Mode + +Validate a resource against the API server without actually creating it: + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="dry-run-ns", dry_run=True) +ns.deploy() # Sends the request with ?dryRun=All — no resource is created +``` + +## Advanced Usage + +### Constructor Parameters Reference + +All resource classes accept these common parameters: + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `client` | `DynamicClient` | `None` | Cluster client connection (will be required in next major release) | +| `name` | `str` | `None` | Resource name | +| `namespace` | `str` | `None` | Namespace (namespaced resources only) | +| `teardown` | `bool` | `True` | Whether `clean_up()` deletes the resource | +| `yaml_file` | `str` | `None` | Path to a YAML manifest file | +| `kind_dict` | `dict` | `None` | Dictionary representation of the resource | +| `delete_timeout` | `int` | `240` | Timeout in seconds for delete operations | +| `dry_run` | `bool` | `False` | Server-side dry run without persisting | +| `label` | `dict` | `None` | Labels to set on the resource | +| `annotations` | `dict` | `None` | Annotations to set on the resource | +| `ensure_exists` | `bool` | `False` | Raise if resource doesn't already exist | +| `wait_for_resource` | `bool` | `False` | Wait for resource creation in context manager | +| `schema_validation_enabled` | `bool` | `False` | Validate against OpenAPI schema on create/replace | +| `hash_log_data` | `bool` | `True` | Mask sensitive data in logs | + +For the full API reference, see [Resource and NamespacedResource API](resource-api.html). + +### Schema Validation on Create and Replace + +Enable automatic schema validation so `create()` and `update_replace()` check your resource against its OpenAPI schema before sending it to the API server: + +```python +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap( + client=client, + name="validated-cm", + namespace="default", + data={"key": "value"}, + schema_validation_enabled=True, # Validates before create() +) +cm.deploy() +``` + +You can also validate on demand or validate a dictionary directly: + +```python +# Validate an existing resource instance +cm.validate() + +# Validate a dictionary without creating a resource +ConfigMap.validate_dict({ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "test"}, + "data": {"key": "value"}, +}) +``` + +Both methods raise `ValidationError` if validation fails. For more details, see [Validating Resources Against OpenAPI Schemas](validating-resources.html). + +### Temporary Edits with ResourceEditor + +`ResourceEditor` applies patches to existing resources and automatically restores original values when used as a context manager: + +```python +from ocp_resources.resource import ResourceEditor + +ns = Namespace(client=client, name="my-namespace", ensure_exists=True) + +with ResourceEditor( + patches={ns: {"metadata": {"labels": {"temporary-label": "true"}}}} +): + # Label is applied + assert ns.labels["temporary-label"] == "true" +# Label is automatically removed +``` + +`ResourceEditor` supports both `update` (merge patch) and `replace` actions: + +```python +with ResourceEditor( + patches={ns: {"metadata": {"labels": {"keep-only-this": "true"}}}}, + action="replace", +): + pass # Full replacement applied, then restored on exit +``` + +For complete coverage, see [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html). + +### Managing Multiple Resources + +#### ResourceList — Create N Copies + +Create multiple instances of the same resource type with indexed names: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import ResourceList + +with ResourceList( + client=client, + resource_class=Namespace, + name="test-ns", + num_resources=3, +) as namespaces: + # Creates test-ns-1, test-ns-2, test-ns-3 + for ns in namespaces: + print(ns.name) +# All three namespaces are deleted on exit +``` + +#### NamespacedResourceList — One Per Namespace + +Create one resource in each namespace from a `ResourceList`: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import NamespacedResourceList, ResourceList + +namespaces = ResourceList( + client=client, + resource_class=Namespace, + name="env", + num_resources=3, +) + +with NamespacedResourceList( + client=client, + resource_class=Pod, + namespaces=namespaces, + name="worker", + containers=[{"name": "app", "image": "myapp:latest"}], +) as pods: + for pod in pods: + print(f"{pod.name} in {pod.namespace}") +``` + +For detailed patterns, see [Managing Bulk Resources with ResourceList](managing-resource-lists.html). + +### Debugging with Environment Variables + +These environment variables control resource lifecycle behavior at runtime without code changes: + +| Variable | Effect | +|----------|--------| +| `REUSE_IF_RESOURCE_EXISTS` | Skip `deploy()` if the resource already exists | +| `SKIP_RESOURCE_TEARDOWN` | Skip `clean_up()` to keep resources for debugging | + +Both accept a YAML-formatted dictionary. Spaces are significant: + +```bash +# Skip creation of all Pods +export REUSE_IF_RESOURCE_EXISTS="{Pod: {}}" + +# Skip creation of a specific Pod in a specific namespace +export REUSE_IF_RESOURCE_EXISTS="{Pod: {my-pod: my-namespace}}" + +# Skip teardown for a Namespace and a Pod +export SKIP_RESOURCE_TEARDOWN="{Namespace: {my-ns:}, Pod: {my-pod: my-namespace}}" +``` + +For the full list of environment variables, see [Environment Variables and Configuration](environment-variables.html). + +## Troubleshooting + +**`MissingRequiredArgumentError` on construction** +You must provide either `name`, `yaml_file`, or `kind_dict`. For namespaced resources, both `name` and `namespace` are required unless using `yaml_file` or `kind_dict`. + +**`ResourceTeardownError` in context manager** +The resource's `clean_up()` returned `False`, meaning deletion failed. Check cluster permissions and resource finalizers. + +**`ValueError: yaml_file and resource_dict are mutually exclusive`** +You passed both `yaml_file` and `kind_dict` to a constructor. Use only one. + +**`ResourceNotFoundError` with `ensure_exists=True`** +The resource doesn't exist on the cluster. Verify the name (and namespace for namespaced resources) are correct. + +**Resource not deleted after context manager exits** +Check if `teardown=False` was passed. Also verify the `SKIP_RESOURCE_TEARDOWN` environment variable is not set. + +## Related Pages + +- [Installing and Creating Your First Resource](quickstart.html) +- [Querying and Watching Resources](querying-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html) +- [Resource and NamespacedResource API](resource-api.html) diff --git a/docs/editing-resources-temporarily.html b/docs/editing-resources-temporarily.html new file mode 100644 index 0000000000..d755a709d3 --- /dev/null +++ b/docs/editing-resources-temporarily.html @@ -0,0 +1,720 @@ + + + + + + Editing Resources Temporarily with ResourceEditor - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by reading the knowledge graph and understanding the repository structure.

+

Now let me find the ResourceEditor source code.

+

Now let me look at how ResourceEditor is used in the NNCP file and any tests:

+

Let me look at the import section and any related constants:

+

Now let me look at the update and update_replace methods on the Resource class to understand the two actions:

+

Now let me look at the imports to verify how ResourceEditor is exported:

+

Now let me check for any additional examples or documentation:

+

Let me look at the full ResourceEditor code once more to make sure I've captured everything, and also check the _apply_patches method details and what action values are supported:

+

Now I have a complete picture of the ResourceEditor class. Let me write the documentation.

+

Editing Resources Temporarily with ResourceEditor

+

Temporarily modify Kubernetes and OpenShift resources during tests and have their original values automatically restored when you're done. This is essential for test isolation—ensuring that changes made during a test don't leak into other tests or persist in the cluster.

+

Prerequisites

+ +
+

Warning: The client used to retrieve resources must have sufficient privileges. Do not use an unprivileged user client with ResourceEditor.

+
+

Quick Example

+
from ocp_resources.resource import ResourceEditor
+from ocp_resources.node import Node
+
+node = Node(name="worker-0")
+
+with ResourceEditor(
+    patches={node: {"metadata": {"labels": {"test-label": "true"}}}}
+):
+    # The node now has the label "test-label": "true"
+    # Run your test logic here...
+    pass
+
+# The label is automatically removed when the block exits
+
+ +

That's it—when the with block ends (even if an exception occurs), the original resource state is restored.

+

Step-by-Step: Using ResourceEditor as a Context Manager

+

1. Import and identify your target resource

+
from ocp_resources.resource import ResourceEditor
+from ocp_resources.config_map import ConfigMap
+
+cm = ConfigMap(name="my-config", namespace="my-namespace")
+
+ +

2. Define your patch

+

Patches are dictionaries that mirror the resource's YAML structure. Only include the fields you want to change:

+
patch = {"data": {"feature_flag": "enabled", "log_level": "debug"}}
+
+ +

3. Wrap your test logic in a with block

+
with ResourceEditor(patches={cm: patch}):
+    # ConfigMap now has updated data fields
+    assert cm.instance.data.feature_flag == "enabled"
+    # Run tests that depend on the modified config...
+
+ +

4. Original values are restored automatically

+

When the with block exits, the original data values are written back to the resource. Fields that didn't exist before the patch are removed (set to None in the restore payload, which tells the API to delete them).

+

Patching Multiple Resources at Once

+

Pass multiple resource-patch pairs in a single ResourceEditor:

+
from ocp_resources.resource import ResourceEditor
+from ocp_resources.namespace import Namespace
+from ocp_resources.config_map import ConfigMap
+
+ns = Namespace(name="test-ns")
+cm = ConfigMap(name="app-config", namespace="test-ns")
+
+patches = {
+    ns: {"metadata": {"labels": {"env": "test"}}},
+    cm: {"data": {"database_url": "postgres://test-db:5432"}},
+}
+
+with ResourceEditor(patches=patches):
+    # Both resources are patched; run tests here
+    pass
+# Both resources are restored to their original state
+
+ +

Using update() and restore() Without a Context Manager

+

If you need finer control over when patches are applied and reverted—for example, in setup/teardown fixtures—use the update() and restore() methods directly:

+
from ocp_resources.resource import ResourceEditor
+from ocp_resources.config_map import ConfigMap
+
+cm = ConfigMap(name="app-config", namespace="default")
+
+editor = ResourceEditor(
+    patches={cm: {"data": {"mode": "maintenance"}}}
+)
+
+# Apply the patch and create backups
+editor.update(backup_resources=True)
+
+# ... run tests ...
+
+# Restore original values
+editor.restore()
+
+ +
+

Note: When calling update() manually, pass backup_resources=True to ensure original values are captured. Without this flag, no backup is created and restore() will have nothing to revert.

+
+

Advanced Usage

+

Replace Instead of Patch

+

By default, ResourceEditor uses the "update" action, which sends a merge-patch (application/merge-patch+json). This merges your changes into the existing resource. If you need to replace the entire resource (for example, to remove fields that merge-patch can't delete), use action="replace":

+
from ocp_resources.resource import ResourceEditor
+
+with ResourceEditor(
+    patches={my_resource: {"spec": {"replicas": 3}}},
+    action="replace",
+):
+    # The resource is fully replaced, not merged
+    pass
+
+ + + + + + + + + + + + + + + + + + + + + +
ActionMethod UsedBehavior
"update" (default)Merge patchMerges patch into existing resource; cannot remove fields by omission
"replace"Full replacementReplaces the entire resource; fields not in the patch are removed
+
+

Warning: With action="replace", the patch must include all required fields for the resource, not just the fields you want to change. The replacement payload automatically includes metadata.name, metadata.namespace, metadata.resourceVersion, kind, and apiVersion.

+
+

Providing Your Own Backups

+

If you already know what the restore state should be (or need custom restore logic), pass user_backups to skip the automatic backup calculation:

+
from ocp_resources.resource import ResourceEditor
+
+custom_backup = {my_resource: {"spec": {"replicas": 1}}}
+
+with ResourceEditor(
+    patches={my_resource: {"spec": {"replicas": 5}}},
+    user_backups=custom_backup,
+):
+    # Resource scaled to 5 replicas
+    pass
+# Restored to 1 replica (your custom backup), regardless of what the original value was
+
+ +

Inspecting Backups

+

Access the automatically generated backup data through the backups property:

+
editor = ResourceEditor(
+    patches={my_resource: {"metadata": {"labels": {"env": "staging"}}}}
+)
+editor.update(backup_resources=True)
+
+# See what was backed up
+print(editor.backups)
+# {<Resource>: {'metadata': {'labels': {'env': 'production'}}}}
+
+editor.restore()
+
+ +

No-Op Detection

+

ResourceEditor automatically detects when a patch produces no actual changes. If the patch values already match the resource's current state, the resource is skipped entirely and a warning is logged:

+
ResourceEdit: no diff found in patch for my-resource -- skipping
+
+ +

This prevents unnecessary API calls and avoids triggering watch events for no-op changes.

+

Automatic Retry on Conflicts

+

All patch and restore operations are wrapped with automatic retry logic. If a ConflictError occurs (for example, due to a concurrent update changing the resourceVersion), the operation is retried automatically with a 5-second sleep interval and a 30-second timeout.

+

API Reference

+

Constructor

+
ResourceEditor(
+    patches: dict,
+    action: str = "update",
+    user_backups: dict | None = None,
+)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
patchesdict(required)Mapping of resource objects to patch dicts: {resource: {yaml_patch}}
actionstr"update"Either "update" (merge patch) or "replace" (full replacement)
user_backupsdict \| NoneNoneCustom backup dicts to use for restore instead of auto-generated ones
+

Properties

+ + + + + + + + + + + + + + + + + + + + +
PropertyReturn TypeDescription
backupsdictThe backup data captured for each patched resource: {resource: backup_dict}
patchesdictThe patches dict provided in the constructor
+

Methods

+ + + + + + + + + + + + + + + + + + + + +
MethodParametersDescription
update(backup_resources=False)backup_resources: boolApply patches. Set backup_resources=True to capture original values for later restore.
restore()Re-apply the stored backup dicts to revert all patched resources.
+

Context Manager Protocol

+ + + + + + + + + + + + + + + + + +
MethodBehavior
__enter__()Calls update(backup_resources=True) and returns the ResourceEditor instance
__exit__()Calls restore() to revert all changes
+

Troubleshooting

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProblemCauseSolution
Restore doesn't revert changesCalled update() without backup_resources=TrueAlways pass backup_resources=True when calling update() manually, or use the context manager (with block) which does this automatically
NotFoundError during backupResource cannot be found by name (known with some CRDs like ServiceMonitor)ResourceEditor handles this automatically by falling back to a field-selector query
ConflictError during patchAnother process modified the resource concurrentlyAutomatic retries handle this; if it persists beyond 30 seconds, check for controllers continuously modifying the resource
Patch has no effectThe resource already has the values in your patchThis is expected—ResourceEditor logs a warning and skips the resource. Check that you're patching the correct fields.
replace action failsPatch is missing required fieldsWhen using action="replace", provide all required spec fields since the entire resource is replaced
+
+

Tip: For creating and cleaning up resources (rather than editing existing ones), see Creating and Managing Resources. For managing groups of similar resources, see Managing Bulk Resources with ResourceList.

+
+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/editing-resources-temporarily.md b/docs/editing-resources-temporarily.md new file mode 100644 index 0000000000..cd7e01a86b --- /dev/null +++ b/docs/editing-resources-temporarily.md @@ -0,0 +1,261 @@ +Let me start by reading the knowledge graph and understanding the repository structure. + +Now let me find the ResourceEditor source code. + +Now let me look at how ResourceEditor is used in the NNCP file and any tests: + +Let me look at the import section and any related constants: + +Now let me look at the `update` and `update_replace` methods on the Resource class to understand the two actions: + +Now let me look at the imports to verify how ResourceEditor is exported: + +Now let me check for any additional examples or documentation: + +Let me look at the full ResourceEditor code once more to make sure I've captured everything, and also check the `_apply_patches` method details and what `action` values are supported: + +Now I have a complete picture of the ResourceEditor class. Let me write the documentation. + +# Editing Resources Temporarily with ResourceEditor + +Temporarily modify Kubernetes and OpenShift resources during tests and have their original values automatically restored when you're done. This is essential for test isolation—ensuring that changes made during a test don't leak into other tests or persist in the cluster. + +## Prerequisites + +- `openshift-python-wrapper` installed in your project (see [Installing and Creating Your First Resource](quickstart.html)) +- A cluster connection established with a privileged client (see [Connecting to Clusters](connecting-to-clusters.html)) +- One or more existing resources on the cluster that you want to temporarily modify + +> **Warning:** The client used to retrieve resources must have sufficient privileges. Do not use an unprivileged user client with `ResourceEditor`. + +## Quick Example + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.node import Node + +node = Node(name="worker-0") + +with ResourceEditor( + patches={node: {"metadata": {"labels": {"test-label": "true"}}}} +): + # The node now has the label "test-label": "true" + # Run your test logic here... + pass + +# The label is automatically removed when the block exits +``` + +That's it—when the `with` block ends (even if an exception occurs), the original resource state is restored. + +## Step-by-Step: Using ResourceEditor as a Context Manager + +### 1. Import and identify your target resource + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap(name="my-config", namespace="my-namespace") +``` + +### 2. Define your patch + +Patches are dictionaries that mirror the resource's YAML structure. Only include the fields you want to change: + +```python +patch = {"data": {"feature_flag": "enabled", "log_level": "debug"}} +``` + +### 3. Wrap your test logic in a `with` block + +```python +with ResourceEditor(patches={cm: patch}): + # ConfigMap now has updated data fields + assert cm.instance.data.feature_flag == "enabled" + # Run tests that depend on the modified config... +``` + +### 4. Original values are restored automatically + +When the `with` block exits, the original `data` values are written back to the resource. Fields that didn't exist before the patch are removed (set to `None` in the restore payload, which tells the API to delete them). + +## Patching Multiple Resources at Once + +Pass multiple resource-patch pairs in a single `ResourceEditor`: + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.namespace import Namespace +from ocp_resources.config_map import ConfigMap + +ns = Namespace(name="test-ns") +cm = ConfigMap(name="app-config", namespace="test-ns") + +patches = { + ns: {"metadata": {"labels": {"env": "test"}}}, + cm: {"data": {"database_url": "postgres://test-db:5432"}}, +} + +with ResourceEditor(patches=patches): + # Both resources are patched; run tests here + pass +# Both resources are restored to their original state +``` + +## Using update() and restore() Without a Context Manager + +If you need finer control over when patches are applied and reverted—for example, in `setup`/`teardown` fixtures—use the `update()` and `restore()` methods directly: + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap(name="app-config", namespace="default") + +editor = ResourceEditor( + patches={cm: {"data": {"mode": "maintenance"}}} +) + +# Apply the patch and create backups +editor.update(backup_resources=True) + +# ... run tests ... + +# Restore original values +editor.restore() +``` + +> **Note:** When calling `update()` manually, pass `backup_resources=True` to ensure original values are captured. Without this flag, no backup is created and `restore()` will have nothing to revert. + +## Advanced Usage + +### Replace Instead of Patch + +By default, `ResourceEditor` uses the `"update"` action, which sends a merge-patch (`application/merge-patch+json`). This merges your changes into the existing resource. If you need to *replace* the entire resource (for example, to remove fields that merge-patch can't delete), use `action="replace"`: + +```python +from ocp_resources.resource import ResourceEditor + +with ResourceEditor( + patches={my_resource: {"spec": {"replicas": 3}}}, + action="replace", +): + # The resource is fully replaced, not merged + pass +``` + +| Action | Method Used | Behavior | +|---|---|---| +| `"update"` (default) | Merge patch | Merges patch into existing resource; cannot remove fields by omission | +| `"replace"` | Full replacement | Replaces the entire resource; fields not in the patch are removed | + +> **Warning:** With `action="replace"`, the patch must include all required fields for the resource, not just the fields you want to change. The replacement payload automatically includes `metadata.name`, `metadata.namespace`, `metadata.resourceVersion`, `kind`, and `apiVersion`. + +### Providing Your Own Backups + +If you already know what the restore state should be (or need custom restore logic), pass `user_backups` to skip the automatic backup calculation: + +```python +from ocp_resources.resource import ResourceEditor + +custom_backup = {my_resource: {"spec": {"replicas": 1}}} + +with ResourceEditor( + patches={my_resource: {"spec": {"replicas": 5}}}, + user_backups=custom_backup, +): + # Resource scaled to 5 replicas + pass +# Restored to 1 replica (your custom backup), regardless of what the original value was +``` + +### Inspecting Backups + +Access the automatically generated backup data through the `backups` property: + +```python +editor = ResourceEditor( + patches={my_resource: {"metadata": {"labels": {"env": "staging"}}}} +) +editor.update(backup_resources=True) + +# See what was backed up +print(editor.backups) +# {: {'metadata': {'labels': {'env': 'production'}}}} + +editor.restore() +``` + +### No-Op Detection + +`ResourceEditor` automatically detects when a patch produces no actual changes. If the patch values already match the resource's current state, the resource is skipped entirely and a warning is logged: + +``` +ResourceEdit: no diff found in patch for my-resource -- skipping +``` + +This prevents unnecessary API calls and avoids triggering watch events for no-op changes. + +### Automatic Retry on Conflicts + +All patch and restore operations are wrapped with automatic retry logic. If a `ConflictError` occurs (for example, due to a concurrent update changing the `resourceVersion`), the operation is retried automatically with a 5-second sleep interval and a 30-second timeout. + +## API Reference + +### Constructor + +```python +ResourceEditor( + patches: dict, + action: str = "update", + user_backups: dict | None = None, +) +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `patches` | `dict` | *(required)* | Mapping of resource objects to patch dicts: `{resource: {yaml_patch}}` | +| `action` | `str` | `"update"` | Either `"update"` (merge patch) or `"replace"` (full replacement) | +| `user_backups` | `dict \| None` | `None` | Custom backup dicts to use for restore instead of auto-generated ones | + +### Properties + +| Property | Return Type | Description | +|---|---|---| +| `backups` | `dict` | The backup data captured for each patched resource: `{resource: backup_dict}` | +| `patches` | `dict` | The patches dict provided in the constructor | + +### Methods + +| Method | Parameters | Description | +|---|---|---| +| `update(backup_resources=False)` | `backup_resources: bool` | Apply patches. Set `backup_resources=True` to capture original values for later restore. | +| `restore()` | — | Re-apply the stored backup dicts to revert all patched resources. | + +### Context Manager Protocol + +| Method | Behavior | +|---|---| +| `__enter__()` | Calls `update(backup_resources=True)` and returns the `ResourceEditor` instance | +| `__exit__()` | Calls `restore()` to revert all changes | + +## Troubleshooting + +| Problem | Cause | Solution | +|---|---|---| +| Restore doesn't revert changes | Called `update()` without `backup_resources=True` | Always pass `backup_resources=True` when calling `update()` manually, or use the context manager (`with` block) which does this automatically | +| `NotFoundError` during backup | Resource cannot be found by name (known with some CRDs like `ServiceMonitor`) | `ResourceEditor` handles this automatically by falling back to a field-selector query | +| `ConflictError` during patch | Another process modified the resource concurrently | Automatic retries handle this; if it persists beyond 30 seconds, check for controllers continuously modifying the resource | +| Patch has no effect | The resource already has the values in your patch | This is expected—`ResourceEditor` logs a warning and skips the resource. Check that you're patching the correct fields. | +| `replace` action fails | Patch is missing required fields | When using `action="replace"`, provide all required spec fields since the entire resource is replaced | + +> **Tip:** For creating and cleaning up resources (rather than editing existing ones), see [Creating and Managing Resources](creating-and-managing-resources.html). For managing groups of similar resources, see [Managing Bulk Resources with ResourceList](managing-resource-lists.html). + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Resource and NamespacedResource API](resource-api.html) +- [Common Resource Patterns](common-patterns.html) +- [Managing Bulk Resources with ResourceList](managing-resource-lists.html) +- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) diff --git a/docs/environment-variables.html b/docs/environment-variables.html new file mode 100644 index 0000000000..ea24660013 --- /dev/null +++ b/docs/environment-variables.html @@ -0,0 +1,1006 @@ + + + + + + Environment Variables and Configuration - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by reading the knowledge graph and page manifest, then explore the codebase for environment variables.

+

Now let me look at the keys_to_hash and hash-related functionality more closely:

+

Now let me look at the get_logger import and how log levels work:

+

Now let me check for any CI/CD or config-related environment variables:

+

Let me also look for any validation-related environment variables and enable_validation_by_default:

+

Now let me look at the get_logger usage and the simple_logger format:

+

Now let me check the deploy() and clean_up() methods more carefully, and also the hash_resource_dict method:

+

Let me also check how replace_key_with_hashed_value is defined:

+

Now let me check the MCP server for environment variables:

+

Now let me check if there are any other environment variable usages in the class_generator:

+

Let me verify there are no other env vars in tests:

+

Let me also check the full get_client function and the save_kubeconfig function:

+

Now let me look for any additional environment variables in fake_kubernetes_client:

+

Let me look at how the deploy and clean_up methods use the skip logic:

+

Now I have all the information needed. Let me write the documentation:

+

Environment Variables and Configuration

+

This page documents all environment variables recognized by openshift-python-wrapper at runtime. These variables control logging behavior, resource lifecycle operations, proxy configuration, cluster connectivity, and sensitive data hashing.

+

Quick Reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Environment VariablePurposeDefault
KUBECONFIGPath to kubeconfig file~/.kube/config
HTTPS_PROXYHTTPS proxy URL for cluster connections(unset)
HTTP_PROXYHTTP proxy URL for cluster connections(unset)
OPENSHIFT_PYTHON_WRAPPER_LOG_LEVELLogging verbosity levelINFO
OPENSHIFT_PYTHON_WRAPPER_LOG_FILEPath to log output file"" (stdout)
OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATAEnable/disable hashing sensitive data in logstrue
REUSE_IF_RESOURCE_EXISTSSkip resource creation if already exists(unset)
SKIP_RESOURCE_TEARDOWNSkip resource deletion during teardown(unset)
+
+

Cluster Connection

+

KUBECONFIG

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Typestr
Default~/.kube/config
Used byget_client()
Sourceocp_resources/resource.py
+

Path to the kubeconfig file used when creating a Kubernetes DynamicClient. Read when no explicit config_file, config_dict, host/token, or username/password arguments are passed to get_client().

+
+

Note: The standard kubernetes Python client reads KUBECONFIG at import time. If you set this variable in code (after import), openshift-python-wrapper handles it by explicitly passing the value to the client constructor. See Connecting to Clusters for all connection options.

+
+
export KUBECONFIG=/path/to/my/kubeconfig
+
+ +
from ocp_resources.resource import get_client
+
+# Automatically uses $KUBECONFIG, or falls back to ~/.kube/config
+client = get_client()
+
+ +
+

HTTPS_PROXY / HTTP_PROXY

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Typestr
Default(unset — no proxy)
Used byget_client()
Sourceocp_resources/resource.py
+

Sets the proxy on the Kubernetes client configuration when no proxy is already configured on the client_configuration object. HTTPS_PROXY takes precedence over HTTP_PROXY when both are set.

+
export HTTPS_PROXY=http://proxy.example.com:8080
+
+ +
from ocp_resources.resource import get_client
+
+# Proxy is automatically applied from environment
+client = get_client()
+
+ +
+

Tip: If you pass a client_configuration object that already has a .proxy set, the environment variables are ignored.

+
+

Precedence order:

+
    +
  1. Explicit client_configuration.proxy (if already set)
  2. +
  3. HTTPS_PROXY environment variable
  4. +
  5. HTTP_PROXY environment variable
  6. +
+
+

Logging

+

OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Typestr
DefaultINFO
Valid valuesDEBUG, INFO, WARNING, ERROR, CRITICAL
Used byResource._set_logger()
Sourceocp_resources/resource.py
+

Controls the log level for each resource instance's logger. The logger is created per-resource using the simple_logger library.

+
export OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG
+
+ +
import os
+os.environ["OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL"] = "DEBUG"
+
+from ocp_resources.pod import Pod
+from ocp_resources.resource import get_client
+
+client = get_client()
+pod = Pod(name="my-pod", namespace="default", client=client)
+# Logger on this Pod instance now uses DEBUG level
+
+ +
+

OPENSHIFT_PYTHON_WRAPPER_LOG_FILE

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Typestr
Default"" (empty string — logs to stdout)
Used byResource._set_logger()
Sourceocp_resources/resource.py
+

Redirects resource log output to a file. When unset or empty, logs are written to stdout.

+
export OPENSHIFT_PYTHON_WRAPPER_LOG_FILE=/var/log/ocp-wrapper.log
+
+ +
import os
+os.environ["OPENSHIFT_PYTHON_WRAPPER_LOG_FILE"] = "/tmp/ocp-wrapper.log"
+
+from ocp_resources.namespace import Namespace
+from ocp_resources.resource import get_client
+
+client = get_client()
+ns = Namespace(name="test-ns", client=client)
+# All log output for this resource is written to /tmp/ocp-wrapper.log
+
+ +
+

Sensitive Data Hashing

+

OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Typestr
Default"true"
Valid values"true", "false"
Used byResource.hash_resource_dict()
Sourceocp_resources/resource.py
+

Controls whether sensitive fields in resource dictionaries are replaced with "*******" when logged. When set to "false", sensitive data is logged in cleartext.

+

Hashing is applied to fields declared in each resource class's keys_to_hash property. The following built-in resources define sensitive keys:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Resource ClassHashed Fields
Secretdata, stringData
ConfigMapdata, binaryData
SealedSecretspec>data, spec>encryptedData
VirtualMachinespec>template>spec>volumes[]>cloudInitNoCloud>userData
+
+

Warning: Setting this to "false" causes sensitive data (e.g., secrets, tokens) to appear in log output. Use only for debugging in secure environments.

+
+
# Disable hashing to see raw values in logs (debug only)
+export OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA=false
+
+ +
import os
+os.environ["OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA"] = "false"
+
+from ocp_resources.secret import Secret
+from ocp_resources.resource import get_client
+
+client = get_client()
+secret = Secret(name="my-secret", namespace="default", client=client)
+# Logs will now show raw secret data instead of "*******"
+
+ +
+

Note: Hashing also depends on the hash_log_data constructor parameter on each resource instance. Both must be enabled for hashing to occur. The hash_log_data parameter defaults to True. See Resource and NamespacedResource API for constructor details.

+
+

Interaction with hash_log_data parameter:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATAhash_log_data paramResult
"true" (default)True (default)Sensitive fields hashed
"true"FalseSensitive fields not hashed
"false"TrueSensitive fields not hashed
"false"FalseSensitive fields not hashed
+
+

Resource Reuse (Skip Creation)

+

REUSE_IF_RESOURCE_EXISTS

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Typestr (YAML dict syntax)
Default(unset — no resources skipped)
Used byResource.deploy()
Sourceocp_resources/resource.py
+

When set, deploy() checks whether the target resource already exists on the cluster. If a match is found, the existing resource is returned without creating a new one. This is intended for debugging and iterative development workflows.

+
+

Warning: Spaces are significant in the value syntax. The value is parsed as YAML.

+
+

Value format:

+
{<Kind>: {<name>: <namespace>}}
+
+ +

Matching rules:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PatternBehavior
{Pod: {}}Skip creation for all Pods (match by kind only)
{Pod: {my-pod:}}Skip creation for Pod named my-pod in any namespace
{Pod: {my-pod: my-ns}}Skip creation for Pod named my-pod in namespace my-ns only
{Kind1: {}, Kind2: {name: ns}}Multiple resource patterns combined
+

Examples:

+
# Skip all Pod creation if the pod already exists
+export REUSE_IF_RESOURCE_EXISTS="{Pod: {}}"
+
+# Skip specific pod in specific namespace
+export REUSE_IF_RESOURCE_EXISTS="{Pod: {my-pod: my-namespace}}"
+
+# Skip namespace and pod creation
+export REUSE_IF_RESOURCE_EXISTS="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}"
+
+ +
from ocp_resources.pod import Pod
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+# If REUSE_IF_RESOURCE_EXISTS is set and a matching Pod exists,
+# deploy() returns the existing resource without calling create()
+pod = Pod(
+    name="my-pod",
+    namespace="my-namespace",
+    client=client,
+    containers=[{"name": "test", "image": "nginx"}],
+)
+pod.deploy()  # Skips creation if resource matches the env var pattern
+
+ +
+

Note: The resource must actually exist on the cluster for the skip to take effect. If the resource does not exist, deploy() proceeds with normal creation.

+
+
+

Skip Teardown

+

SKIP_RESOURCE_TEARDOWN

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Typestr (YAML dict syntax)
Default(unset — no resources skipped)
Used byResource.clean_up()
Sourceocp_resources/resource.py
+

When set, clean_up() skips deletion for matching resources and returns True without calling delete(). This is intended for debugging — preserving resources on the cluster after tests finish.

+
+

Warning: Spaces are significant in the value syntax. The value is parsed as YAML.

+
+

Value format:

+

Uses the same YAML dict syntax as REUSE_IF_RESOURCE_EXISTS.

+

Matching rules:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PatternBehavior
{Pod: {}}Skip teardown for all Pods
{Pod: {my-pod:}}Skip teardown for Pod named my-pod in any namespace
{Pod: {my-pod: my-ns}}Skip teardown for Pod named my-pod in namespace my-ns only
{Kind1: {}, Kind2: {name: ns}}Multiple resource patterns combined
+

Examples:

+
# Keep all Namespaces after test run
+export SKIP_RESOURCE_TEARDOWN="{Namespace: {}}"
+
+# Keep specific resources
+export SKIP_RESOURCE_TEARDOWN="{Namespace: {test-ns:}, Pod: {debug-pod: test-ns}}"
+
+ +
from ocp_resources.pod import Pod
+from ocp_resources.resource import get_client
+
+client = get_client()
+pod = Pod(
+    name="debug-pod",
+    namespace="test-ns",
+    client=client,
+    containers=[{"name": "test", "image": "nginx"}],
+)
+pod.deploy()
+
+# If SKIP_RESOURCE_TEARDOWN is set with a matching pattern,
+# clean_up() returns True without deleting the resource
+pod.clean_up()  # Resource remains on the cluster
+
+ +
+

Tip: Use REUSE_IF_RESOURCE_EXISTS and SKIP_RESOURCE_TEARDOWN together for a fast debug loop: skip creation if a resource already exists, and skip teardown so it persists between runs.

+
+
export REUSE_IF_RESOURCE_EXISTS="{Pod: {debug-pod: test-ns}}"
+export SKIP_RESOURCE_TEARDOWN="{Pod: {debug-pod: test-ns}}"
+
+ +
+

Combined Usage Patterns

+

Debug iteration loop

+

Set both skip variables to avoid re-creating and tearing down resources between test runs:

+
export REUSE_IF_RESOURCE_EXISTS="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}"
+export SKIP_RESOURCE_TEARDOWN="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}"
+
+ +

Full debug logging with unhashed data

+
export OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG
+export OPENSHIFT_PYTHON_WRAPPER_LOG_FILE=/tmp/debug.log
+export OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA=false
+
+ +
+

Warning: This combination logs sensitive data in plaintext. Do not use in production or shared environments.

+
+

Custom kubeconfig with proxy

+
export KUBECONFIG=/path/to/custom/kubeconfig
+export HTTPS_PROXY=http://corporate-proxy.example.com:3128
+
+ +
+

Programmatic Configuration Alternatives

+

Several behaviors controlled by environment variables can also be set programmatically via constructor parameters or function arguments. See Resource and NamespacedResource API for full constructor parameter documentation.

+ + + + + + + + + + + + + + + + + + + + + +
Environment VariableProgrammatic Alternative
KUBECONFIGget_client(config_file="/path/to/kubeconfig")
HTTPS_PROXY / HTTP_PROXYclient_configuration.proxy = "http://proxy:port"
OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATAResource(hash_log_data=False, ...) constructor parameter
+
+

Note: OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL, OPENSHIFT_PYTHON_WRAPPER_LOG_FILE, REUSE_IF_RESOURCE_EXISTS, and SKIP_RESOURCE_TEARDOWN can only be configured via environment variables.

+
+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/environment-variables.md b/docs/environment-variables.md new file mode 100644 index 0000000000..204aa1d1ea --- /dev/null +++ b/docs/environment-variables.md @@ -0,0 +1,403 @@ +Let me start by reading the knowledge graph and page manifest, then explore the codebase for environment variables. + +Now let me look at the `keys_to_hash` and hash-related functionality more closely: + +Now let me look at the `get_logger` import and how log levels work: + +Now let me check for any CI/CD or config-related environment variables: + +Let me also look for any validation-related environment variables and enable_validation_by_default: + +Now let me look at the `get_logger` usage and the `simple_logger` format: + +Now let me check the deploy() and clean_up() methods more carefully, and also the hash_resource_dict method: + +Let me also check how `replace_key_with_hashed_value` is defined: + +Now let me check the MCP server for environment variables: + +Now let me check if there are any other environment variable usages in the class_generator: + +Let me verify there are no other env vars in tests: + +Let me also check the full `get_client` function and the `save_kubeconfig` function: + +Now let me look for any additional environment variables in fake_kubernetes_client: + +Let me look at how the deploy and clean_up methods use the skip logic: + +Now I have all the information needed. Let me write the documentation: + +# Environment Variables and Configuration + +This page documents all environment variables recognized by `openshift-python-wrapper` at runtime. These variables control logging behavior, resource lifecycle operations, proxy configuration, cluster connectivity, and sensitive data hashing. + +## Quick Reference + +| Environment Variable | Purpose | Default | +|---|---|---| +| `KUBECONFIG` | Path to kubeconfig file | `~/.kube/config` | +| `HTTPS_PROXY` | HTTPS proxy URL for cluster connections | _(unset)_ | +| `HTTP_PROXY` | HTTP proxy URL for cluster connections | _(unset)_ | +| `OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL` | Logging verbosity level | `INFO` | +| `OPENSHIFT_PYTHON_WRAPPER_LOG_FILE` | Path to log output file | `""` (stdout) | +| `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` | Enable/disable hashing sensitive data in logs | `true` | +| `REUSE_IF_RESOURCE_EXISTS` | Skip resource creation if already exists | _(unset)_ | +| `SKIP_RESOURCE_TEARDOWN` | Skip resource deletion during teardown | _(unset)_ | + +--- + +## Cluster Connection + +### `KUBECONFIG` + +| Property | Value | +|---|---| +| **Type** | `str` | +| **Default** | `~/.kube/config` | +| **Used by** | `get_client()` | +| **Source** | `ocp_resources/resource.py` | + +Path to the kubeconfig file used when creating a Kubernetes `DynamicClient`. Read when no explicit `config_file`, `config_dict`, `host`/`token`, or `username`/`password` arguments are passed to `get_client()`. + +> **Note:** The standard `kubernetes` Python client reads `KUBECONFIG` at import time. If you set this variable in code (after import), `openshift-python-wrapper` handles it by explicitly passing the value to the client constructor. See [Connecting to Clusters](connecting-to-clusters.html) for all connection options. + +```bash +export KUBECONFIG=/path/to/my/kubeconfig +``` + +```python +from ocp_resources.resource import get_client + +# Automatically uses $KUBECONFIG, or falls back to ~/.kube/config +client = get_client() +``` + +--- + +### `HTTPS_PROXY` / `HTTP_PROXY` + +| Property | Value | +|---|---| +| **Type** | `str` | +| **Default** | _(unset — no proxy)_ | +| **Used by** | `get_client()` | +| **Source** | `ocp_resources/resource.py` | + +Sets the proxy on the Kubernetes client configuration when no proxy is already configured on the `client_configuration` object. `HTTPS_PROXY` takes precedence over `HTTP_PROXY` when both are set. + +```bash +export HTTPS_PROXY=http://proxy.example.com:8080 +``` + +```python +from ocp_resources.resource import get_client + +# Proxy is automatically applied from environment +client = get_client() +``` + +> **Tip:** If you pass a `client_configuration` object that already has a `.proxy` set, the environment variables are ignored. + +**Precedence order:** + +1. Explicit `client_configuration.proxy` (if already set) +2. `HTTPS_PROXY` environment variable +3. `HTTP_PROXY` environment variable + +--- + +## Logging + +### `OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL` + +| Property | Value | +|---|---| +| **Type** | `str` | +| **Default** | `INFO` | +| **Valid values** | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | +| **Used by** | `Resource._set_logger()` | +| **Source** | `ocp_resources/resource.py` | + +Controls the log level for each resource instance's logger. The logger is created per-resource using the `simple_logger` library. + +```bash +export OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG +``` + +```python +import os +os.environ["OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL"] = "DEBUG" + +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() +pod = Pod(name="my-pod", namespace="default", client=client) +# Logger on this Pod instance now uses DEBUG level +``` + +--- + +### `OPENSHIFT_PYTHON_WRAPPER_LOG_FILE` + +| Property | Value | +|---|---| +| **Type** | `str` | +| **Default** | `""` (empty string — logs to stdout) | +| **Used by** | `Resource._set_logger()` | +| **Source** | `ocp_resources/resource.py` | + +Redirects resource log output to a file. When unset or empty, logs are written to stdout. + +```bash +export OPENSHIFT_PYTHON_WRAPPER_LOG_FILE=/var/log/ocp-wrapper.log +``` + +```python +import os +os.environ["OPENSHIFT_PYTHON_WRAPPER_LOG_FILE"] = "/tmp/ocp-wrapper.log" + +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() +ns = Namespace(name="test-ns", client=client) +# All log output for this resource is written to /tmp/ocp-wrapper.log +``` + +--- + +## Sensitive Data Hashing + +### `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` + +| Property | Value | +|---|---| +| **Type** | `str` | +| **Default** | `"true"` | +| **Valid values** | `"true"`, `"false"` | +| **Used by** | `Resource.hash_resource_dict()` | +| **Source** | `ocp_resources/resource.py` | + +Controls whether sensitive fields in resource dictionaries are replaced with `"*******"` when logged. When set to `"false"`, sensitive data is logged in cleartext. + +Hashing is applied to fields declared in each resource class's `keys_to_hash` property. The following built-in resources define sensitive keys: + +| Resource Class | Hashed Fields | +|---|---| +| `Secret` | `data`, `stringData` | +| `ConfigMap` | `data`, `binaryData` | +| `SealedSecret` | `spec>data`, `spec>encryptedData` | +| `VirtualMachine` | `spec>template>spec>volumes[]>cloudInitNoCloud>userData` | + +> **Warning:** Setting this to `"false"` causes sensitive data (e.g., secrets, tokens) to appear in log output. Use only for debugging in secure environments. + +```bash +# Disable hashing to see raw values in logs (debug only) +export OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA=false +``` + +```python +import os +os.environ["OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA"] = "false" + +from ocp_resources.secret import Secret +from ocp_resources.resource import get_client + +client = get_client() +secret = Secret(name="my-secret", namespace="default", client=client) +# Logs will now show raw secret data instead of "*******" +``` + +> **Note:** Hashing also depends on the `hash_log_data` constructor parameter on each resource instance. Both must be enabled for hashing to occur. The `hash_log_data` parameter defaults to `True`. See [Resource and NamespacedResource API](resource-api.html) for constructor details. + +**Interaction with `hash_log_data` parameter:** + +| `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` | `hash_log_data` param | Result | +|---|---|---| +| `"true"` (default) | `True` (default) | Sensitive fields hashed | +| `"true"` | `False` | Sensitive fields **not** hashed | +| `"false"` | `True` | Sensitive fields **not** hashed | +| `"false"` | `False` | Sensitive fields **not** hashed | + +--- + +## Resource Reuse (Skip Creation) + +### `REUSE_IF_RESOURCE_EXISTS` + +| Property | Value | +|---|---| +| **Type** | `str` (YAML dict syntax) | +| **Default** | _(unset — no resources skipped)_ | +| **Used by** | `Resource.deploy()` | +| **Source** | `ocp_resources/resource.py` | + +When set, `deploy()` checks whether the target resource already exists on the cluster. If a match is found, the existing resource is returned without creating a new one. This is intended for debugging and iterative development workflows. + +> **Warning:** Spaces are significant in the value syntax. The value is parsed as YAML. + +**Value format:** + +``` +{: {: }} +``` + +**Matching rules:** + +| Pattern | Behavior | +|---|---| +| `{Pod: {}}` | Skip creation for **all** Pods (match by kind only) | +| `{Pod: {my-pod:}}` | Skip creation for Pod named `my-pod` in **any** namespace | +| `{Pod: {my-pod: my-ns}}` | Skip creation for Pod named `my-pod` in namespace `my-ns` only | +| `{Kind1: {}, Kind2: {name: ns}}` | Multiple resource patterns combined | + +**Examples:** + +```bash +# Skip all Pod creation if the pod already exists +export REUSE_IF_RESOURCE_EXISTS="{Pod: {}}" + +# Skip specific pod in specific namespace +export REUSE_IF_RESOURCE_EXISTS="{Pod: {my-pod: my-namespace}}" + +# Skip namespace and pod creation +export REUSE_IF_RESOURCE_EXISTS="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}" +``` + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +# If REUSE_IF_RESOURCE_EXISTS is set and a matching Pod exists, +# deploy() returns the existing resource without calling create() +pod = Pod( + name="my-pod", + namespace="my-namespace", + client=client, + containers=[{"name": "test", "image": "nginx"}], +) +pod.deploy() # Skips creation if resource matches the env var pattern +``` + +> **Note:** The resource must actually exist on the cluster for the skip to take effect. If the resource does not exist, `deploy()` proceeds with normal creation. + +--- + +## Skip Teardown + +### `SKIP_RESOURCE_TEARDOWN` + +| Property | Value | +|---|---| +| **Type** | `str` (YAML dict syntax) | +| **Default** | _(unset — no resources skipped)_ | +| **Used by** | `Resource.clean_up()` | +| **Source** | `ocp_resources/resource.py` | + +When set, `clean_up()` skips deletion for matching resources and returns `True` without calling `delete()`. This is intended for debugging — preserving resources on the cluster after tests finish. + +> **Warning:** Spaces are significant in the value syntax. The value is parsed as YAML. + +**Value format:** + +Uses the same YAML dict syntax as [`REUSE_IF_RESOURCE_EXISTS`](#reuse_if_resource_exists). + +**Matching rules:** + +| Pattern | Behavior | +|---|---| +| `{Pod: {}}` | Skip teardown for **all** Pods | +| `{Pod: {my-pod:}}` | Skip teardown for Pod named `my-pod` in **any** namespace | +| `{Pod: {my-pod: my-ns}}` | Skip teardown for Pod named `my-pod` in namespace `my-ns` only | +| `{Kind1: {}, Kind2: {name: ns}}` | Multiple resource patterns combined | + +**Examples:** + +```bash +# Keep all Namespaces after test run +export SKIP_RESOURCE_TEARDOWN="{Namespace: {}}" + +# Keep specific resources +export SKIP_RESOURCE_TEARDOWN="{Namespace: {test-ns:}, Pod: {debug-pod: test-ns}}" +``` + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() +pod = Pod( + name="debug-pod", + namespace="test-ns", + client=client, + containers=[{"name": "test", "image": "nginx"}], +) +pod.deploy() + +# If SKIP_RESOURCE_TEARDOWN is set with a matching pattern, +# clean_up() returns True without deleting the resource +pod.clean_up() # Resource remains on the cluster +``` + +> **Tip:** Use `REUSE_IF_RESOURCE_EXISTS` and `SKIP_RESOURCE_TEARDOWN` together for a fast debug loop: skip creation if a resource already exists, and skip teardown so it persists between runs. + +```bash +export REUSE_IF_RESOURCE_EXISTS="{Pod: {debug-pod: test-ns}}" +export SKIP_RESOURCE_TEARDOWN="{Pod: {debug-pod: test-ns}}" +``` + +--- + +## Combined Usage Patterns + +### Debug iteration loop + +Set both skip variables to avoid re-creating and tearing down resources between test runs: + +```bash +export REUSE_IF_RESOURCE_EXISTS="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}" +export SKIP_RESOURCE_TEARDOWN="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}" +``` + +### Full debug logging with unhashed data + +```bash +export OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG +export OPENSHIFT_PYTHON_WRAPPER_LOG_FILE=/tmp/debug.log +export OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA=false +``` + +> **Warning:** This combination logs sensitive data in plaintext. Do not use in production or shared environments. + +### Custom kubeconfig with proxy + +```bash +export KUBECONFIG=/path/to/custom/kubeconfig +export HTTPS_PROXY=http://corporate-proxy.example.com:3128 +``` + +--- + +## Programmatic Configuration Alternatives + +Several behaviors controlled by environment variables can also be set programmatically via constructor parameters or function arguments. See [Resource and NamespacedResource API](resource-api.html) for full constructor parameter documentation. + +| Environment Variable | Programmatic Alternative | +|---|---| +| `KUBECONFIG` | `get_client(config_file="/path/to/kubeconfig")` | +| `HTTPS_PROXY` / `HTTP_PROXY` | `client_configuration.proxy = "http://proxy:port"` | +| `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` | `Resource(hash_log_data=False, ...)` constructor parameter | + +> **Note:** `OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL`, `OPENSHIFT_PYTHON_WRAPPER_LOG_FILE`, `REUSE_IF_RESOURCE_EXISTS`, and `SKIP_RESOURCE_TEARDOWN` can **only** be configured via environment variables. + +## Related Pages + +- [Connecting to Clusters](connecting-to-clusters.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Resource and NamespacedResource API](resource-api.html) +- [Common Resource Patterns](common-patterns.html) +- [Error Handling and Exception Reference](error-handling.html) diff --git a/docs/error-handling.html b/docs/error-handling.html new file mode 100644 index 0000000000..e58bd282f9 --- /dev/null +++ b/docs/error-handling.html @@ -0,0 +1,1312 @@ + + + + + + Error Handling and Exception Reference - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me explore the repository structure and find all relevant exception definitions and usages.

+

Now let me look at the ConditionError usage more closely and the MissingTemplateVariables and MissingResourceError exceptions:

+

Now let me look at how the deploy method uses ValidationError:

+

Now let me see where validate is called during create:

+

Now let me look at the Pod.execute method more closely:

+

Now let me look at how client_with_basic_auth works:

+

Now let me see the NNCP methods in full context:

+

Let me also look at MissingTemplateVariables usage more closely:

+

Now let me check the __init__ of Resource to see how schema_validation_enabled is handled:

+

Now let me also check the wait_for_condition method signature more closely:

+

Let me also look at MissingResourceResError usage:

+

Now let me check the test for ResourceTeardownError:

+

Let me see how SecretTestExit is defined:

+

Let me check the import path for all exceptions:

+

Now I have a thorough understanding of the codebase. Let me write the reference documentation.

+

Error Handling and Exception Reference

+

All custom exceptions are defined in ocp_resources.exceptions and can be imported directly:

+
from ocp_resources.exceptions import (
+    ExecOnPodError,
+    MissingRequiredArgumentError,
+    ResourceTeardownError,
+    ValidationError,
+    ConditionError,
+    NNCPConfigurationFailed,
+    ClientWithBasicAuthError,
+    MissingResourceError,
+    MissingTemplateVariables,
+)
+
+ +
+

Tip: All exceptions inherit from Python's built-in Exception class and can be caught with a bare except Exception if needed.

+
+
+

ExecOnPodError

+

Raised when a command executed inside a pod via Pod.execute() fails. See Executing Commands in Pods and Retrieving Logs for full usage details.

+

Import:

+
from ocp_resources.exceptions import ExecOnPodError
+
+ +

Constructor Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
commandlist[str]The command that was executed
rcintReturn code (-1 if the return code could not be determined)
outstrStandard output captured from the command
errAnyStandard error output or error channel details
+

Attributes on the caught exception:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeTypeDescription
cmdlist[str]The command that was executed
rcintThe return code
outstrStandard output
errAnyStandard error or error channel dict
+

Raised by: Pod.execute() in the following scenarios:

+
    +
  • Command times out (stream response closes before completion) — rc=-1
  • +
  • Error channel returns no status — rc=-1
  • +
  • Error channel status is "Failure"rc=-1, err contains the full error channel dict
  • +
  • Command exits with a non-zero exit code — rc contains the actual exit code
  • +
+

String representation:

+
Command execution failure: ['ls', '/nonexistent'], RC: 2, OUT: , ERR: ls: cannot access '/nonexistent'
+
+ +

Example:

+
from ocp_resources.pod import Pod
+from ocp_resources.exceptions import ExecOnPodError
+
+pod = Pod(client=client, name="my-pod", namespace="default")
+
+try:
+    output = pod.execute(command=["ls", "/nonexistent"], timeout=30)
+except ExecOnPodError as e:
+    print(f"Command: {e.cmd}")
+    print(f"Return code: {e.rc}")
+    print(f"Stdout: {e.out}")
+    print(f"Stderr: {e.err}")
+
+ +

Handling non-zero exit codes without exceptions:

+
# Use ignore_rc=True to suppress ExecOnPodError on non-zero exit codes
+output = pod.execute(command=["grep", "pattern", "/var/log/messages"], ignore_rc=True)
+
+ +
+

MissingRequiredArgumentError

+

Raised when a resource is instantiated without providing required arguments and no yaml_file or kind_dict was supplied. This error is raised during to_dict(), which is called automatically by create() and deploy().

+

Import:

+
from ocp_resources.exceptions import MissingRequiredArgumentError
+
+ +

Constructor Parameters:

+ + + + + + + + + + + + + + + +
ParameterTypeDescription
argumentstrName of the missing required argument(s)
+

Attributes on the caught exception:

+ + + + + + + + + + + + + + + +
AttributeTypeDescription
argumentstrThe missing argument name
+

String representation:

+
Missing required argument/s. Either provide yaml_file, kind_dict or pass self.containers
+
+ +

Raised by: The to_dict() method of resource subclasses when required spec fields are not provided. Examples of resources that raise this exception:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Resource ClassRequired Arguments
Podcontainers
StorageClassprovisioner
ClusterRoleBindingcluster_role
ClusterResourceQuotaquota, selector
CronJobjob_template, schedule
InferenceServicepredictor
IPAddressPooladdresses
ResourceQuotahard
UserDefinedNetworktopology
+
+

Note: This exception is not raised if you construct the resource using yaml_file or kind_dict, since those bypass the to_dict() logic.

+
+

Example:

+
from ocp_resources.pod import Pod
+from ocp_resources.exceptions import MissingRequiredArgumentError
+
+try:
+    pod = Pod(client=client, name="my-pod", namespace="default")
+    pod.deploy()
+except MissingRequiredArgumentError as e:
+    print(f"Missing: {e.argument}")
+    # Output: Missing required argument/s. Either provide yaml_file, kind_dict or pass self.containers
+
+ +
+

ResourceTeardownError

+

Raised when a resource's clean_up() method returns False during context manager exit. This indicates the resource could not be deleted.

+

Import:

+
from ocp_resources.exceptions import ResourceTeardownError
+
+ +

Constructor Parameters:

+ + + + + + + + + + + + + + + +
ParameterTypeDescription
resourceAnyThe resource object that failed to tear down
+

Attributes on the caught exception:

+ + + + + + + + + + + + + + + +
AttributeTypeDescription
resourceAnyThe resource object that failed teardown
+

String representation:

+
Failed to execute teardown for resource <resource>
+
+ +

Raised by: Resource.__exit__() — the context manager exit handler. Specifically, this is raised when:

+
    +
  1. The resource was created with teardown=True (the default).
  2. +
  3. The clean_up() method returns False.
  4. +
+

Example:

+
from ocp_resources.pod import Pod
+from ocp_resources.exceptions import ResourceTeardownError
+
+try:
+    with Pod(
+        client=client,
+        name="my-pod",
+        namespace="default",
+        containers=[{"name": "nginx", "image": "nginx:latest"}],
+    ) as pod:
+        # Use pod...
+        pass
+    # clean_up() is called automatically here
+except ResourceTeardownError as e:
+    print(f"Could not delete: {e.resource}")
+
+ +
+

Tip: Set teardown=False on the resource constructor if you do not want automatic cleanup on context manager exit, and thus never want this exception raised.

+
+
+

ValidationError

+

Raised when a resource fails schema validation against the OpenAPI specification. See Validating Resources Against OpenAPI Schemas for full validation guide.

+

Import:

+
from ocp_resources.exceptions import ValidationError
+
+ +

Constructor Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
messagestr(required)Human-readable error description
pathstr""JSONPath to the invalid field (e.g., "spec.containers[0].image")
schema_errorAnyNoneOriginal jsonschema validation error for debugging
+

Attributes on the caught exception:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeTypeDescription
messagestrHuman-readable error message
pathstrJSONPath to the invalid field
schema_errorAnyOriginal jsonschema.ValidationError if available
+

String representation:

+
Validation error at 'spec.containers[0].image': Invalid type
+
+ +

When path is empty:

+
Validation error: Field is required
+
+ +

Raised by:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
MethodTrigger
resource.validate()Called explicitly by user
resource.create()When schema_validation_enabled=True
resource.update_replace()When schema_validation_enabled=True
Resource.validate_dict()Class method for validating raw dicts
+
+

Note: resource.update() does not trigger validation even when schema_validation_enabled=True, because updates send partial patches that would fail full schema validation.

+
+

Example — explicit validation:

+
from ocp_resources.pod import Pod
+from ocp_resources.exceptions import ValidationError
+
+pod = Pod(client=client, name="my-pod", namespace="default",
+          containers=[{"name": "nginx", "image": "nginx:latest"}])
+
+try:
+    pod.validate()
+except ValidationError as e:
+    print(f"Error: {e.message}")
+    print(f"Path: {e.path}")
+    if e.schema_error:
+        print(f"Original error: {e.schema_error}")
+
+ +

Example — auto-validation on create:

+
from ocp_resources.pod import Pod
+from ocp_resources.exceptions import ValidationError
+
+try:
+    pod = Pod(
+        client=client,
+        name="my-pod",
+        namespace="default",
+        containers=[{"name": "nginx", "image": "nginx:latest"}],
+        schema_validation_enabled=True,
+    )
+    pod.deploy()
+except ValidationError as e:
+    print(f"Invalid resource: {e}")
+
+ +

Example — validate a raw dictionary:

+
from ocp_resources.deployment import Deployment
+from ocp_resources.exceptions import ValidationError
+
+deployment_dict = {
+    "apiVersion": "apps/v1",
+    "kind": "Deployment",
+    "metadata": {"name": "my-deploy"},
+    "spec": {"replicas": "three"},  # Wrong type — should be int
+}
+
+try:
+    Deployment.validate_dict(resource_dict=deployment_dict)
+except ValidationError as e:
+    print(f"Validation failed: {e}")
+
+ +
+

ConditionError

+

Raised when a resource reaches an undesired stop condition during wait_for_condition(). See Waiting for Resource Conditions and Status for details.

+

Import:

+
from ocp_resources.exceptions import ConditionError
+
+ +

Constructor Parameters: Standard Exception — accepts a single string message.

+

Raised by: Resource.wait_for_condition() when the stop_condition parameter matches before the desired condition is met.

+

wait_for_condition() parameters relevant to ConditionError:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
conditionstr(required)Condition type to wait for
statusstr(required)Expected status value
timeoutint300Maximum wait time in seconds
sleep_timeint1Polling interval in seconds
stop_conditionstr \| NoneNoneCondition type that should abort the wait
stop_statusstr"True"Status value for the stop condition
+

String representation:

+
Deployment my-deploy reached stop_condition 'Failed' in status 'True':
+{'type': 'Failed', 'status': 'True', 'reason': 'DeadlineExceeded', 'message': '...'}
+
+ +
+

Note: When stop_condition is None (the default), ConditionError is never raised. Instead, TimeoutExpiredError is raised if the desired condition is not met within the timeout.

+
+

Example:

+
from ocp_resources.deployment import Deployment
+from ocp_resources.exceptions import ConditionError
+from timeout_sampler import TimeoutExpiredError
+
+deploy = Deployment(client=client, name="my-deploy", namespace="default")
+
+try:
+    deploy.wait_for_condition(
+        condition="Available",
+        status="True",
+        timeout=120,
+        stop_condition="Failed",
+        stop_status="True",
+    )
+except ConditionError as e:
+    print(f"Resource entered failure state: {e}")
+except TimeoutExpiredError:
+    print("Timed out waiting for condition")
+
+ +
+

NNCPConfigurationFailed

+

Raised when a NodeNetworkConfigurationPolicy (NNCP) fails to configure on one or more nodes.

+

Import:

+
from ocp_resources.exceptions import NNCPConfigurationFailed
+
+ +

Constructor Parameters: Standard Exception — accepts a single string message describing the failure reason and error details.

+

Raised by: NodeNetworkConfigurationPolicy.wait_for_status_success() in two scenarios:

+ + + + + + + + + + + + + + + + + +
ScenarioMessage Pattern
No matching node found"{name}. Reason: NoMatchingNode"
Configuration failed on nodes"Reason: FailedToConfigure\n{error_details}"
+
+

Note: When wait_for_status_success() catches TimeoutExpiredError or NNCPConfigurationFailed, it logs the error with node details and re-raises the exception.

+
+

Example:

+
from ocp_resources.node_network_configuration_policy import NodeNetworkConfigurationPolicy
+from ocp_resources.exceptions import NNCPConfigurationFailed
+from timeout_sampler import TimeoutExpiredError
+
+nncp = NodeNetworkConfigurationPolicy(
+    client=client,
+    name="my-nncp",
+    desired_state={"interfaces": [{"name": "eth1", "type": "ethernet", "state": "up"}]},
+)
+
+try:
+    nncp.deploy()
+    nncp.wait_for_status_success()
+except NNCPConfigurationFailed as e:
+    print(f"NNCP configuration failed: {e}")
+except TimeoutExpiredError:
+    print("NNCP configuration timed out")
+
+ +
+

ClientWithBasicAuthError

+

Raised during OAuth-based client authentication when using username/password credentials to connect to an OpenShift cluster. See Connecting to Clusters for connection methods.

+

Import:

+
from ocp_resources.exceptions import ClientWithBasicAuthError
+
+ +

Constructor Parameters: Standard Exception — accepts a single string message.

+

Raised by: client_configuration_with_basic_auth() in the following scenarios:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ScenarioMessage
OAuth well-known endpoint not reachable"No well-known file found at endpoint"
Authorization code not returned after login"No authorization code found"
No authorization_endpoint in OAuth config"No authorization_endpoint found in well-known file"
Token exchange fails"Failed to authenticate with basic auth"
+

Example:

+
from ocp_resources.exceptions import ClientWithBasicAuthError
+from ocp_resources.resource import client_configuration_with_basic_auth
+
+import kubernetes
+
+configuration = kubernetes.client.Configuration()
+configuration.verify_ssl = False
+
+try:
+    api_client = client_configuration_with_basic_auth(
+        username="admin",
+        password="password",
+        host="https://api.cluster.example.com:6443",
+        configuration=configuration,
+    )
+except ClientWithBasicAuthError as e:
+    print(f"Authentication failed: {e}")
+
+ +
+

MissingResourceError

+

Raised when a resource object fails to generate its internal representation.

+

Import:

+
from ocp_resources.exceptions import MissingResourceError
+
+ +

Constructor Parameters:

+ + + + + + + + + + + + + + + +
ParameterTypeDescription
namestrName of the resource that could not be generated
+

Attributes on the caught exception:

+ + + + + + + + + + + + + + + +
AttributeTypeDescription
resource_namestrThe resource name
+

String representation:

+
Failed to generate resource: my-resource
+
+ +

Example:

+
from ocp_resources.exceptions import MissingResourceError
+
+try:
+    # Operations that may fail to generate a resource
+    ...
+except MissingResourceError as e:
+    print(f"Resource generation failed: {e.resource_name}")
+
+ +
+

MissingTemplateVariables

+

Raised when rendering a YAML template and not all required template variables have been provided.

+

Import:

+
from ocp_resources.exceptions import MissingTemplateVariables
+
+ +

Constructor Parameters:

+ + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
varstrName of the missing template variable
templatestrPath to the template file
+

Attributes on the caught exception:

+ + + + + + + + + + + + + + + + + + + + +
AttributeTypeDescription
varstrThe missing variable name
templatestrThe template file path
+

String representation:

+
Missing variables image for template tests/manifests/vm.yaml
+
+ +

Example:

+
from ocp_resources.exceptions import MissingTemplateVariables
+
+try:
+    result = generate_yaml_from_template(name="my-vm")
+    # If template also requires 'image', raises MissingTemplateVariables
+except MissingTemplateVariables as e:
+    print(f"Variable '{e.var}' missing in template '{e.template}'")
+
+ +
+

Common Error-Handling Patterns

+

Catching Multiple Library Exceptions

+
from ocp_resources.exceptions import (
+    ExecOnPodError,
+    MissingRequiredArgumentError,
+    ResourceTeardownError,
+    ValidationError,
+)
+
+try:
+    with Pod(client=client, name="worker", namespace="default",
+             containers=[{"name": "app", "image": "myapp:latest"}],
+             schema_validation_enabled=True) as pod:
+        pod.execute(command=["python", "run_task.py"])
+except ValidationError as e:
+    print(f"Invalid resource spec: {e}")
+except ExecOnPodError as e:
+    print(f"Task failed (rc={e.rc}): {e.err}")
+except ResourceTeardownError as e:
+    print(f"Pod cleanup failed: {e}")
+except MissingRequiredArgumentError as e:
+    print(f"Missing field: {e.argument}")
+
+ +

Safe Condition Waiting with Early Abort

+
from ocp_resources.exceptions import ConditionError
+from timeout_sampler import TimeoutExpiredError
+
+try:
+    resource.wait_for_condition(
+        condition="Ready",
+        status="True",
+        timeout=180,
+        stop_condition="Failed",
+        stop_status="True",
+    )
+except ConditionError:
+    # Resource entered a terminal failure state — no point waiting further
+    resource.clean_up()
+    raise
+except TimeoutExpiredError:
+    # Condition not met within timeout — may be transient
+    print("Resource is taking too long, investigating...")
+
+ +

Ignoring Non-Critical Command Failures

+
from ocp_resources.exceptions import ExecOnPodError
+
+# Method 1: Use ignore_rc=True
+output = pod.execute(command=["grep", "ERROR", "/var/log/app.log"], ignore_rc=True)
+
+# Method 2: Catch and inspect the return code
+try:
+    output = pod.execute(command=["test", "-f", "/tmp/lockfile"])
+except ExecOnPodError as e:
+    if e.rc == 1:
+        print("Lock file does not exist — proceeding")
+    else:
+        raise  # Re-raise unexpected errors
+
+ +

Validation Before Batch Deployment

+
from ocp_resources.exceptions import ValidationError
+
+resources = [pod1, pod2, deployment1, service1]
+
+# Pre-validate all resources before deploying any
+errors = []
+for resource in resources:
+    try:
+        resource.validate()
+    except ValidationError as e:
+        errors.append((resource.name, str(e)))
+
+if errors:
+    for name, err in errors:
+        print(f"  {name}: {err}")
+    raise SystemExit("Fix validation errors before deploying")
+
+for resource in resources:
+    resource.deploy()
+
+ +
+

Exception Hierarchy Summary

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExceptionParentModule
ExecOnPodErrorExceptionocp_resources.exceptions
MissingRequiredArgumentErrorExceptionocp_resources.exceptions
ResourceTeardownErrorExceptionocp_resources.exceptions
ValidationErrorExceptionocp_resources.exceptions
ConditionErrorExceptionocp_resources.exceptions
NNCPConfigurationFailedExceptionocp_resources.exceptions
ClientWithBasicAuthErrorExceptionocp_resources.exceptions
MissingResourceErrorExceptionocp_resources.exceptions
MissingTemplateVariablesExceptionocp_resources.exceptions
+
+

Warning: MissingResourceResError is deprecated and will be removed in a future release. Use MissingResourceError instead.

+
+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/error-handling.md b/docs/error-handling.md new file mode 100644 index 0000000000..0d54d199c6 --- /dev/null +++ b/docs/error-handling.md @@ -0,0 +1,695 @@ +Let me explore the repository structure and find all relevant exception definitions and usages. + +Now let me look at the `ConditionError` usage more closely and the `MissingTemplateVariables` and `MissingResourceError` exceptions: + +Now let me look at how the deploy method uses ValidationError: + +Now let me see where `validate` is called during `create`: + +Now let me look at the `Pod.execute` method more closely: + +Now let me look at how `client_with_basic_auth` works: + +Now let me see the NNCP methods in full context: + +Let me also look at `MissingTemplateVariables` usage more closely: + +Now let me check the `__init__` of Resource to see how `schema_validation_enabled` is handled: + +Now let me also check the `wait_for_condition` method signature more closely: + +Let me also look at `MissingResourceResError` usage: + +Now let me check the test for `ResourceTeardownError`: + +Let me see how SecretTestExit is defined: + +Let me check the import path for all exceptions: + +Now I have a thorough understanding of the codebase. Let me write the reference documentation. + +# Error Handling and Exception Reference + +All custom exceptions are defined in `ocp_resources.exceptions` and can be imported directly: + +```python +from ocp_resources.exceptions import ( + ExecOnPodError, + MissingRequiredArgumentError, + ResourceTeardownError, + ValidationError, + ConditionError, + NNCPConfigurationFailed, + ClientWithBasicAuthError, + MissingResourceError, + MissingTemplateVariables, +) +``` + +> **Tip:** All exceptions inherit from Python's built-in `Exception` class and can be caught with a bare `except Exception` if needed. + +--- + +## ExecOnPodError + +Raised when a command executed inside a pod via `Pod.execute()` fails. See [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) for full usage details. + +**Import:** + +```python +from ocp_resources.exceptions import ExecOnPodError +``` + +**Constructor Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `command` | `list[str]` | The command that was executed | +| `rc` | `int` | Return code (`-1` if the return code could not be determined) | +| `out` | `str` | Standard output captured from the command | +| `err` | `Any` | Standard error output or error channel details | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `cmd` | `list[str]` | The command that was executed | +| `rc` | `int` | The return code | +| `out` | `str` | Standard output | +| `err` | `Any` | Standard error or error channel dict | + +**Raised by:** `Pod.execute()` in the following scenarios: + +- Command times out (stream response closes before completion) — `rc=-1` +- Error channel returns no status — `rc=-1` +- Error channel status is `"Failure"` — `rc=-1`, `err` contains the full error channel dict +- Command exits with a non-zero exit code — `rc` contains the actual exit code + +**String representation:** + +``` +Command execution failure: ['ls', '/nonexistent'], RC: 2, OUT: , ERR: ls: cannot access '/nonexistent' +``` + +**Example:** + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ExecOnPodError + +pod = Pod(client=client, name="my-pod", namespace="default") + +try: + output = pod.execute(command=["ls", "/nonexistent"], timeout=30) +except ExecOnPodError as e: + print(f"Command: {e.cmd}") + print(f"Return code: {e.rc}") + print(f"Stdout: {e.out}") + print(f"Stderr: {e.err}") +``` + +**Handling non-zero exit codes without exceptions:** + +```python +# Use ignore_rc=True to suppress ExecOnPodError on non-zero exit codes +output = pod.execute(command=["grep", "pattern", "/var/log/messages"], ignore_rc=True) +``` + +--- + +## MissingRequiredArgumentError + +Raised when a resource is instantiated without providing required arguments and no `yaml_file` or `kind_dict` was supplied. This error is raised during `to_dict()`, which is called automatically by `create()` and `deploy()`. + +**Import:** + +```python +from ocp_resources.exceptions import MissingRequiredArgumentError +``` + +**Constructor Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `argument` | `str` | Name of the missing required argument(s) | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `argument` | `str` | The missing argument name | + +**String representation:** + +``` +Missing required argument/s. Either provide yaml_file, kind_dict or pass self.containers +``` + +**Raised by:** The `to_dict()` method of resource subclasses when required spec fields are not provided. Examples of resources that raise this exception: + +| Resource Class | Required Arguments | +|---|---| +| `Pod` | `containers` | +| `StorageClass` | `provisioner` | +| `ClusterRoleBinding` | `cluster_role` | +| `ClusterResourceQuota` | `quota`, `selector` | +| `CronJob` | `job_template`, `schedule` | +| `InferenceService` | `predictor` | +| `IPAddressPool` | `addresses` | +| `ResourceQuota` | `hard` | +| `UserDefinedNetwork` | `topology` | + +> **Note:** This exception is **not** raised if you construct the resource using `yaml_file` or `kind_dict`, since those bypass the `to_dict()` logic. + +**Example:** + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import MissingRequiredArgumentError + +try: + pod = Pod(client=client, name="my-pod", namespace="default") + pod.deploy() +except MissingRequiredArgumentError as e: + print(f"Missing: {e.argument}") + # Output: Missing required argument/s. Either provide yaml_file, kind_dict or pass self.containers +``` + +--- + +## ResourceTeardownError + +Raised when a resource's `clean_up()` method returns `False` during context manager exit. This indicates the resource could not be deleted. + +**Import:** + +```python +from ocp_resources.exceptions import ResourceTeardownError +``` + +**Constructor Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `resource` | `Any` | The resource object that failed to tear down | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `resource` | `Any` | The resource object that failed teardown | + +**String representation:** + +``` +Failed to execute teardown for resource +``` + +**Raised by:** `Resource.__exit__()` — the context manager exit handler. Specifically, this is raised when: + +1. The resource was created with `teardown=True` (the default). +2. The `clean_up()` method returns `False`. + +**Example:** + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ResourceTeardownError + +try: + with Pod( + client=client, + name="my-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + ) as pod: + # Use pod... + pass + # clean_up() is called automatically here +except ResourceTeardownError as e: + print(f"Could not delete: {e.resource}") +``` + +> **Tip:** Set `teardown=False` on the resource constructor if you do not want automatic cleanup on context manager exit, and thus never want this exception raised. + +--- + +## ValidationError + +Raised when a resource fails schema validation against the OpenAPI specification. See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for full validation guide. + +**Import:** + +```python +from ocp_resources.exceptions import ValidationError +``` + +**Constructor Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `message` | `str` | *(required)* | Human-readable error description | +| `path` | `str` | `""` | JSONPath to the invalid field (e.g., `"spec.containers[0].image"`) | +| `schema_error` | `Any` | `None` | Original `jsonschema` validation error for debugging | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `message` | `str` | Human-readable error message | +| `path` | `str` | JSONPath to the invalid field | +| `schema_error` | `Any` | Original `jsonschema.ValidationError` if available | + +**String representation:** + +``` +Validation error at 'spec.containers[0].image': Invalid type +``` + +When `path` is empty: + +``` +Validation error: Field is required +``` + +**Raised by:** + +| Method | Trigger | +|--------|---------| +| `resource.validate()` | Called explicitly by user | +| `resource.create()` | When `schema_validation_enabled=True` | +| `resource.update_replace()` | When `schema_validation_enabled=True` | +| `Resource.validate_dict()` | Class method for validating raw dicts | + +> **Note:** `resource.update()` does **not** trigger validation even when `schema_validation_enabled=True`, because updates send partial patches that would fail full schema validation. + +**Example — explicit validation:** + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ValidationError + +pod = Pod(client=client, name="my-pod", namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}]) + +try: + pod.validate() +except ValidationError as e: + print(f"Error: {e.message}") + print(f"Path: {e.path}") + if e.schema_error: + print(f"Original error: {e.schema_error}") +``` + +**Example — auto-validation on create:** + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ValidationError + +try: + pod = Pod( + client=client, + name="my-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + schema_validation_enabled=True, + ) + pod.deploy() +except ValidationError as e: + print(f"Invalid resource: {e}") +``` + +**Example — validate a raw dictionary:** + +```python +from ocp_resources.deployment import Deployment +from ocp_resources.exceptions import ValidationError + +deployment_dict = { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": {"name": "my-deploy"}, + "spec": {"replicas": "three"}, # Wrong type — should be int +} + +try: + Deployment.validate_dict(resource_dict=deployment_dict) +except ValidationError as e: + print(f"Validation failed: {e}") +``` + +--- + +## ConditionError + +Raised when a resource reaches an undesired stop condition during `wait_for_condition()`. See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for details. + +**Import:** + +```python +from ocp_resources.exceptions import ConditionError +``` + +**Constructor Parameters:** Standard `Exception` — accepts a single string message. + +**Raised by:** `Resource.wait_for_condition()` when the `stop_condition` parameter matches before the desired condition is met. + +**`wait_for_condition()` parameters relevant to `ConditionError`:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `condition` | `str` | *(required)* | Condition type to wait for | +| `status` | `str` | *(required)* | Expected status value | +| `timeout` | `int` | `300` | Maximum wait time in seconds | +| `sleep_time` | `int` | `1` | Polling interval in seconds | +| `stop_condition` | `str \| None` | `None` | Condition type that should abort the wait | +| `stop_status` | `str` | `"True"` | Status value for the stop condition | + +**String representation:** + +``` +Deployment my-deploy reached stop_condition 'Failed' in status 'True': +{'type': 'Failed', 'status': 'True', 'reason': 'DeadlineExceeded', 'message': '...'} +``` + +> **Note:** When `stop_condition` is `None` (the default), `ConditionError` is never raised. Instead, `TimeoutExpiredError` is raised if the desired condition is not met within the timeout. + +**Example:** + +```python +from ocp_resources.deployment import Deployment +from ocp_resources.exceptions import ConditionError +from timeout_sampler import TimeoutExpiredError + +deploy = Deployment(client=client, name="my-deploy", namespace="default") + +try: + deploy.wait_for_condition( + condition="Available", + status="True", + timeout=120, + stop_condition="Failed", + stop_status="True", + ) +except ConditionError as e: + print(f"Resource entered failure state: {e}") +except TimeoutExpiredError: + print("Timed out waiting for condition") +``` + +--- + +## NNCPConfigurationFailed + +Raised when a `NodeNetworkConfigurationPolicy` (NNCP) fails to configure on one or more nodes. + +**Import:** + +```python +from ocp_resources.exceptions import NNCPConfigurationFailed +``` + +**Constructor Parameters:** Standard `Exception` — accepts a single string message describing the failure reason and error details. + +**Raised by:** `NodeNetworkConfigurationPolicy.wait_for_status_success()` in two scenarios: + +| Scenario | Message Pattern | +|----------|-----------------| +| No matching node found | `"{name}. Reason: NoMatchingNode"` | +| Configuration failed on nodes | `"Reason: FailedToConfigure\n{error_details}"` | + +> **Note:** When `wait_for_status_success()` catches `TimeoutExpiredError` or `NNCPConfigurationFailed`, it logs the error with node details and re-raises the exception. + +**Example:** + +```python +from ocp_resources.node_network_configuration_policy import NodeNetworkConfigurationPolicy +from ocp_resources.exceptions import NNCPConfigurationFailed +from timeout_sampler import TimeoutExpiredError + +nncp = NodeNetworkConfigurationPolicy( + client=client, + name="my-nncp", + desired_state={"interfaces": [{"name": "eth1", "type": "ethernet", "state": "up"}]}, +) + +try: + nncp.deploy() + nncp.wait_for_status_success() +except NNCPConfigurationFailed as e: + print(f"NNCP configuration failed: {e}") +except TimeoutExpiredError: + print("NNCP configuration timed out") +``` + +--- + +## ClientWithBasicAuthError + +Raised during OAuth-based client authentication when using username/password credentials to connect to an OpenShift cluster. See [Connecting to Clusters](connecting-to-clusters.html) for connection methods. + +**Import:** + +```python +from ocp_resources.exceptions import ClientWithBasicAuthError +``` + +**Constructor Parameters:** Standard `Exception` — accepts a single string message. + +**Raised by:** `client_configuration_with_basic_auth()` in the following scenarios: + +| Scenario | Message | +|----------|---------| +| OAuth well-known endpoint not reachable | `"No well-known file found at endpoint"` | +| Authorization code not returned after login | `"No authorization code found"` | +| No `authorization_endpoint` in OAuth config | `"No authorization_endpoint found in well-known file"` | +| Token exchange fails | `"Failed to authenticate with basic auth"` | + +**Example:** + +```python +from ocp_resources.exceptions import ClientWithBasicAuthError +from ocp_resources.resource import client_configuration_with_basic_auth + +import kubernetes + +configuration = kubernetes.client.Configuration() +configuration.verify_ssl = False + +try: + api_client = client_configuration_with_basic_auth( + username="admin", + password="password", + host="https://api.cluster.example.com:6443", + configuration=configuration, + ) +except ClientWithBasicAuthError as e: + print(f"Authentication failed: {e}") +``` + +--- + +## MissingResourceError + +Raised when a resource object fails to generate its internal representation. + +**Import:** + +```python +from ocp_resources.exceptions import MissingResourceError +``` + +**Constructor Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `name` | `str` | Name of the resource that could not be generated | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `resource_name` | `str` | The resource name | + +**String representation:** + +``` +Failed to generate resource: my-resource +``` + +**Example:** + +```python +from ocp_resources.exceptions import MissingResourceError + +try: + # Operations that may fail to generate a resource + ... +except MissingResourceError as e: + print(f"Resource generation failed: {e.resource_name}") +``` + +--- + +## MissingTemplateVariables + +Raised when rendering a YAML template and not all required template variables have been provided. + +**Import:** + +```python +from ocp_resources.exceptions import MissingTemplateVariables +``` + +**Constructor Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `var` | `str` | Name of the missing template variable | +| `template` | `str` | Path to the template file | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `var` | `str` | The missing variable name | +| `template` | `str` | The template file path | + +**String representation:** + +``` +Missing variables image for template tests/manifests/vm.yaml +``` + +**Example:** + +```python +from ocp_resources.exceptions import MissingTemplateVariables + +try: + result = generate_yaml_from_template(name="my-vm") + # If template also requires 'image', raises MissingTemplateVariables +except MissingTemplateVariables as e: + print(f"Variable '{e.var}' missing in template '{e.template}'") +``` + +--- + +## Common Error-Handling Patterns + +### Catching Multiple Library Exceptions + +```python +from ocp_resources.exceptions import ( + ExecOnPodError, + MissingRequiredArgumentError, + ResourceTeardownError, + ValidationError, +) + +try: + with Pod(client=client, name="worker", namespace="default", + containers=[{"name": "app", "image": "myapp:latest"}], + schema_validation_enabled=True) as pod: + pod.execute(command=["python", "run_task.py"]) +except ValidationError as e: + print(f"Invalid resource spec: {e}") +except ExecOnPodError as e: + print(f"Task failed (rc={e.rc}): {e.err}") +except ResourceTeardownError as e: + print(f"Pod cleanup failed: {e}") +except MissingRequiredArgumentError as e: + print(f"Missing field: {e.argument}") +``` + +### Safe Condition Waiting with Early Abort + +```python +from ocp_resources.exceptions import ConditionError +from timeout_sampler import TimeoutExpiredError + +try: + resource.wait_for_condition( + condition="Ready", + status="True", + timeout=180, + stop_condition="Failed", + stop_status="True", + ) +except ConditionError: + # Resource entered a terminal failure state — no point waiting further + resource.clean_up() + raise +except TimeoutExpiredError: + # Condition not met within timeout — may be transient + print("Resource is taking too long, investigating...") +``` + +### Ignoring Non-Critical Command Failures + +```python +from ocp_resources.exceptions import ExecOnPodError + +# Method 1: Use ignore_rc=True +output = pod.execute(command=["grep", "ERROR", "/var/log/app.log"], ignore_rc=True) + +# Method 2: Catch and inspect the return code +try: + output = pod.execute(command=["test", "-f", "/tmp/lockfile"]) +except ExecOnPodError as e: + if e.rc == 1: + print("Lock file does not exist — proceeding") + else: + raise # Re-raise unexpected errors +``` + +### Validation Before Batch Deployment + +```python +from ocp_resources.exceptions import ValidationError + +resources = [pod1, pod2, deployment1, service1] + +# Pre-validate all resources before deploying any +errors = [] +for resource in resources: + try: + resource.validate() + except ValidationError as e: + errors.append((resource.name, str(e))) + +if errors: + for name, err in errors: + print(f" {name}: {err}") + raise SystemExit("Fix validation errors before deploying") + +for resource in resources: + resource.deploy() +``` + +--- + +## Exception Hierarchy Summary + +| Exception | Parent | Module | +|-----------|--------|--------| +| `ExecOnPodError` | `Exception` | `ocp_resources.exceptions` | +| `MissingRequiredArgumentError` | `Exception` | `ocp_resources.exceptions` | +| `ResourceTeardownError` | `Exception` | `ocp_resources.exceptions` | +| `ValidationError` | `Exception` | `ocp_resources.exceptions` | +| `ConditionError` | `Exception` | `ocp_resources.exceptions` | +| `NNCPConfigurationFailed` | `Exception` | `ocp_resources.exceptions` | +| `ClientWithBasicAuthError` | `Exception` | `ocp_resources.exceptions` | +| `MissingResourceError` | `Exception` | `ocp_resources.exceptions` | +| `MissingTemplateVariables` | `Exception` | `ocp_resources.exceptions` | + +> **Warning:** `MissingResourceResError` is deprecated and will be removed in a future release. Use `MissingResourceError` instead. + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Validating Resources Against OpenAPI Schemas](validating-resources.html) +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) +- [Connecting to Clusters](connecting-to-clusters.html) diff --git a/docs/generating-resource-classes.html b/docs/generating-resource-classes.html new file mode 100644 index 0000000000..f33b35efef --- /dev/null +++ b/docs/generating-resource-classes.html @@ -0,0 +1,714 @@ + + + + + + Generating New Resource Classes with class-generator - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by exploring the repository structure and the knowledge graph.

+

Now let me check the generated output example and the test generation flow:

+

Now let me look at a generated file with a spec section, and check one with duplicate kinds:

+

Let me also check the --help output structure and look at a few more details:

+

Now let me check a dry-run example and verify what shell completion setup looks like:

+

Now I have a thorough understanding of the codebase. Let me write the documentation:

+

Generating New Resource Classes with class-generator

+

Scaffold Python wrapper classes for any Kubernetes or OpenShift custom resource definition (CRD) so you can manage those resources using the same patterns as built-in resources. The class-generator CLI introspects your cluster's OpenAPI schemas and produces ready-to-use Python files complete with typed constructors, docstrings, and to_dict() serialization.

+

Prerequisites

+
    +
  • openshift-python-wrapper installed (see Installing and Creating Your First Resource)
  • +
  • oc or kubectl CLI available on your PATH
  • +
  • An active connection to a Kubernetes or OpenShift cluster with admin privileges (for --kind generation and schema updates)
  • +
+

Quick Example

+

Generate a wrapper class for the Deployment resource:

+
class-generator -k Deployment
+
+ +

This creates a Python file in ocp_resources/ with a fully typed class you can import and use immediately:

+
from ocp_resources.deployment import Deployment
+
+dep = Deployment(
+    name="my-app",
+    namespace="default",
+    replicas=3,
+    selector={"matchLabels": {"app": "my-app"}},
+    template={...},
+)
+dep.deploy()
+
+ +

Step-by-Step: Generating a Resource Class

+

1. Generate the class file

+

Pass the CRD's Kind (case-sensitive) to --kind / -k:

+
class-generator -k StorageCluster
+
+ +

The output file is automatically named using snake_case — ocp_resources/storage_cluster.py.

+

2. Preview without writing (dry run)

+

See exactly what would be generated without touching the filesystem:

+
class-generator -k StorageCluster --dry-run
+
+ +

The rendered Python code is printed to the console with syntax highlighting.

+

3. Review the generated file

+

Every generated file contains:

+
    +
  • A class inheriting from Resource (cluster-scoped) or NamespacedResource (namespace-scoped)
  • +
  • Typed __init__ parameters extracted from the OpenAPI spec and status fields
  • +
  • A to_dict() method that serializes required and optional fields
  • +
  • A # End of generated code marker — any code you add below this line is preserved on regeneration
  • +
+

Example generated output for ConfigMap:

+
from typing import Any
+from ocp_resources.resource import NamespacedResource
+
+class ConfigMap(NamespacedResource):
+    """
+    ConfigMap holds configuration data for pods to consume.
+    """
+
+    api_version: str = NamespacedResource.ApiVersion.V1
+
+    def __init__(
+        self,
+        binary_data: dict[str, Any] | None = None,
+        data: dict[str, Any] | None = None,
+        immutable: bool | None = None,
+        **kwargs: Any,
+    ) -> None:
+        ...
+
+ +

4. Generate multiple kinds at once

+

Pass a comma-separated list (no spaces):

+
class-generator -k Pod,Service,ConfigMap
+
+ +

Multiple kinds are generated in parallel for speed.

+

5. Overwrite an existing file

+

By default, existing files are not overwritten. Pass --overwrite to replace them:

+
class-generator -k Deployment --overwrite
+
+ +

To create a timestamped backup before overwriting:

+
class-generator -k Deployment --overwrite --backup
+
+ +

Backups are stored in .backups/backup-YYYYMMDD-HHMMSS/ preserving the original directory structure.

+

6. Write to a custom output path

+
class-generator -k MyCustomResource -o my_package/custom_resource.py
+
+ +

Handling Duplicate Kinds

+

Some Kubernetes kinds exist in multiple API groups. For example, DNS exists in both config.openshift.io and operator.openshift.io. When this happens, the generator automatically creates separate files with group-based suffixes:

+ + + + + + + + + + + + + + + + + + + + +
KindAPI GroupGenerated File
DNSconfig.openshift.iodns_config_openshift_io.py
DNSoperator.openshift.iodns_operator_openshift_io.py
+

Both files define a class named DNS but with different api_group attributes. Import the one matching the API group you need.

+

Adding User Code to Generated Files

+

Any code you add below the # End of generated code marker is preserved when you regenerate the file with --overwrite. This is how built-in resources like ConfigMap include custom properties:

+
    # End of generated code
+
+    @property
+    def keys_to_hash(self):
+        return ["data", "binaryData"]
+
+ +

Custom imports you add at the top of the file are also preserved during regeneration.

+

Updating Schemas

+

The generator uses a local schema cache to resolve resource definitions. If a CRD is missing (e.g., you just installed a new operator), update the cache first.

+

Full schema update

+

Fetch all schemas from the connected cluster:

+
class-generator --update-schema
+
+ +
+

Note: If connected to an older cluster, existing schemas are preserved. Only new or missing resources are added.

+
+

Single-resource schema update

+

Update just one resource without touching the rest:

+
class-generator --update-schema-for LlamaStackDistribution
+
+ +

Then generate the class:

+
class-generator -k LlamaStackDistribution --overwrite
+
+ +
+

Warning: --update-schema and --update-schema-for are mutually exclusive. Use one or the other.

+
+

Discovering Missing Resources

+

Find resources on your cluster that don't have wrapper classes yet:

+
class-generator --discover-missing
+
+ +

Coverage report

+

Get a summary of how many resources have wrapper classes:

+
class-generator --coverage-report
+
+ +

Output in JSON for CI/CD pipelines:

+
class-generator --coverage-report --json
+
+ +

Auto-generate all missing resources

+

Update schemas and generate classes for every resource that's missing:

+
class-generator --update-schema --generate-missing
+
+ +
+

Tip: Use --dry-run with --generate-missing to preview what would be generated without writing any files.

+
+

Advanced Usage

+

Batch regeneration

+

Regenerate all existing generated resource classes to pick up schema changes:

+
class-generator --regenerate-all
+
+ +

With backups:

+
class-generator --regenerate-all --backup
+
+ +

Filter to a subset of resources using glob patterns:

+
class-generator --regenerate-all --filter "Pod*"
+class-generator --regenerate-all --filter "*Service"
+
+ +

Adding tests

+

Generate test files alongside the resource class:

+
class-generator -k Pod --add-tests
+
+ +

This creates test manifests and runs them automatically with pytest. Tests are placed under the test manifests directory and validate that the generated class can be parsed correctly.

+

Shell completion

+

Add this to your ~/.bashrc or ~/.zshrc for tab completion:

+
# For zsh
+if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=zsh_source class-generator)"; fi
+
+# For bash
+if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=bash_source class-generator)"; fi
+
+ +

Verbose output

+

Enable debug logging to see exactly what the generator is doing:

+
class-generator -k Deployment -v
+
+ +

When a kind is missing from the schema

+

If the kind you request is not in the local schema cache, the CLI prompts you:

+
deployment not found in schema mapping, Do you want to run --update-schema and retry? [Y/N]:
+
+ +

Answering y triggers a schema update and retries generation automatically.

+
+

Note: The prompt only appears in interactive mode. When generating multiple kinds or running in batch mode, missing kinds are skipped with a warning. Run --update-schema separately beforehand in CI environments.

+
+

API group and version warnings

+

If a generated resource uses an API group or version that hasn't been registered in the base Resource class, the generator logs a warning like:

+
Missing API Group in Resource
+Please add `Resource.ApiGroup.MY_GROUP_IO = my-group.io` manually into
+ocp_resources/resource.py under Resource class > ApiGroup class.
+
+ +

Follow the instructions to register the API group so the resource can connect to the correct API endpoint. See Understanding the Resource Class Hierarchy for details on how ApiGroup and ApiVersion are used.

+

CLI Options Reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDescription
-k, --kindKind(s) to generate (comma-separated)
-o, --output-fileCustom output file path
--overwriteOverwrite existing files
--dry-runPreview output without writing files
--add-testsGenerate test files for the kind
--update-schemaUpdate all schema files from cluster
--update-schema-forUpdate schema for a single kind
--discover-missingFind resources without wrapper classes
--coverage-reportShow coverage statistics
--jsonOutput reports in JSON format
--generate-missingGenerate classes for all missing resources
--regenerate-allRegenerate all existing generated classes
--backupCreate backup before overwriting/regenerating
--filterGlob pattern to filter --regenerate-all
-v, --verboseEnable debug logging
+

For the full CLI reference, see class-generator CLI Reference.

+

Troubleshooting

+

"Neither 'oc' nor 'kubectl' binary found in PATH" +Install oc from the OpenShift mirror or kubectl from the Kubernetes docs. Ensure the binary is on your PATH.

+

"Resource kind 'X' not found" +The kind is not in the local schema cache. Run class-generator --update-schema while connected to a cluster that has the CRD installed, then retry.

+

Generated filename looks wrong (e.g., c_d_i_config.py) +Single-letter segments in snake_case names indicate the camelCase-to-snake_case converter doesn't recognize an acronym. The generator raises an error with instructions to add the acronym to the converter.

+

"Failed to fetch OpenAPI v3 index" +Ensure you are logged in to the cluster with sufficient privileges. The generator needs to access the /openapi/v3 endpoint, which typically requires cluster-admin access.

+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/generating-resource-classes.md b/docs/generating-resource-classes.md new file mode 100644 index 0000000000..d72eca3dc7 --- /dev/null +++ b/docs/generating-resource-classes.md @@ -0,0 +1,338 @@ +Let me start by exploring the repository structure and the knowledge graph. + +Now let me check the generated output example and the test generation flow: + +Now let me look at a generated file with a spec section, and check one with duplicate kinds: + +Let me also check the `--help` output structure and look at a few more details: + +Now let me check a dry-run example and verify what shell completion setup looks like: + +Now I have a thorough understanding of the codebase. Let me write the documentation: + +# Generating New Resource Classes with class-generator + +Scaffold Python wrapper classes for any Kubernetes or OpenShift custom resource definition (CRD) so you can manage those resources using the same patterns as built-in resources. The `class-generator` CLI introspects your cluster's OpenAPI schemas and produces ready-to-use Python files complete with typed constructors, docstrings, and `to_dict()` serialization. + +## Prerequisites + +- **openshift-python-wrapper** installed (see [Installing and Creating Your First Resource](quickstart.html)) +- `oc` or `kubectl` CLI available on your `PATH` +- An active connection to a Kubernetes or OpenShift cluster with admin privileges (for `--kind` generation and schema updates) + +## Quick Example + +Generate a wrapper class for the `Deployment` resource: + +```bash +class-generator -k Deployment +``` + +This creates a Python file in `ocp_resources/` with a fully typed class you can import and use immediately: + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment( + name="my-app", + namespace="default", + replicas=3, + selector={"matchLabels": {"app": "my-app"}}, + template={...}, +) +dep.deploy() +``` + +## Step-by-Step: Generating a Resource Class + +### 1. Generate the class file + +Pass the CRD's `Kind` (case-sensitive) to `--kind` / `-k`: + +```bash +class-generator -k StorageCluster +``` + +The output file is automatically named using snake_case — `ocp_resources/storage_cluster.py`. + +### 2. Preview without writing (dry run) + +See exactly what would be generated without touching the filesystem: + +```bash +class-generator -k StorageCluster --dry-run +``` + +The rendered Python code is printed to the console with syntax highlighting. + +### 3. Review the generated file + +Every generated file contains: + +- A class inheriting from `Resource` (cluster-scoped) or `NamespacedResource` (namespace-scoped) +- Typed `__init__` parameters extracted from the OpenAPI spec and `status` fields +- A `to_dict()` method that serializes required and optional fields +- A `# End of generated code` marker — any code you add **below** this line is preserved on regeneration + +Example generated output for `ConfigMap`: + +```python +from typing import Any +from ocp_resources.resource import NamespacedResource + +class ConfigMap(NamespacedResource): + """ + ConfigMap holds configuration data for pods to consume. + """ + + api_version: str = NamespacedResource.ApiVersion.V1 + + def __init__( + self, + binary_data: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + immutable: bool | None = None, + **kwargs: Any, + ) -> None: + ... +``` + +### 4. Generate multiple kinds at once + +Pass a comma-separated list (no spaces): + +```bash +class-generator -k Pod,Service,ConfigMap +``` + +Multiple kinds are generated in parallel for speed. + +### 5. Overwrite an existing file + +By default, existing files are not overwritten. Pass `--overwrite` to replace them: + +```bash +class-generator -k Deployment --overwrite +``` + +To create a timestamped backup before overwriting: + +```bash +class-generator -k Deployment --overwrite --backup +``` + +Backups are stored in `.backups/backup-YYYYMMDD-HHMMSS/` preserving the original directory structure. + +### 6. Write to a custom output path + +```bash +class-generator -k MyCustomResource -o my_package/custom_resource.py +``` + +## Handling Duplicate Kinds + +Some Kubernetes kinds exist in multiple API groups. For example, `DNS` exists in both `config.openshift.io` and `operator.openshift.io`. When this happens, the generator automatically creates separate files with group-based suffixes: + +| Kind | API Group | Generated File | +|------|-----------|----------------| +| `DNS` | `config.openshift.io` | `dns_config_openshift_io.py` | +| `DNS` | `operator.openshift.io` | `dns_operator_openshift_io.py` | + +Both files define a class named `DNS` but with different `api_group` attributes. Import the one matching the API group you need. + +## Adding User Code to Generated Files + +Any code you add **below** the `# End of generated code` marker is preserved when you regenerate the file with `--overwrite`. This is how built-in resources like `ConfigMap` include custom properties: + +```python + # End of generated code + + @property + def keys_to_hash(self): + return ["data", "binaryData"] +``` + +Custom imports you add at the top of the file are also preserved during regeneration. + +## Updating Schemas + +The generator uses a local schema cache to resolve resource definitions. If a CRD is missing (e.g., you just installed a new operator), update the cache first. + +### Full schema update + +Fetch all schemas from the connected cluster: + +```bash +class-generator --update-schema +``` + +> **Note:** If connected to an older cluster, existing schemas are preserved. Only new or missing resources are added. + +### Single-resource schema update + +Update just one resource without touching the rest: + +```bash +class-generator --update-schema-for LlamaStackDistribution +``` + +Then generate the class: + +```bash +class-generator -k LlamaStackDistribution --overwrite +``` + +> **Warning:** `--update-schema` and `--update-schema-for` are mutually exclusive. Use one or the other. + +## Discovering Missing Resources + +Find resources on your cluster that don't have wrapper classes yet: + +```bash +class-generator --discover-missing +``` + +### Coverage report + +Get a summary of how many resources have wrapper classes: + +```bash +class-generator --coverage-report +``` + +Output in JSON for CI/CD pipelines: + +```bash +class-generator --coverage-report --json +``` + +### Auto-generate all missing resources + +Update schemas and generate classes for every resource that's missing: + +```bash +class-generator --update-schema --generate-missing +``` + +> **Tip:** Use `--dry-run` with `--generate-missing` to preview what would be generated without writing any files. + +## Advanced Usage + +### Batch regeneration + +Regenerate all existing generated resource classes to pick up schema changes: + +```bash +class-generator --regenerate-all +``` + +With backups: + +```bash +class-generator --regenerate-all --backup +``` + +Filter to a subset of resources using glob patterns: + +```bash +class-generator --regenerate-all --filter "Pod*" +class-generator --regenerate-all --filter "*Service" +``` + +### Adding tests + +Generate test files alongside the resource class: + +```bash +class-generator -k Pod --add-tests +``` + +This creates test manifests and runs them automatically with `pytest`. Tests are placed under the test manifests directory and validate that the generated class can be parsed correctly. + +### Shell completion + +Add this to your `~/.bashrc` or `~/.zshrc` for tab completion: + +```bash +# For zsh +if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=zsh_source class-generator)"; fi + +# For bash +if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=bash_source class-generator)"; fi +``` + +### Verbose output + +Enable debug logging to see exactly what the generator is doing: + +```bash +class-generator -k Deployment -v +``` + +### When a kind is missing from the schema + +If the kind you request is not in the local schema cache, the CLI prompts you: + +``` +deployment not found in schema mapping, Do you want to run --update-schema and retry? [Y/N]: +``` + +Answering `y` triggers a schema update and retries generation automatically. + +> **Note:** The prompt only appears in interactive mode. When generating multiple kinds or running in batch mode, missing kinds are skipped with a warning. Run `--update-schema` separately beforehand in CI environments. + +### API group and version warnings + +If a generated resource uses an API group or version that hasn't been registered in the base `Resource` class, the generator logs a warning like: + +``` +Missing API Group in Resource +Please add `Resource.ApiGroup.MY_GROUP_IO = my-group.io` manually into +ocp_resources/resource.py under Resource class > ApiGroup class. +``` + +Follow the instructions to register the API group so the resource can connect to the correct API endpoint. See [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) for details on how `ApiGroup` and `ApiVersion` are used. + +## CLI Options Reference + +| Option | Description | +|--------|-------------| +| `-k`, `--kind` | Kind(s) to generate (comma-separated) | +| `-o`, `--output-file` | Custom output file path | +| `--overwrite` | Overwrite existing files | +| `--dry-run` | Preview output without writing files | +| `--add-tests` | Generate test files for the kind | +| `--update-schema` | Update all schema files from cluster | +| `--update-schema-for` | Update schema for a single kind | +| `--discover-missing` | Find resources without wrapper classes | +| `--coverage-report` | Show coverage statistics | +| `--json` | Output reports in JSON format | +| `--generate-missing` | Generate classes for all missing resources | +| `--regenerate-all` | Regenerate all existing generated classes | +| `--backup` | Create backup before overwriting/regenerating | +| `--filter` | Glob pattern to filter `--regenerate-all` | +| `-v`, `--verbose` | Enable debug logging | + +For the full CLI reference, see [class-generator CLI Reference](class-generator-cli.html). + +## Troubleshooting + +**"Neither 'oc' nor 'kubectl' binary found in PATH"** +Install `oc` from the [OpenShift mirror](https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/stable/) or `kubectl` from the [Kubernetes docs](https://kubernetes.io/docs/tasks/tools/). Ensure the binary is on your `PATH`. + +**"Resource kind 'X' not found"** +The kind is not in the local schema cache. Run `class-generator --update-schema` while connected to a cluster that has the CRD installed, then retry. + +**Generated filename looks wrong (e.g., `c_d_i_config.py`)** +Single-letter segments in snake_case names indicate the camelCase-to-snake_case converter doesn't recognize an acronym. The generator raises an error with instructions to add the acronym to the converter. + +**"Failed to fetch OpenAPI v3 index"** +Ensure you are logged in to the cluster with sufficient privileges. The generator needs to access the `/openapi/v3` endpoint, which typically requires cluster-admin access. + +## Related Pages + +- [class-generator CLI Reference](class-generator-cli.html) +- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) +- [Resource and NamespacedResource API](resource-api.html) +- [Validating Resources Against OpenAPI Schemas](validating-resources.html) diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000000..d64012c659 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,591 @@ + + + + + + openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+

openshift-python-wrapper

+

Manage OpenShift and Kubernetes resources with clean, Pythonic code instead of raw API calls

+ + + + + + + + + + + + + + + + + + + + Get Started → + +
+ +
+ +
+

Getting Started

+ + + Explore → + +
+ +
+

User Guides

+ + + Explore → + +
+ +
+

Recipes

+
    + +
  • + Common Resource Patterns + +

    Copy-paste recipes for pods, deployments, networking, RBAC roles, data volumes, and handling resources with duplicate API groups

    + +
  • + +
  • + Integrating with AI Assistants via MCP Server + +

    Set up and use the MCP server with Cursor, Claude Desktop, or other MCP-compatible AI tools to manage cluster resources

    + +
  • + +
+ + Explore → + +
+ +
+

Reference

+
    + +
  • + Resource and NamespacedResource API + +

    Complete API reference for the Resource and NamespacedResource base classes including all methods, properties, and constructor parameters

    + +
  • + +
  • + class-generator CLI Reference + +

    All CLI options for class-generator including kind generation, schema updates, coverage reports, batch regeneration, and test generation

    + +
  • + +
  • + Environment Variables and Configuration + +

    All supported environment variables for logging, resource reuse, teardown skipping, and hash control

    + +
  • + +
+ + Explore → + +
+ +
+

Internals

+ + + Explore → + +
+ +
+

Using Resources

+
    + +
  • + Working with Kubernetes Events + +

    How to use the standalone Event class to watch, list, and delete Kubernetes events. Covers Event.get() for streaming events in real-time, Event.list() for fetching existing events with time-based filtering, Event.delete_events() for cleanup, and the resource.events() method for getting events related to a specific resource. Includes field selectors, namespace filtering, and common debugging patterns.

    + +
  • + +
  • + Executing Commands in Pods and Retrieving Logs + +

    How to use Pod.execute() to run commands inside pod containers with timeout handling, container selection, and error handling via ExecOnPodError. Covers Pod.log() for retrieving pod logs, accessing pod properties like node, IP address, and practical recipes for debugging running workloads.

    + +
  • + +
+ + Explore → + +
+ +
+

API Reference

+
    + +
  • + Error Handling and Exception Reference + +

    Reference for all custom exceptions raised by the library including ExecOnPodError, MissingRequiredArgumentError, ResourceTeardownError, ValidationError, ConditionError, NNCPConfigurationFailed, and ClientWithBasicAuthError. Covers which operations raise each exception, how to catch and handle them, and common error-handling patterns for robust automation scripts.

    + +
  • + +
+ + Explore → + +
+ +
+ +
+
🤖
+
+ AI-friendly documentation +

This documentation is optimized for AI consumption.

+ +
+
+ +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/llms-full.txt b/docs/llms-full.txt new file mode 100644 index 0000000000..294d78539f --- /dev/null +++ b/docs/llms-full.txt @@ -0,0 +1,10588 @@ +# openshift-python-wrapper + +> Manage OpenShift and Kubernetes resources with clean, Pythonic code instead of raw API calls + +--- + +Source: quickstart.md + +Let me explore the repository structure and key files to understand the codebase before writing the documentation. + +Now let me look at the `NamespacedResource` class and the `get` classmethod for listing resources: + +Now let me check the `get_client` with the `utils/client_config.py` for understanding connection options: + +Now I have a thorough understanding of the codebase. Let me write the documentation: + +Now I have a comprehensive understanding of the codebase. Let me write the guide: + +# Installing and Creating Your First Resource + +Get up and running with openshift-python-wrapper so you can programmatically create, query, and delete Kubernetes and OpenShift resources from Python. This guide takes you from installation to your first working resource in under 5 minutes. + +## Prerequisites + +- Python 3.10 or later +- Access to a Kubernetes or OpenShift cluster (with a valid kubeconfig) +- `pip` or another Python package manager + +## Quick Example + +```bash +pip install openshift-python-wrapper +``` + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +with Namespace(client=client, name="my-first-namespace") as ns: + ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) + print(f"Namespace {ns.name} is active: {ns.exists}") +# Namespace is automatically deleted when the block exits +``` + +That's it — the namespace is created when entering the `with` block and cleaned up when leaving it. + +## Step 1: Install the Package + +```bash +pip install openshift-python-wrapper +``` + +> **Tip:** For isolated environments, use a virtual environment: +> ```bash +> python -m venv .venv && source .venv/bin/activate +> pip install openshift-python-wrapper +> ``` + +## Step 2: Connect to Your Cluster + +The `get_client()` function returns a dynamic client connected to your cluster. By default it reads your kubeconfig from the `KUBECONFIG` environment variable or `~/.kube/config`: + +```python +from ocp_resources.resource import get_client + +client = get_client() +``` + +You can also point to a specific kubeconfig file: + +```python +client = get_client(config_file="/path/to/kubeconfig") +``` + +Or connect with a token and host directly: + +```python +client = get_client(host="https://api.mycluster.example.com:6443", token="sha256~...") +``` + +> **Note:** For the full set of connection options — including basic auth, in-cluster config, and environment variables — see [Connecting to Clusters](connecting-to-clusters.html). + +## Step 3: Create a Resource + +There are two ways to create a resource: explicitly with `deploy()`/`clean_up()`, or automatically with a context manager (`with` statement). + +### Option A: Context Manager (Recommended) + +The context manager automatically deletes the resource when the block exits, which prevents leftover resources: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +with Namespace(client=client, name="namespace-example-2") as ns: + ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) + assert ns.exists +# Resource is automatically cleaned up here +``` + +### Option B: Explicit Deploy and Clean Up + +Use `deploy()` and `clean_up()` when you need more control over the lifecycle: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +ns = Namespace(client=client, name="namespace-example-1") +ns.deploy() +assert ns.exists +ns.clean_up() +``` + +> **Tip:** Set `teardown=False` on the resource to prevent `clean_up()` from being called when using a context manager. This is useful when you want the resource to persist after the block exits. + +## Step 4: Create a Namespaced Resource + +Most Kubernetes resources (Pods, ConfigMaps, Deployments, etc.) live inside a namespace. These require both `name` and `namespace`: + +```python +from ocp_resources.config_map import ConfigMap +from ocp_resources.resource import get_client + +client = get_client() + +with ConfigMap( + client=client, + name="app-config", + namespace="default", + data={"app.properties": "debug=true\nport=8080"}, +) as cm: + assert cm.exists + print(f"ConfigMap {cm.name} created in namespace {cm.namespace}") +``` + +## Step 5: Query Resources + +Use the `get()` class method to list resources from the cluster. It returns a generator you can iterate over: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +for pod in Pod.get(client=client, namespace="default"): + print(pod.name) +``` + +Filter by labels using `label_selector`: + +```python +for pod in Pod.get(client=client, label_selector="app=nginx"): + node = pod.node + print(f"Pod {pod.name} is running on node {node.name}") +``` + +Check if a specific resource exists: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +ns = Namespace(client=client, name="default") +if ns.exists: + print("Namespace exists!") +``` + +> **Note:** For more on querying, filtering, and watching resources, see [Querying and Watching Resources](querying-resources.html). + +## Step 6: Delete a Resource + +If you didn't use a context manager, call `clean_up()` to delete the resource: + +```python +ns = Namespace(client=client, name="my-temp-namespace") +ns.deploy() +# ... do work ... +ns.clean_up() +``` + +The `clean_up()` method waits for the resource to be fully deleted by default. Pass `wait=False` to return immediately: + +```python +ns.clean_up(wait=False) +``` + +> **Note:** For full details on resource lifecycle management, see [Creating and Managing Resources](creating-and-managing-resources.html). + +## Putting It All Together + +Here's a complete script that creates a namespace, deploys a ConfigMap inside it, reads it back, and cleans everything up: + +```python +from ocp_resources.config_map import ConfigMap +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +with Namespace(client=client, name="quickstart-demo") as ns: + ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) + + with ConfigMap( + client=client, + name="demo-config", + namespace="quickstart-demo", + data={"greeting": "hello from openshift-python-wrapper"}, + ) as cm: + assert cm.exists + print(f"Created ConfigMap '{cm.name}' in namespace '{ns.name}'") + + # Query it back + for config_map in ConfigMap.get(client=client, namespace="quickstart-demo"): + print(f" Found: {config_map.name}") + # ConfigMap cleaned up here +# Namespace cleaned up here +``` + +## Advanced Usage + +### Creating Resources from YAML + +You can create any resource from an existing YAML file instead of specifying parameters in Python: + +```python +from ocp_resources.config_map import ConfigMap +from ocp_resources.resource import get_client + +client = get_client() + +cm = ConfigMap(client=client, yaml_file="my-configmap.yaml") +cm.deploy() +``` + +### Using a Fake Client for Testing + +You can run your code without a live cluster by using a fake client — useful for unit tests and local development: + +```python +from ocp_resources.config_map import ConfigMap +from ocp_resources.resource import get_client + +client = get_client(fake=True) + +with ConfigMap( + client=client, + name="test-config", + namespace="default", + data={"key": "value"}, +) as cm: + cm.create() + print(f"Created (fake): {cm.name}") +``` + +> **Note:** See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for full details on the fake client. + +### Schema Validation Before Creating + +Enable schema validation to catch configuration errors before sending requests to the cluster: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +pod = Pod( + client=client, + name="validated-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + schema_validation_enabled=True, # Validates on create +) +pod.deploy() +``` + +You can also validate manually without creating: + +```python +pod = Pod( + name="check-me", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], +) +pod.validate() # Raises ValidationError if invalid +``` + +> **Note:** See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for more validation options. + +### Waiting for Resource Conditions + +After creating a resource, you can wait for it to reach a specific status or condition: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +with Namespace(client=client, name="wait-demo") as ns: + ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) +``` + +> **Note:** See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for advanced waiting patterns. + +## Troubleshooting + +### "KUBECONFIG not found" or connection errors + +Make sure your kubeconfig is in the expected location or explicitly set the path: + +```bash +export KUBECONFIG=/path/to/kubeconfig +``` + +Or pass it directly: + +```python +client = get_client(config_file="/path/to/kubeconfig") +``` + +### Resource not found after creation + +Some resources take time to become available. Use `wait_for_status()` or `wait()` to confirm the resource is ready before interacting with it: + +```python +ns.deploy() +ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) +``` + +### Missing `name` or `namespace` errors + +- Cluster-scoped resources (like `Namespace`) require only `name`. +- Namespaced resources (like `Pod`, `ConfigMap`, `Deployment`) require both `name` and `namespace`. + +```python +# Cluster-scoped — only name needed +Namespace(client=client, name="my-ns") + +# Namespaced — both name and namespace required +ConfigMap(client=client, name="my-cm", namespace="my-ns") +``` + +## Related Pages + +- [Connecting to Clusters](connecting-to-clusters.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Querying and Watching Resources](querying-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) + +--- + +Source: connecting-to-clusters.md + +Let me start by exploring the repository structure and the knowledge graph. + +Now let me explore the client configuration and authentication code: + +Now let me look at how the `client` parameter is used in the Resource class: + +Now let me look at how resources accept the client parameter: + +Now let me look at the NamespacedResource and how it handles client: + +Now let me look at the tests for the client configuration: + +Let me also check how environment variables relate to client configuration: + +Now let me check how `get_client` is used with `generate_kubeconfig`: + +Now let me look at the `conftest.py` for the tests to understand how fake_client is set up: + +Let me also check the test file for more patterns of client usage: + +Now let me check what environment variables are documented: + +Now let me check the `generate_kubeconfig` related tests more carefully and how `DynamicClientWithKubeconfig` is used: + +Now let me also look at how `get_client` is imported/used in other contexts to understand its full interface: + +Let me look at some example files to see real usage patterns: + +Now let me check the `get` classmethod for how client is used: + +Now let me check for the `client_configuration` parameter and how `verify_ssl` is used: + +Now I have all the information I need. Let me write the documentation page. + +# Connecting to Clusters + +Establish authenticated connections to Kubernetes and OpenShift clusters so you can create, query, and manage resources with openshift-python-wrapper. + +## Prerequisites + +- openshift-python-wrapper installed (see [Installing and Creating Your First Resource](quickstart.html)) +- Access to a Kubernetes or OpenShift cluster (kubeconfig file, token, or in-cluster credentials) + +## Quick Example + +```python +from ocp_resources.resource import get_client +from ocp_resources.namespace import Namespace + +# Connect using default kubeconfig (~/.kube/config or $KUBECONFIG) +client = get_client() + +# Use the client with any resource +for ns in Namespace.get(client=client): + print(ns.name) +``` + +## Creating a Client + +All cluster connections go through `get_client()`, which returns a Kubernetes `DynamicClient`. Pass this client to every resource you create or query. + +### Using the Default Kubeconfig + +If you have a kubeconfig at `~/.kube/config` or the `KUBECONFIG` environment variable is set, no arguments are needed: + +```python +from ocp_resources.resource import get_client + +client = get_client() +``` + +The lookup order is: + +1. The `config_file` parameter (if provided) +2. The `KUBECONFIG` environment variable +3. `~/.kube/config` + +### Using a Specific Kubeconfig File + +```python +client = get_client(config_file="/path/to/my/kubeconfig") +``` + +### Selecting a Context + +If your kubeconfig has multiple contexts, select one by name: + +```python +client = get_client(config_file="/path/to/kubeconfig", context="my-staging-cluster") +``` + +### Authenticating with a Bearer Token + +Connect to a cluster API server directly using a host URL and token: + +```python +client = get_client( + host="https://api.my-cluster.example.com:6443", + token="sha256~my-bearer-token", +) +``` + +### Authenticating with Username and Password (Basic Auth) + +For OpenShift clusters that support OAuth-based basic authentication: + +```python +client = get_client( + host="https://api.my-cluster.example.com:6443", + username="my-user", + password="my-password", +) +``` + +> **Note:** Basic auth requires all three parameters: `host`, `username`, and `password`. This uses OpenShift's OAuth flow internally — it is not plain HTTP Basic Authentication. + +### Using a Kubeconfig Dictionary + +Pass a kubeconfig as a Python dictionary instead of a file path: + +```python +config_dict = { + "apiVersion": "v1", + "kind": "Config", + "clusters": [{"name": "my-cluster", "cluster": {"server": "https://api.example.com:6443"}}], + "users": [{"name": "my-user", "user": {"token": "my-token"}}], + "contexts": [{"name": "my-context", "context": {"cluster": "my-cluster", "user": "my-user"}}], + "current-context": "my-context", +} + +client = get_client(config_dict=config_dict) +``` + +## Passing the Client to Resources + +Once you have a client, pass it to resource constructors and class methods: + +```python +from ocp_resources.resource import get_client +from ocp_resources.namespace import Namespace +from ocp_resources.pod import Pod + +client = get_client() + +# Creating a resource +ns = Namespace(client=client, name="my-namespace") +ns.deploy() + +# Querying resources +for pod in Pod.get(client=client, namespace="my-namespace"): + print(pod.name) + +# Using context managers +with Namespace(client=client, name="temp-namespace") as ns: + ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) +``` + +> **Warning:** The `client` parameter will become mandatory in the next major release. Always pass it explicitly. If omitted, the library creates a client automatically using the default kubeconfig, but this triggers a `FutureWarning`. + +## Environment Variables + +`get_client()` respects several environment variables automatically: + +| Variable | Purpose | Default | +|---|---|---| +| `KUBECONFIG` | Path to the kubeconfig file | `~/.kube/config` | +| `HTTPS_PROXY` | HTTPS proxy for cluster connections | None | +| `HTTP_PROXY` | HTTP proxy (used if `HTTPS_PROXY` is not set) | None | + +> **Tip:** `HTTPS_PROXY` takes precedence over `HTTP_PROXY` when both are set. + +For other environment variables that control logging, resource reuse, and teardown behavior, see [Environment Variables and Configuration](environment-variables.html). + +## Advanced Usage + +### Disabling TLS Verification + +For development clusters with self-signed certificates: + +```python +client = get_client( + host="https://api.dev-cluster.example.com:6443", + token="sha256~my-token", + verify_ssl=False, +) +``` + +> **Warning:** Never disable TLS verification in production environments. + +### In-Cluster Configuration + +When your code runs inside a pod on the cluster (for example, in a CI/CD pipeline or operator), `get_client()` automatically falls back to in-cluster configuration if the kubeconfig-based connection fails: + +```python +# Inside a pod — no config_file or host needed +client = get_client() +``` + +The library first attempts to connect using the kubeconfig. If that fails with a connection error, it loads credentials from the pod's service account token mounted at `/var/run/secrets/kubernetes.io/serviceaccount/`. + +### Generating a Kubeconfig File from a Token Connection + +When you connect with `host` and `token`, you might need a kubeconfig file on disk (for example, to pass to external tools). Use `generate_kubeconfig=True`: + +```python +client = get_client( + host="https://api.my-cluster.example.com:6443", + token="sha256~my-token", + generate_kubeconfig=True, +) + +# Access the generated kubeconfig path +print(client.kubeconfig) +``` + +The generated kubeconfig file: +- Is written to a temporary file with `0o600` permissions +- Is automatically cleaned up when the process exits + +If you already passed a `config_file`, the `generate_kubeconfig` flag reuses that file path instead of creating a new one. + +### Passing a Custom Client Configuration + +For fine-grained control over TLS settings, timeouts, or proxy configuration, pass a `kubernetes.client.Configuration` object: + +```python +import kubernetes +from ocp_resources.resource import get_client + +config = kubernetes.client.Configuration() +config.verify_ssl = False +config.proxy = "http://my-proxy:8080" + +client = get_client( + host="https://api.my-cluster.example.com:6443", + token="sha256~my-token", + client_configuration=config, +) +``` + +### Connecting to Multiple Clusters + +Create separate clients for each cluster and pass them to different resources: + +```python +from ocp_resources.resource import get_client +from ocp_resources.namespace import Namespace + +prod_client = get_client(config_file="/path/to/prod-kubeconfig") +staging_client = get_client(config_file="/path/to/staging-kubeconfig") + +# Query namespaces on both clusters +prod_namespaces = list(Namespace.get(client=prod_client)) +staging_namespaces = list(Namespace.get(client=staging_client)) +``` + +### Using the Fake Client for Testing + +For unit tests that don't need a real cluster, pass `fake=True`: + +```python +client = get_client(fake=True) +``` + +This returns a fake client that simulates Kubernetes API operations in memory. See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for full details. + +## `get_client()` API Reference + +```python +from ocp_resources.resource import get_client + +get_client( + config_file: str | None = None, + config_dict: dict | None = None, + context: str | None = None, + client_configuration: kubernetes.client.Configuration | None = None, + persist_config: bool = True, + temp_file_path: str | None = None, + try_refresh_token: bool = True, + username: str | None = None, + password: str | None = None, + host: str | None = None, + verify_ssl: bool | None = None, + token: str | None = None, + fake: bool = False, + generate_kubeconfig: bool = False, +) -> DynamicClient +``` + +| Parameter | Type | Description | +|---|---|---| +| `config_file` | `str \| None` | Path to a kubeconfig file | +| `config_dict` | `dict \| None` | Kubeconfig as a Python dictionary | +| `context` | `str \| None` | Name of the kubeconfig context to use | +| `client_configuration` | `Configuration \| None` | Custom `kubernetes.client.Configuration` object | +| `persist_config` | `bool` | Whether to persist config changes back to the kubeconfig file | +| `temp_file_path` | `str \| None` | Path for temporary kubeconfig file (used with `config_dict`) | +| `try_refresh_token` | `bool` | Attempt to refresh the authentication token (for in-cluster config) | +| `username` | `str \| None` | Username for basic authentication | +| `password` | `str \| None` | Password for basic authentication | +| `host` | `str \| None` | Cluster API server URL (e.g., `https://api.example.com:6443`) | +| `verify_ssl` | `bool \| None` | Set to `False` to skip TLS certificate verification | +| `token` | `str \| None` | Bearer token for authentication | +| `fake` | `bool` | Return a fake in-memory client for testing | +| `generate_kubeconfig` | `bool` | Save connection info to a temporary kubeconfig file accessible via `client.kubeconfig` | + +**Returns:** `DynamicClient` — a Kubernetes dynamic client ready for resource operations. + +**Raises:** +- `kubernetes.config.ConfigException` — if no valid kubeconfig is found and in-cluster config is unavailable +- `urllib3.exceptions.MaxRetryError` — if the cluster is unreachable (before falling back to in-cluster config) +- `ClientWithBasicAuthError` — if basic auth (username/password) fails + +## Authentication Method Priority + +When multiple authentication parameters are provided, `get_client()` uses this priority order: + +1. **Basic auth** — if `username`, `password`, and `host` are all provided +2. **Token auth** — if `host` and `token` are provided +3. **Config dict** — if `config_dict` is provided +4. **Kubeconfig file** — uses `config_file`, `KUBECONFIG` env var, or `~/.kube/config` +5. **In-cluster config** — automatic fallback if the kubeconfig connection fails + +## Troubleshooting + +**"FutureWarning: 'client' arg will be mandatory"** +You are creating a resource without passing a `client`. Always call `get_client()` first and pass the result: + +```python +client = get_client() +ns = Namespace(client=client, name="my-ns") # Pass client explicitly +``` + +**Connection refused or timeout errors** +Verify your cluster is reachable and the kubeconfig or token is correct. Check that `HTTPS_PROXY` / `HTTP_PROXY` environment variables are set correctly if you are behind a proxy. + +**"KUBECONFIG environment variable set in code is not detected"** +The Kubernetes Python client reads `KUBECONFIG` at module import time. If you set it dynamically in your code, pass the path directly instead: + +```python +import os +# Don't rely on: os.environ["KUBECONFIG"] = "/path/to/config" +# Instead, pass it explicitly: +client = get_client(config_file=os.environ.get("KUBECONFIG", "/path/to/config")) +``` + +**Basic auth fails with `ClientWithBasicAuthError`** +Basic auth uses the OpenShift OAuth flow. Ensure your cluster has a well-known OAuth endpoint at `/.well-known/oauth-authorization-server`. This method does not work with vanilla Kubernetes clusters. + +## Related Pages + +- [Installing and Creating Your First Resource](quickstart.html) +- [Environment Variables and Configuration](environment-variables.html) +- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Error Handling and Exception Reference](error-handling.html) + +--- + +Source: creating-and-managing-resources.md + +Let me start by exploring the repository structure and the knowledge graph. + +Now let me look at the YAML-related functions and some example resource files: + +Now let me look at some concrete resource examples and tests: + +Now let me look at tests for create, update, delete operations and the exceptions: + +Now let me look at the exceptions module and the project resource: + +Now let me look at how `to_yaml`, `yaml_file`, and `kind_dict` usage works, and the `dry_run` option: + +Now let me check how `ensure_exists` and `wait_for_resource` work: + +Now let me look at how the `service.py` and `route.py` look to understand namespaced resources better: + +Let me also look at the `node_network_configuration_policy.py` for a more complex `deploy/clean_up` example: + +Now let me look at the `to_yaml` method and some environment variable usage: + +Let me also check the `dry_run` behavior and check for how `label` and `annotations` work: + +Now I have a thorough understanding of the codebase. Let me write the documentation. + +# Creating and Managing Resources + +Deploy Kubernetes and OpenShift resources to your cluster, update them in place, and clean them up reliably using the openshift-python-wrapper's resource lifecycle methods. + +## Prerequisites + +- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html)) +- A connected client — either a real cluster connection or a fake client for testing (see [Connecting to Clusters](connecting-to-clusters.html)) + +## Quick Example + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +# Create, use, and automatically delete a Namespace +with Namespace(client=client, name="my-app") as ns: + print(f"{ns.name} is ready") +# Namespace is deleted when the block exits +``` + +## Creating Resources + +Every resource provides three ways to create it: calling `deploy()` directly, using a context manager, or loading from a YAML file. + +### Using `deploy()` and `clean_up()` + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-namespace") +ns.deploy() + +# ... do work ... + +ns.clean_up() # Deletes the resource and waits for deletion +``` + +`deploy()` returns the resource instance, so you can chain: + +```python +ns = Namespace(client=client, name="my-namespace").deploy() +``` + +Pass `wait=True` to `deploy()` to block until the resource reaches its ready state: + +```python +ns = Namespace(client=client, name="my-namespace") +ns.deploy(wait=True) +``` + +### Using a Context Manager + +The context manager calls `deploy()` on entry and `clean_up()` on exit, guaranteeing cleanup even if an exception occurs: + +```python +from ocp_resources.config_map import ConfigMap + +with ConfigMap( + client=client, + name="app-config", + namespace="my-namespace", + data={"database_url": "postgres://db:5432/myapp"}, +) as cm: + print(cm.instance.data) +# ConfigMap is automatically deleted here +``` + +> **Note:** If cleanup fails during context manager exit, a `ResourceTeardownError` is raised so failures are never silently ignored. + +### Namespaced vs. Cluster-Scoped Resources + +Resources that live within a namespace inherit from `NamespacedResource` and require both `name` and `namespace`: + +```python +from ocp_resources.pod import Pod + +pod = Pod( + client=client, + name="nginx", + namespace="my-namespace", + containers=[{"name": "nginx", "image": "nginx:latest"}], +) +pod.deploy() +``` + +Cluster-scoped resources (like `Namespace`) inherit from `Resource` and need only `name`: + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-namespace") +ns.deploy() +``` + +### Creating Resources from a YAML File + +Pass `yaml_file` to create any resource directly from a YAML manifest. The resource name and namespace are read from the file: + +```python +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap(client=client, yaml_file="manifests/configmap.yaml") +cm.deploy() +``` + +`yaml_file` also accepts a `StringIO` object for in-memory YAML: + +```python +from io import StringIO +from ocp_resources.config_map import ConfigMap + +yaml_content = StringIO(""" +apiVersion: v1 +kind: ConfigMap +metadata: + name: dynamic-config + namespace: my-namespace +data: + setting: "enabled" +""") + +cm = ConfigMap(client=client, yaml_file=yaml_content) +cm.deploy() +``` + +### Creating Resources from a Dictionary + +Use `kind_dict` to pass a pre-built dictionary. When `kind_dict` is provided, the resource class's `to_dict()` logic is bypassed entirely: + +```python +from ocp_resources.config_map import ConfigMap + +resource_dict = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "from-dict", + "namespace": "my-namespace", + }, + "data": {"key": "value"}, +} + +cm = ConfigMap(client=client, kind_dict=resource_dict) +cm.deploy() +``` + +> **Warning:** `yaml_file` and `kind_dict` are mutually exclusive. Passing both raises a `ValueError`. + +### Adding Labels and Annotations + +Pass `label` and `annotations` at construction time: + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace( + client=client, + name="labeled-ns", + label={"team": "platform", "env": "staging"}, + annotations={"description": "Staging environment namespace"}, +) +ns.deploy() +``` + +## Updating Resources + +### Patching with `update()` + +`update()` sends a merge patch — it adds or modifies fields without removing existing ones: + +```python +cm = ConfigMap( + client=client, + name="app-config", + namespace="my-namespace", + ensure_exists=True, +) + +cm.update(resource_dict={ + "data": {"new_key": "new_value"}, +}) +``` + +### Replacing with `update_replace()` + +`update_replace()` performs a full replacement of the resource. Use this when you need to **remove** fields that `update()` would leave untouched: + +```python +# Get the current state +current = cm.instance.to_dict() + +# Remove a key +current["data"].pop("old_key", None) + +# Replace the entire resource +cm.update_replace(resource_dict=current) +``` + +| Method | HTTP Verb | Behavior | Use When | +|--------|-----------|----------|----------| +| `update()` | PATCH | Merges fields into existing resource | Adding or changing fields | +| `update_replace()` | PUT | Replaces entire resource | Removing fields or doing full replacements | + +## Deleting Resources + +### Using `clean_up()` + +```python +ns.clean_up() # Deletes and waits for removal (default: wait=True) +``` + +Control the deletion behavior: + +```python +# Delete without waiting +ns.clean_up(wait=False) + +# Delete with a custom timeout (in seconds) +ns.clean_up(timeout=120) +``` + +`clean_up()` returns `True` if the resource was successfully deleted, `False` otherwise. + +### Using `delete()` + +`delete()` is the lower-level method called by `clean_up()`: + +```python +ns.delete(wait=True, timeout=240) +``` + +You can also pass a custom body for delete options: + +```python +ns.delete(body={"propagationPolicy": "Background"}) +``` + +> **Tip:** If you delete a resource that doesn't exist, `delete()` logs a warning and returns `True` instead of raising an error. + +### Controlling Teardown + +Set `teardown=False` at construction to prevent automatic deletion in context managers: + +```python +with Namespace(client=client, name="persistent-ns", teardown=False) as ns: + print("This namespace will NOT be deleted on exit") +``` + +## Checking Resource State + +### Check If a Resource Exists + +```python +ns = Namespace(client=client, name="my-namespace") +if ns.exists: + print("Namespace exists") +``` + +### Ensure a Resource Already Exists + +Use `ensure_exists=True` to raise `ResourceNotFoundError` if the resource is not present on the cluster: + +```python +from ocp_resources.config_map import ConfigMap + +# Raises ResourceNotFoundError if the ConfigMap doesn't exist +cm = ConfigMap( + client=client, + name="must-exist", + namespace="default", + ensure_exists=True, +) +``` + +### Get the Live Instance + +The `instance` property fetches the current state from the API server: + +```python +ns = Namespace(client=client, name="my-namespace", ensure_exists=True) +print(ns.instance.metadata.labels) +print(ns.status) +``` + +### Export as YAML + +```python +ns = Namespace(client=client, name="my-namespace") +print(ns.to_yaml()) +``` + +For more on querying, watching, and status checks, see [Querying and Watching Resources](querying-resources.html). + +## Dry Run Mode + +Validate a resource against the API server without actually creating it: + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="dry-run-ns", dry_run=True) +ns.deploy() # Sends the request with ?dryRun=All — no resource is created +``` + +## Advanced Usage + +### Constructor Parameters Reference + +All resource classes accept these common parameters: + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `client` | `DynamicClient` | `None` | Cluster client connection (will be required in next major release) | +| `name` | `str` | `None` | Resource name | +| `namespace` | `str` | `None` | Namespace (namespaced resources only) | +| `teardown` | `bool` | `True` | Whether `clean_up()` deletes the resource | +| `yaml_file` | `str` | `None` | Path to a YAML manifest file | +| `kind_dict` | `dict` | `None` | Dictionary representation of the resource | +| `delete_timeout` | `int` | `240` | Timeout in seconds for delete operations | +| `dry_run` | `bool` | `False` | Server-side dry run without persisting | +| `label` | `dict` | `None` | Labels to set on the resource | +| `annotations` | `dict` | `None` | Annotations to set on the resource | +| `ensure_exists` | `bool` | `False` | Raise if resource doesn't already exist | +| `wait_for_resource` | `bool` | `False` | Wait for resource creation in context manager | +| `schema_validation_enabled` | `bool` | `False` | Validate against OpenAPI schema on create/replace | +| `hash_log_data` | `bool` | `True` | Mask sensitive data in logs | + +For the full API reference, see [Resource and NamespacedResource API](resource-api.html). + +### Schema Validation on Create and Replace + +Enable automatic schema validation so `create()` and `update_replace()` check your resource against its OpenAPI schema before sending it to the API server: + +```python +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap( + client=client, + name="validated-cm", + namespace="default", + data={"key": "value"}, + schema_validation_enabled=True, # Validates before create() +) +cm.deploy() +``` + +You can also validate on demand or validate a dictionary directly: + +```python +# Validate an existing resource instance +cm.validate() + +# Validate a dictionary without creating a resource +ConfigMap.validate_dict({ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "test"}, + "data": {"key": "value"}, +}) +``` + +Both methods raise `ValidationError` if validation fails. For more details, see [Validating Resources Against OpenAPI Schemas](validating-resources.html). + +### Temporary Edits with ResourceEditor + +`ResourceEditor` applies patches to existing resources and automatically restores original values when used as a context manager: + +```python +from ocp_resources.resource import ResourceEditor + +ns = Namespace(client=client, name="my-namespace", ensure_exists=True) + +with ResourceEditor( + patches={ns: {"metadata": {"labels": {"temporary-label": "true"}}}} +): + # Label is applied + assert ns.labels["temporary-label"] == "true" +# Label is automatically removed +``` + +`ResourceEditor` supports both `update` (merge patch) and `replace` actions: + +```python +with ResourceEditor( + patches={ns: {"metadata": {"labels": {"keep-only-this": "true"}}}}, + action="replace", +): + pass # Full replacement applied, then restored on exit +``` + +For complete coverage, see [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html). + +### Managing Multiple Resources + +#### ResourceList — Create N Copies + +Create multiple instances of the same resource type with indexed names: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import ResourceList + +with ResourceList( + client=client, + resource_class=Namespace, + name="test-ns", + num_resources=3, +) as namespaces: + # Creates test-ns-1, test-ns-2, test-ns-3 + for ns in namespaces: + print(ns.name) +# All three namespaces are deleted on exit +``` + +#### NamespacedResourceList — One Per Namespace + +Create one resource in each namespace from a `ResourceList`: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import NamespacedResourceList, ResourceList + +namespaces = ResourceList( + client=client, + resource_class=Namespace, + name="env", + num_resources=3, +) + +with NamespacedResourceList( + client=client, + resource_class=Pod, + namespaces=namespaces, + name="worker", + containers=[{"name": "app", "image": "myapp:latest"}], +) as pods: + for pod in pods: + print(f"{pod.name} in {pod.namespace}") +``` + +For detailed patterns, see [Managing Bulk Resources with ResourceList](managing-resource-lists.html). + +### Debugging with Environment Variables + +These environment variables control resource lifecycle behavior at runtime without code changes: + +| Variable | Effect | +|----------|--------| +| `REUSE_IF_RESOURCE_EXISTS` | Skip `deploy()` if the resource already exists | +| `SKIP_RESOURCE_TEARDOWN` | Skip `clean_up()` to keep resources for debugging | + +Both accept a YAML-formatted dictionary. Spaces are significant: + +```bash +# Skip creation of all Pods +export REUSE_IF_RESOURCE_EXISTS="{Pod: {}}" + +# Skip creation of a specific Pod in a specific namespace +export REUSE_IF_RESOURCE_EXISTS="{Pod: {my-pod: my-namespace}}" + +# Skip teardown for a Namespace and a Pod +export SKIP_RESOURCE_TEARDOWN="{Namespace: {my-ns:}, Pod: {my-pod: my-namespace}}" +``` + +For the full list of environment variables, see [Environment Variables and Configuration](environment-variables.html). + +## Troubleshooting + +**`MissingRequiredArgumentError` on construction** +You must provide either `name`, `yaml_file`, or `kind_dict`. For namespaced resources, both `name` and `namespace` are required unless using `yaml_file` or `kind_dict`. + +**`ResourceTeardownError` in context manager** +The resource's `clean_up()` returned `False`, meaning deletion failed. Check cluster permissions and resource finalizers. + +**`ValueError: yaml_file and resource_dict are mutually exclusive`** +You passed both `yaml_file` and `kind_dict` to a constructor. Use only one. + +**`ResourceNotFoundError` with `ensure_exists=True`** +The resource doesn't exist on the cluster. Verify the name (and namespace for namespaced resources) are correct. + +**Resource not deleted after context manager exits** +Check if `teardown=False` was passed. Also verify the `SKIP_RESOURCE_TEARDOWN` environment variable is not set. + +## Related Pages + +- [Installing and Creating Your First Resource](quickstart.html) +- [Querying and Watching Resources](querying-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html) +- [Resource and NamespacedResource API](resource-api.html) + +--- + +Source: querying-resources.md + +Let me start by reading the knowledge graph and understanding the codebase structure. + +Now let me explore the key source files related to querying, listing, watching, existence checks, and status retrieval. + +Now let me look at the `_prepare_resources` method and the `get` method more closely to understand the filtering parameters: + +Now let me look at the event module and test files for querying: + +Now let me look at the test files for get/list/watch operations: + +Now let me check the conftest to understand fake_client and look at the `Condition` attribute and `Status` attribute patterns: + +Now let me check how `get` is called with selectors: + +Let me look at how `get` passes `field_selector`, `label_selector`, and `namespace` through kwargs: + +Now let me look at more examples of how get() is used with various kwargs, and check what other files show: + +Let me check how `_build_event_field_selector` and events work: + +Now let me check the namespace.py to see how get() is used in a simple resource: + +Now let me look at the node.py for a cluster-scoped resource usage of get: + +Now let me check the exceptions module for ConditionError: + +Let me also check `ensure_exists`: + +Let me also look for properties that relate to querying the resource (uuid, labels): + +Now let me check the test for events and any resource-specific tests about querying: + +Let me also look at the fake_client to understand what kwargs `get` passes through: + +Now I have a thorough understanding of the codebase. Let me write the documentation page. + +# Querying and Watching Resources + +Find, filter, and monitor Kubernetes and OpenShift resources in your cluster — whether you need to list all pods by label, check if a resource exists, watch for real-time changes, or inspect status and conditions. + +## Prerequisites + +- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html)) +- A connected `DynamicClient` instance (see [Connecting to Clusters](connecting-to-clusters.html)) + +## Quick Example + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +# List all pods in a namespace +for pod in Pod.get(client=client, namespace="my-namespace"): + print(f"{pod.name} — {pod.status}") +``` + +## Listing Resources + +Every resource class provides a `get()` class method that returns a generator of resource objects. The method accepts standard Kubernetes query parameters as keyword arguments. + +### List all resources of a type + +```python +from ocp_resources.namespace import Namespace + +for ns in Namespace.get(client=client): + print(ns.name) +``` + +### Filter by namespace + +Namespaced resources (like `Pod`, `Service`, `ConfigMap`) accept a `namespace` keyword: + +```python +from ocp_resources.pod import Pod + +for pod in Pod.get(client=client, namespace="default"): + print(pod.name) +``` + +### Filter by label selector + +Use `label_selector` with standard Kubernetes label selector syntax: + +```python +from ocp_resources.pod import Pod + +# Single label +for pod in Pod.get(client=client, namespace="my-app", label_selector="app=nginx"): + print(pod.name) + +# Multiple labels (comma-separated) +for pod in Pod.get(client=client, label_selector="app=nginx,tier=frontend"): + print(pod.name) +``` + +### Filter by field selector + +Use `field_selector` with standard Kubernetes field selector syntax: + +```python +from ocp_resources.pod import Pod + +# Find pods on a specific node +for pod in Pod.get(client=client, field_selector="spec.nodeName=worker-1"): + print(pod.name) + +# Find pods by status phase +for pod in Pod.get(client=client, namespace="default", field_selector="status.phase=Running"): + print(pod.name) +``` + +### Get raw resource objects + +By default, `get()` returns wrapper objects. Pass `raw=True` to get the underlying `ResourceInstance` or `ResourceField` objects directly: + +```python +from ocp_resources.pod import Pod + +for pod in Pod.get(client=client, namespace="default", raw=True): + # Returns raw ResourceField objects instead of Pod instances + print(pod.metadata.name, pod.status.phase) +``` + +### List all cluster resources + +To iterate over every resource type in the cluster: + +```python +from ocp_resources.resource import Resource + +for resource in Resource.get_all_cluster_resources(client=client): + print(f"{resource.kind}: {resource.metadata.name}") + +# Filter with label selector +for resource in Resource.get_all_cluster_resources(client=client, label_selector="my-label=value"): + print(f"{resource.kind}: {resource.metadata.name}") +``` + +## `get()` Method Reference + +```python +@classmethod +def get( + cls, + client: DynamicClient | None = None, + singular_name: str = "", + exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS, + raw: bool = False, + *args, + **kwargs, +) -> Generator +``` + +| Parameter | Type | Description | +|---|---|---| +| `client` | `DynamicClient` | Kubernetes dynamic client (required) | +| `singular_name` | `str` | Resource kind in lowercase; used when multiple resources match the same kind | +| `raw` | `bool` | If `True`, return raw `ResourceInstance`/`ResourceField` objects instead of wrapper instances | +| `exceptions_dict` | `dict` | Exceptions to retry on during API calls | +| `**kwargs` | | Passed through to the Kubernetes API: `namespace`, `label_selector`, `field_selector`, `limit`, `resource_version`, `timeout_seconds`, etc. | + +**Returns:** A generator yielding resource objects. + +## Checking Resource Existence + +The `exists` property queries the API server and returns the resource instance if it exists, or `None` if it does not. + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-namespace") + +if ns.exists: + print("Namespace exists") +else: + print("Namespace not found") +``` + +### Require existence at initialization + +Use `ensure_exists=True` to raise `ResourceNotFoundError` immediately if the resource is not found: + +```python +from ocp_resources.pod import Pod + +# Raises ResourceNotFoundError if the pod doesn't exist +pod = Pod(client=client, name="my-pod", namespace="default", ensure_exists=True) +``` + +> **Tip:** Use `ensure_exists=True` when you need a reference to a resource that must already be on the cluster, such as a pre-existing ConfigMap or Secret. + +## Getting the Resource Instance + +The `instance` property fetches the full live resource from the API server: + +```python +pod = Pod(client=client, name="my-pod", namespace="default") + +# Get the full resource instance +inst = pod.instance +print(inst.metadata.name) +print(inst.metadata.uid) +print(inst.metadata.resourceVersion) + +# Convert to a plain dictionary +resource_dict = pod.instance.to_dict() +``` + +> **Note:** Each access to `.instance` makes an API call. Cache the result in a local variable if you need multiple fields from the same snapshot. + +## Retrieving Labels + +The `labels` property returns the resource's labels: + +```python +ns = Namespace(client=client, name="my-namespace") +print(ns.labels) # {'kubernetes.io/metadata.name': 'my-namespace', ...} + +# Check a specific label +if "app" in ns.labels: + print(f"App label: {ns.labels['app']}") +``` + +## Retrieving Resource Status + +### Status phase + +The `status` property returns the current phase (e.g., `Running`, `Pending`, `Active`): + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-namespace") +print(ns.status) # "Active" +``` + +Each resource class defines status constants you can compare against: + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") +if pod.status == Pod.Status.RUNNING: + print("Pod is running") +``` + +Common status constants available on all resources: + +| Constant | Value | +|---|---| +| `Status.RUNNING` | `"Running"` | +| `Status.PENDING` | `"Pending"` | +| `Status.SUCCEEDED` | `"Succeeded"` | +| `Status.FAILED` | `"Failed"` | +| `Status.ACTIVE` | `"Active"` | +| `Status.TERMINATING` | `"Terminating"` | +| `Status.COMPLETED` | `"Completed"` | +| `Status.ERROR` | `"Error"` | + +### Condition messages + +Use `get_condition_message()` to retrieve the message for a specific condition type: + +```python +pod = Pod(client=client, name="my-pod", namespace="default") + +# Get message for a condition type +msg = pod.get_condition_message(condition_type="Ready") +print(msg) + +# Optionally filter by condition status +msg = pod.get_condition_message( + condition_type="Ready", + condition_status="False", +) +print(msg) # Returns empty string if status doesn't match +``` + +**Signature:** +```python +def get_condition_message( + self, + condition_type: str, + condition_status: str = "", +) -> str +``` + +| Parameter | Type | Description | +|---|---|---| +| `condition_type` | `str` | The condition type to look up (e.g., `"Ready"`, `"Available"`) | +| `condition_status` | `str` | If provided, only return the message when the condition has this status | + +**Returns:** The condition message string, or `""` if not found or status doesn't match. + +### Condition constants + +Resources provide built-in condition constants: + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") + +# Use condition constants +msg = pod.get_condition_message( + condition_type=Pod.Condition.READY, + condition_status=Pod.Condition.Status.TRUE, +) +``` + +Common condition constants: + +| Constant | Value | +|---|---| +| `Condition.READY` | `"Ready"` | +| `Condition.AVAILABLE` | `"Available"` | +| `Condition.PROGRESSING` | `"Progressing"` | +| `Condition.DEGRADED` | `"Degraded"` | +| `Condition.UPGRADEABLE` | `"Upgradeable"` | +| `Condition.Status.TRUE` | `"True"` | +| `Condition.Status.FALSE` | `"False"` | +| `Condition.Status.UNKNOWN` | `"Unknown"` | + +## Watching for Changes + +### Watch a specific resource + +The `watcher()` method streams events for a specific resource instance: + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") + +for event in pod.watcher(timeout=60): + print(f"Event type: {event['type']}") # ADDED, MODIFIED, DELETED + print(f"Object: {event['object'].metadata.name}") + print(f"Raw: {event['raw_object']}") +``` + +**Signature:** +```python +def watcher( + self, + timeout: int, + resource_version: str = "", +) -> Generator[dict[str, Any], None, None] +``` + +| Parameter | Type | Description | +|---|---|---| +| `timeout` | `int` | How long to watch (in seconds) | +| `resource_version` | `str` | Only receive events newer than this version. Defaults to the resource version at creation time. | + +Each yielded event is a dict with these keys: + +| Key | Description | +|---|---| +| `type` | Event type: `"ADDED"`, `"MODIFIED"`, or `"DELETED"` | +| `object` | A `ResourceInstance` wrapping the resource | +| `raw_object` | A plain dict representing the resource | + +## Querying Events + +### Stream events for a resource + +The `events()` method watches Kubernetes events associated with a resource: + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") + +for event in pod.events(timeout=30): + print(f"Reason: {event.object.reason}") + print(f"Message: {event.object.message}") +``` + +**Signature:** +```python +def events( + self, + name: str = "", + label_selector: str = "", + field_selector: str = "", + resource_version: str = "", + timeout: int = 240, +) -> Generator +``` + +| Parameter | Type | Description | +|---|---|---| +| `name` | `str` | Filter by event name | +| `label_selector` | `str` | Filter by labels (comma-separated `key=value`) | +| `field_selector` | `str` | Additional field filters (merged with auto-generated `involvedObject.name` filter) | +| `resource_version` | `str` | Only return events newer than this version | +| `timeout` | `int` | Watch timeout in seconds (default: 240) | + +```python +# Example: filter for Warning events with a specific reason +for event in pod.events( + field_selector="type==Warning,reason==BackOff", + timeout=10, +): + print(event.object.message) +``` + +### List existing events (non-streaming) + +Use `Event.list()` to get a snapshot of existing events without streaming: + +```python +from ocp_resources.event import Event + +# List Warning events from the last 5 minutes +events = Event.list( + client=client, + namespace="my-namespace", + field_selector="type==Warning", +) +for event in events: + print(f"{event.reason}: {event.message}") + +# List events from the last 10 minutes +events = Event.list( + client=client, + namespace="my-namespace", + since_seconds=600, +) +``` + +**Signature:** +```python +@classmethod +def Event.list( + cls, + client: DynamicClient, + namespace: str | None = None, + field_selector: str | None = None, + label_selector: str | None = None, + since_seconds: int = 300, +) -> list +``` + +Events are returned sorted by timestamp (most recent first). + +### Delete events + +Clean up events before tests to avoid false positives: + +```python +from ocp_resources.event import Event + +Event.delete_events( + client=client, + namespace="my-namespace", + field_selector="reason=AnEventReason", +) +``` + +## Advanced Usage + +### Handling resources with duplicate API groups + +Some resource kinds exist in multiple API groups. Use `singular_name` to disambiguate: + +```python +from ocp_resources.dns import DNS + +# When a kind exists in multiple API groups, pass singular_name +for dns in DNS.get(client=client, singular_name="dns"): + print(dns.name) +``` + +### Using `full_api()` for low-level access + +The `full_api()` method returns the raw Kubernetes API resource object, giving you direct access to the API: + +```python +ns = Namespace(client=client, name="my-namespace") +api = ns.full_api() + +# Use the API directly for custom queries +result = api.get( + label_selector="app=nginx", + field_selector="status.phase=Active", + limit=10, +) +``` + +`full_api()` accepts these keyword arguments: + +| Parameter | Description | +|---|---| +| `pretty` | Pretty-print output | +| `_continue` | Continuation token for paginated results | +| `field_selector` | Filter by fields | +| `label_selector` | Filter by labels | +| `limit` | Maximum number of results | +| `resource_version` | Filter by resource version | +| `timeout_seconds` | Server-side timeout | +| `watch` | Enable watch mode | + +### Converting resources to YAML + +```python +ns = Namespace(client=client, name="my-namespace") +print(ns.to_yaml()) +``` + +### Wait for conditions and status + +For polling-based waiting (e.g., wait until a pod is `Running` or a condition is `True`), see [Waiting for Resource Conditions and Status](waiting-for-conditions.html). + +### Querying during tests with the fake client + +You can use all querying methods with the fake client for unit testing: + +```python +from ocp_resources.resource import get_client +from ocp_resources.namespace import Namespace + +client = get_client(fake=True) +ns = Namespace(client=client, name="test-ns") +ns.deploy() + +# All query methods work the same way +assert ns.exists +assert ns.instance +assert ns.status == Namespace.Status.ACTIVE + +# Listing works too +for ns in Namespace.get(client=client): + print(ns.name) +``` + +See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for details. + +## Troubleshooting + +**`ResourceNotFoundError` when using `ensure_exists=True`** +The resource does not exist on the cluster. Verify the name, namespace, and that the resource has been created before initializing the wrapper. + +**`NotImplementedError: Couldn't find in `** +The resource kind is not available on your cluster. This typically means the CRD has not been installed or the API group is incorrect. + +**`get()` returns no results** +- Verify the `namespace` is correct (omit it for cluster-scoped resources). +- Check your `label_selector` and `field_selector` syntax — use `==` for equality (e.g., `field_selector="status.phase==Running"`). +- Ensure the client has permissions to list the resource type. + +**`status` property raises `AttributeError`** +Not all resources have a `status.phase` field. Access `instance.status` directly to inspect what status fields are available for your resource type. + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Working with Kubernetes Events](working-with-events.html) +- [Common Resource Patterns](common-patterns.html) +- [Resource and NamespacedResource API](resource-api.html) + +--- + +Source: waiting-for-conditions.md + +Let me start by reading the knowledge graph and page manifest, then explore the relevant source code. + +Now let me examine the resource-specific wait methods, the `wait_for_condition` more closely, and the constants used: + +Now let me look at the `Status` class, `Condition` class, and how `create` integrates with `wait`: + +Now let me look at how Status/Condition are exposed on the base Resource class and how `deploy` / `create` uses wait: + +Now let me see the `deploy` method more fully and how `wait` is passed through: + +Now let me check the `sriov_network_node_state.py` and `virtual_machine_restore.py` for more wait patterns, and also look at the `TimeoutExpiredError` import: + +Now let me check the `cdi_config.py` wait method and look at the `wait_for_resource` parameter in the constructor: + +Now let me look at how `create` uses `wait`: + +Now let me look at more of the `status` property: + +Now I have all the information I need. Let me also check the `TimeoutSampler` import to understand the exception: + +Let me check if there are any virtual machine import wait patterns with more stop_status details: + +Now let me look at the `node_network_state.py` wait patterns: + +I now have comprehensive understanding of all the wait methods. Let me compile the documentation. + +# Waiting for Resource Conditions and Status + +Wait for Kubernetes and OpenShift resources to reach a desired status, condition, or lifecycle state using built-in polling methods with configurable timeouts and early exit on failure. + +## Prerequisites + +- `openshift-python-wrapper` installed in your project +- A connected client (see [Connecting to Clusters](connecting-to-clusters.html)) +- A resource instance to wait on (see [Creating and Managing Resources](creating-and-managing-resources.html)) + +## Quick Example + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") +pod.create(wait=True) # Creates the pod and waits for it to exist + +# Wait for the pod to reach "Running" status +pod.wait_for_status(status=Pod.Status.RUNNING) +``` + +## Waiting for a Resource to Exist + +The `wait()` method polls until a resource exists on the cluster. This is useful after creating a resource or when waiting for an external controller to produce one. + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-namespace") +ns.create() +ns.wait(timeout=120, sleep=2) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `timeout` | `int` | `240` (4 minutes) | Maximum seconds to wait | +| `sleep` | `int` | `1` | Seconds between polls | + +**Raises:** `TimeoutExpiredError` if the resource does not appear before the timeout. + +> **Tip:** Pass `wait=True` to `create()` or `deploy()` to combine creation and existence waiting in one call: +> ```python +> pod.create(wait=True) +> # or +> pod.deploy(wait=True) +> ``` + +## Waiting for Deletion + +The `wait_deleted()` method polls until a resource no longer exists: + +```python +pod.delete(wait=True, timeout=120) + +# Or call wait_deleted() separately: +pod.delete() +success = pod.wait_deleted(timeout=120) +if not success: + print("Resource still exists after timeout") +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `timeout` | `int` | `240` (4 minutes) | Maximum seconds to wait | + +**Returns:** `True` if the resource was deleted, `False` if the timeout was reached. + +> **Note:** Unlike most wait methods, `wait_deleted()` returns `False` on timeout rather than raising an exception. + +## Waiting for Status (Phase) + +The `wait_for_status()` method polls `status.phase` until it matches the expected value: + +```python +from ocp_resources.datavolume import DataVolume + +dv = DataVolume(client=client, name="my-dv", namespace="default") +dv.wait_for_status(status=DataVolume.Status.SUCCEEDED, timeout=600) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `status` | `str` | *(required)* | Expected `status.phase` value | +| `timeout` | `int` | `240` (4 minutes) | Maximum seconds to wait | +| `stop_status` | `str \| None` | `Status.FAILED` | Status that aborts the wait immediately | +| `sleep` | `int` | `1` | Seconds between polls | +| `exceptions_dict` | `dict` | Protocol + cluster retry errors | Exceptions to catch and retry | + +**Raises:** `TimeoutExpiredError` if the resource does not reach the desired status, or if `stop_status` is encountered. + +### Using `stop_status` for early failure + +By default, `wait_for_status()` aborts immediately if the resource enters a `"Failed"` phase. Override this to detect a different failure phase: + +```python +from ocp_resources.virtual_machine_instance import VirtualMachineInstance + +vmi = VirtualMachineInstance(client=client, name="my-vmi", namespace="default") +vmi.wait_for_status( + status=VirtualMachineInstance.Status.RUNNING, + stop_status="ErrorUnschedulable", + timeout=300, +) +``` + +### Built-in status constants + +Every resource inherits status constants from its base class. Resource-specific subclasses extend these with additional values: + +```python +# Base status constants (available on all resources) +from ocp_resources.resource import Resource + +Resource.Status.SUCCEEDED # "Succeeded" +Resource.Status.FAILED # "Failed" +Resource.Status.RUNNING # "Running" +Resource.Status.PENDING # "Pending" + +# Resource-specific statuses +from ocp_resources.virtual_machine import VirtualMachine + +VirtualMachine.Status.MIGRATING # "Migrating" +VirtualMachine.Status.STOPPED # "Stopped" +VirtualMachine.Status.STARTING # "Starting" +VirtualMachine.Status.PAUSED # "Paused" + +from ocp_resources.persistent_volume_claim import PersistentVolumeClaim + +PersistentVolumeClaim.Status.BOUND # "Bound" +``` + +## Waiting for Conditions + +The `wait_for_condition()` method waits for a specific entry in `status.conditions[]` to match a desired type, status, and optionally a reason or message: + +```python +from ocp_resources.user_defined_network import UserDefinedNetwork + +udn = UserDefinedNetwork(client=client, name="my-net", namespace="default") +udn.wait_for_condition( + condition=UserDefinedNetwork.Condition.NETWORK_READY, + status=UserDefinedNetwork.Condition.Status.TRUE, + timeout=30, +) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `condition` | `str` | *(required)* | Condition type to match (e.g. `"Ready"`, `"Available"`) | +| `status` | `str` | *(required)* | Expected condition status (`"True"`, `"False"`, `"Unknown"`) | +| `timeout` | `int` | `300` (5 minutes) | Maximum seconds to wait | +| `sleep_time` | `int` | `1` | Seconds between polls | +| `reason` | `str \| None` | `None` | If set, the condition's `reason` field must also match | +| `message` | `str` | `""` | Text that must appear in the condition's `message` field | +| `stop_condition` | `str \| None` | `None` | A condition type that aborts the wait immediately | +| `stop_status` | `str` | `"True"` | The status of the stop condition that triggers early abort | + +**Raises:** +- `TimeoutExpiredError` — if the condition is not met before the timeout +- `ConditionError` — if the `stop_condition` is detected before the desired condition + +### Matching by reason and message + +```python +pod.wait_for_condition( + condition="Ready", + status="True", + reason="PodCompleted", + message="containers with", + timeout=120, +) +``` + +The `reason` must be an exact match. The `message` is checked using substring inclusion — the condition's message field must *contain* the provided string. + +### Using `stop_condition` for early abort + +Stop conditions let you fail fast when an unrecoverable condition appears: + +```python +from ocp_resources.exceptions import ConditionError + +try: + resource.wait_for_condition( + condition="Ready", + status="True", + stop_condition="Failed", + stop_status="True", + timeout=300, + ) +except ConditionError as e: + print(f"Resource hit a failure condition: {e}") +``` + +> **Note:** The `stop_condition` matching only checks the condition `type` and `status`. The `reason` and `message` parameters are ignored when evaluating stop conditions. + +### Built-in condition constants + +```python +from ocp_resources.resource import Resource + +# Condition types +Resource.Condition.READY # "Ready" +Resource.Condition.AVAILABLE # "Available" +Resource.Condition.DEGRADED # "Degraded" +Resource.Condition.PROGRESSING # "Progressing" +Resource.Condition.UPGRADEABLE # "Upgradeable" +Resource.Condition.NETWORK_READY # "NetworkReady" + +# Condition statuses +Resource.Condition.Status.TRUE # "True" +Resource.Condition.Status.FALSE # "False" +Resource.Condition.Status.UNKNOWN # "Unknown" + +# Condition reasons +Resource.Condition.Reason.ALL_REQUIREMENTS_MET # "AllRequirementsMet" +Resource.Condition.Reason.INSTALL_SUCCEEDED # "InstallSucceeded" +``` + +## Waiting with Context Managers + +When using a resource as a context manager, set `wait_for_resource=True` to automatically wait for the resource to exist after creation: + +```python +from ocp_resources.pod import Pod + +with Pod( + client=client, + name="my-pod", + namespace="default", + wait_for_resource=True, +) as pod: + # Pod exists when you reach this line + pod.wait_for_status(status=Pod.Status.RUNNING) +``` + +See [Creating and Managing Resources](creating-and-managing-resources.html) for more on context manager usage. + +## Advanced Usage + +### Waiting for any conditions to appear + +The `wait_for_conditions()` method waits up to 30 seconds for the resource's `status.conditions` list to be populated (non-empty). This is useful when you need to inspect conditions but aren't sure they've been set yet: + +```python +resource.wait_for_conditions() +# Now resource.instance.status.conditions is available +``` + +### Resource-specific wait methods + +Many resource types provide specialized wait methods beyond the base `wait_for_status()` and `wait_for_condition()`. These methods encapsulate domain-specific logic. + +#### Deployments + +```python +from ocp_resources.deployment import Deployment + +deploy = Deployment(client=client, name="my-app", namespace="default") +deploy.wait_for_replicas(deployed=True, timeout=240) # All replicas ready +deploy.wait_for_replicas(deployed=False, timeout=120) # Scale to zero complete +``` + +#### DaemonSets + +```python +from ocp_resources.daemonset import DaemonSet + +ds = DaemonSet(client=client, name="my-ds", namespace="default") +ds.wait_until_deployed(timeout=240) # All desired pods are ready +``` + +#### VirtualMachines (KubeVirt) + +```python +from ocp_resources.virtual_machine import VirtualMachine +from ocp_resources.virtual_machine_instance import VirtualMachineInstance + +vm = VirtualMachine(client=client, name="my-vm", namespace="default") + +# Wait for VM ready status (True = running, None = stopped) +vm.wait_for_ready_status(status=True, timeout=240) + +# Wait for a specific status field to become None +vm.wait_for_status_none(status="snapshotInProgress", timeout=240) + +# Wait for VMI to be running +vmi = VirtualMachineInstance(client=client, name="my-vm", namespace="default") +vmi.wait_until_running(timeout=240, logs=True) + +# Wait for VMI pause state +vmi.wait_for_pause_status(pause=True, timeout=240) +``` + +See [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) for the full VM lifecycle. + +#### DataVolumes + +```python +from ocp_resources.datavolume import DataVolume + +dv = DataVolume(client=client, name="my-dv", namespace="default") +dv.wait_for_dv_success( + timeout=600, + failure_timeout=120, + pvc_wait_for_bound_timeout=60, +) +``` + +The `wait_for_dv_success()` method handles the full DataVolume lifecycle: it waits for the status to leave `Pending`, then waits for `Succeeded`, and finally confirms the PVC is `Bound`. + +You can pass a `stop_status_func` callback to abort early on custom conditions: + +```python +def dv_is_stuck(dv): + return dv.instance.status.conditions.restartCount > 3 + +dv.wait_for_dv_success( + stop_status_func=dv_is_stuck, + dv=dv, # passed as keyword argument to the function +) +``` + +#### VirtualMachineSnapshot / VirtualMachineRestore + +```python +from ocp_resources.virtual_machine_snapshot import VirtualMachineSnapshot +from ocp_resources.virtual_machine_restore import VirtualMachineRestore + +snapshot = VirtualMachineSnapshot(client=client, name="snap-1", namespace="default") +snapshot.wait_snapshot_done(timeout=240) # readyToUse + VM snapshotInProgress is None + +restore = VirtualMachineRestore(client=client, name="restore-1", namespace="default") +restore.wait_restore_done(timeout=240) # complete + VM restoreInProgress is None +``` + +#### MachineSet + +```python +from ocp_resources.machine_set import MachineSet + +ms = MachineSet(client=client, name="worker-set", namespace="openshift-machine-api") +ms.wait_for_replicas(timeout=300, sleep=1) +``` + +Returns `True` when ready replicas match desired replicas, `False` on timeout. + +#### NodeNetworkConfigurationPolicy + +```python +from ocp_resources.node_network_configuration_policy import NodeNetworkConfigurationPolicy + +nncp = NodeNetworkConfigurationPolicy(client=client, name="br1-policy") +nncp.wait_for_status_success() # Uses the resource's own success_timeout +``` + +Raises `NNCPConfigurationFailed` if the policy fails or finds no matching nodes. + +### Timeout constants + +The library provides predefined timeout constants you can use with any wait method: + +```python +from ocp_resources.utils.constants import ( + TIMEOUT_1SEC, # 1 + TIMEOUT_5SEC, # 5 + TIMEOUT_10SEC, # 10 + TIMEOUT_30SEC, # 30 + TIMEOUT_1MINUTE, # 60 + TIMEOUT_2MINUTES, # 120 + TIMEOUT_4MINUTES, # 240 + TIMEOUT_10MINUTES, # 600 +) + +pod.wait_for_status(status=Pod.Status.RUNNING, timeout=TIMEOUT_10MINUTES) +``` + +### Custom exception retries + +Wait methods handle transient cluster errors automatically (connection resets, etcd leader changes, protocol errors). You can customize which exceptions are retried: + +```python +from ocp_resources.utils.constants import ( + PROTOCOL_ERROR_EXCEPTION_DICT, + DEFAULT_CLUSTER_RETRY_EXCEPTIONS, +) + +# Combine default retry exceptions +pod.wait_for_status( + status=Pod.Status.RUNNING, + exceptions_dict={ + **PROTOCOL_ERROR_EXCEPTION_DICT, + **DEFAULT_CLUSTER_RETRY_EXCEPTIONS, + }, +) +``` + +The default retry exceptions cover: +- `ProtocolError` — network-level failures +- `MaxRetryError`, `ConnectionAbortedError`, `ConnectionResetError` — connection failures +- `InternalServerError` — etcd leader changes, webhook failures, RPC errors +- `ServerTimeoutError` — API server timeouts +- `ForbiddenError` — context deadline exceeded errors + +## Troubleshooting + +### `TimeoutExpiredError` is raised unexpectedly + +- **Increase the timeout.** The default is 240 seconds (4 minutes), which may not be enough for slow operations like DataVolume imports or VM migrations. +- **Check `stop_status`.** `wait_for_status()` defaults `stop_status` to `Status.FAILED`. If your resource legitimately passes through a `"Failed"` phase, set `stop_status` to a value that won't match, or set it to `None`. +- **Inspect the current status.** The error log includes the last observed status. Read it to understand where the resource got stuck. + +### `ConditionError` on `wait_for_condition()` + +This means the `stop_condition` was matched before the desired condition. The error message includes the full condition details. Review your `stop_condition` and `stop_status` settings. + +### Resource exists but `wait_for_status()` times out + +The `wait_for_status()` method reads from `status.phase`. Some resources use different status fields (e.g., `status.ready`, `status.readyToUse`, `status.syncStatus`). For those resources, use the resource-specific wait methods documented above, or use `wait_for_condition()` to match on `status.conditions[]` entries instead. + +## Related Pages + +- [Querying and Watching Resources](querying-resources.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) +- [Error Handling and Exception Reference](error-handling.html) +- [Resource and NamespacedResource API](resource-api.html) + +--- + +Source: validating-resources.md + +Let me start by reading the exclusions file and pages manifest, then explore the codebase. + +Now let me look at how the Resource base class integrates validation: + +Now let me check the exceptions module: + +Now let me check how `deploy` and context managers interact with validation: + +Now I have a comprehensive understanding of the validation feature. Let me write the documentation. + +# Validating Resources Against OpenAPI Schemas + +Catch configuration mistakes before they reach the Kubernetes API by validating resource definitions against OpenAPI schemas. This saves debugging time and prevents failed deployments caused by typos, missing fields, or incorrect types. + +## Prerequisites + +- openshift-python-wrapper installed (see [Installing and Creating Your First Resource](quickstart.html)) +- A connection to a cluster, or use the fake client for offline validation (see [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html)) + +## Quick Example + +```python +from ocp_resources.pod import Pod + +pod = Pod( + name="nginx-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], +) + +# Validate against the OpenAPI schema — raises ValidationError if invalid +pod.validate() +``` + +If the resource is valid, `validate()` returns silently. If something is wrong, it raises a `ValidationError` with a clear message pointing to the problematic field. + +## Manual Validation + +Call `.validate()` on any resource instance to check it against the OpenAPI schema for its kind: + +```python +from ocp_resources.exceptions import ValidationError +from ocp_resources.service import Service + +service = Service( + name="nginx-service", + namespace="default", + selector={"app": "nginx"}, + ports=[{"port": 80, "targetPort": 80, "protocol": "TCP"}], +) + +try: + service.validate() + print("Service configuration is valid") +except ValidationError as e: + print(f"Validation failed: {e}") +``` + +> **Tip:** Use manual validation in CI pipelines or pre-commit hooks to catch errors before applying resources to a cluster. + +## Validating Dictionaries Without Creating Objects + +Use the `validate_dict()` class method to validate a raw resource dictionary without instantiating a resource object: + +```python +from ocp_resources.deployment import Deployment +from ocp_resources.exceptions import ValidationError + +deployment_dict = { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": {"name": "nginx-deployment", "namespace": "default"}, + "spec": { + "replicas": 3, + "selector": {"matchLabels": {"app": "nginx"}}, + "template": { + "metadata": {"labels": {"app": "nginx"}}, + "spec": { + "containers": [ + {"name": "nginx", "image": "nginx:1.21", "ports": [{"containerPort": 80}]} + ], + }, + }, + }, +} + +try: + Deployment.validate_dict(resource_dict=deployment_dict) + print("Deployment configuration is valid") +except ValidationError as e: + print(f"Invalid: {e}") +``` + +This is useful when you have a dictionary from a YAML file or external source and want to validate it before creating the resource. + +## Enabling Auto-Validation on Create + +Set `schema_validation_enabled=True` when creating a resource instance to automatically validate before every `create()`, `deploy()`, or `update_replace()` call: + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ValidationError + +pod = Pod( + client=client, + name="auto-validated-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + schema_validation_enabled=True, +) + +# Validation runs automatically before the resource is sent to the API +pod.deploy() +``` + +If validation fails, the resource is **not** created and a `ValidationError` is raised: + +```python +invalid_pod = Pod( + client=client, + name="bad-pod", + namespace="default", + containers=[{"name": "nginx"}], # Missing required 'image' field + schema_validation_enabled=True, +) + +try: + invalid_pod.deploy() +except ValidationError as e: + print(f"Blocked: {e}") +``` + +> **Note:** Auto-validation is **disabled by default** (`schema_validation_enabled=False`). You must opt in per instance. + +### When Auto-Validation Runs + +| Operation | Auto-validates? | Notes | +|--------------------|-----------------|---------------------------------------------| +| `create()` / `deploy()` | ✅ Yes | Validates the full resource before sending | +| `update_replace()` | ✅ Yes | Validates the replacement dictionary | +| `update()` | ❌ No | Sends a partial patch, not a full resource | + +Auto-validation also applies when using context managers, since they call `deploy()` internally. See [Creating and Managing Resources](creating-and-managing-resources.html) for details on resource lifecycle methods. + +### Toggling Validation at Runtime + +You can enable or disable validation on an existing instance: + +```python +pod = Pod( + client=client, + name="my-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], +) + +# Enable after creation +pod.schema_validation_enabled = True +pod.deploy() # Will validate first + +# Disable for a subsequent operation +pod.schema_validation_enabled = False +``` + +## Advanced Usage + +### Validating Resources with Duplicate API Groups + +Some resource kinds (like `DNS`) exist in multiple API groups. The validator uses the resource's `api_group` to select the correct schema automatically: + +```python +# These two resources share the kind "DNS" but use different API groups. +# Each is validated against its own schema. +dns_config.validate() # Uses config.openshift.io schema +dns_operator.validate() # Uses operator.openshift.io schema +``` + +See [Common Resource Patterns](common-patterns.html) for more on working with resources that share a kind name. + +### Pre-Warming the Schema Cache + +The first validation for a resource kind loads and resolves the schema (~25ms). Subsequent validations for the same kind use a cache and are much faster (~2ms). Pre-warm the cache at application startup for critical resource types: + +```python +from ocp_resources.pod import Pod +from ocp_resources.deployment import Deployment +from ocp_resources.service import Service +from ocp_resources.exceptions import ValidationError + +for resource_cls in [Pod, Deployment, Service]: + try: + resource_cls.validate_dict({ + "apiVersion": "v1", + "kind": resource_cls.kind, + "metadata": {"name": "warmup"} + }) + except ValidationError: + pass # Expected — we just want to load the schemas +``` + +### Disabling Validation for Bulk Operations + +When creating many resources in a loop, disable auto-validation to avoid repeated checks if you've already validated your templates: + +```python +# Validate the template once +Pod.validate_dict(resource_dict=pod_template) + +# Then create many instances without per-instance validation +for i in range(100): + pod = Pod( + client=client, + name=f"worker-{i}", + namespace="default", + containers=[{"name": "app", "image": "myapp:latest"}], + schema_validation_enabled=False, # Skip — already validated + ) + pod.deploy() +``` + +## Troubleshooting + +### Reading Validation Error Messages + +Validation errors include three key pieces of information: + +``` +Resource validation failed for Pod/my-pod + Field: spec.containers[0].image + Error: 123 is not of type 'string' + Expected type: string +``` + +- **Resource identifier** — which resource failed (kind and name) +- **Field path** — the JSON path to the problematic field +- **Error details** — what went wrong and what was expected + +### Common Validation Error Patterns + +| Error message pattern | Cause | Fix | +|----------------------------------------------|-------------------------------------|--------------------------------------------------| +| `'X' is a required property` | Missing a required field | Add the missing field to your resource definition | +| `'X' is not of type 'Y'` | Wrong data type | Use the correct type (e.g., `3` not `"3"`) | +| `'X' does not match pattern` | Invalid format | Follow Kubernetes naming rules (lowercase, alphanumeric, hyphens) | +| `'X' is not one of [...]` | Invalid enum value | Use one of the allowed values listed in the error | +| `Additional properties are not allowed` | Unknown field name | Check for typos in field names | + +### Validation Passes But API Rejects the Resource + +OpenAPI schema validation catches structural issues (wrong types, missing fields, invalid formats) but cannot check everything the API server validates. For example: + +- Unique name constraints within a namespace +- Resource quota limits +- Admission webhook rules +- Cross-resource references (e.g., a ConfigMap that doesn't exist) + +If validation passes but the API still rejects your resource, the issue is likely a server-side constraint. + +### No Schema Available for a Resource + +If no schema is bundled for a resource kind, `validate()` silently succeeds instead of raising an error. A debug-level log message is emitted: + +``` +No schema found for MyCustomResource, skipping validation +``` + +To update bundled schemas, see [class-generator CLI Reference](class-generator-cli.html). + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) +- [Error Handling and Exception Reference](error-handling.html) +- [class-generator CLI Reference](class-generator-cli.html) +- [Resource and NamespacedResource API](resource-api.html) + +--- + +Source: managing-resource-lists.md + +Let me explore the codebase to understand ResourceList and NamespacedResourceList. + +Now let me check the example file more carefully and also look at how the `NamespacedResourceList` actually works in the example vs the source code — I noticed the example passes `namespaces=[ns.name for ns in namespaces]` (a list of strings) but the source code expects a `ResourceList` object. Let me verify. + +The example file seems to have an outdated API for `NamespacedResourceList` (passing a list of strings), but the actual source code expects a `ResourceList` object. The tests confirm it passes a `ResourceList`. Let me check if the example is outdated: + +The example file is inconsistent with the actual source code. The source code validates that each element in `namespaces` has `ns.kind == "Namespace"`, meaning it expects actual Namespace resource objects (from a ResourceList), not strings. The test confirms this by passing a `ResourceList` fixture. I'll document based on the actual source code, not the potentially outdated example. + +Let me also check what methods and properties are available: + +Now I have all the information I need. Let me write the documentation. + +# Managing Bulk Resources with ResourceList + +When you need to create several copies of the same resource or deploy a resource across multiple namespaces, doing it one at a time is tedious and error-prone. `ResourceList` and `NamespacedResourceList` let you create, deploy, and clean up groups of resources in a single operation. + +## Prerequisites + +- A working cluster connection (see [Connecting to Clusters](connecting-to-clusters.html)) +- Familiarity with creating individual resources (see [Creating and Managing Resources](creating-and-managing-resources.html)) + +## Quick Example + +Create three namespaces and deploy them in two lines: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import ResourceList, get_client + +client = get_client() + +namespaces = ResourceList(resource_class=Namespace, num_resources=3, client=client, name="my-ns") +namespaces.deploy() +# Creates: my-ns-1, my-ns-2, my-ns-3 +``` + +Or use a context manager for automatic cleanup: + +```python +with ResourceList(client=client, resource_class=Namespace, name="my-ns", num_resources=3) as namespaces: + print(namespaces[0].name) # "my-ns-1" +# All three namespaces are deleted when the block exits +``` + +## Creating Multiple Copies of a Resource + +`ResourceList` creates N copies of a resource type, each with an auto-generated name based on a base name you provide. + +**Step 1:** Choose a resource class and specify how many copies you want: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import ResourceList, get_client + +client = get_client() +namespaces = ResourceList( + resource_class=Namespace, + num_resources=3, + client=client, + name="test-ns", +) +``` + +This creates three `Namespace` objects named `test-ns-1`, `test-ns-2`, and `test-ns-3`. They are not yet deployed to the cluster. + +**Step 2:** Deploy all resources: + +```python +namespaces.deploy() +``` + +**Step 3:** Work with individual resources by index or iteration: + +```python +# Access by index +first_ns = namespaces[0] +print(first_ns.name) # "test-ns-1" + +# Iterate over all resources +for ns in namespaces: + print(ns.name) + +# Check how many resources are in the list +print(len(namespaces)) # 3 +``` + +**Step 4:** Clean up all resources when done: + +```python +namespaces.clean_up() +``` + +> **Note:** `clean_up()` deletes resources in reverse order. This ensures dependent resources are removed before the resources they depend on. + +## Deploying One Resource Per Namespace + +`NamespacedResourceList` creates one instance of a namespaced resource in each namespace from a `ResourceList`. + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.pod import Pod +from ocp_resources.resource import NamespacedResourceList, ResourceList, get_client + +client = get_client() + +# First, create the namespaces +namespaces = ResourceList(resource_class=Namespace, num_resources=3, client=client, name="app-ns") +namespaces.deploy() + +# Then create one pod in each namespace +pods = NamespacedResourceList( + client=client, + resource_class=Pod, + namespaces=namespaces, + name="web-server", + containers=[{"name": "nginx", "image": "nginx:latest"}], +) +pods.deploy() +``` + +This creates three pods, all named `web-server`, one in each namespace (`app-ns-1`, `app-ns-2`, `app-ns-3`). + +```python +for pod in pods: + print(f"{pod.name} in {pod.namespace}") +# web-server in app-ns-1 +# web-server in app-ns-2 +# web-server in app-ns-3 +``` + +> **Warning:** The `namespaces` parameter must be a `ResourceList` containing only Namespace resources. Passing any other resource type raises a `TypeError`. + +## Passing Extra Resource Parameters + +Any additional keyword arguments you pass are forwarded to each resource's constructor. This lets you configure resources beyond just the name: + +```python +from ocp_resources.role import Role +from ocp_resources.resource import NamespacedResourceList + +roles = NamespacedResourceList( + client=client, + resource_class=Role, + namespaces=namespaces, + name="viewer-role", + rules=[ + { + "apiGroups": [""], + "resources": ["pods"], + "verbs": ["get", "list", "watch"], + } + ], +) +roles.deploy() +``` + +## Advanced Usage + +### Context Managers for Automatic Lifecycle + +Both `ResourceList` and `NamespacedResourceList` support context managers. Resources are deployed on entry and cleaned up on exit — ideal for tests: + +```python +with ResourceList(client=client, resource_class=Namespace, name="test-ns", num_resources=3) as namespaces: + with NamespacedResourceList( + client=client, + resource_class=Pod, + namespaces=namespaces, + name="test-pod", + containers=[{"name": "app", "image": "busybox"}], + ) as pods: + # All namespaces and pods are deployed + assert len(pods) == 3 + # Pods are cleaned up here +# Namespaces are cleaned up here +``` + +> **Tip:** Nest the context managers so that namespaced resources are cleaned up before their namespaces are deleted. + +### Waiting During Deploy and Cleanup + +Pass `wait=True` to `deploy()` to wait for each resource to reach a ready state. By default, `clean_up()` already waits for deletion to complete. + +```python +namespaces = ResourceList(resource_class=Namespace, num_resources=2, client=client, name="ns") +namespaces.deploy(wait=True) + +# Later... +namespaces.clean_up(wait=True) # wait=True is the default +namespaces.clean_up(wait=False) # Skip waiting for deletion +``` + +For more on waiting for specific conditions, see [Waiting for Resource Conditions and Status](waiting-for-conditions.html). + +### Comparison: ResourceList vs NamespacedResourceList + +| Feature | `ResourceList` | `NamespacedResourceList` | +|---|---|---| +| **Purpose** | Create N copies of any resource | Create one resource per namespace | +| **Naming** | Auto-generates `{name}-1`, `{name}-2`, … | Same name in each namespace | +| **Required params** | `resource_class`, `num_resources`, `name` | `resource_class`, `namespaces`, `name` | +| **Use case** | Multiple namespaces, nodes, cluster-scoped resources | Pods, services, roles — anything namespace-scoped | +| **Context manager** | ✅ | ✅ | + +## Troubleshooting + +**TypeError: "All the resources in namespaces should be namespaces"** + +You passed a `ResourceList` that contains non-Namespace resources to `NamespacedResourceList`. Make sure the `namespaces` parameter is a `ResourceList` created with `resource_class=Namespace`. + +**Resources not appearing on the cluster** + +Creating a `ResourceList` or `NamespacedResourceList` only instantiates the Python objects — it does not deploy them. Call `.deploy()` or use a context manager (`with ... as`) to actually create the resources on the cluster. + +**Cleanup order issues** + +Always clean up namespaced resources before their namespaces. When using context managers, nest them so the inner block (namespaced resources) exits first. When calling `clean_up()` manually, clean up dependent resources before their parents. + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) +- [Common Resource Patterns](common-patterns.html) +- [Resource and NamespacedResource API](resource-api.html) + +--- + +Source: editing-resources-temporarily.md + +Let me start by reading the knowledge graph and understanding the repository structure. + +Now let me find the ResourceEditor source code. + +Now let me look at how ResourceEditor is used in the NNCP file and any tests: + +Let me look at the import section and any related constants: + +Now let me look at the `update` and `update_replace` methods on the Resource class to understand the two actions: + +Now let me look at the imports to verify how ResourceEditor is exported: + +Now let me check for any additional examples or documentation: + +Let me look at the full ResourceEditor code once more to make sure I've captured everything, and also check the `_apply_patches` method details and what `action` values are supported: + +Now I have a complete picture of the ResourceEditor class. Let me write the documentation. + +# Editing Resources Temporarily with ResourceEditor + +Temporarily modify Kubernetes and OpenShift resources during tests and have their original values automatically restored when you're done. This is essential for test isolation—ensuring that changes made during a test don't leak into other tests or persist in the cluster. + +## Prerequisites + +- `openshift-python-wrapper` installed in your project (see [Installing and Creating Your First Resource](quickstart.html)) +- A cluster connection established with a privileged client (see [Connecting to Clusters](connecting-to-clusters.html)) +- One or more existing resources on the cluster that you want to temporarily modify + +> **Warning:** The client used to retrieve resources must have sufficient privileges. Do not use an unprivileged user client with `ResourceEditor`. + +## Quick Example + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.node import Node + +node = Node(name="worker-0") + +with ResourceEditor( + patches={node: {"metadata": {"labels": {"test-label": "true"}}}} +): + # The node now has the label "test-label": "true" + # Run your test logic here... + pass + +# The label is automatically removed when the block exits +``` + +That's it—when the `with` block ends (even if an exception occurs), the original resource state is restored. + +## Step-by-Step: Using ResourceEditor as a Context Manager + +### 1. Import and identify your target resource + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap(name="my-config", namespace="my-namespace") +``` + +### 2. Define your patch + +Patches are dictionaries that mirror the resource's YAML structure. Only include the fields you want to change: + +```python +patch = {"data": {"feature_flag": "enabled", "log_level": "debug"}} +``` + +### 3. Wrap your test logic in a `with` block + +```python +with ResourceEditor(patches={cm: patch}): + # ConfigMap now has updated data fields + assert cm.instance.data.feature_flag == "enabled" + # Run tests that depend on the modified config... +``` + +### 4. Original values are restored automatically + +When the `with` block exits, the original `data` values are written back to the resource. Fields that didn't exist before the patch are removed (set to `None` in the restore payload, which tells the API to delete them). + +## Patching Multiple Resources at Once + +Pass multiple resource-patch pairs in a single `ResourceEditor`: + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.namespace import Namespace +from ocp_resources.config_map import ConfigMap + +ns = Namespace(name="test-ns") +cm = ConfigMap(name="app-config", namespace="test-ns") + +patches = { + ns: {"metadata": {"labels": {"env": "test"}}}, + cm: {"data": {"database_url": "postgres://test-db:5432"}}, +} + +with ResourceEditor(patches=patches): + # Both resources are patched; run tests here + pass +# Both resources are restored to their original state +``` + +## Using update() and restore() Without a Context Manager + +If you need finer control over when patches are applied and reverted—for example, in `setup`/`teardown` fixtures—use the `update()` and `restore()` methods directly: + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap(name="app-config", namespace="default") + +editor = ResourceEditor( + patches={cm: {"data": {"mode": "maintenance"}}} +) + +# Apply the patch and create backups +editor.update(backup_resources=True) + +# ... run tests ... + +# Restore original values +editor.restore() +``` + +> **Note:** When calling `update()` manually, pass `backup_resources=True` to ensure original values are captured. Without this flag, no backup is created and `restore()` will have nothing to revert. + +## Advanced Usage + +### Replace Instead of Patch + +By default, `ResourceEditor` uses the `"update"` action, which sends a merge-patch (`application/merge-patch+json`). This merges your changes into the existing resource. If you need to *replace* the entire resource (for example, to remove fields that merge-patch can't delete), use `action="replace"`: + +```python +from ocp_resources.resource import ResourceEditor + +with ResourceEditor( + patches={my_resource: {"spec": {"replicas": 3}}}, + action="replace", +): + # The resource is fully replaced, not merged + pass +``` + +| Action | Method Used | Behavior | +|---|---|---| +| `"update"` (default) | Merge patch | Merges patch into existing resource; cannot remove fields by omission | +| `"replace"` | Full replacement | Replaces the entire resource; fields not in the patch are removed | + +> **Warning:** With `action="replace"`, the patch must include all required fields for the resource, not just the fields you want to change. The replacement payload automatically includes `metadata.name`, `metadata.namespace`, `metadata.resourceVersion`, `kind`, and `apiVersion`. + +### Providing Your Own Backups + +If you already know what the restore state should be (or need custom restore logic), pass `user_backups` to skip the automatic backup calculation: + +```python +from ocp_resources.resource import ResourceEditor + +custom_backup = {my_resource: {"spec": {"replicas": 1}}} + +with ResourceEditor( + patches={my_resource: {"spec": {"replicas": 5}}}, + user_backups=custom_backup, +): + # Resource scaled to 5 replicas + pass +# Restored to 1 replica (your custom backup), regardless of what the original value was +``` + +### Inspecting Backups + +Access the automatically generated backup data through the `backups` property: + +```python +editor = ResourceEditor( + patches={my_resource: {"metadata": {"labels": {"env": "staging"}}}} +) +editor.update(backup_resources=True) + +# See what was backed up +print(editor.backups) +# {: {'metadata': {'labels': {'env': 'production'}}}} + +editor.restore() +``` + +### No-Op Detection + +`ResourceEditor` automatically detects when a patch produces no actual changes. If the patch values already match the resource's current state, the resource is skipped entirely and a warning is logged: + +``` +ResourceEdit: no diff found in patch for my-resource -- skipping +``` + +This prevents unnecessary API calls and avoids triggering watch events for no-op changes. + +### Automatic Retry on Conflicts + +All patch and restore operations are wrapped with automatic retry logic. If a `ConflictError` occurs (for example, due to a concurrent update changing the `resourceVersion`), the operation is retried automatically with a 5-second sleep interval and a 30-second timeout. + +## API Reference + +### Constructor + +```python +ResourceEditor( + patches: dict, + action: str = "update", + user_backups: dict | None = None, +) +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `patches` | `dict` | *(required)* | Mapping of resource objects to patch dicts: `{resource: {yaml_patch}}` | +| `action` | `str` | `"update"` | Either `"update"` (merge patch) or `"replace"` (full replacement) | +| `user_backups` | `dict \| None` | `None` | Custom backup dicts to use for restore instead of auto-generated ones | + +### Properties + +| Property | Return Type | Description | +|---|---|---| +| `backups` | `dict` | The backup data captured for each patched resource: `{resource: backup_dict}` | +| `patches` | `dict` | The patches dict provided in the constructor | + +### Methods + +| Method | Parameters | Description | +|---|---|---| +| `update(backup_resources=False)` | `backup_resources: bool` | Apply patches. Set `backup_resources=True` to capture original values for later restore. | +| `restore()` | — | Re-apply the stored backup dicts to revert all patched resources. | + +### Context Manager Protocol + +| Method | Behavior | +|---|---| +| `__enter__()` | Calls `update(backup_resources=True)` and returns the `ResourceEditor` instance | +| `__exit__()` | Calls `restore()` to revert all changes | + +## Troubleshooting + +| Problem | Cause | Solution | +|---|---|---| +| Restore doesn't revert changes | Called `update()` without `backup_resources=True` | Always pass `backup_resources=True` when calling `update()` manually, or use the context manager (`with` block) which does this automatically | +| `NotFoundError` during backup | Resource cannot be found by name (known with some CRDs like `ServiceMonitor`) | `ResourceEditor` handles this automatically by falling back to a field-selector query | +| `ConflictError` during patch | Another process modified the resource concurrently | Automatic retries handle this; if it persists beyond 30 seconds, check for controllers continuously modifying the resource | +| Patch has no effect | The resource already has the values in your patch | This is expected—`ResourceEditor` logs a warning and skips the resource. Check that you're patching the correct fields. | +| `replace` action fails | Patch is missing required fields | When using `action="replace"`, provide all required spec fields since the entire resource is replaced | + +> **Tip:** For creating and cleaning up resources (rather than editing existing ones), see [Creating and Managing Resources](creating-and-managing-resources.html). For managing groups of similar resources, see [Managing Bulk Resources with ResourceList](managing-resource-lists.html). + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Resource and NamespacedResource API](resource-api.html) +- [Common Resource Patterns](common-patterns.html) +- [Managing Bulk Resources with ResourceList](managing-resource-lists.html) +- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) + +--- + +Source: working-with-virtual-machines.md + +Let me start by exploring the knowledge graph and understanding the repository structure. + +Now let me look at the remaining VM-related resources and check the `RunStrategy` and `Status` constants, as well as the `data_volume.py`: + +Now let me check how the `api_request` method works in the base `Resource` class: + +Now let me look at the `data_volume.py` to check any VM-related interactions: + +Now let me check the `RunStrategy` constants in VirtualMachineInstance: + +Now let me look at the `Status` class in the base `Resource` for context and the VMI's `RunStrategy`: + +Let me find the Status class definition: + +Now let me check the `ApiGroup` constants for KubeVirt: + +Now let me check for any tests related to VMs: + +Now let me check the `deploy` and `clean_up` method signatures: + +Now I have a thorough understanding of the VM-related API. Let me write the documentation: + +# Working with Virtual Machines (KubeVirt) + +Manage the full lifecycle of KubeVirt virtual machines on OpenShift — create VMs, start and stop them, live-migrate between nodes, take snapshots, and interact with their running instances — all from Python. + +## Prerequisites + +- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html)) +- A cluster connection configured (see [Connecting to Clusters](connecting-to-clusters.html)) +- KubeVirt / OpenShift Virtualization installed on your cluster + +## Quick Example + +```python +from ocp_resources.virtual_machine import VirtualMachine + +vm = VirtualMachine( + name="my-vm", + namespace="my-namespace", + body={ + "spec": { + "domain": { + "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]}, + "resources": {"requests": {"memory": "1Gi"}}, + }, + "volumes": [{"name": "rootdisk", "containerDisk": {"image": "quay.io/containerdisks/fedora"}}], + } + }, +) + +with vm: + vm.start(wait=True) + print(f"VM ready: {vm.ready}") # True + print(f"Status: {vm.printable_status}") # "Running" + vm.stop(wait=True) +# VM is automatically cleaned up when the context manager exits +``` + +## Creating a VirtualMachine + +```python +from ocp_resources.virtual_machine import VirtualMachine + +vm = VirtualMachine( + name="my-vm", + namespace="my-namespace", + body={ + "spec": { + "domain": { + "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]}, + "resources": {"requests": {"memory": "2Gi"}}, + }, + "volumes": [ + { + "name": "rootdisk", + "containerDisk": {"image": "quay.io/containerdisks/fedora"}, + } + ], + } + }, +) +vm.deploy() +``` + +The `body` parameter accepts a dictionary that maps to the KubeVirt VM `spec.template` structure. If you omit `body`, a minimal skeleton (`{"template": {"spec": {}}}`) is created automatically. + +You can also create a VM from a YAML file: + +```python +vm = VirtualMachine( + name="my-vm", + namespace="my-namespace", + yaml_file="vm-definition.yaml", +) +vm.deploy() +``` + +### Constructor Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `name` | `str` | `None` | Name of the VirtualMachine resource | +| `namespace` | `str` | `None` | Target namespace | +| `client` | `DynamicClient` | `None` | Kubernetes client; auto-resolved if omitted | +| `body` | `dict` | `None` | VM spec body dictionary | +| `teardown` | `bool` | `True` | Whether to delete the resource on cleanup | +| `yaml_file` | `str` | `None` | Path to a YAML file defining the VM | +| `delete_timeout` | `int` | `240` (seconds) | Timeout for delete operations | + +> **Tip:** Use the context manager (`with vm:`) to ensure automatic cleanup. See [Creating and Managing Resources](creating-and-managing-resources.html) for details on `deploy()`, `clean_up()`, and context managers. + +## Starting, Stopping, and Restarting + +### Start a VM + +```python +# Fire-and-forget +vm.start() + +# Wait until the VM is ready +vm.start(wait=True) + +# Custom timeout (seconds) +vm.start(timeout=600, wait=True) +``` + +### Stop a VM + +```python +# Fire-and-forget +vm.stop() + +# Wait for the VM to stop and the VMI to be deleted +vm.stop(wait=True) + +# Custom timeouts +vm.stop(timeout=300, vmi_delete_timeout=300, wait=True) +``` + +### Restart a VM + +```python +vm.restart(wait=True) +``` + +When `wait=True`, `restart()` waits for the old virt-launcher pod to be deleted and the new VMI to reach the `Running` state. + +## Checking VM Status + +```python +# Boolean ready status: True if running, None if stopped +vm.ready # True + +# Human-readable status string +vm.printable_status # "Running", "Stopped", "Starting", "Migrating", etc. + +# Wait for ready status +vm.wait_for_ready_status(status=True, timeout=300) # Wait for running +vm.wait_for_ready_status(status=None, timeout=300) # Wait for stopped + +# Wait for a specific status field to clear +vm.wait_for_status_none(status="snapshotInProgress") +``` + +### Run Strategy Constants + +Use built-in constants for the `runStrategy` field: + +```python +VirtualMachine.RunStrategy.ALWAYS # "Always" +VirtualMachine.RunStrategy.HALTED # "Halted" +VirtualMachine.RunStrategy.MANUAL # "Manual" +VirtualMachine.RunStrategy.RERUNONFAILURE # "RerunOnFailure" +``` + +### Status Constants + +```python +VirtualMachine.Status.STARTING # "Starting" +VirtualMachine.Status.RUNNING # "Running" (inherited) +VirtualMachine.Status.STOPPED # "Stopped" +VirtualMachine.Status.STOPPING # "Stopping" +VirtualMachine.Status.MIGRATING # "Migrating" +VirtualMachine.Status.PAUSED # "Paused" +VirtualMachine.Status.PROVISIONING # "Provisioning" +VirtualMachine.Status.ERROR_UNSCHEDULABLE # "ErrorUnschedulable" +VirtualMachine.Status.DATAVOLUME_ERROR # "DataVolumeError" +``` + +## Working with VirtualMachineInstances + +Every running VM has an associated `VirtualMachineInstance` (VMI). Access it through the VM or create one directly. + +### Accessing the VMI from a VM + +```python +vmi = vm.vmi # Returns a VirtualMachineInstance object +``` + +### Creating a Standalone VMI + +```python +from ocp_resources.virtual_machine_instance import VirtualMachineInstance + +vmi = VirtualMachineInstance( + name="my-vmi", + namespace="my-namespace", + domain={ + "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]}, + "resources": {"requests": {"memory": "1Gi"}}, + }, + volumes=[ + {"name": "rootdisk", "containerDisk": {"image": "quay.io/containerdisks/fedora"}} + ], +) +vmi.deploy() +``` + +> **Note:** The `domain` parameter is **required** when creating a VMI programmatically. Omitting it raises `MissingRequiredArgumentError`. + +### VMI Constructor Parameters + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `domain` | `dict` | **Yes** | Domain spec (CPU, memory, devices) | +| `volumes` | `list` | No | Volumes to mount | +| `networks` | `list` | No | Network interfaces | +| `affinity` | `dict` | No | Scheduling affinity rules | +| `node_selector` | `dict` | No | Node label selector | +| `tolerations` | `list` | No | Toleration rules | +| `eviction_strategy` | `str` | No | `"LiveMigrate"`, `"LiveMigrateIfPossible"`, `"None"`, or `"External"` | +| `termination_grace_period_seconds` | `int` | No | Seconds to wait before force-killing | +| `hostname` | `str` | No | VM hostname | +| `priority_class_name` | `str` | No | Pod priority class | +| `scheduler_name` | `str` | No | Custom scheduler name | +| `start_strategy` | `str` | No | Set to `"Paused"` to start paused | +| `access_credentials` | `list` | No | Public keys to inject | +| `topology_spread_constraints` | `list` | No | Topology spread rules | + +### Waiting for a VMI to Run + +```python +vmi.wait_until_running(timeout=300) +``` + +If the VMI fails to start, `wait_until_running` raises `TimeoutExpiredError` and logs the virt-launcher pod status and logs for debugging. Set `logs=False` to suppress this: + +```python +vmi.wait_until_running(timeout=300, logs=False) +``` + +You can also specify a `stop_status` to abort early if the VMI enters a failure state: + +```python +vmi.wait_until_running(timeout=300, stop_status="Failed") +``` + +### Pausing and Unpausing + +```python +vmi.pause(wait=True) # Pause the VMI +vmi.unpause(wait=True) # Resume the VMI +``` + +### Resetting a VMI + +```python +vmi.reset() # Hard-reset the VMI (like pressing the reset button) +``` + +### Getting the VMI Node + +```python +node = vmi.get_node() +print(node.name) +``` + +### Network Interfaces + +```python +# All interfaces from the VMI status +interfaces = vmi.interfaces + +# Get IP address for a specific interface +ip = vmi.interface_ip("eth0") +``` + +### Guest Agent Information + +When the QEMU guest agent is installed in the VM: + +```python +vmi.guest_os_info # OS information +vmi.guest_fs_info # Filesystem information +vmi.guest_user_info # Logged-in user information +vmi.os_version # OS version string +``` + +> **Note:** If no guest agent is installed, `os_version` returns an empty dict and logs a warning. + +### Accessing the Virt-Launcher Pod + +```python +pod = vmi.get_virt_launcher_pod() +print(pod.name) +print(pod.status) + +# Access logs from the compute container +logs = pod.log(container="compute") +``` + +For clusters where a privileged client is required: + +```python +pod = vmi.get_virt_launcher_pod(privileged_client=admin_client) +``` + +### Accessing the Virt-Handler Pod + +```python +handler_pod = vmi.get_virt_handler_pod(privileged_client=admin_client) +``` + +### Executing Virsh Commands + +Run virsh commands inside the virt-launcher pod: + +```python +# Get the XML definition +xml_output = vmi.get_xml() + +# Get the XML as a parsed Python dict +xml_dict = vmi.get_xml_dict() + +# Get domain memory stats +memstats = vmi.get_dommemstat() + +# Run any arbitrary virsh command +output = vmi.execute_virsh_command(command="dominfo") +``` + +### Checking Root Status + +```python +pod = vmi.get_virt_launcher_pod() + +# Check if the virt-launcher pod runs as root +VirtualMachineInstance.is_pod_root(pod=pod) # True or False + +# Get the pod's runAsUser UID +VirtualMachineInstance.get_pod_user_uid(pod=pod) # int or None +``` + +## Live Migration + +Trigger a live migration by creating a `VirtualMachineInstanceMigration`: + +```python +from ocp_resources.virtual_machine_instance_migration import VirtualMachineInstanceMigration + +migration = VirtualMachineInstanceMigration( + name="migrate-my-vm", + namespace="my-namespace", + vmi_name="my-vm", +) +migration.deploy() +``` + +Monitor migration by checking the VMI's status. Once migration completes, the VMI runs on the target node. See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for condition-based waiting. + +### Migration Resource Quotas + +Control resource allocation for migrations: + +```python +from ocp_resources.virtual_machine_migration_resource_quota import VirtualMachineMigrationResourceQuota + +quota = VirtualMachineMigrationResourceQuota( + name="migration-quota", + namespace="my-namespace", + requests_cpu="2", + requests_memory="4Gi", + limits_cpu="4", + limits_memory="8Gi", +) +quota.deploy() +``` + +## Snapshots and Restores + +### Taking a Snapshot + +```python +from ocp_resources.virtual_machine_snapshot import VirtualMachineSnapshot + +snapshot = VirtualMachineSnapshot( + name="my-vm-snapshot", + namespace="my-namespace", + vm_name="my-vm", +) +snapshot.deploy() + +# Wait for the snapshot to be ready +snapshot.wait_ready_to_use(timeout=300) + +# Wait for both snapshot readiness AND the VM's snapshotInProgress to clear +snapshot.wait_snapshot_done(timeout=300) +``` + +### Restoring from a Snapshot + +```python +from ocp_resources.virtual_machine_restore import VirtualMachineRestore + +restore = VirtualMachineRestore( + name="my-vm-restore", + namespace="my-namespace", + vm_name="my-vm", + snapshot_name="my-vm-snapshot", +) +restore.deploy() + +# Wait for restore to complete +restore.wait_restore_done(timeout=300) +``` + +The `wait_restore_done()` method checks both the restore's `complete` status and that the VM's `restoreInProgress` field is cleared. + +| Parameter | Type | Description | +|---|---|---| +| `vm_name` | `str` | Target VirtualMachine name | +| `snapshot_name` | `str` | Source VirtualMachineSnapshot name | +| `volume_restore_policy` | `str` | Optional policy for volume restoration | + +## Cloning VMs + +```python +from ocp_resources.virtual_machine_clone import VirtualMachineClone + +clone = VirtualMachineClone( + name="clone-my-vm", + namespace="my-namespace", + source_name="my-vm", + target_name="my-vm-clone", +) +clone.deploy() +``` + +> **Note:** `source_name` is **required**. If omitted, `MissingRequiredArgumentError` is raised. + +### Clone Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `source_name` | `str` | **Required** | Name of the source VM | +| `source_kind` | `str` | `"VirtualMachine"` | Kind of the source resource | +| `target_name` | `str` | Auto-generated | Name for the cloned VM | +| `label_filters` | `list` | `None` | Labels to copy, e.g. `["*", "!someKey/*"]` | +| `annotation_filters` | `list` | `None` | Annotations to copy | +| `new_mac_addresses` | `dict` | `None` | Override MACs: `{"nic0": "00:11:22:33:44:55"}` | +| `new_smbios_serial` | `str` | `None` | Override SMBIOS serial number | +| `volume_name_policy` | `str` | `None` | Volume naming policy for the clone | + +## Exporting VMs + +```python +from ocp_resources.virtual_machine_export import VirtualMachineExport + +export = VirtualMachineExport( + name="export-my-vm", + namespace="my-namespace", + source={"apiGroup": "kubevirt.io", "kind": "VirtualMachine", "name": "my-vm"}, + token_secret_ref="my-export-token", + ttl_duration="1h", +) +export.deploy() +``` + +> **Note:** The `source` parameter is **required**. If omitted, `MissingRequiredArgumentError` is raised. + +## Advanced Usage + +### Instance Types and Preferences + +KubeVirt instance types define reusable VM sizing profiles. Preferences define non-sizing defaults (clock, firmware, devices). + +#### Namespaced Instance Type + +```python +from ocp_resources.virtual_machine_instancetype import VirtualMachineInstancetype + +instancetype = VirtualMachineInstancetype( + name="small", + namespace="my-namespace", + cpu={"guest": 2}, + memory={"guest": "4Gi"}, +) +instancetype.deploy() +``` + +Both `cpu` and `memory` are **required**. Omitting either raises `MissingRequiredArgumentError`. + +#### Cluster-Scoped Instance Type + +```python +from ocp_resources.virtual_machine_cluster_instancetype import VirtualMachineClusterInstancetype + +cluster_instancetype = VirtualMachineClusterInstancetype( + name="large", + cpu={"guest": 8}, + memory={"guest": "16Gi"}, + gpus=[{"deviceName": "nvidia.com/A100", "name": "gpu0"}], +) +cluster_instancetype.deploy() +``` + +#### Namespaced Preference + +```python +from ocp_resources.virtual_machine_preference import VirtualMachinePreference + +preference = VirtualMachinePreference( + name="linux-pref", + namespace="my-namespace", + cpu={"preferredCPUTopology": "preferSockets"}, + firmware={"preferredUseEfi": True}, +) +preference.deploy() +``` + +#### Cluster-Scoped Preference + +```python +from ocp_resources.virtual_machine_cluster_preference import VirtualMachineClusterPreference + +cluster_preference = VirtualMachineClusterPreference( + name="windows-pref", + clock={"preferredTimer": {"hpet": {"present": False}}}, + features={"preferredHyperv": {"spinlocks": {"spinlocks": 8191}}}, +) +cluster_preference.deploy() +``` + +### VMI Replica Sets + +Scale out VMIs horizontally: + +```python +from ocp_resources.virtual_machine_instance_replica_set import VirtualMachineInstanceReplicaSet + +replica_set = VirtualMachineInstanceReplicaSet( + name="my-vmi-rs", + namespace="my-namespace", + replicas=3, + selector={"matchLabels": {"app": "my-vmi"}}, + template={ + "metadata": {"labels": {"app": "my-vmi"}}, + "spec": { + "domain": { + "devices": {}, + "resources": {"requests": {"memory": "1Gi"}}, + } + }, + }, +) +replica_set.deploy() +``` + +Both `selector` and `template` are **required**. + +### Storage Migration + +Migrate VM storage between storage classes: + +```python +from ocp_resources.virtual_machine_storage_migration_plan import VirtualMachineStorageMigrationPlan +from ocp_resources.virtual_machine_storage_migration import VirtualMachineStorageMigration + +# Create a migration plan +plan = VirtualMachineStorageMigrationPlan( + name="migrate-storage", + namespace="my-namespace", + virtual_machines=[ + {"name": "my-vm", "volumes": [{"name": "rootdisk", "target": {"storageClassName": "fast-ssd"}}]} + ], + retention_policy="deleteSource", +) +plan.deploy() + +# Execute the migration +migration = VirtualMachineStorageMigration( + name="migrate-storage-exec", + namespace="my-namespace", + virtual_machine_storage_migration_plan_ref={"name": "migrate-storage", "namespace": "my-namespace"}, +) +migration.deploy() +``` + +### VM Templates + +```python +from ocp_resources.virtual_machine_template import VirtualMachineTemplate + +template = VirtualMachineTemplate( + name="fedora-template", + namespace="my-namespace", + virtual_machine={ + "spec": { + "template": { + "spec": { + "domain": { + "devices": {}, + "resources": {"requests": {"memory": "2Gi"}}, + } + } + } + } + }, + parameters=[ + {"name": "VM_NAME", "description": "Name of the VM", "required": True} + ], + message="Use this template to create Fedora VMs.", +) +template.deploy() +``` + +### Listing VMs Across Namespaces + +```python +# List all VMs in a namespace +for vm in VirtualMachine.get(namespace="my-namespace"): + print(f"{vm.name}: {vm.instance.get('status', {}).get('printableStatus')}") + +# List with label selectors +for vm in VirtualMachine.get( + namespace="my-namespace", + label_selector="app=webserver", +): + print(vm.name) +``` + +See [Querying and Watching Resources](querying-resources.html) for more query options. + +### Getting VM Network Interfaces + +```python +interfaces = vm.get_interfaces() +for iface in interfaces: + print(f"Name: {iface['name']}, Model: {iface.get('model', 'default')}") +``` + +## Troubleshooting + +### VM Stuck in "Starting" Status + +Check the VMI's virt-launcher pod for scheduling or image-pull issues: + +```python +vmi = vm.vmi +try: + pod = vmi.get_virt_launcher_pod() + print(f"Pod status: {pod.status}") + print(pod.log(container="compute")) +except ResourceNotFoundError: + print("Virt-launcher pod not found — VM may not have been scheduled yet") +``` + +### TimeoutExpiredError on start/stop + +Increase the timeout parameter: + +```python +vm.start(timeout=600, wait=True) # 10 minutes instead of default 4 +``` + +### MissingRequiredArgumentError + +This is raised when a required constructor parameter is missing. Common cases: + +| Resource | Required Parameters | +|---|---| +| `VirtualMachineInstance` | `domain` | +| `VirtualMachineInstancetype` | `cpu`, `memory` | +| `VirtualMachineClone` | `source_name` | +| `VirtualMachineExport` | `source` | +| `VirtualMachineInstanceReplicaSet` | `selector`, `template` | +| `VirtualMachineStorageMigration` | `virtual_machine_storage_migration_plan_ref` | +| `VirtualMachineTemplate` | `virtual_machine` | + +### Guest Agent Not Reporting + +If `vmi.os_version` returns an empty dict, the QEMU guest agent is not installed or not running inside the VM. Install and start `qemu-guest-agent` in the guest OS. + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Querying and Watching Resources](querying-resources.html) +- [Common Resource Patterns](common-patterns.html) +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) + +--- + +Source: testing-without-cluster.md + +Let me start by exploring the repository structure and the knowledge graph. + +Now let me look at the test files for the fake client: + +Let me look at a few more test files to understand patterns used: + +Now let me look at how `get_client` works with `fake=True` and how resource classes use the client: + +Now let me look at a few more test examples to understand patterns: + +Now let me check how resources use the fake client and how `deploy`, `clean_up`, `update` work: + +Let me look at additional test files for more patterns - ConfigMap, events, and the configmap resource: + +Now let me check how watch and events are used, and how to use the lower-level API: + +Now let me check the `events` and `watch` implementation in the Resource class: + +Now let me look at a few more resource test files and the low-level API usage: + +Let me check for import paths and the `ocp_resources.resource` module to understand the public API: + +Let me check how the `wait_for_condition` and other resource methods work with the fake client: + +Now I have enough information to write the documentation page. Let me check a couple more things: + +Now I have all the information I need. Let me write the documentation page. + +# Testing Without a Cluster Using the Fake Client + +Write unit tests that create, read, update, and delete Kubernetes resources — including status checks, watch events, and custom CRDs — without connecting to a real cluster. + +## Prerequisites + +- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html)) +- `pytest` installed for running tests + +## Quick Example + +```python +import pytest +from ocp_resources.resource import get_client +from ocp_resources.namespace import Namespace + +@pytest.fixture(scope="class") +def fake_client(): + return get_client(fake=True) + +class TestMyFeature: + def test_create_namespace(self, fake_client): + ns = Namespace(client=fake_client, name="my-namespace") + ns.deploy() + + assert ns.exists + assert ns.kind == "Namespace" + assert ns.status == Namespace.Status.ACTIVE + + ns.clean_up(wait=False) + assert not ns.exists +``` + +Run it with `pytest` — no cluster, no kubeconfig, no network calls. + +## How It Works + +Calling `get_client(fake=True)` returns an in-memory client that simulates the Kubernetes API. It supports all standard resources (Pods, Deployments, Services, Namespaces, etc.) and OpenShift resources (Routes, Projects, etc.) out of the box. + +All resource classes (`Pod`, `Deployment`, `Namespace`, etc.) accept this fake client through their `client` parameter, and all CRUD methods work identically to a real cluster. + +## Step-by-Step: CRUD Operations + +### 1. Set up a shared fixture + +Create a `conftest.py` in your test directory: + +```python +import pytest +from ocp_resources.resource import get_client + +@pytest.fixture(scope="class") +def fake_client(): + """Provides a fake Kubernetes client for all tests.""" + return get_client(fake=True) +``` + +> **Tip:** Use `scope="class"` so resources created in one test method persist for subsequent methods in the same class. + +### 2. Create a resource + +```python +from ocp_resources.pod import Pod + +def test_create_pod(fake_client): + pod = Pod( + client=fake_client, + name="my-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + ) + result = pod.deploy() + + assert result.name == "my-pod" + assert pod.exists +``` + +The fake client automatically generates metadata (`uid`, `resourceVersion`, `creationTimestamp`) and a realistic `status` section for the resource. + +### 3. Read and query resources + +```python +from ocp_resources.namespace import Namespace + +def test_list_resources(fake_client): + # Create some namespaces first + Namespace(client=fake_client, name="ns-a").deploy() + Namespace(client=fake_client, name="ns-b").deploy() + + # Iterate over all Namespace resources + for ns in Namespace.get(client=fake_client): + assert ns.name +``` + +Access individual resource data via the `instance` property: + +```python +def test_get_instance(fake_client): + ns = Namespace(client=fake_client, name="test-ns") + ns.deploy() + + assert ns.instance + assert ns.kind == "Namespace" +``` + +### 4. Update a resource + +Use `update()` to patch a resource or `update_replace()` to replace it entirely: + +```python +def test_update_labels(fake_client): + ns = Namespace(client=fake_client, name="labeled-ns") + ns.deploy() + + # Patch: add a label + resource_dict = ns.instance.to_dict() + resource_dict["metadata"]["labels"]["env"] = "staging" + ns.update(resource_dict=resource_dict) + + assert ns.labels["env"] == "staging" +``` + +```python +def test_replace_labels(fake_client): + ns = Namespace(client=fake_client, name="replace-ns") + ns.deploy() + + resource_dict = ns.instance.to_dict() + resource_dict["metadata"]["labels"] = {"new-label": "value"} + ns.update_replace(resource_dict=resource_dict) + + assert "new-label" in ns.labels.keys() +``` + +### 5. Delete a resource + +```python +def test_delete_resource(fake_client): + ns = Namespace(client=fake_client, name="temp-ns") + ns.deploy() + assert ns.exists + + ns.clean_up(wait=False) + assert not ns.exists +``` + +### 6. Use context managers for automatic cleanup + +Resources support Python's `with` statement. The resource deploys on entry and is cleaned up on exit: + +```python +from ocp_resources.secret import Secret + +def test_context_manager(fake_client): + with Secret(name="my-secret", namespace="default", client=fake_client) as sec: + assert sec.exists + + # Automatically cleaned up + assert not sec.exists +``` + +## Testing Status and Conditions + +The fake client generates realistic status objects so you can test code that reads resource status. + +### Checking resource status + +```python +def test_namespace_status(fake_client): + ns = Namespace(client=fake_client, name="status-ns") + ns.deploy() + + assert ns.status == Namespace.Status.ACTIVE +``` + +### Waiting for conditions + +```python +from ocp_resources.pod import Pod + +def test_wait_for_condition(fake_client): + pod = Pod( + client=fake_client, + name="ready-pod", + namespace="default", + containers=[{"name": "app", "image": "nginx:latest"}], + ) + pod.deploy() + + pod.wait_for_status(status=Pod.Status.RUNNING, timeout=5) +``` + +### Simulating "not ready" resources + +Control a resource's readiness state with the `fake-client.io/ready` annotation: + +```python +def test_not_ready_pod(fake_client): + pod = Pod( + client=fake_client, + name="failing-pod", + namespace="default", + containers=[{"name": "app", "image": "nginx:latest"}], + annotations={"fake-client.io/ready": "false"}, + ) + pod.deploy() + + # The pod will have Ready condition set to False + pod.wait_for_condition( + condition=pod.Condition.READY, + status=pod.Condition.Status.FALSE, + timeout=5, + ) + + message = pod.get_condition_message( + condition_type=pod.Condition.READY, + condition_status=pod.Condition.Status.FALSE, + ) + assert message +``` + +Resource-specific status behavior when marked "not ready": + +| Resource | Ready behavior | Not-ready behavior | +|---|---|---| +| Pod | `phase: Running`, containers ready | Containers in `waiting` state | +| Deployment | `readyReplicas` matches `spec.replicas` | `readyReplicas: 0`, `unavailableReplicas` set | +| Namespace | `phase: Active` | `phase: Terminating` | +| All others | `Ready` condition `True` | `Ready` condition `False` | + +## Testing with Events + +The fake client automatically generates Kubernetes events for create, update, and delete operations. You can query them through the resource's `events()` method: + +```python +def test_resource_events(fake_client): + pod = Pod( + client=fake_client, + name="event-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + ) + pod.deploy() + + events = list(pod.events(timeout=1)) + assert events +``` + +## Testing with Deployments + +Deployments get a full status template including replica counts, conditions, and observed generation: + +```python +from ocp_resources.deployment import Deployment + +def test_deployment(fake_client): + dep = Deployment( + client=fake_client, + name="my-deploy", + namespace="default", + replicas=3, + selector={"matchLabels": {"app": "web"}}, + template={ + "metadata": {"labels": {"app": "web"}}, + "spec": {"containers": [{"name": "web", "image": "nginx:latest"}]}, + }, + ) + dep.deploy() + + assert dep.exists + assert dep.kind == "Deployment" + + dep.clean_up(wait=False) + assert not dep.exists +``` + +## Testing with ResourceList + +Test bulk resource creation with `ResourceList` and `NamespacedResourceList`: + +```python +from ocp_resources.resource import ResourceList, NamespacedResourceList + +def test_resource_list(fake_client): + with ResourceList( + client=fake_client, + resource_class=Namespace, + name="batch-ns", + num_resources=3, + ) as namespaces: + assert len(namespaces) == 3 + # All three namespaces are automatically cleaned up +``` + +See [Managing Bulk Resources with ResourceList](managing-resource-lists.html) for more details. + +## Advanced Usage + +### Using the low-level API directly + +You can bypass resource classes and work with the fake client's API directly — useful for testing custom logic: + +```python +def test_low_level_api(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + + # Create + pod = api.create( + body={ + "apiVersion": "v1", + "kind": "Pod", + "metadata": {"name": "raw-pod", "namespace": "default"}, + "spec": {"containers": [{"name": "app", "image": "nginx:latest"}]}, + }, + namespace="default", + ) + assert pod.metadata.name == "raw-pod" + assert pod.status.phase == "Running" + + # List + pods = api.get(namespace="default") + assert len(pods.items) >= 1 + + # Patch + updated = api.patch( + name="raw-pod", + namespace="default", + body={"metadata": {"labels": {"env": "test"}}}, + ) + assert updated.metadata.labels["env"] == "test" + + # Delete + api.delete(name="raw-pod", namespace="default") +``` + +### Label and field selectors + +The fake client supports filtering resources by labels and fields: + +```python +def test_label_selector(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + + api.create(body={ + "apiVersion": "v1", "kind": "Pod", + "metadata": {"name": "pod-a", "namespace": "default", "labels": {"app": "web"}}, + "spec": {"containers": [{"name": "c", "image": "nginx"}]}, + }, namespace="default") + + api.create(body={ + "apiVersion": "v1", "kind": "Pod", + "metadata": {"name": "pod-b", "namespace": "default", "labels": {"app": "api"}}, + "spec": {"containers": [{"name": "c", "image": "nginx"}]}, + }, namespace="default") + + # Filter by label + web_pods = api.get(namespace="default", label_selector="app=web") + assert len(web_pods.items) == 1 + assert web_pods.items[0].metadata.name == "pod-a" +``` + +Field selectors support `metadata.name` and `metadata.namespace`: + +```python +def test_field_selector(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + result = api.get(namespace="default", field_selector="metadata.name=my-pod") +``` + +### Watch for resource changes + +The `watch()` method yields existing resources as `ADDED` events: + +```python +def test_watch(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + + api.create(body={ + "apiVersion": "v1", "kind": "Pod", + "metadata": {"name": "watch-pod", "namespace": "default"}, + "spec": {"containers": [{"name": "c", "image": "nginx"}]}, + }, namespace="default") + + for event in api.watch(namespace="default"): + assert event["type"] == "ADDED" + assert event["object"].metadata.name + break # Stop after first event +``` + +### Registering Custom Resource Definitions + +To test with CRDs not bundled in the default schema, register them on the client: + +```python +def test_custom_resource(fake_client): + # Register the CRD + fake_client.register_resources({ + "kind": "MyApp", + "api_version": "v1alpha1", + "group": "apps.example.com", + "namespaced": True, + }) + + # Use it through the low-level API + api = fake_client.resources.get( + api_version="apps.example.com/v1alpha1", + kind="MyApp", + ) + + created = api.create( + body={ + "apiVersion": "apps.example.com/v1alpha1", + "kind": "MyApp", + "metadata": {"name": "my-app", "namespace": "default"}, + "spec": {"replicas": 2}, + }, + namespace="default", + ) + assert created.metadata.name == "my-app" +``` + +You can also register multiple CRDs at once: + +```python +fake_client.register_resources([ + {"kind": "MyApp", "api_version": "v1", "group": "apps.example.com"}, + {"kind": "MyConfig", "api_version": "v1beta1", "group": "config.example.com", "namespaced": False}, +]) +``` + +### Testing incremental workflows + +Use `@pytest.mark.incremental` to test a resource through its full lifecycle. Each test depends on the previous one succeeding: + +```python +import pytest +from ocp_resources.pod import Pod + +@pytest.mark.incremental +class TestPodLifecycle: + @pytest.fixture(scope="class") + def pod(self, fake_client): + return Pod( + client=fake_client, + name="lifecycle-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + ) + + def test_01_create(self, pod): + deployed = pod.deploy() + assert deployed.name == "lifecycle-pod" + assert pod.exists + + def test_02_read(self, pod): + assert pod.instance + assert pod.kind == "Pod" + + def test_03_update(self, pod): + resource_dict = pod.instance.to_dict() + resource_dict["metadata"]["labels"] = {"updated": "true"} + pod.update(resource_dict=resource_dict) + assert pod.labels["updated"] == "true" + + def test_04_delete(self, pod): + pod.clean_up(wait=False) + assert not pod.exists +``` + +> **Note:** The `@pytest.mark.incremental` marker requires custom `conftest.py` hooks. Add the following to your `conftest.py`: +> ```python +> def pytest_runtest_makereport(item, call): +> if call.excinfo is not None and "incremental" in item.keywords: +> parent = item.parent +> parent._previousfailed = item +> +> def pytest_runtest_setup(item): +> if "incremental" in item.keywords: +> previousfailed = getattr(item.parent, "_previousfailed", None) +> if previousfailed is not None: +> pytest.xfail(f"previous test failed ({previousfailed.name})") +> ``` + +### Conflict detection + +The fake client raises errors for duplicate creates and missing resources, just like a real cluster: + +```python +from fake_kubernetes_client import NotFoundError, ConflictError + +def test_conflict_on_duplicate_create(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Namespace") + + api.create(body={"metadata": {"name": "unique-ns"}}) + + with pytest.raises(ConflictError): + api.create(body={"metadata": {"name": "unique-ns"}}) + +def test_not_found_on_missing(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + + with pytest.raises(NotFoundError): + api.get(name="does-not-exist", namespace="default") +``` + +### Accessing response data + +The fake client returns response objects that support both attribute access and dictionary access: + +```python +def test_response_access(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + + created = api.create( + body={ + "apiVersion": "v1", "kind": "Pod", + "metadata": {"name": "access-pod", "namespace": "default"}, + "spec": {"containers": [{"name": "c", "image": "nginx"}]}, + }, + namespace="default", + ) + + # Attribute access + assert created.metadata.name == "access-pod" + assert created.status.phase == "Running" + + # Dictionary-style access + assert created["metadata"]["name"] == "access-pod" + + # Convert to a plain dict + data = created.to_dict() + assert isinstance(data, dict) + assert data["metadata"]["name"] == "access-pod" +``` + +## Troubleshooting + +**`ResourceNotFoundError` when creating a resource** + +The fake client must recognize the resource's API group and kind. Standard Kubernetes and OpenShift resources are pre-registered. For CRDs, call `fake_client.register_resources(...)` before using them. + +**Status fields are missing or empty** + +Pods, Deployments, Services, and Namespaces have hardcoded status templates. Other resources get a generic status with a `Ready` condition. If the resource has a schema in the bundled OpenAPI definitions, a richer status is generated automatically. + +**Tests interfere with each other** + +Each call to `get_client(fake=True)` creates an independent in-memory store. Use separate fixtures or narrower fixture scopes to isolate tests that share a client. + +## Related Pages + +- [Connecting to Clusters](connecting-to-clusters.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Querying and Watching Resources](querying-resources.html) +- [Managing Bulk Resources with ResourceList](managing-resource-lists.html) +- [Common Resource Patterns](common-patterns.html) + +--- + +Source: generating-resource-classes.md + +Let me start by exploring the repository structure and the knowledge graph. + +Now let me check the generated output example and the test generation flow: + +Now let me look at a generated file with a spec section, and check one with duplicate kinds: + +Let me also check the `--help` output structure and look at a few more details: + +Now let me check a dry-run example and verify what shell completion setup looks like: + +Now I have a thorough understanding of the codebase. Let me write the documentation: + +# Generating New Resource Classes with class-generator + +Scaffold Python wrapper classes for any Kubernetes or OpenShift custom resource definition (CRD) so you can manage those resources using the same patterns as built-in resources. The `class-generator` CLI introspects your cluster's OpenAPI schemas and produces ready-to-use Python files complete with typed constructors, docstrings, and `to_dict()` serialization. + +## Prerequisites + +- **openshift-python-wrapper** installed (see [Installing and Creating Your First Resource](quickstart.html)) +- `oc` or `kubectl` CLI available on your `PATH` +- An active connection to a Kubernetes or OpenShift cluster with admin privileges (for `--kind` generation and schema updates) + +## Quick Example + +Generate a wrapper class for the `Deployment` resource: + +```bash +class-generator -k Deployment +``` + +This creates a Python file in `ocp_resources/` with a fully typed class you can import and use immediately: + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment( + name="my-app", + namespace="default", + replicas=3, + selector={"matchLabels": {"app": "my-app"}}, + template={...}, +) +dep.deploy() +``` + +## Step-by-Step: Generating a Resource Class + +### 1. Generate the class file + +Pass the CRD's `Kind` (case-sensitive) to `--kind` / `-k`: + +```bash +class-generator -k StorageCluster +``` + +The output file is automatically named using snake_case — `ocp_resources/storage_cluster.py`. + +### 2. Preview without writing (dry run) + +See exactly what would be generated without touching the filesystem: + +```bash +class-generator -k StorageCluster --dry-run +``` + +The rendered Python code is printed to the console with syntax highlighting. + +### 3. Review the generated file + +Every generated file contains: + +- A class inheriting from `Resource` (cluster-scoped) or `NamespacedResource` (namespace-scoped) +- Typed `__init__` parameters extracted from the OpenAPI spec and `status` fields +- A `to_dict()` method that serializes required and optional fields +- A `# End of generated code` marker — any code you add **below** this line is preserved on regeneration + +Example generated output for `ConfigMap`: + +```python +from typing import Any +from ocp_resources.resource import NamespacedResource + +class ConfigMap(NamespacedResource): + """ + ConfigMap holds configuration data for pods to consume. + """ + + api_version: str = NamespacedResource.ApiVersion.V1 + + def __init__( + self, + binary_data: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + immutable: bool | None = None, + **kwargs: Any, + ) -> None: + ... +``` + +### 4. Generate multiple kinds at once + +Pass a comma-separated list (no spaces): + +```bash +class-generator -k Pod,Service,ConfigMap +``` + +Multiple kinds are generated in parallel for speed. + +### 5. Overwrite an existing file + +By default, existing files are not overwritten. Pass `--overwrite` to replace them: + +```bash +class-generator -k Deployment --overwrite +``` + +To create a timestamped backup before overwriting: + +```bash +class-generator -k Deployment --overwrite --backup +``` + +Backups are stored in `.backups/backup-YYYYMMDD-HHMMSS/` preserving the original directory structure. + +### 6. Write to a custom output path + +```bash +class-generator -k MyCustomResource -o my_package/custom_resource.py +``` + +## Handling Duplicate Kinds + +Some Kubernetes kinds exist in multiple API groups. For example, `DNS` exists in both `config.openshift.io` and `operator.openshift.io`. When this happens, the generator automatically creates separate files with group-based suffixes: + +| Kind | API Group | Generated File | +|------|-----------|----------------| +| `DNS` | `config.openshift.io` | `dns_config_openshift_io.py` | +| `DNS` | `operator.openshift.io` | `dns_operator_openshift_io.py` | + +Both files define a class named `DNS` but with different `api_group` attributes. Import the one matching the API group you need. + +## Adding User Code to Generated Files + +Any code you add **below** the `# End of generated code` marker is preserved when you regenerate the file with `--overwrite`. This is how built-in resources like `ConfigMap` include custom properties: + +```python + # End of generated code + + @property + def keys_to_hash(self): + return ["data", "binaryData"] +``` + +Custom imports you add at the top of the file are also preserved during regeneration. + +## Updating Schemas + +The generator uses a local schema cache to resolve resource definitions. If a CRD is missing (e.g., you just installed a new operator), update the cache first. + +### Full schema update + +Fetch all schemas from the connected cluster: + +```bash +class-generator --update-schema +``` + +> **Note:** If connected to an older cluster, existing schemas are preserved. Only new or missing resources are added. + +### Single-resource schema update + +Update just one resource without touching the rest: + +```bash +class-generator --update-schema-for LlamaStackDistribution +``` + +Then generate the class: + +```bash +class-generator -k LlamaStackDistribution --overwrite +``` + +> **Warning:** `--update-schema` and `--update-schema-for` are mutually exclusive. Use one or the other. + +## Discovering Missing Resources + +Find resources on your cluster that don't have wrapper classes yet: + +```bash +class-generator --discover-missing +``` + +### Coverage report + +Get a summary of how many resources have wrapper classes: + +```bash +class-generator --coverage-report +``` + +Output in JSON for CI/CD pipelines: + +```bash +class-generator --coverage-report --json +``` + +### Auto-generate all missing resources + +Update schemas and generate classes for every resource that's missing: + +```bash +class-generator --update-schema --generate-missing +``` + +> **Tip:** Use `--dry-run` with `--generate-missing` to preview what would be generated without writing any files. + +## Advanced Usage + +### Batch regeneration + +Regenerate all existing generated resource classes to pick up schema changes: + +```bash +class-generator --regenerate-all +``` + +With backups: + +```bash +class-generator --regenerate-all --backup +``` + +Filter to a subset of resources using glob patterns: + +```bash +class-generator --regenerate-all --filter "Pod*" +class-generator --regenerate-all --filter "*Service" +``` + +### Adding tests + +Generate test files alongside the resource class: + +```bash +class-generator -k Pod --add-tests +``` + +This creates test manifests and runs them automatically with `pytest`. Tests are placed under the test manifests directory and validate that the generated class can be parsed correctly. + +### Shell completion + +Add this to your `~/.bashrc` or `~/.zshrc` for tab completion: + +```bash +# For zsh +if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=zsh_source class-generator)"; fi + +# For bash +if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=bash_source class-generator)"; fi +``` + +### Verbose output + +Enable debug logging to see exactly what the generator is doing: + +```bash +class-generator -k Deployment -v +``` + +### When a kind is missing from the schema + +If the kind you request is not in the local schema cache, the CLI prompts you: + +``` +deployment not found in schema mapping, Do you want to run --update-schema and retry? [Y/N]: +``` + +Answering `y` triggers a schema update and retries generation automatically. + +> **Note:** The prompt only appears in interactive mode. When generating multiple kinds or running in batch mode, missing kinds are skipped with a warning. Run `--update-schema` separately beforehand in CI environments. + +### API group and version warnings + +If a generated resource uses an API group or version that hasn't been registered in the base `Resource` class, the generator logs a warning like: + +``` +Missing API Group in Resource +Please add `Resource.ApiGroup.MY_GROUP_IO = my-group.io` manually into +ocp_resources/resource.py under Resource class > ApiGroup class. +``` + +Follow the instructions to register the API group so the resource can connect to the correct API endpoint. See [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) for details on how `ApiGroup` and `ApiVersion` are used. + +## CLI Options Reference + +| Option | Description | +|--------|-------------| +| `-k`, `--kind` | Kind(s) to generate (comma-separated) | +| `-o`, `--output-file` | Custom output file path | +| `--overwrite` | Overwrite existing files | +| `--dry-run` | Preview output without writing files | +| `--add-tests` | Generate test files for the kind | +| `--update-schema` | Update all schema files from cluster | +| `--update-schema-for` | Update schema for a single kind | +| `--discover-missing` | Find resources without wrapper classes | +| `--coverage-report` | Show coverage statistics | +| `--json` | Output reports in JSON format | +| `--generate-missing` | Generate classes for all missing resources | +| `--regenerate-all` | Regenerate all existing generated classes | +| `--backup` | Create backup before overwriting/regenerating | +| `--filter` | Glob pattern to filter `--regenerate-all` | +| `-v`, `--verbose` | Enable debug logging | + +For the full CLI reference, see [class-generator CLI Reference](class-generator-cli.html). + +## Troubleshooting + +**"Neither 'oc' nor 'kubectl' binary found in PATH"** +Install `oc` from the [OpenShift mirror](https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/stable/) or `kubectl` from the [Kubernetes docs](https://kubernetes.io/docs/tasks/tools/). Ensure the binary is on your `PATH`. + +**"Resource kind 'X' not found"** +The kind is not in the local schema cache. Run `class-generator --update-schema` while connected to a cluster that has the CRD installed, then retry. + +**Generated filename looks wrong (e.g., `c_d_i_config.py`)** +Single-letter segments in snake_case names indicate the camelCase-to-snake_case converter doesn't recognize an acronym. The generator raises an error with instructions to add the acronym to the converter. + +**"Failed to fetch OpenAPI v3 index"** +Ensure you are logged in to the cluster with sufficient privileges. The generator needs to access the `/openapi/v3` endpoint, which typically requires cluster-admin access. + +## Related Pages + +- [class-generator CLI Reference](class-generator-cli.html) +- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) +- [Resource and NamespacedResource API](resource-api.html) +- [Validating Resources Against OpenAPI Schemas](validating-resources.html) + +--- + +Source: common-patterns.md + +Let me start by reading the knowledge graph and the pages manifest, then explore the codebase. + +Now let me look at more resource files for networking, RBAC, and DataVolume: + +Now let me look at the secret, network policy, network attachment definition, and the duplicate API groups handling: + +Now let me look at the duplicate API groups handling: + +Now let me look at some files with duplicate kinds to understand the pattern: + +Now let me look at how to exec in pods and also look at the user_defined_network and apply_yaml approach: + +Now let me look at how the `yaml_file` parameter works and look at the event-related code: + +Now let me check some gateway files to see the pattern for duplicate API groups: + +Now let me look at the `user_defined_network.py` for newer networking resources: + +Now I have enough information. Let me write the documentation page: + +# Common Resource Patterns + +Copy-paste recipes for the most common Kubernetes and OpenShift resource operations using openshift-python-wrapper. + +> **Note:** All recipes assume you already have a connected client. See [Connecting to Clusters](connecting-to-clusters.html) for setup instructions. + +```python +# Preamble used by all recipes below +from ocp_resources.resource import get_client +client = get_client() +``` + +--- + +## Create and Run a Pod + +Create a pod with a single container and wait for it to start. + +```python +from ocp_resources.pod import Pod + +pod = Pod( + client=client, + name="my-nginx", + namespace="default", + containers=[{ + "name": "nginx", + "image": "nginx:1.25", + "ports": [{"containerPort": 80}], + }], + restart_policy="Never", +) +pod.deploy() +pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120) +``` + +The `deploy()` method creates the pod on the cluster. `wait_for_status` polls until the pod reaches `Running`. Use `restart_policy="Always"` for long-running pods. + +--- + +## Create a Pod as a Context Manager + +Automatically clean up a pod when the block exits — ideal for tests. + +```python +from ocp_resources.pod import Pod + +with Pod( + client=client, + name="test-curl", + namespace="default", + containers=[{ + "name": "curl", + "image": "curlimages/curl:8.5.0", + "command": ["sleep", "3600"], + }], +) as pod: + pod.wait_for_status(status=Pod.Status.RUNNING, timeout=60) + output = pod.execute(command=["curl", "-s", "http://httpbin.org/get"]) + print(output) +# Pod is automatically deleted here +``` + +The context manager calls `deploy()` on enter and `clean_up()` on exit. Set `teardown=False` to skip automatic deletion. + +> **Tip:** See [Creating and Managing Resources](creating-and-managing-resources.html) for all lifecycle management patterns. + +--- + +## Execute a Command Inside a Pod + +Run a shell command inside an already-running pod and capture the output. + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True) +output = pod.execute(command=["cat", "/etc/nginx/nginx.conf"], timeout=30) +print(output) +``` + +`execute()` streams output via the Kubernetes exec API. Use `container="sidecar"` to target a specific container in multi-container pods. Set `ignore_rc=True` to suppress errors from non-zero exit codes. + +--- + +## Get Pod Logs + +Retrieve logs from a running or completed pod. + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True) + +# Full logs +print(pod.log()) + +# Last 50 lines +print(pod.log(tail_lines=50)) + +# Logs from a specific container +print(pod.log(container="sidecar")) +``` + +The `log()` method passes kwargs through to the Kubernetes `read_namespaced_pod_log` API, so all standard parameters like `tail_lines`, `since_seconds`, and `container` are supported. + +--- + +## Create a Deployment and Wait for Replicas + +Deploy an application with multiple replicas and wait until they are all ready. + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment( + client=client, + name="web-app", + namespace="default", + replicas=3, + selector={"matchLabels": {"app": "web-app"}}, + template={ + "metadata": {"labels": {"app": "web-app"}}, + "spec": { + "containers": [{ + "name": "app", + "image": "nginx:1.25", + "ports": [{"containerPort": 80}], + }] + }, + }, +) +dep.deploy() +dep.wait_for_replicas(deployed=True, timeout=300) +``` + +`wait_for_replicas` polls until `availableReplicas == readyReplicas == updatedReplicas == spec.replicas`. Pass `deployed=False` to wait for all replicas to be scaled down. + +--- + +## Scale a Deployment + +Change the replica count of an existing deployment. + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment(client=client, name="web-app", namespace="default", ensure_exists=True) + +# Scale up +dep.scale_replicas(replica_count=5) +dep.wait_for_replicas(deployed=True, timeout=300) + +# Scale down +dep.scale_replicas(replica_count=1) +dep.wait_for_replicas(deployed=True, timeout=300) +``` + +`scale_replicas` patches the deployment's `spec.replicas` field. Always follow with `wait_for_replicas` to confirm the rollout completes. + +--- + +## Create a Service + +Expose a deployment via a ClusterIP service. + +```python +from ocp_resources.service import Service + +svc = Service( + client=client, + name="web-app-svc", + namespace="default", + selector={"app": "web-app"}, + ports=[{ + "protocol": "TCP", + "port": 80, + "targetPort": 80, + }], + type="ClusterIP", +) +svc.deploy() +``` + +Change `type` to `"NodePort"` or `"LoadBalancer"` as needed. The `selector` must match labels on your target pods. + +--- + +## Create an OpenShift Route + +Expose a service externally via an OpenShift Route. + +```python +from ocp_resources.route import Route + +route = Route( + client=client, + name="web-app-route", + namespace="default", + service="web-app-svc", +) +route.deploy() + +# Get the assigned hostname +print(route.host) +``` + +For TLS re-encrypt routes, pass `destination_ca_cert=""`. Access the exposed service name with `route.exposed_service` and the TLS termination type with `route.termination`. + +--- + +## Create a Namespace + +Create a namespace (or use as a context manager for automatic cleanup). + +```python +from ocp_resources.namespace import Namespace + +# Simple creation +ns = Namespace(client=client, name="test-ns") +ns.deploy() + +# As a context manager (deleted on exit) +with Namespace(client=client, name="ephemeral-ns") as ns: + # ... do work in the namespace ... + pass +``` + +`Namespace` extends `Resource` (cluster-scoped), so no `namespace` parameter is needed. + +--- + +## Create a ConfigMap + +Store configuration data for pods to consume. + +```python +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap( + client=client, + name="app-config", + namespace="default", + data={ + "DATABASE_URL": "postgres://db:5432/myapp", + "LOG_LEVEL": "info", + }, +) +cm.deploy() +``` + +Use `binary_data` for non-UTF-8 content. Set `immutable=True` to prevent changes after creation. + +--- + +## Create a Secret + +Store sensitive data such as credentials. + +```python +from ocp_resources.secret import Secret + +secret = Secret( + client=client, + name="db-credentials", + namespace="default", + string_data={ + "username": "admin", + "password": "s3cur3-pa$$word", + }, + type="Opaque", +) +secret.deploy() +``` + +Use `data_dict` instead of `string_data` if your values are already base64-encoded. Secret data is automatically hashed in log output for security. + +--- + +## Create a Job + +Run a one-off task to completion. + +```python +from ocp_resources.job import Job + +job = Job( + client=client, + name="db-migration", + namespace="default", + backoff_limit=3, + restart_policy="Never", + containers=[{ + "name": "migrate", + "image": "my-app:latest", + "command": ["python", "manage.py", "migrate"], + }], +) +job.deploy() +job.wait_for_condition( + condition=Job.Condition.COMPLETE, + status=Job.Condition.Status.TRUE, + timeout=300, +) +``` + +Set `background_propagation_policy="Background"` to delete leftover pods when the job is cleaned up. The `backoff_limit` controls how many times the job retries on failure. + +--- + +## Create a PersistentVolumeClaim + +Request persistent storage for your workloads. + +```python +from ocp_resources.persistent_volume_claim import PersistentVolumeClaim + +pvc = PersistentVolumeClaim( + client=client, + name="app-data", + namespace="default", + accessmodes=PersistentVolumeClaim.AccessMode.RWO, + size="10Gi", + storage_class="gp3-csi", + volume_mode=PersistentVolumeClaim.VolumeMode.FILE, +) +pvc.deploy() +pvc.wait_for_status(status=PersistentVolumeClaim.Status.BOUND, timeout=120) +``` + +Use the `AccessMode` and `VolumeMode` constants instead of raw strings. The `storage_class` must match an available StorageClass on your cluster. + +--- + +## Create a DataVolume (KubeVirt / CDI) + +Import a VM disk image into a PVC using the Containerized Data Importer. + +```python +from ocp_resources.datavolume import DataVolume + +dv = DataVolume( + client=client, + name="fedora-disk", + namespace="default", + source_dict={"http": {"url": "https://download.fedoraproject.org/pub/fedora/linux/releases/40/Cloud/x86_64/images/Fedora-Cloud-Base-40-1.14.x86_64.qcow2"}}, + size="30Gi", + storage_class="ocs-storagecluster-ceph-rbd-virtualization", + access_modes=DataVolume.AccessMode.RWX, + volume_mode=DataVolume.VolumeMode.BLOCK, + api_name="storage", +) +dv.deploy() +dv.wait_for_dv_success(timeout=600) +``` + +Always pass `api_name="storage"` explicitly — the default will change in a future release. Use `source_dict={"blank": {}}` for empty disks, or `source_dict={"pvc": {"name": "source-pvc", "namespace": "default"}}` for cloning. + +- **Clone a DataVolume:** + ```python + dv_clone = DataVolume( + client=client, + name="fedora-disk-clone", + namespace="default", + source_dict={"pvc": {"name": "fedora-disk", "namespace": "default"}}, + size="30Gi", + storage_class="ocs-storagecluster-ceph-rbd-virtualization", + access_modes=DataVolume.AccessMode.RWX, + volume_mode=DataVolume.VolumeMode.BLOCK, + api_name="storage", + ) + ``` + +> **Tip:** See [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) for full VM lifecycle recipes. + +--- + +## Create a NetworkPolicy + +Restrict traffic to pods matching a label selector. + +```python +from ocp_resources.network_policy import NetworkPolicy + +netpol = NetworkPolicy( + client=client, + name="allow-http-only", + namespace="default", + pod_selector={"matchLabels": {"app": "web-app"}}, + policy_types=["Ingress"], + ingress=[{ + "ports": [{"protocol": "TCP", "port": 80}], + "from": [ + {"namespaceSelector": {"matchLabels": {"env": "production"}}}, + ], + }], +) +netpol.deploy() +``` + +This allows TCP port 80 ingress only from namespaces labeled `env=production`. Omit `ingress` and set `policy_types=["Ingress"]` to deny all inbound traffic. + +--- + +## Create a NetworkAttachmentDefinition + +Attach pods to a secondary network using Multus. + +```python +from ocp_resources.network_attachment_definition import LinuxBridgeNetworkAttachmentDefinition + +nad = LinuxBridgeNetworkAttachmentDefinition( + client=client, + name="br-100", + namespace="default", + bridge_name="br-100", + cni_type="cnv-bridge", + vlan=100, + mtu=1500, +) +nad.deploy() +``` + +Variant classes are available for different bridge types: +- `LinuxBridgeNetworkAttachmentDefinition` — Linux bridge (cnv-bridge) +- `OvsBridgeNetworkAttachmentDefinition` — OVS bridge +- `OVNOverlayNetworkAttachmentDefinition` — OVN overlay (layer 2/3) + +--- + +## Create an RBAC Role and RoleBinding + +Grant specific permissions to a service account within a namespace. + +```python +from ocp_resources.role import Role +from ocp_resources.role_binding import RoleBinding +from ocp_resources.service_account import ServiceAccount + +sa = ServiceAccount( + client=client, + name="app-sa", + namespace="default", +) +sa.deploy() + +role = Role( + client=client, + name="pod-reader", + namespace="default", + rules=[{ + "apiGroups": [""], + "resources": ["pods", "pods/log"], + "verbs": ["get", "list", "watch"], + }], +) +role.deploy() + +binding = RoleBinding( + client=client, + name="app-sa-pod-reader", + namespace="default", + subjects_kind="ServiceAccount", + subjects_name="app-sa", + subjects_namespace="default", + role_ref_kind="Role", + role_ref_name="pod-reader", +) +binding.deploy() +``` + +This grants the `app-sa` service account read-only access to pods in the `default` namespace. + +--- + +## Create a ClusterRole and ClusterRoleBinding + +Grant cluster-wide permissions to a service account. + +```python +from ocp_resources.cluster_role import ClusterRole +from ocp_resources.cluster_role_binding import ClusterRoleBinding + +cr = ClusterRole( + client=client, + name="node-viewer", + rules=[{ + "apiGroups": [""], + "resources": ["nodes"], + "verbs": ["get", "list", "watch"], + }], +) +cr.deploy() + +crb = ClusterRoleBinding( + client=client, + name="app-sa-node-viewer", + cluster_role="node-viewer", + subjects=[{ + "kind": "ServiceAccount", + "name": "app-sa", + "namespace": "default", + }], +) +crb.deploy() +``` + +`ClusterRole` and `ClusterRoleBinding` are cluster-scoped (`Resource` subclasses), so no `namespace` parameter is needed on the role or binding itself. + +--- + +## List and Filter Resources + +Query existing resources using label and field selectors. + +```python +from ocp_resources.pod import Pod +from ocp_resources.namespace import Namespace + +# List all pods in a namespace +for pod in Pod.get(client=client, namespace="default"): + print(f"{pod.name} — {pod.status}") + +# Filter by label +for pod in Pod.get(client=client, namespace="default", label_selector="app=web-app"): + print(pod.name) + +# Filter by field +for pod in Pod.get(client=client, namespace="default", field_selector="status.phase=Running"): + print(pod.name) + +# List cluster-scoped resources (no namespace) +for ns in Namespace.get(client=client, label_selector="env=staging"): + print(ns.name) +``` + +The `get()` class method returns a generator of resource objects. Pass `raw=True` to get raw `ResourceInstance` objects instead. + +> **Tip:** See [Querying and Watching Resources](querying-resources.html) for advanced querying patterns. + +--- + +## Create a Resource from a YAML File + +Load a resource definition from an existing YAML file. + +```python +from ocp_resources.pod import Pod + +pod = Pod( + client=client, + yaml_file="manifests/my-pod.yaml", +) +pod.deploy() +``` + +When using `yaml_file`, the `name` and `namespace` are read from the file automatically. You cannot combine `yaml_file` with `kind_dict`. + +--- + +## Create a Resource from a Dictionary + +Pass a raw Kubernetes resource dict directly. + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment( + client=client, + kind_dict={ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "from-dict-app", + "namespace": "default", + }, + "spec": { + "replicas": 2, + "selector": {"matchLabels": {"app": "from-dict"}}, + "template": { + "metadata": {"labels": {"app": "from-dict"}}, + "spec": { + "containers": [{ + "name": "app", + "image": "nginx:1.25", + }], + }, + }, + }, + }, +) +dep.deploy() +``` + +Using `kind_dict` bypasses all constructor logic in `to_dict()` — the dictionary is sent as-is to the API. This is useful when migrating existing manifests. + +--- + +## Handle Resources with Duplicate API Groups + +Some Kubernetes resource kinds (e.g., `DNS`, `Ingress`, `Gateway`) exist in multiple API groups. The wrapper provides separate modules for each variant. + +```python +# DNS from config.openshift.io (cluster DNS config) +from ocp_resources.dns_config_openshift_io import DNS as DNSConfig + +dns_config = DNSConfig(client=client, name="cluster", ensure_exists=True) +print(dns_config.instance.spec.baseDomain) + +# DNS from operator.openshift.io (CoreDNS operator) +from ocp_resources.dns_operator_openshift_io import DNS as DNSOperator + +dns_operator = DNSOperator(client=client, name="default", ensure_exists=True) +print(dns_operator.instance.spec.logLevel) +``` + +```python +# Ingress from config.openshift.io (cluster ingress config) +from ocp_resources.ingress_config_openshift_io import Ingress as IngressConfig + +ingress = IngressConfig(client=client, name="cluster", ensure_exists=True) +print(ingress.instance.spec.domain) + +# Ingress from networking.k8s.io (K8s Ingress rules) +from ocp_resources.ingress_networking_k8s_io import Ingress as K8sIngress + +k8s_ingress = K8sIngress( + client=client, + name="my-app-ingress", + rules=[{ + "host": "app.example.com", + "http": { + "paths": [{ + "path": "/", + "pathType": "Prefix", + "backend": {"service": {"name": "web-app-svc", "port": {"number": 80}}}, + }], + }, + }], +) +``` + +The naming convention for duplicate-kind modules is `_.py`. Use Python import aliases (`as`) to distinguish them in your code. + +**Common duplicate kinds and their modules:** + +| Kind | Module | API Group | +|------|--------|-----------| +| `DNS` | `dns_config_openshift_io` | `config.openshift.io` | +| `DNS` | `dns_operator_openshift_io` | `operator.openshift.io` | +| `Ingress` | `ingress_config_openshift_io` | `config.openshift.io` | +| `Ingress` | `ingress_networking_k8s_io` | `networking.k8s.io` | +| `Gateway` | `gateway` | `networking.istio.io` | +| `Gateway` | `gateway_gateway_networking_k8s_io` | `gateway.networking.k8s.io` | +| `Gateway` | `gateway_networking_istio_io` | `networking.istio.io` | + +> **Warning:** Importing the wrong module will target the wrong API group, causing `NotFoundError` or unexpected behavior. Always verify the `api_group` attribute on your resource class. + +--- + +## Manage Multiple Similar Resources with ResourceList + +Create N copies of a resource with auto-numbered names. + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import ResourceList + +with ResourceList( + resource_class=Namespace, + num_resources=3, + client=client, + name="perf-test-ns", +) as namespaces: + # Creates: perf-test-ns-1, perf-test-ns-2, perf-test-ns-3 + for ns in namespaces: + print(ns.name) +# All 3 namespaces are deleted on exit +``` + +> **Tip:** See [Managing Bulk Resources with ResourceList](managing-resource-lists.html) for `NamespacedResourceList` and other bulk patterns. + +--- + +## Deploy One Resource Per Namespace with NamespacedResourceList + +Create an identical namespaced resource in each of several namespaces. + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.config_map import ConfigMap +from ocp_resources.resource import ResourceList, NamespacedResourceList + +with ResourceList( + resource_class=Namespace, + num_resources=3, + client=client, + name="team-ns", +) as namespaces: + with NamespacedResourceList( + resource_class=ConfigMap, + namespaces=namespaces, + client=client, + name="shared-config", + data={"REGION": "us-east-1"}, + ) as configmaps: + # One ConfigMap "shared-config" in each of team-ns-1, team-ns-2, team-ns-3 + for cm in configmaps: + print(f"{cm.namespace}/{cm.name}") +``` + +`NamespacedResourceList` requires a `ResourceList` of `Namespace` objects. All resources are cleaned up in reverse order on exit. + +--- + +## Temporarily Edit a Resource with ResourceEditor + +Apply temporary patches during a test and automatically restore original values. + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.config_map import ConfigMap + +cm = ConfigMap(client=client, name="app-config", namespace="default", ensure_exists=True) + +with ResourceEditor( + patches={cm: {"data": {"LOG_LEVEL": "debug"}}}, + action="update", +): + # LOG_LEVEL is now "debug" + print(cm.instance.data.LOG_LEVEL) +# Original LOG_LEVEL is restored here +``` + +`ResourceEditor` backs up original values on enter and restores them on exit. Use `action="replace"` when you need to remove fields entirely rather than patch them. + +> **Tip:** See [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html) for complete details. + +--- + +## Validate a Resource Before Creating It + +Catch schema errors before submitting to the API server. + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ValidationError + +pod = Pod( + client=client, + name="validated-pod", + namespace="default", + containers=[{ + "name": "app", + "image": "nginx:1.25", + }], + schema_validation_enabled=True, # Auto-validate on create() +) + +# Manual validation +try: + pod.validate() + print("Resource is valid") +except ValidationError as e: + print(f"Validation failed: {e}") + +# Or validate a raw dict without instantiation +try: + Pod.validate_dict({ + "apiVersion": "v1", + "kind": "Pod", + "metadata": {"name": "test"}, + "spec": {"containers": [{"name": "app", "image": "nginx"}]}, + }) +except ValidationError as e: + print(f"Dict validation failed: {e}") +``` + +When `schema_validation_enabled=True`, `create()` and `update_replace()` automatically validate before sending to the API. Validation requires OpenAPI schemas to be available. + +> **Tip:** See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for schema setup and troubleshooting. + +--- + +## Check If a Resource Exists + +Verify a resource exists before operating on it. + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment(client=client, name="web-app", namespace="default") + +if dep.exists: + print(f"Deployment exists, status: {dep.instance.status.availableReplicas} replicas available") +else: + print("Deployment not found") +``` + +`exists` returns the resource instance if found or `None` if not. For fail-fast behavior, use `ensure_exists=True` in the constructor — it raises `ResourceNotFoundError` immediately if the resource is missing. + +--- + +## Watch Resource Events + +Stream events related to a specific resource. + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True) + +# Watch events for this pod (10 second window) +for event in pod.events(timeout=10): + ev = event["object"] + print(f"[{ev.type}] {ev.reason}: {ev.message}") +``` + +Use `field_selector` to narrow results further, for example: `field_selector="type==Warning"`. The `events()` method automatically filters by `involvedObject.name`. + +--- + +## List Recent Events in a Namespace + +Get existing events (not a watch stream) from the last N seconds. + +```python +from ocp_resources.event import Event + +# Warning events from the last 5 minutes +events = Event.list( + client=client, + namespace="default", + field_selector="type==Warning", + since_seconds=300, +) +for ev in events: + print(f"[{ev.reason}] {ev.message}") +``` + +`Event.list()` returns a sorted list (most recent first), unlike `Event.get()` which is a live watch stream. + +--- + +## Wait for a Resource Condition + +Block until a resource reaches a specific condition. + +```python +from ocp_resources.deployment import Deployment + +dep = Deployment(client=client, name="web-app", namespace="default", ensure_exists=True) + +dep.wait_for_condition( + condition="Available", + status="True", + timeout=300, +) +``` + +Use `stop_condition` to fail fast if an unrecoverable condition is detected: + +```python +dep.wait_for_condition( + condition="Available", + status="True", + timeout=300, + stop_condition="ReplicaFailure", + stop_status="True", +) +``` + +> **Tip:** See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for advanced waiting patterns. + +--- + +## Get a Resource's YAML Representation + +Dump the intended resource dict as YAML (useful for debugging). + +```python +from ocp_resources.pod import Pod + +pod = Pod( + client=client, + name="debug-pod", + namespace="default", + containers=[{"name": "app", "image": "busybox", "command": ["sleep", "3600"]}], +) +print(pod.to_yaml()) +``` + +`to_yaml()` calls `to_dict()` internally and returns the YAML string. This shows what would be sent to the API, not the live state. + +--- + +## Get the Pod's Node and IP + +Access commonly needed runtime properties of a pod. + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True) + +# Node where the pod is running +print(f"Node: {pod.node.name}") + +# Pod IP address +print(f"IP: {pod.ip}") +``` + +The `node` property returns a `Node` resource object. The `ip` property reads from `status.podIP`. + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Querying and Watching Resources](querying-resources.html) +- [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) +- [Working with Kubernetes Events](working-with-events.html) + +--- + +Source: mcp-server-integration.md + +# Integrating with AI Assistants via MCP Server + +The openshift-python-wrapper ships with a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that lets AI assistants like Cursor, Claude Desktop, and other MCP-compatible tools directly manage your OpenShift/Kubernetes cluster resources. + +## Configure the MCP Server for Cursor + +Connect your Cursor IDE to your cluster so the AI assistant can list, create, update, and delete resources on your behalf. + +```json +// ~/.cursor/mcp.json +{ + "mcpServers": { + "openshift-python-wrapper": { + "command": "openshift-mcp-server" + } + } +} +``` + +After saving, restart Cursor and reference the server with `@openshift-python-wrapper` in your prompts. The server uses your active kubeconfig context automatically. + +> **Note:** The `openshift-mcp-server` command is installed as a script entry point when you install the package. See [Installing and Creating Your First Resource](quickstart.html) for installation instructions. + +## Configure the MCP Server for Claude Desktop + +Set up Claude Desktop to interact with your cluster through natural language. + +```json +// macOS: ~/Library/Application Support/Claude/claude_desktop_config.json +// Windows: %APPDATA%\Claude\claude_desktop_config.json +// Linux: ~/.config/Claude/claude_desktop_config.json +{ + "mcpServers": { + "openshift-python-wrapper": { + "command": "openshift-mcp-server" + } + } +} +``` + +Claude Desktop will connect to the server on startup. You can then ask it to list pods, create resources, check logs, and more using natural language. + +## Configure a Custom Kubeconfig Path + +Point the MCP server at a specific kubeconfig when your default location doesn't apply. + +```json +{ + "mcpServers": { + "openshift-python-wrapper": { + "command": "openshift-mcp-server", + "env": { + "KUBECONFIG": "/home/user/.kube/my-cluster-config" + } + } + } +} +``` + +The `KUBECONFIG` environment variable is passed to the server process at startup. This is useful when managing multiple clusters or when your kubeconfig is in a non-standard location. + +> **Tip:** See [Connecting to Clusters](connecting-to-clusters.html) for all authentication methods supported by openshift-python-wrapper, including tokens, basic auth, and in-cluster config. + +## Run the MCP Server from Source (Development) + +Run the server directly from a cloned repository for development or testing. + +```bash +git clone https://github.com/RedHatQE/openshift-python-wrapper.git +cd openshift-python-wrapper +uv run mcp_server/server.py +``` + +For MCP client configuration when running from source: + +```json +{ + "mcpServers": { + "openshift-python-wrapper": { + "command": "uv", + "args": ["run", "mcp_server/server.py"], + "cwd": "/path/to/openshift-python-wrapper" + } + } +} +``` + +This bypasses the installed entry point and runs the server module directly, which is useful when testing local changes. + +## List Cluster Resources via AI Chat + +Ask your AI assistant to list resources — the MCP server exposes a `list_resources` tool that supports type filtering, namespaces, label selectors, and limits. + +``` +Prompt: "List all pods in the openshift-monitoring namespace with label app=prometheus" +``` + +The AI will call the `list_resources` tool, which maps to: + +```python +list_resources( + resource_type="pod", + namespace="openshift-monitoring", + label_selector="app=prometheus", + limit=20 +) +``` + +Returned data includes name, namespace, UID, creation timestamp, labels, phase, and conditions for each matched resource. + +> **Tip:** Use `get_resource_types` to discover all available resource types. The server dynamically scans every module in `ocp_resources/` at startup, so any resource supported by the library — including OpenShift-specific types like `route`, `virtualmachine`, or `clusterserviceversion` — is available. + +## Get Detailed Resource Information + +Retrieve a single resource with full metadata, status, and optionally the raw YAML or JSON. + +``` +Prompt: "Show me the deployment 'api-server' in namespace 'production' as YAML" +``` + +The AI calls: + +```python +get_resource( + resource_type="deployment", + name="api-server", + namespace="production", + output_format="yaml" # also accepts "json" or "info" +) +``` + +The `info` format (default) returns structured metadata plus resource-specific details: for pods you get container statuses and node placement; for deployments you get replica counts; for services you get ports and cluster IP. + +## Create Resources Through Natural Language + +Ask the AI to create resources — it can use either YAML content or structured spec parameters. + +``` +Prompt: "Create a ConfigMap called 'app-settings' in the 'staging' namespace with keys +database_host=db.staging.svc and log_level=debug" +``` + +The AI calls: + +```python +create_resource( + resource_type="configmap", + name="app-settings", + namespace="staging", + spec={"data": {"database_host": "db.staging.svc", "log_level": "debug"}}, + labels={"managed-by": "mcp-server"} +) +``` + +You can also provide full YAML: + +```python +create_resource( + resource_type="pod", + name="nginx", + yaml_content=""" +apiVersion: v1 +kind: Pod +metadata: + name: nginx + namespace: default +spec: + containers: + - name: nginx + image: nginx:1.27 + ports: + - containerPort: 80 +""" +) +``` + +> **Warning:** The MCP server creates real resources on your cluster. Make sure your AI assistant is targeting the correct cluster and namespace before confirming creation operations. + +## Apply Multi-Document YAML Manifests + +Deploy a complete application stack from a single YAML block containing multiple resources. + +``` +Prompt: "Apply this manifest to create a namespace, deployment, and service for my app" +``` + +The AI calls: + +```python +apply_yaml(yaml_content=""" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: my-app +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + namespace: my-app +spec: + replicas: 3 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + spec: + containers: + - name: api + image: myapp/backend:v1.0 + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: backend + namespace: my-app +spec: + selector: + app: backend + ports: + - port: 80 + targetPort: 8080 +""") +``` + +The response includes a summary with `total_resources`, `successful`, and `failed` counts, plus per-resource results. Each YAML document is parsed and deployed independently. + +## Update Resources with Patches + +Modify existing resources using merge or strategic-merge patches. + +``` +Prompt: "Scale the 'web-frontend' deployment in 'production' to 5 replicas" +``` + +The AI calls: + +```python +update_resource( + resource_type="deployment", + name="web-frontend", + namespace="production", + patch={"spec": {"replicas": 5}}, + patch_type="merge" # or "strategic" +) +``` + +The `patch_type` parameter controls the content type: `"merge"` uses `application/merge-patch+json`, while `"strategic"` uses `application/strategic-merge-patch+json`. + +## Delete Resources + +Remove resources with optional wait-for-deletion behavior. + +``` +Prompt: "Delete the pod 'debug-pod' in namespace 'default' and wait for it to be gone" +``` + +The AI calls: + +```python +delete_resource( + resource_type="pod", + name="debug-pod", + namespace="default", + wait=True, + timeout=60 +) +``` + +If the resource doesn't exist, the server returns a success response with a warning rather than an error, making the operation idempotent. + +## Retrieve Pod Logs + +Fetch logs from running or crashed pods with filtering options. + +``` +Prompt: "Show me the last 200 lines of logs from pod 'api-server-7f8b9c' container 'app' +in namespace 'production'" +``` + +The AI calls: + +```python +get_pod_logs( + name="api-server-7f8b9c", + namespace="production", + container="app", + tail_lines=200 +) +``` + +- Use `since_seconds=3600` to get logs from the last hour +- Use `previous=True` to get logs from a crashed container's previous instance +- Omit `container` for single-container pods + +## Execute Commands in Pods + +Run diagnostic commands inside a running pod container. + +``` +Prompt: "Check the nginx config in pod 'web-server' namespace 'production'" +``` + +The AI calls: + +```python +exec_in_pod( + name="web-server", + namespace="production", + command=["nginx", "-t"], + container="nginx" +) +``` + +The response includes `stdout`, `stderr`, and `returncode`. If the command fails, the exit code and error output are captured rather than raising an exception. + +> **Warning:** Pod exec gives shell-level access to your containers. Ensure your RBAC policies restrict which service accounts and users can perform `exec` operations. + +## Get Resource Events + +Retrieve Kubernetes events related to a specific resource for troubleshooting. + +``` +Prompt: "Show me the events for pod 'crashloop-pod' in 'default' namespace" +``` + +The AI calls: + +```python +get_resource_events( + resource_type="pod", + name="crashloop-pod", + namespace="default", + limit=10 +) +``` + +Events are filtered using field selectors on `involvedObject.name`, `involvedObject.namespace`, and `involvedObject.kind`. Each event includes type (Normal/Warning), reason, message, count, timestamps, and source component. + +## Troubleshoot a Failing Pod (End-to-End Workflow) + +Combine multiple MCP tools in sequence to diagnose pod issues — the AI assistant will chain these calls automatically. + +``` +Prompt: "Pod 'payment-svc-6d4f8' in namespace 'checkout' keeps crashing. Help me figure out why." +``` + +The AI will typically execute this sequence: + +1. **Check status:** `get_resource(resource_type="pod", name="payment-svc-6d4f8", namespace="checkout")` — gets phase, container statuses, restart count +2. **Review events:** `get_resource_events(resource_type="pod", name="payment-svc-6d4f8", namespace="checkout")` — shows scheduling, pulling, crash events +3. **Read current logs:** `get_pod_logs(name="payment-svc-6d4f8", namespace="checkout", tail_lines=100)` — application output before crash +4. **Read previous crash logs:** `get_pod_logs(name="payment-svc-6d4f8", namespace="checkout", previous=True)` — logs from the last terminated container +5. **Inspect config:** `exec_in_pod(name="payment-svc-6d4f8", namespace="checkout", command=["cat", "/etc/app/config.yaml"])` — verify mounted configuration + +The AI then synthesizes all the data into a diagnosis with suggested fixes. + +## Write a Programmatic MCP Client + +Connect to the MCP server programmatically using `FastMCPClient` for automation scripts. + +```python +import asyncio +from fastmcp import FastMCPClient + + +async def main(): + async with FastMCPClient() as client: + await client.connect_stdio(cmd=["openshift-mcp-server"]) + + # Discover available resource types + types = await client.call_tool( + name="get_resource_types", + arguments={"random_string": "x"}, + ) + print(f"Available types: {types['total_count']}") + + # List pods in a namespace + pods = await client.call_tool( + name="list_resources", + arguments={"resource_type": "pod", "namespace": "default", "limit": 5}, + ) + for pod in pods["resources"]: + print(f" {pod['name']} — {pod.get('phase', 'Unknown')}") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +This uses the same stdio transport that AI assistants use. The `FastMCPClient` is provided by the `fastmcp` library, which is already a dependency of openshift-python-wrapper. + +## Debug MCP Server Issues + +Enable debug logging and inspect the server log file when tools aren't behaving as expected. + +```bash +# Check the MCP server log file +tail -f /tmp/mcp_server_debug.log +``` + +```bash +# Run with debug-level logging from the simple-logger +SIMPLE_LOGGER_LEVEL=DEBUG openshift-mcp-server +``` + +The server writes debug logs to `/tmp/mcp_server_debug.log` by default, including every resource scan at startup, client creation events, and detailed error tracebacks. + +- Verify cluster connectivity independently: `kubectl cluster-info` +- Check your permissions: `kubectl auth can-i --list` +- Confirm the entry point is installed: `which openshift-mcp-server` + +> **Tip:** See [Environment Variables and Configuration](environment-variables.html) for other environment variables that affect library behavior. + +## Available MCP Tools Reference + +The MCP server exposes ten tools. All resource type names are case-insensitive (e.g., `"Pod"`, `"pod"`, and `"POD"` all work). + +| Tool | Purpose | Required Parameters | +|------|---------|-------------------| +| `list_resources` | List resources by type with optional filters | `resource_type` | +| `get_resource` | Get a single resource by name | `resource_type`, `name` | +| `create_resource` | Create a resource from spec or YAML | `resource_type`, `name`, plus `spec` or `yaml_content` | +| `update_resource` | Patch an existing resource | `resource_type`, `name`, `patch` | +| `delete_resource` | Delete a resource | `resource_type`, `name` | +| `get_pod_logs` | Retrieve pod container logs | `name`, `namespace` | +| `exec_in_pod` | Execute a command in a pod | `name`, `namespace`, `command` | +| `get_resource_events` | Get events for a resource | `resource_type`, `name` | +| `apply_yaml` | Apply one or more YAML documents | `yaml_content` | +| `get_resource_types` | List all discovered resource types | `random_string` (any value) | + +> **Note:** Namespaced resources (pods, deployments, configmaps, etc.) require a `namespace` parameter. Cluster-scoped resources (nodes, namespaces, cluster roles) do not. The server validates this automatically and returns a clear error if a namespace is missing. + +## Related Pages + +- [Connecting to Clusters](connecting-to-clusters.html) +- [Installing and Creating Your First Resource](quickstart.html) +- [Environment Variables and Configuration](environment-variables.html) +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) +- [Querying and Watching Resources](querying-resources.html) + +--- + +Source: resource-api.md + +Let me start by reading the knowledge graph and the pages manifest, then explore the source code. + +Now let me examine the constants and exceptions: + +Now let me check the `apply_yaml` function and the `get_client` function signature, plus see what `ResourceConstants` inherits: + +Let me look for key public methods in resource.py that I might have missed (the file was truncated before): + +Let me also check for import paths: + +Now let me look at the `NamespacedResource._base_body` more carefully and check for anything more: + +Now let me check what the `Event` class looks like for the events method reference: + +Let me also check for `generate_yaml_from_template`: + +That's a test utility. Let me check for the `skip_existing_resource_creation_teardown` function: + +Now I have all the information needed. Let me write the comprehensive API reference page. + +# Resource and NamespacedResource API + +Complete API reference for the `Resource` and `NamespacedResource` base classes, the `get_client` factory function, and supporting classes (`ResourceEditor`, `ResourceList`, `NamespacedResourceList`, `KubeAPIVersion`). + +**Import path:** + +```python +from ocp_resources.resource import ( + Resource, + NamespacedResource, + ResourceEditor, + ResourceList, + NamespacedResourceList, + get_client, +) +``` + +--- + +## `get_client` + +Factory function to obtain a Kubernetes `DynamicClient`. + +```python +from ocp_resources.resource import get_client + +client = get_client(config_file="~/.kube/config") +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `config_file` | `str \| None` | `None` | Path to a kubeconfig file. Falls back to `KUBECONFIG` env var or `~/.kube/config`. | +| `config_dict` | `dict[str, Any] \| None` | `None` | Dict with kubeconfig configuration. Mutually exclusive with `config_file`. | +| `context` | `str \| None` | `None` | Name of the kubeconfig context to use. | +| `client_configuration` | `kubernetes.client.Configuration \| None` | `None` | Pre-built Kubernetes client configuration object. | +| `persist_config` | `bool` | `True` | Whether to persist config file changes. | +| `temp_file_path` | `str \| None` | `None` | Path to a temporary kubeconfig file. | +| `try_refresh_token` | `bool` | `True` | Try to refresh the authentication token. | +| `username` | `str \| None` | `None` | Username for basic auth. Requires `password` and `host`. | +| `password` | `str \| None` | `None` | Password for basic auth. Requires `username` and `host`. | +| `host` | `str \| None` | `None` | Cluster host URL. | +| `verify_ssl` | `bool \| None` | `None` | Whether to verify SSL certificates. | +| `token` | `str \| None` | `None` | Bearer token for authentication. Requires `host`. | +| `fake` | `bool` | `False` | Return a `FakeDynamicClient` for testing without a cluster. | +| `generate_kubeconfig` | `bool` | `False` | Save the resolved kubeconfig to a temp file and attach the path to the client. | + +**Returns:** `DynamicClient | FakeDynamicClient` + +```python +# Default kubeconfig +client = get_client() + +# With explicit config file and context +client = get_client(config_file="/path/to/kubeconfig", context="my-cluster") + +# Token-based auth +client = get_client(host="https://api.cluster.example.com:6443", token="sha256~abc123") + +# Basic auth +client = get_client(host="https://api.cluster.example.com:6443", username="admin", password="secret") + +# Fake client for unit testing +client = get_client(fake=True) +``` + +> **Note:** If neither `config_file` nor `config_dict` is provided, the client falls back to the `KUBECONFIG` environment variable, then `~/.kube/config`, and finally in-cluster configuration. + +See [Connecting to Clusters](connecting-to-clusters.html) for connection patterns. See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for `fake=True` usage. + +--- + +## `Resource` + +```python +from ocp_resources.resource import Resource +``` + +Base class for **cluster-scoped** Kubernetes/OpenShift resources (e.g., `Namespace`, `Node`, `ClusterRole`). Inherits from `ResourceConstants`. All concrete resource classes inherit from either `Resource` or `NamespacedResource`. + +See [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) for the inheritance model. + +### Class Attributes + +| Attribute | Type | Default | Description | +|---|---|---|---| +| `api_group` | `str` | `""` | API group for the resource (e.g., `"apps"`, `"batch"`). | +| `api_version` | `str` | `""` | Full API version string (e.g., `"v1"`, `"apps/v1"`). Resolved automatically if `api_group` is set. | +| `singular_name` | `str` | `""` | Singular resource name for API calls. Used to disambiguate when multiple resources match the same kind. | +| `timeout_seconds` | `int` | `60` | Default timeout for API list/watch operations. | + +### Constructor + +```python +Resource( + client=client, + name="my-resource", +) +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `client` | `DynamicClient \| None` | `None` | Kubernetes dynamic client. Will be mandatory in the next major release. | +| `name` | `str \| None` | `None` | Resource name. Required unless `yaml_file` or `kind_dict` is provided. | +| `teardown` | `bool` | `True` | Whether this resource should be deleted when used as a context manager. | +| `yaml_file` | `str \| None` | `None` | Path to a YAML file defining the resource. Mutually exclusive with `kind_dict`. | +| `delete_timeout` | `int` | `240` | Timeout in seconds for delete operations. | +| `dry_run` | `bool` | `False` | If `True`, create operations use dry-run mode. | +| `node_selector` | `dict[str, Any] \| None` | `None` | Node selector for scheduling. | +| `node_selector_labels` | `dict[str, str] \| None` | `None` | Node selector labels for scheduling. | +| `config_file` | `str \| None` | `None` | Path to kubeconfig. Deprecated; pass `client` instead. | +| `config_dict` | `dict[str, Any] \| None` | `None` | Kubeconfig dict. | +| `context` | `str \| None` | `None` | Kubeconfig context name. Deprecated; pass `client` instead. | +| `label` | `dict[str, str] \| None` | `None` | Labels to set on the resource. | +| `annotations` | `dict[str, str] \| None` | `None` | Annotations to set on the resource. | +| `api_group` | `str` | `""` | Override the class-level `api_group`. | +| `hash_log_data` | `bool` | `True` | Hash sensitive fields (defined by `keys_to_hash`) in log output. | +| `ensure_exists` | `bool` | `False` | Check that the resource already exists on the cluster at init time. Raises `ResourceNotFoundError` if not. | +| `kind_dict` | `dict[Any, Any] \| None` | `None` | Complete resource dict. Mutually exclusive with `yaml_file`. Bypasses `to_dict()` logic. | +| `wait_for_resource` | `bool` | `False` | When used as a context manager, wait for the resource to exist after creation. | +| `schema_validation_enabled` | `bool` | `False` | Enable automatic OpenAPI schema validation on `create()` and `update_replace()`. | + +**Raises:** + +| Exception | Condition | +|---|---| +| `ValueError` | Both `yaml_file` and `kind_dict` are provided. | +| `NotImplementedError` | Neither `api_group` nor `api_version` is defined on the class. | +| `MissingRequiredArgumentError` | None of `name`, `yaml_file`, or `kind_dict` is provided. | +| `ResourceNotFoundError` | `ensure_exists=True` and the resource does not exist. | + +```python +from ocp_resources.namespace import Namespace + +# Basic creation +ns = Namespace(client=client, name="my-namespace") + +# From a YAML file +ns = Namespace(client=client, yaml_file="namespace.yaml") + +# From a dict +ns = Namespace(client=client, kind_dict={"apiVersion": "v1", "kind": "Namespace", "metadata": {"name": "test"}}) + +# With labels and annotations +ns = Namespace(client=client, name="my-ns", label={"env": "test"}, annotations={"owner": "team-a"}) + +# Verify it already exists +ns = Namespace(client=client, name="default", ensure_exists=True) +``` + +--- + +### Context Manager Protocol + +`Resource` supports the Python context manager protocol for automatic creation and cleanup. + +```python +from ocp_resources.namespace import Namespace + +with Namespace(client=client, name="temp-ns") as ns: + # Resource is created on __enter__ + print(ns.name) +# Resource is deleted on __exit__ (if teardown=True) +``` + +| Method | Description | +|---|---| +| `__enter__()` | Calls `deploy(wait=self.wait_for_resource)`. Registers a `SIGINT` handler on the main thread to ensure cleanup on Ctrl+C. Returns `self`. | +| `__exit__(...)` | Calls `clean_up()` if `teardown=True`. Raises `ResourceTeardownError` if cleanup fails. | + +> **Tip:** Set `teardown=False` to prevent automatic deletion on context exit. + +--- + +### CRUD Methods + +#### `create` + +```python +def create( + self, + wait: bool = False, + exceptions_dict: dict[type[Exception], list[str]] = ..., +) -> ResourceInstance | None +``` + +Create the resource on the cluster. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `wait` | `bool` | `False` | Wait for the resource to exist after creation. | +| `exceptions_dict` | `dict[type[Exception], list[str]]` | `DEFAULT_CLUSTER_RETRY_EXCEPTIONS \| PROTOCOL_ERROR_EXCEPTION_DICT` | Exceptions to retry on. | + +**Returns:** `ResourceInstance | None` + +**Raises:** `ValidationError` if `schema_validation_enabled=True` and the resource dict is invalid. + +```python +ns = Namespace(client=client, name="my-ns") +ns.create(wait=True) +``` + +#### `delete` + +```python +def delete( + self, + wait: bool = False, + timeout: int = 240, + body: dict[str, Any] | None = None, +) -> bool +``` + +Delete the resource from the cluster. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `wait` | `bool` | `False` | Wait for the resource to be fully deleted. | +| `timeout` | `int` | `240` | Timeout in seconds when `wait=True`. | +| `body` | `dict[str, Any] \| None` | `None` | Optional delete options body. | + +**Returns:** `bool` — `True` if deleted or not found; `False` only if wait timed out. + +```python +ns.delete(wait=True, timeout=120) +``` + +#### `update` + +```python +def update(self, resource_dict: dict[str, Any]) -> None +``` + +Patch the resource with a merge-patch (`application/merge-patch+json`). Only updates the fields present in `resource_dict`. + +| Parameter | Type | Description | +|---|---|---| +| `resource_dict` | `dict[str, Any]` | Partial resource dictionary with fields to update. | + +> **Note:** Schema validation is **not** applied on `update()` because patches are partial and would fail full-schema validation. + +```python +ns.update(resource_dict={"metadata": {"labels": {"env": "staging"}}}) +``` + +#### `update_replace` + +```python +def update_replace(self, resource_dict: dict[str, Any]) -> None +``` + +Replace the full resource. Use this to **remove** existing fields (unlike `update()`, which only adds/modifies). + +| Parameter | Type | Description | +|---|---|---| +| `resource_dict` | `dict[str, Any]` | Complete resource dictionary to replace with. | + +**Raises:** `ValidationError` if `schema_validation_enabled=True` and the dict is invalid. + +```python +full_dict = ns.instance.to_dict() +full_dict["metadata"]["labels"] = {"new-label": "only"} +ns.update_replace(resource_dict=full_dict) +``` + +--- + +### Deploy / Clean Up + +#### `deploy` + +```python +def deploy(self, wait: bool = False) -> Self +``` + +Create the resource (respects `REUSE_IF_RESOURCE_EXISTS` environment variable). + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `wait` | `bool` | `False` | Wait for the resource after creation. | + +**Returns:** `Self` + +See [Environment Variables and Configuration](environment-variables.html) for `REUSE_IF_RESOURCE_EXISTS` details. + +#### `clean_up` + +```python +def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool +``` + +Delete the resource (respects `SKIP_RESOURCE_TEARDOWN` environment variable). + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `wait` | `bool` | `True` | Wait for deletion to complete. | +| `timeout` | `int \| None` | `None` | Timeout in seconds. Defaults to `delete_timeout`. | + +**Returns:** `bool` — `True` if deleted successfully. + +See [Environment Variables and Configuration](environment-variables.html) for `SKIP_RESOURCE_TEARDOWN` details. + +--- + +### Query Methods and Properties + +#### `exists` + +```python +@property +def exists(self) -> ResourceInstance | None +``` + +Check if the resource exists on the cluster. + +**Returns:** `ResourceInstance` if found, `None` if not. + +```python +if ns.exists: + print("Namespace exists") +``` + +#### `instance` + +```python +@property +def instance(self) -> ResourceInstance +``` + +Get the live resource instance from the cluster. Retries on transient cluster errors. + +**Returns:** `ResourceInstance` + +**Raises:** `NotFoundError` if the resource does not exist. + +```python +resource_version = ns.instance.metadata.resourceVersion +``` + +#### `status` + +```python +@property +def status(self) -> str +``` + +Get `status.phase` from the resource instance. + +**Returns:** `str` — The status phase string (e.g., `"Running"`, `"Active"`, `"Pending"`). + +```python +print(ns.status) # "Active" +``` + +#### `labels` + +```python +@property +def labels(self) -> ResourceField +``` + +Get resource labels from `metadata.labels`. + +**Returns:** `ResourceField` + +```python +for key, value in ns.labels.items(): + print(f"{key}={value}") +``` + +#### `kind` + +```python +@ClassProperty +def kind(cls) -> str | None +``` + +Get the resource kind name derived from the class hierarchy. This is a **class-level property** — accessible on both the class and instances. + +**Returns:** `str | None` + +```python +from ocp_resources.namespace import Namespace +print(Namespace.kind) # "Namespace" +``` + +#### `api` + +```python +@property +def api(self) -> ResourceInstance +``` + +Get the resource API object (shortcut for `full_api()` with no extra kwargs). + +**Returns:** `ResourceInstance` + +#### `full_api` + +```python +def full_api(self, **kwargs: Any) -> ResourceInstance +``` + +Get the resource API object with optional filtering kwargs. + +| Keyword Argument | Description | +|---|---| +| `pretty` | Pretty-print output. | +| `_continue` | Continuation token for list pagination. | +| `field_selector` | Filter by field. | +| `label_selector` | Filter by label. | +| `limit` | Maximum number of results. | +| `resource_version` | Filter by resource version. | +| `timeout_seconds` | Request timeout. | +| `watch` | Enable watch mode. | + +**Returns:** `ResourceInstance` + +--- + +### Class Method: `get` + +```python +@classmethod +def get( + cls, + client: DynamicClient | None = None, + dyn_client: DynamicClient | None = None, + config_file: str = "", + singular_name: str = "", + exceptions_dict: dict[type[Exception], list[str]] = ..., + raw: bool = False, + context: str | None = None, + *args: Any, + **kwargs: Any, +) -> Generator[Any, None, None] +``` + +List resources of this kind from the cluster. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `client` | `DynamicClient \| None` | `None` | Kubernetes client. | +| `dyn_client` | `DynamicClient \| None` | `None` | Deprecated alias for `client`. | +| `config_file` | `str` | `""` | Path to kubeconfig. Deprecated; pass `client`. | +| `singular_name` | `str` | `""` | Singular resource name for disambiguation. | +| `exceptions_dict` | `dict` | `DEFAULT_CLUSTER_RETRY_EXCEPTIONS` | Exceptions to retry. | +| `raw` | `bool` | `False` | If `True`, yield raw `ResourceInstance` objects instead of wrapper instances. | +| `context` | `str \| None` | `None` | Kubeconfig context. Deprecated; pass `client`. | +| `**kwargs` | | | Passed through to the API (e.g., `label_selector`, `field_selector`). | + +**Returns:** `Generator` of resource instances. + +> **Note:** For `Resource` (cluster-scoped), yielded objects are constructed with `name` only. For `NamespacedResource`, yielded objects include both `name` and `namespace`. + +```python +from ocp_resources.namespace import Namespace + +for ns in Namespace.get(client=client): + print(ns.name) + +# With label selector +for ns in Namespace.get(client=client, label_selector="env=production"): + print(ns.name) + +# Raw mode +for raw_ns in Namespace.get(client=client, raw=True): + print(raw_ns.metadata.name, raw_ns.status.phase) +``` + +See [Querying and Watching Resources](querying-resources.html) for advanced list/filter patterns. + +--- + +### Static Method: `get_all_cluster_resources` + +```python +@staticmethod +def get_all_cluster_resources( + client: DynamicClient | None = None, + config_file: str = "", + context: str | None = None, + config_dict: dict[str, Any] | None = None, + *args: Any, + **kwargs: Any, +) -> Generator[ResourceField, None, None] +``` + +Yield all resources across **all** API groups in the cluster. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `client` | `DynamicClient \| None` | `None` | Kubernetes client. | +| `**kwargs` | | | Passed to the API (e.g., `label_selector`). | + +**Yields:** `ResourceField` + +```python +for resource in Resource.get_all_cluster_resources(client=client, label_selector="app=myapp"): + print(f"{resource.kind}: {resource.metadata.name}") +``` + +--- + +### Wait Methods + +#### `wait` + +```python +def wait(self, timeout: int = 240, sleep: int = 1) -> None +``` + +Wait until the resource exists on the cluster. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `timeout` | `int` | `240` | Maximum wait time in seconds. | +| `sleep` | `int` | `1` | Sleep interval between retries. | + +**Raises:** `TimeoutExpiredError` if the resource does not exist within the timeout. + +#### `wait_deleted` + +```python +def wait_deleted(self, timeout: int = 240) -> bool +``` + +Wait until the resource no longer exists. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `timeout` | `int` | `240` | Maximum wait time in seconds. | + +**Returns:** `bool` — `True` if deleted, `False` if timed out. + +#### `wait_for_status` + +```python +def wait_for_status( + self, + status: str, + timeout: int = 240, + stop_status: str | None = None, + sleep: int = 1, + exceptions_dict: dict[type[Exception], list[str]] = ..., +) -> None +``` + +Wait for `status.phase` to reach the expected value. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `status` | `str` | — | Expected status phase string. | +| `timeout` | `int` | `240` | Maximum wait time in seconds. | +| `stop_status` | `str \| None` | `None` | Stop and fail immediately if this status is reached. Defaults to `Status.FAILED`. | +| `sleep` | `int` | `1` | Sleep interval between retries. | +| `exceptions_dict` | `dict` | `PROTOCOL_ERROR_EXCEPTION_DICT \| DEFAULT_CLUSTER_RETRY_EXCEPTIONS` | Exceptions to retry. | + +**Raises:** `TimeoutExpiredError` if the desired status is not reached. + +```python +from ocp_resources.pod import Pod + +pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120) +``` + +#### `wait_for_condition` + +```python +def wait_for_condition( + self, + condition: str, + status: str, + timeout: int = 300, + sleep_time: int = 1, + reason: str | None = None, + message: str = "", + stop_condition: str | None = None, + stop_status: str = "True", +) -> None +``` + +Wait for a specific condition to reach the desired state. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `condition` | `str` | — | Condition type name (e.g., `"Ready"`, `"Available"`). | +| `status` | `str` | — | Expected condition status (e.g., `"True"`, `"False"`). | +| `timeout` | `int` | `300` | Maximum wait time in seconds. | +| `sleep_time` | `int` | `1` | Interval between retries. | +| `reason` | `str \| None` | `None` | Expected condition reason. If set, must match exactly. | +| `message` | `str` | `""` | Expected substring within the condition message. | +| `stop_condition` | `str \| None` | `None` | Condition type that, if matched, stops the wait and fails immediately. | +| `stop_status` | `str` | `"True"` | Status for `stop_condition` matching. | + +**Raises:** + +| Exception | Condition | +|---|---| +| `TimeoutExpiredError` | Condition not met within timeout. | +| `ConditionError` | `stop_condition` is detected with matching `stop_status`. | + +```python +ns.wait_for_condition( + condition="Ready", + status="True", + timeout=60, +) +``` + +See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for more patterns. + +#### `wait_for_conditions` + +```python +def wait_for_conditions(self) -> None +``` + +Wait for the resource to have any conditions populated in its status. Uses a 30-second timeout. + +--- + +### Serialization + +#### `to_dict` + +```python +def to_dict(self) -> None +``` + +Populate `self.res` with the intended dict representation of the resource. Called automatically before `create()`. Override this in subclasses to add resource-specific fields. + +> **Note:** If `kind_dict` or `yaml_file` was provided, `to_dict()` uses those directly instead of building from individual parameters. + +#### `to_yaml` + +```python +def to_yaml(self) -> str +``` + +**Returns:** `str` — YAML string representation of the resource dict. + +```python +print(ns.to_yaml()) +``` + +--- + +### Watch / Events + +#### `watcher` + +```python +def watcher( + self, + timeout: int, + resource_version: str = "", +) -> Generator[dict[str, Any], None, None] +``` + +Watch for changes to this specific resource. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `timeout` | `int` | — | Watch duration in seconds. | +| `resource_version` | `str` | `""` | Only return events after this version. Defaults to the version at resource creation time. | + +**Yields:** Event dicts with keys `type` (`"ADDED"`, `"MODIFIED"`, `"DELETED"`), `raw_object`, and `object`. + +```python +for event in ns.watcher(timeout=30): + print(event["type"], event["object"].metadata.name) +``` + +#### `events` + +```python +def events( + self, + name: str = "", + label_selector: str = "", + field_selector: str = "", + resource_version: str = "", + timeout: int = 240, +) -> Generator[Any, Any, None] +``` + +Get Kubernetes events related to this resource. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `name` | `str` | `""` | Filter by event name. | +| `label_selector` | `str` | `""` | Comma-separated `key=value` label filters. | +| `field_selector` | `str` | `""` | Additional field selectors (auto-prefixed with `involvedObject.name==`). | +| `resource_version` | `str` | `""` | Filter events by resource version. | +| `timeout` | `int` | `240` | Timeout in seconds. | + +**Yields:** `Event` objects. + +```python +for event in pod.events(field_selector="type==Warning", timeout=10): + print(event.object) +``` + +--- + +### Validation + +#### `validate` + +```python +def validate(self) -> None +``` + +Validate `self.res` against the OpenAPI schema for this resource kind. Called automatically during `create()` and `update_replace()` when `schema_validation_enabled=True`. + +**Raises:** `ValidationError` if the resource dict is invalid. + +```python +pod = Pod(client=client, name="test", namespace="default") +pod.to_dict() +pod.validate() # Raises ValidationError on invalid spec +``` + +#### `validate_dict` (classmethod) + +```python +@classmethod +def validate_dict(cls, resource_dict: dict[str, Any]) -> None +``` + +Validate an arbitrary resource dictionary against the schema without creating a resource instance. + +| Parameter | Type | Description | +|---|---|---| +| `resource_dict` | `dict[str, Any]` | Complete resource dictionary. | + +**Raises:** `ValidationError` if validation fails. + +```python +from ocp_resources.pod import Pod + +Pod.validate_dict({ + "apiVersion": "v1", + "kind": "Pod", + "metadata": {"name": "test"}, + "spec": {"containers": [{"name": "web", "image": "nginx"}]}, +}) +``` + +See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for full validation guide. + +--- + +### API Request + +#### `api_request` + +```python +def api_request( + self, + method: str, + action: str, + url: str, + retry_params: dict[str, int] | None = None, + **params: Any, +) -> dict[str, Any] +``` + +Send a raw HTTP request to the resource's API endpoint. Used internally for subresource actions (e.g., VirtualMachine start/stop). + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `method` | `str` | — | HTTP method (`"GET"`, `"PUT"`, `"POST"`, etc.). | +| `action` | `str` | — | Subresource action to append to the URL (e.g., `"start"`, `"stop"`). | +| `url` | `str` | — | Base URL of the resource. | +| `retry_params` | `dict[str, int] \| None` | `None` | Dict with `timeout` and `sleep_time` keys for retry behavior. | +| `**params` | | | Additional params passed to the HTTP request. | + +**Returns:** `dict[str, Any]` — Parsed JSON response, or raw response data if not valid JSON. + +--- + +### Utility Methods + +#### `retry_cluster_exceptions` (static) + +```python +@staticmethod +def retry_cluster_exceptions( + func: Callable, + exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS, + timeout: int = 10, + sleep_time: int = 1, + **kwargs: Any, +) -> Any +``` + +Retry a callable on transient cluster errors. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `func` | `Callable` | — | Function to call. | +| `exceptions_dict` | `dict` | `DEFAULT_CLUSTER_RETRY_EXCEPTIONS` | Map of exception types to message substrings to match. | +| `timeout` | `int` | `10` | Total retry timeout in seconds. | +| `sleep_time` | `int` | `1` | Sleep between retries. | +| `**kwargs` | | | Passed to `func`. | + +**Returns:** The return value of `func`. + +**Raises:** The last exception if timeout is reached. + +#### `get_condition_message` + +```python +def get_condition_message( + self, + condition_type: str, + condition_status: str = "", +) -> str +``` + +Get the message for a specific condition. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `condition_type` | `str` | — | Condition type name. | +| `condition_status` | `str` | `""` | If set, only return the message when the condition status matches. | + +**Returns:** `str` — The condition message, or `""` if not found or status doesn't match. + +#### `hash_resource_dict` + +```python +def hash_resource_dict(self, resource_dict: dict[Any, Any]) -> dict[Any, Any] +``` + +Replace sensitive fields (defined by `keys_to_hash`) with `"*******"` for logging. + +| Parameter | Type | Description | +|---|---|---| +| `resource_dict` | `dict` | The resource dict to hash. | + +**Returns:** `dict` — A copy with sensitive values replaced. + +> **Tip:** Override the `keys_to_hash` property in subclasses to define which fields to mask. + +#### `keys_to_hash` + +```python +@property +def keys_to_hash(self) -> list[str] +``` + +List of key paths to mask in logs. Uses `>` as separator, `[]` for list elements. + +**Returns:** `list[str]` — Default: `[]` (no hashing). + +```python +# Example override in a Secret subclass: +@property +def keys_to_hash(self): + return ["data", "stringData"] +``` + +--- + +### Inner Classes + +#### `Resource.Status` + +Pre-defined status phase constants (inherited from `ResourceConstants`). + +| Constant | Value | +|---|---| +| `Status.SUCCEEDED` | `"Succeeded"` | +| `Status.FAILED` | `"Failed"` | +| `Status.DELETING` | `"Deleting"` | +| `Status.DEPLOYED` | `"Deployed"` | +| `Status.PENDING` | `"Pending"` | +| `Status.COMPLETED` | `"Completed"` | +| `Status.RUNNING` | `"Running"` | +| `Status.READY` | `"Ready"` | +| `Status.TERMINATING` | `"Terminating"` | +| `Status.ERROR` | `"Error"` | +| `Status.ACTIVE` | `"Active"` | +| `Status.SCHEDULING` | `"Scheduling"` | +| `Status.CRASH_LOOPBACK_OFF` | `"CrashLoopBackOff"` | +| `Status.IMAGE_PULL_BACK_OFF` | `"ImagePullBackOff"` | + +```python +pod.wait_for_status(status=Pod.Status.RUNNING) +``` + +#### `Resource.Condition` + +Pre-defined condition type and status constants. + +| Constant | Value | +|---|---| +| `Condition.READY` | `"Ready"` | +| `Condition.AVAILABLE` | `"Available"` | +| `Condition.DEGRADED` | `"Degraded"` | +| `Condition.PROGRESSING` | `"Progressing"` | +| `Condition.UPGRADEABLE` | `"Upgradeable"` | +| `Condition.Status.TRUE` | `"True"` | +| `Condition.Status.FALSE` | `"False"` | +| `Condition.Status.UNKNOWN` | `"Unknown"` | + +```python +ns.wait_for_condition( + condition=Resource.Condition.READY, + status=Resource.Condition.Status.TRUE, +) +``` + +#### `Resource.ApiGroup` + +Pre-defined API group string constants. A selection of commonly used values: + +| Constant | Value | +|---|---| +| `ApiGroup.APPS` | `"apps"` | +| `ApiGroup.BATCH` | `"batch"` | +| `ApiGroup.NETWORKING_K8S_IO` | `"networking.k8s.io"` | +| `ApiGroup.RBAC_AUTHORIZATION_K8S_IO` | `"rbac.authorization.k8s.io"` | +| `ApiGroup.STORAGE_K8S_IO` | `"storage.k8s.io"` | +| `ApiGroup.CONFIG_OPENSHIFT_IO` | `"config.openshift.io"` | +| `ApiGroup.KUBEVIRT_IO` | `"kubevirt.io"` | +| `ApiGroup.CDI_KUBEVIRT_IO` | `"cdi.kubevirt.io"` | +| `ApiGroup.ROUTE_OPENSHIFT_IO` | `"route.openshift.io"` | +| `ApiGroup.MACHINE_OPENSHIFT_IO` | `"machine.openshift.io"` | + +> **Note:** Over 100 API group constants are available. Use IDE auto-complete to discover all values. + +#### `Resource.ApiVersion` + +| Constant | Value | +|---|---| +| `ApiVersion.V1` | `"v1"` | +| `ApiVersion.V1BETA1` | `"v1beta1"` | +| `ApiVersion.V1ALPHA1` | `"v1alpha1"` | +| `ApiVersion.V1ALPHA3` | `"v1alpha3"` | + +--- + +## `NamespacedResource` + +```python +from ocp_resources.resource import NamespacedResource +``` + +Base class for **namespace-scoped** resources (e.g., `Pod`, `Deployment`, `Service`, `ConfigMap`). Extends `Resource` with namespace awareness. + +### Constructor + +```python +NamespacedResource( + client=client, + name="my-pod", + namespace="my-namespace", +) +``` + +All parameters from [`Resource`](#constructor) are accepted, plus: + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `namespace` | `str \| None` | `None` | Kubernetes namespace. Required unless `yaml_file` or `kind_dict` is provided. | + +**Raises:** `MissingRequiredArgumentError` if neither (`name` + `namespace`) nor `yaml_file` / `kind_dict` is provided. + +```python +from ocp_resources.pod import Pod + +pod = Pod( + client=client, + name="nginx", + namespace="default", + label={"app": "web"}, +) +``` + +### Overridden Methods + +#### `instance` + +```python +@property +def instance(self) -> ResourceInstance +``` + +Get the live resource instance, scoped to `self.namespace`. + +**Returns:** `ResourceInstance` + +#### `to_dict` + +```python +def to_dict(self) -> None +``` + +Populates `self.res` and sets `metadata.namespace`. If using `yaml_file` or `kind_dict`, reads the namespace from the YAML/dict. + +**Raises:** `MissingRequiredArgumentError` if namespace is still not set after processing. + +#### `get` (classmethod) + +Behaves like `Resource.get()`, but yields instances constructed with both `name` and `namespace`. + +```python +from ocp_resources.pod import Pod + +for pod in Pod.get(client=client, namespace="default"): + print(f"{pod.namespace}/{pod.name}") + +# With label selector +for pod in Pod.get(client=client, namespace="default", label_selector="app=web"): + print(pod.name) +``` + +--- + +## `ResourceEditor` + +```python +from ocp_resources.resource import ResourceEditor +``` + +Temporarily patch resources and automatically restore original values. Designed for test scenarios. + +### Constructor + +```python +ResourceEditor( + patches: dict[Any, Any], + action: str = "update", + user_backups: dict[Any, Any] | None = None, +) +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `patches` | `dict` | — | Map of `{Resource: patch_dict}`. | +| `action` | `str` | `"update"` | `"update"` for merge-patch, `"replace"` for full replacement. | +| `user_backups` | `dict \| None` | `None` | Pre-computed backup dicts. If provided, skips automatic backup creation. | + +### Context Manager Usage + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-ns", ensure_exists=True) + +with ResourceEditor( + patches={ns: {"metadata": {"labels": {"temporary": "true"}}}} +) as editor: + # Labels are patched + assert ns.instance.metadata.labels.temporary == "true" +# Labels are restored to original values +``` + +### Methods + +| Method | Signature | Description | +|---|---|---| +| `update` | `update(backup_resources: bool = False) -> None` | Apply patches. If `backup_resources=True`, back up original values first. | +| `restore` | `restore() -> None` | Restore all backed-up values. | + +### Properties + +| Property | Type | Description | +|---|---|---| +| `backups` | `dict[Any, Any]` | Backed-up original values for each patched resource. | +| `patches` | `dict[Any, Any]` | The patches dict from the constructor. | + +> **Warning:** The `DynamicClient` used to obtain the resource objects must have sufficient privileges to patch and replace resources. + +See [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html) for detailed patterns. + +--- + +## `ResourceList` + +```python +from ocp_resources.resource import ResourceList +``` + +Create and manage N copies of a cluster-scoped resource with indexed names. + +### Constructor + +```python +ResourceList( + resource_class: type[Resource], + num_resources: int, + client: DynamicClient, + **kwargs: Any, +) +``` + +| Parameter | Type | Description | +|---|---|---| +| `resource_class` | `type[Resource]` | The resource class to instantiate. | +| `num_resources` | `int` | Number of resource copies to create. | +| `client` | `DynamicClient` | Kubernetes client. | +| `**kwargs` | | Passed to each resource constructor. Must include `name` (used as base name). | + +Resources are named `{name}-1`, `{name}-2`, ..., `{name}-N`. + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import ResourceList + +namespaces = ResourceList( + resource_class=Namespace, + num_resources=3, + client=client, + name="test-ns", +) + +with namespaces: + # Creates test-ns-1, test-ns-2, test-ns-3 + for ns in namespaces: + print(ns.name) +# All three namespaces are deleted +``` + +### Methods (inherited from `BaseResourceList`) + +| Method | Signature | Returns | Description | +|---|---|---|---| +| `deploy` | `deploy(wait=False)` | `list[Resource]` | Deploy all resources. | +| `clean_up` | `clean_up(wait=True)` | `bool` | Delete all resources in reverse order. | +| `__len__` | `__len__()` | `int` | Number of resources. | +| `__getitem__` | `__getitem__(index)` | `Resource` | Access by index. | +| `__iter__` | `__iter__()` | `Generator` | Iterate over resources. | + +--- + +## `NamespacedResourceList` + +```python +from ocp_resources.resource import NamespacedResourceList +``` + +Create one instance of a namespaced resource in each namespace from a `ResourceList`. + +### Constructor + +```python +NamespacedResourceList( + resource_class: type[NamespacedResource], + namespaces: ResourceList, + client: DynamicClient, + **kwargs: Any, +) +``` + +| Parameter | Type | Description | +|---|---|---| +| `resource_class` | `type[NamespacedResource]` | The namespaced resource class (e.g., `Pod`). | +| `namespaces` | `ResourceList` | A `ResourceList` of `Namespace` resources. | +| `client` | `DynamicClient` | Kubernetes client. | +| `**kwargs` | | Passed to each resource constructor. Must include `name`. | + +**Raises:** `TypeError` if any resource in `namespaces` is not a `Namespace`. + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.pod import Pod +from ocp_resources.resource import ResourceList, NamespacedResourceList + +namespaces = ResourceList( + resource_class=Namespace, num_resources=2, client=client, name="test-ns" +) + +pods = NamespacedResourceList( + resource_class=Pod, + namespaces=namespaces, + client=client, + name="nginx", +) + +with namespaces: + with pods: + # Creates nginx in test-ns-1 and test-ns-2 + for pod in pods: + print(f"{pod.namespace}/{pod.name}") +``` + +See [Managing Bulk Resources with ResourceList](managing-resource-lists.html) for full usage patterns. + +--- + +## `KubeAPIVersion` + +```python +from ocp_resources.resource import KubeAPIVersion +``` + +Implements [Kubernetes API versioning](https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-versioning) with comparison operators. Extends `packaging.version.Version`. + +### Constructor + +```python +KubeAPIVersion(vstring: str) +``` + +| Parameter | Type | Description | +|---|---|---| +| `vstring` | `str` | Version string (e.g., `"v1"`, `"v1beta1"`, `"v1alpha2"`). | + +**Raises:** `ValueError` if the version string does not conform to Kubernetes versioning. + +### Ordering + +Versions are ordered: `v1alpha1 < v1alpha2 < v1beta1 < v1beta2 < v1 < v2`. + +```python +assert KubeAPIVersion("v1") > KubeAPIVersion("v1beta1") +assert KubeAPIVersion("v1beta1") > KubeAPIVersion("v1alpha1") +assert KubeAPIVersion("v1") == KubeAPIVersion("v1") +``` + +--- + +## Exceptions + +All exceptions are importable from `ocp_resources.exceptions`. + +```python +from ocp_resources.exceptions import ( + MissingRequiredArgumentError, + ResourceTeardownError, + ValidationError, + ConditionError, +) +``` + +| Exception | Raised By | Description | +|---|---|---| +| `MissingRequiredArgumentError` | `Resource.__init__`, `NamespacedResource.__init__`, `NamespacedResource._base_body` | Required parameters (`name`, `namespace`) not provided. | +| `ResourceTeardownError` | `Resource.__exit__` | `clean_up()` returned `False` during context manager exit. | +| `ValidationError` | `validate()`, `validate_dict()`, `create()`, `update_replace()` | Resource dict fails OpenAPI schema validation. Has `message`, `path`, and `schema_error` attributes. | +| `ConditionError` | `wait_for_condition()` | A `stop_condition` was detected during condition waiting. | +| `MissingResourceResError` | `Resource._base_body` | Deprecated. `self.res` is empty after `_base_body()`. | + +--- + +## Timeout Constants + +Available from `ocp_resources.utils.constants`: + +```python +from ocp_resources.utils.constants import ( + TIMEOUT_1SEC, # 1 + TIMEOUT_5SEC, # 5 + TIMEOUT_10SEC, # 10 + TIMEOUT_30SEC, # 30 + TIMEOUT_1MINUTE, # 60 + TIMEOUT_2MINUTES, # 120 + TIMEOUT_4MINUTES, # 240 + TIMEOUT_10MINUTES, # 600 +) +``` + +--- + +## Module-Level Functions + +### `replace_key_with_hashed_value` + +```python +from ocp_resources.resource import replace_key_with_hashed_value + +replace_key_with_hashed_value(resource_dict: dict, key_name: str) -> dict +``` + +Recursively replace the value at a key path with `"*******"`. Supports dict paths (`a>b>c`) and list paths (`a>b[]>c`). + +| Parameter | Type | Description | +|---|---|---| +| `resource_dict` | `dict` | The nested dictionary. | +| `key_name` | `str` | Key path using `>` as separator, `[]` for list elements. | + +**Returns:** `dict` — A deep copy with the value replaced. + +```python +data = {"spec": {"secret": {"token": "sensitive"}}} +hashed = replace_key_with_hashed_value(data, "spec>secret>token") +# {"spec": {"secret": {"token": "*******"}}} + +# With list path +data = {"spec": {"containers": [{"env": [{"value": "secret"}]}]}} +hashed = replace_key_with_hashed_value(data, "spec>containers[]>env[]>value") +``` + +## Related Pages + +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Querying and Watching Resources](querying-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Environment Variables and Configuration](environment-variables.html) + +--- + +Source: class-generator-cli.md + +I'll start by reading the knowledge graph and page manifest, then explore the class-generator source code in detail. + +Now let me check the entry point configuration and any additional schema-related functions: + +Now let me check the test files for CLI tests and the entry point: + +Now let me check the parsers and any additional CLI-relevant components: + +Now I have all the information I need. Let me compile the comprehensive CLI reference page. + +# class-generator CLI Reference + +The `class-generator` command generates Python wrapper classes for Kubernetes and OpenShift resources. It is installed as a console script entry point from the `openshift-python-wrapper` package. + +``` +class-generator = "class_generator.cli:main" +``` + +```bash +pip install openshift-python-wrapper +# or +uv tool install openshift-python-wrapper +``` + +> **Tip:** Enable shell completion by adding to `~/.bashrc` or `~/.zshrc`: +> ```bash +> if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=zsh_source class-generator)"; fi +> ``` + +## Synopsis + +``` +class-generator [OPTIONS] +``` + +At least one action option must be specified: `--kind`, `--update-schema`, `--update-schema-for`, `--discover-missing`, `--coverage-report`, `--generate-missing`, or `--regenerate-all`. + +--- + +## Options Reference + +### Kind Generation + +#### `-k`, `--kind` + +| Property | Value | +|----------|-------| +| **Type** | `STRING` | +| **Default** | `None` | +| **Required** | No (but at least one action must be specified) | +| **Requires** | Connected cluster with admin privileges | + +Generate Python wrapper classes for the specified Kubernetes resource Kind(s). Multiple kinds can be comma-separated (no spaces). + +When a kind is not found in the local schema mapping file, the CLI interactively prompts to run `--update-schema` (only in interactive CLI mode). + +```bash +# Single kind +class-generator -k Pod + +# Multiple kinds (processed in parallel) +class-generator -k Deployment,Pod,ConfigMap +``` + +When multiple kinds share the same Kind name but belong to different API groups (e.g., `DNS` from `config.openshift.io` and `operator.openshift.io`), separate files are generated with API group suffixes: + +``` +dns_config_openshift_io.py +dns_operator_openshift_io.py +``` + +--- + +#### `-o`, `--output-file` + +| Property | Value | +|----------|-------| +| **Type** | `PATH` | +| **Default** | `ocp_resources/.py` | +| **Required** | No | + +Override the output file path for the generated Python module. If not provided, the filename is derived from the Kind using `convert_camel_case_to_snake_case`. + +```bash +class-generator -k Pod -o my_custom_pod.py +``` + +> **Note:** When generating multiple comma-separated kinds, the `--output-file` value applies to all kinds. For independent output paths, run separate commands. + +--- + +#### `--overwrite` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Overwrite an existing output file. Without this flag, if the target file already exists, a `_TEMP.py` suffixed file is created instead. + +When overwriting, any user-added code blocks (code after the `# End of generated code` marker) and user imports are preserved in the regenerated file. + +```bash +class-generator -k Pod --overwrite +``` + +--- + +#### `--dry-run` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Preview the generated output without writing any files. The generated Python code is printed to the console with syntax highlighting and line numbers using Rich. + +```bash +class-generator -k Pod --dry-run +``` + +--- + +#### `--backup` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | +| **Requires** | `--regenerate-all` or `--overwrite` | + +Create a timestamped backup of existing files before overwriting or regenerating. Backups are stored in `.backups/backup-YYYYMMDD-HHMMSS/` preserving the original directory structure. + +```bash +class-generator -k Pod --overwrite --backup +``` + +> **Note:** Using `--backup` without either `--regenerate-all` or `--overwrite` results in a constraint error. + +--- + +### Test Generation + +#### `--add-tests` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | +| **Requires** | `-k`/`--kind` | + +Generate test files for the specified Kind and run them. This performs two actions: + +1. Generates a test manifest in `class_generator/tests/manifests//` and regenerates `class_generator/tests/test_class_generator.py` from the Jinja2 template. +2. Runs the generated test file using `uv run --group tests pytest class_generator/tests/test_class_generator.py`. + +```bash +class-generator -k Pod --add-tests +``` + +> **Warning:** `--add-tests` cannot be used without `-k`/`--kind`. Running `class-generator --add-tests` alone exits with a non-zero status. + +--- + +### Schema Management + +#### `--update-schema` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | +| **Requires** | Connected cluster; `oc` or `kubectl` in PATH | +| **Mutually exclusive with** | `--update-schema-for` | + +Fetch all resource schemas from the connected cluster's OpenAPI v3 endpoints and update the local schema files: + +- `class_generator/schema/__resources-mappings.json` (compressed as `.json.gz`) +- `class_generator/schema/_definitions.json` + +The update strategy depends on the cluster version: + +| Cluster Version | Behavior | +|-----------------|----------| +| Same or newer than last update | Full update — fetches all schemas, updates existing resources | +| Older than last update | Incremental — only adds missing resources, preserves existing schemas | + +When used alone, exits after updating. When combined with `--generate-missing`, continues to resource generation after the update. + +```bash +# Update schema only +class-generator --update-schema + +# Update schema then generate missing resources +class-generator --update-schema --generate-missing +``` + +> **Note:** When `--update-schema` is used without `--generate-missing`, it cannot be combined with `-k`, `--discover-missing`, `--coverage-report`, `--dry-run`, `--overwrite`, `-o`, `--add-tests`, or `--regenerate-all`. + +--- + +#### `--update-schema-for` + +| Property | Value | +|----------|-------| +| **Type** | `STRING` | +| **Default** | `None` | +| **Requires** | Connected cluster; `oc` or `kubectl` in PATH | +| **Mutually exclusive with** | `--update-schema` | + +Update the schema for a single resource Kind without affecting other resources. The Kind name is **case-sensitive**. + +This fetches only the API paths relevant to the specified Kind, updates (or adds) its schema in the mapping file, and exits. + +```bash +class-generator --update-schema-for LlamaStackDistribution +``` + +Use cases: +- Connected to an older cluster but need to update a specific CRD +- A new operator was installed and you need its resource schema +- Refreshing just one resource without a full schema update + +After updating, regenerate the class: + +```bash +class-generator --update-schema-for LlamaStackDistribution +class-generator -k LlamaStackDistribution --overwrite +``` + +**Errors:** + +| Error | Cause | +|-------|-------| +| `ResourceNotFoundError` | The Kind is not found on the cluster (CRD not installed or name misspelled) | +| `RuntimeError` | API paths not found or schema extraction failed | + +> **Note:** Cannot be combined with `-k`, `--discover-missing`, `--coverage-report`, `--generate-missing`, or `--regenerate-all`. + +--- + +### Coverage and Discovery + +#### `--discover-missing` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Analyze resource coverage by comparing schema-mapped resources against implemented wrapper classes in `ocp_resources/`. Generates a console report showing coverage statistics and missing resources. + +```bash +class-generator --discover-missing +``` + +--- + +#### `--coverage-report` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Generate a detailed coverage report showing: + +| Metric | Description | +|--------|-------------| +| Total Resources in Schema | Number of resource Kinds in the schema mapping | +| Auto-Generated Resources | Wrapper classes with the generated marker | +| Coverage | Percentage of mapped resources with generated classes | +| Missing (Not Generated) | Resources in schema but without generated classes | +| Manual Implementations | Resource classes without the generated marker | + +```bash +# Console output (default) +class-generator --coverage-report + +# JSON output +class-generator --coverage-report --json +``` + +--- + +#### `--json` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Output reports in JSON format instead of Rich console tables. Applies to `--coverage-report`, `--discover-missing`, and `--generate-missing`. + +The JSON output structure: + +```json +{ + "generated_resources": ["ConfigMap", "Deployment", "Pod"], + "manual_resources": ["VirtualMachine"], + "missing_resources": [{"kind": "Binding"}, {"kind": "ComponentStatus"}], + "coverage_stats": { + "total_in_mapping": 400, + "total_generated": 197, + "total_manual": 25, + "coverage_percentage": 49.25, + "missing_count": 203 + } +} +``` + +```bash +class-generator --coverage-report --json +``` + +--- + +#### `--generate-missing` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Generate wrapper classes for all resources found in the schema mapping that do not yet have generated files. Each missing resource Kind is passed to `class_generator()` individually. + +Can be combined with `--update-schema` to first refresh the schema, then generate all missing classes. + +```bash +# Generate missing resources +class-generator --generate-missing + +# Update schema first, then generate missing +class-generator --update-schema --generate-missing + +# Dry run to preview +class-generator --generate-missing --dry-run +``` + +--- + +### Batch Regeneration + +#### `--regenerate-all` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Regenerate all existing generated resource classes using the latest schemas. Only files containing the `# Generated using` marker in `ocp_resources/` are processed. + +Regeneration runs in parallel (up to 10 workers). User-added code (below `# End of generated code`) is preserved during regeneration. + +```bash +# Regenerate all resources +class-generator --regenerate-all + +# With backup +class-generator --regenerate-all --backup + +# Dry run +class-generator --regenerate-all --dry-run + +# Filter to specific resources +class-generator --regenerate-all --filter "Pod*" +``` + +Output summary: + +``` +Regeneration complete: 195 succeeded, 2 failed +Backup files stored in: .backups/backup-20260705-143022 +``` + +--- + +#### `--filter` + +| Property | Value | +|----------|-------| +| **Type** | `STRING` | +| **Default** | `None` | +| **Requires** | `--regenerate-all` | + +Filter which resources to regenerate using a glob pattern matched against the resource Kind name. Uses `fnmatch` semantics. + +```bash +# Regenerate only Pod-related resources +class-generator --regenerate-all --filter "Pod*" + +# Regenerate resources ending in "Service" +class-generator --regenerate-all --filter "*Service" + +# Regenerate a specific resource +class-generator --regenerate-all --filter "VirtualMachine" +``` + +--- + +### Logging + +#### `-v`, `--verbose` + +| Property | Value | +|----------|-------| +| **Type** | Flag | +| **Default** | `False` | + +Enable verbose output with debug-level logs. Sets `DEBUG` level on the following loggers: + +- `class_generator.core.schema` +- `class_generator.core.generator` +- `class_generator.core.coverage` +- `class_generator.core.discovery` +- `class_generator.cli` +- `class_generator.utils` +- `ocp_resources` + +```bash +class-generator -k Pod -v +``` + +--- + +## Constraint Rules + +The CLI enforces the following option constraints: + +| Constraint | Rule | +|------------|------| +| `--update-schema` ↔ `--update-schema-for` | Mutually exclusive | +| `--update-schema` (alone) | Cannot combine with `-k`, `--discover-missing`, `--coverage-report`, `--dry-run`, `--overwrite`, `-o`, `--add-tests`, `--regenerate-all` | +| `--update-schema-for` | Cannot combine with `-k`, `--discover-missing`, `--coverage-report`, `--generate-missing`, `--regenerate-all` | +| `--backup` | Requires `--regenerate-all` or `--overwrite` | +| `--filter` | Requires `--regenerate-all` | +| No options | Exits with error — at least one action required | + +--- + +## Execution Order + +When multiple compatible options are specified together, the CLI processes them in this fixed order: + +1. `--update-schema-for` (exits after completion) +2. `--update-schema` (exits unless `--generate-missing` is also set) +3. `--coverage-report` / `--discover-missing` / `--generate-missing` (coverage analysis and reporting) +4. `--generate-missing` (generates classes for missing resources) +5. `--regenerate-all` (batch regeneration, exits after completion) +6. `-k`/`--kind` (normal kind generation) +7. `--add-tests` (test generation and execution) + +--- + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | Generation failure, schema update failure, resource not found, or any kind in a batch failed | +| `2` | Invalid CLI arguments or constraint violation | + +--- + +## Programmatic API + +The generation logic can be invoked directly from Python. See [Generating New Resource Classes with class-generator](generating-resource-classes.html) for usage examples. + +### `class_generator.core.generator.class_generator()` + +```python +from class_generator.core.generator import class_generator + +generated_files: list[str] = class_generator( + kind="Pod", + overwrite=False, + dry_run=False, + output_file="", + output_dir="", + add_tests=False, + called_from_cli=True, + update_schema_executed=False, +) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `kind` | `str` | *(required)* | Kubernetes resource Kind | +| `overwrite` | `bool` | `False` | Overwrite existing files | +| `dry_run` | `bool` | `False` | Preview output without writing files | +| `output_file` | `str` | `""` | Specific output file path | +| `output_dir` | `str` | `""` | Output directory (defaults to `ocp_resources`) | +| `add_tests` | `bool` | `False` | Generate test files | +| `called_from_cli` | `bool` | `True` | Enables interactive prompts when `True` | +| `update_schema_executed` | `bool` | `False` | Whether schema update was already performed | + +**Returns:** `list[str]` — List of generated file paths. Empty list if the kind is not found in the schema mapping (when `called_from_cli=False`). + +**Raises:** +- `RuntimeError` — Kind not found after schema update, or user declined schema update +- `ValueError` — Generated filename contains invalid patterns (single-letter segments) + +--- + +### `class_generator.core.generator.generate_resource_file_from_dict()` + +```python +from class_generator.core.generator import generate_resource_file_from_dict + +orig_filename, generated_filename = generate_resource_file_from_dict( + resource_dict={"kind": "Pod", ...}, + overwrite=False, + dry_run=False, + output_file="", + add_tests=False, + output_file_suffix="", + output_dir="", +) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `resource_dict` | `dict[str, Any]` | *(required)* | Dictionary containing parsed resource information | +| `overwrite` | `bool` | `False` | Overwrite existing files | +| `dry_run` | `bool` | `False` | Preview without writing | +| `output_file` | `str` | `""` | Specific output file path | +| `add_tests` | `bool` | `False` | Generate test files under `class_generator/tests/manifests/` | +| `output_file_suffix` | `str` | `""` | Suffix appended to filename (for API group disambiguation) | +| `output_dir` | `str` | `""` | Output directory (defaults to `ocp_resources`) | + +**Returns:** `tuple[str, str]` — `(original_filename, generated_filename)`. These differ when a `_TEMP.py` file is created. + +--- + +### `class_generator.core.schema.update_kind_schema()` + +```python +from class_generator.core.schema import update_kind_schema + +update_kind_schema(client=None) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `client` | `str \| None` | `None` | Path to `oc`/`kubectl` binary. Auto-detected if `None`. | + +**Raises:** +- `ClusterVersionError` — Cannot determine cluster version +- `RuntimeError` — Failed to fetch OpenAPI v3 index +- `OSError` — Failed to write schema files + +--- + +### `class_generator.core.schema.update_single_resource_schema()` + +```python +from class_generator.core.schema import update_single_resource_schema + +update_single_resource_schema(kind="LlamaStackDistribution", client=None) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `kind` | `str` | *(required)* | Resource Kind name (case-sensitive) | +| `client` | `str \| None` | `None` | Path to `oc`/`kubectl` binary. Auto-detected if `None`. | + +**Raises:** +- `ResourceNotFoundError` — Kind not found on the cluster +- `RuntimeError` — Schema extraction or save failed + +--- + +### `class_generator.core.coverage.analyze_coverage()` + +```python +from class_generator.core.coverage import analyze_coverage + +result: dict[str, Any] = analyze_coverage(resources_dir="ocp_resources") +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `resources_dir` | `str` | `"ocp_resources"` | Directory to scan for wrapper classes | + +**Returns:** `dict[str, Any]` with keys: + +| Key | Type | Description | +|-----|------|-------------| +| `generated_resources` | `list[str]` | Sorted list of auto-generated resource class names | +| `manual_resources` | `list[str]` | Sorted list of manually written resource class names | +| `missing_resources` | `list[dict]` | List of `{"kind": "..."}` for resources in schema but not generated | +| `coverage_stats` | `dict` | Statistics including `total_in_mapping`, `total_generated`, `total_manual`, `coverage_percentage`, `missing_count` | + +--- + +### `class_generator.core.discovery.discover_generated_resources()` + +```python +from class_generator.core.discovery import discover_generated_resources + +resources: list[dict[str, Any]] = discover_generated_resources() +``` + +**Returns:** `list[dict[str, Any]]` — Each dict contains: + +| Key | Type | Description | +|-----|------|-------------| +| `path` | `str` | Full path to the resource file | +| `kind` | `str` | Resource class name | +| `filename` | `str` | File name without extension | +| `has_user_code` | `bool` | Whether file contains user modifications below `# End of generated code` | + +--- + +### `class_generator.core.discovery.discover_cluster_resources()` + +```python +from class_generator.core.discovery import discover_cluster_resources + +resources: dict[str, list[dict[str, Any]]] = discover_cluster_resources( + client=None, + api_group_filter=None, +) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `client` | `DynamicClient \| None` | `None` | Kubernetes dynamic client. Creates one if `None`. | +| `api_group_filter` | `str \| None` | `None` | Filter by API group name | + +**Returns:** `dict[str, list[dict[str, Any]]]` — Mapping of API version to list of resource dicts (`name`, `kind`, `namespaced`). + +**Raises:** `ValueError` — If client is not a `DynamicClient` instance. + +--- + +## Exceptions + +### `class_generator.exceptions.ResourceNotFoundError` + +```python +from class_generator.exceptions import ResourceNotFoundError +``` + +Raised when a resource Kind is not found in the schema mapping or on the cluster. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `kind` | `str` | The Kind that was not found | + +### `class_generator.core.schema.ClusterVersionError` + +```python +from class_generator.core.schema import ClusterVersionError +``` + +Raised when the cluster version cannot be determined (client binary missing, cluster unreachable, or authentication failure). + +--- + +## Schema Files + +The CLI manages two schema files under `class_generator/schema/`: + +| File | Purpose | +|------|---------| +| `__resources-mappings.json.gz` | Compressed JSON mapping of lowercase Kind → list of schemas with GVK metadata and namespaced status | +| `_definitions.json` | JSON Schema definitions for `$ref` resolution during validation | +| `__cluster_version__.txt` | Last cluster version used for schema generation | + +See [Schema Validation and Code Generation Architecture](schema-validation-internals.html) for details on how these files are structured and consumed. + +--- + +## Common Workflows + +### Generate a new resource class + +```bash +class-generator -k MyCustomResource +``` + +### Update schema and regenerate all classes + +```bash +class-generator --update-schema +class-generator --regenerate-all --backup +``` + +### Add a new CRD to an older cluster + +```bash +class-generator --update-schema-for MyNewCRD +class-generator -k MyNewCRD +``` + +### CI/CD coverage check + +```bash +class-generator --coverage-report --json > coverage.json +``` + +### Preview changes before writing + +```bash +class-generator -k Pod --dry-run +class-generator --regenerate-all --dry-run +``` + +--- + +## Related Pages + +- [Generating New Resource Classes with class-generator](generating-resource-classes.html) — Step-by-step guide for scaffolding resource classes +- [Resource and NamespacedResource API](resource-api.html) — API reference for the base classes that generated code extends +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) — How generated subclasses fit into the class hierarchy +- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) — Internals of schema fetching, caching, and code generation +- [Environment Variables and Configuration](environment-variables.html) — Environment variables that affect runtime behavior + +## Related Pages + +- [Generating New Resource Classes with class-generator](generating-resource-classes.html) +- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) +- [Resource and NamespacedResource API](resource-api.html) +- [Environment Variables and Configuration](environment-variables.html) + +--- + +Source: environment-variables.md + +Let me start by reading the knowledge graph and page manifest, then explore the codebase for environment variables. + +Now let me look at the `keys_to_hash` and hash-related functionality more closely: + +Now let me look at the `get_logger` import and how log levels work: + +Now let me check for any CI/CD or config-related environment variables: + +Let me also look for any validation-related environment variables and enable_validation_by_default: + +Now let me look at the `get_logger` usage and the `simple_logger` format: + +Now let me check the deploy() and clean_up() methods more carefully, and also the hash_resource_dict method: + +Let me also check how `replace_key_with_hashed_value` is defined: + +Now let me check the MCP server for environment variables: + +Now let me check if there are any other environment variable usages in the class_generator: + +Let me verify there are no other env vars in tests: + +Let me also check the full `get_client` function and the `save_kubeconfig` function: + +Now let me look for any additional environment variables in fake_kubernetes_client: + +Let me look at how the deploy and clean_up methods use the skip logic: + +Now I have all the information needed. Let me write the documentation: + +# Environment Variables and Configuration + +This page documents all environment variables recognized by `openshift-python-wrapper` at runtime. These variables control logging behavior, resource lifecycle operations, proxy configuration, cluster connectivity, and sensitive data hashing. + +## Quick Reference + +| Environment Variable | Purpose | Default | +|---|---|---| +| `KUBECONFIG` | Path to kubeconfig file | `~/.kube/config` | +| `HTTPS_PROXY` | HTTPS proxy URL for cluster connections | _(unset)_ | +| `HTTP_PROXY` | HTTP proxy URL for cluster connections | _(unset)_ | +| `OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL` | Logging verbosity level | `INFO` | +| `OPENSHIFT_PYTHON_WRAPPER_LOG_FILE` | Path to log output file | `""` (stdout) | +| `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` | Enable/disable hashing sensitive data in logs | `true` | +| `REUSE_IF_RESOURCE_EXISTS` | Skip resource creation if already exists | _(unset)_ | +| `SKIP_RESOURCE_TEARDOWN` | Skip resource deletion during teardown | _(unset)_ | + +--- + +## Cluster Connection + +### `KUBECONFIG` + +| Property | Value | +|---|---| +| **Type** | `str` | +| **Default** | `~/.kube/config` | +| **Used by** | `get_client()` | +| **Source** | `ocp_resources/resource.py` | + +Path to the kubeconfig file used when creating a Kubernetes `DynamicClient`. Read when no explicit `config_file`, `config_dict`, `host`/`token`, or `username`/`password` arguments are passed to `get_client()`. + +> **Note:** The standard `kubernetes` Python client reads `KUBECONFIG` at import time. If you set this variable in code (after import), `openshift-python-wrapper` handles it by explicitly passing the value to the client constructor. See [Connecting to Clusters](connecting-to-clusters.html) for all connection options. + +```bash +export KUBECONFIG=/path/to/my/kubeconfig +``` + +```python +from ocp_resources.resource import get_client + +# Automatically uses $KUBECONFIG, or falls back to ~/.kube/config +client = get_client() +``` + +--- + +### `HTTPS_PROXY` / `HTTP_PROXY` + +| Property | Value | +|---|---| +| **Type** | `str` | +| **Default** | _(unset — no proxy)_ | +| **Used by** | `get_client()` | +| **Source** | `ocp_resources/resource.py` | + +Sets the proxy on the Kubernetes client configuration when no proxy is already configured on the `client_configuration` object. `HTTPS_PROXY` takes precedence over `HTTP_PROXY` when both are set. + +```bash +export HTTPS_PROXY=http://proxy.example.com:8080 +``` + +```python +from ocp_resources.resource import get_client + +# Proxy is automatically applied from environment +client = get_client() +``` + +> **Tip:** If you pass a `client_configuration` object that already has a `.proxy` set, the environment variables are ignored. + +**Precedence order:** + +1. Explicit `client_configuration.proxy` (if already set) +2. `HTTPS_PROXY` environment variable +3. `HTTP_PROXY` environment variable + +--- + +## Logging + +### `OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL` + +| Property | Value | +|---|---| +| **Type** | `str` | +| **Default** | `INFO` | +| **Valid values** | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | +| **Used by** | `Resource._set_logger()` | +| **Source** | `ocp_resources/resource.py` | + +Controls the log level for each resource instance's logger. The logger is created per-resource using the `simple_logger` library. + +```bash +export OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG +``` + +```python +import os +os.environ["OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL"] = "DEBUG" + +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() +pod = Pod(name="my-pod", namespace="default", client=client) +# Logger on this Pod instance now uses DEBUG level +``` + +--- + +### `OPENSHIFT_PYTHON_WRAPPER_LOG_FILE` + +| Property | Value | +|---|---| +| **Type** | `str` | +| **Default** | `""` (empty string — logs to stdout) | +| **Used by** | `Resource._set_logger()` | +| **Source** | `ocp_resources/resource.py` | + +Redirects resource log output to a file. When unset or empty, logs are written to stdout. + +```bash +export OPENSHIFT_PYTHON_WRAPPER_LOG_FILE=/var/log/ocp-wrapper.log +``` + +```python +import os +os.environ["OPENSHIFT_PYTHON_WRAPPER_LOG_FILE"] = "/tmp/ocp-wrapper.log" + +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() +ns = Namespace(name="test-ns", client=client) +# All log output for this resource is written to /tmp/ocp-wrapper.log +``` + +--- + +## Sensitive Data Hashing + +### `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` + +| Property | Value | +|---|---| +| **Type** | `str` | +| **Default** | `"true"` | +| **Valid values** | `"true"`, `"false"` | +| **Used by** | `Resource.hash_resource_dict()` | +| **Source** | `ocp_resources/resource.py` | + +Controls whether sensitive fields in resource dictionaries are replaced with `"*******"` when logged. When set to `"false"`, sensitive data is logged in cleartext. + +Hashing is applied to fields declared in each resource class's `keys_to_hash` property. The following built-in resources define sensitive keys: + +| Resource Class | Hashed Fields | +|---|---| +| `Secret` | `data`, `stringData` | +| `ConfigMap` | `data`, `binaryData` | +| `SealedSecret` | `spec>data`, `spec>encryptedData` | +| `VirtualMachine` | `spec>template>spec>volumes[]>cloudInitNoCloud>userData` | + +> **Warning:** Setting this to `"false"` causes sensitive data (e.g., secrets, tokens) to appear in log output. Use only for debugging in secure environments. + +```bash +# Disable hashing to see raw values in logs (debug only) +export OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA=false +``` + +```python +import os +os.environ["OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA"] = "false" + +from ocp_resources.secret import Secret +from ocp_resources.resource import get_client + +client = get_client() +secret = Secret(name="my-secret", namespace="default", client=client) +# Logs will now show raw secret data instead of "*******" +``` + +> **Note:** Hashing also depends on the `hash_log_data` constructor parameter on each resource instance. Both must be enabled for hashing to occur. The `hash_log_data` parameter defaults to `True`. See [Resource and NamespacedResource API](resource-api.html) for constructor details. + +**Interaction with `hash_log_data` parameter:** + +| `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` | `hash_log_data` param | Result | +|---|---|---| +| `"true"` (default) | `True` (default) | Sensitive fields hashed | +| `"true"` | `False` | Sensitive fields **not** hashed | +| `"false"` | `True` | Sensitive fields **not** hashed | +| `"false"` | `False` | Sensitive fields **not** hashed | + +--- + +## Resource Reuse (Skip Creation) + +### `REUSE_IF_RESOURCE_EXISTS` + +| Property | Value | +|---|---| +| **Type** | `str` (YAML dict syntax) | +| **Default** | _(unset — no resources skipped)_ | +| **Used by** | `Resource.deploy()` | +| **Source** | `ocp_resources/resource.py` | + +When set, `deploy()` checks whether the target resource already exists on the cluster. If a match is found, the existing resource is returned without creating a new one. This is intended for debugging and iterative development workflows. + +> **Warning:** Spaces are significant in the value syntax. The value is parsed as YAML. + +**Value format:** + +``` +{: {: }} +``` + +**Matching rules:** + +| Pattern | Behavior | +|---|---| +| `{Pod: {}}` | Skip creation for **all** Pods (match by kind only) | +| `{Pod: {my-pod:}}` | Skip creation for Pod named `my-pod` in **any** namespace | +| `{Pod: {my-pod: my-ns}}` | Skip creation for Pod named `my-pod` in namespace `my-ns` only | +| `{Kind1: {}, Kind2: {name: ns}}` | Multiple resource patterns combined | + +**Examples:** + +```bash +# Skip all Pod creation if the pod already exists +export REUSE_IF_RESOURCE_EXISTS="{Pod: {}}" + +# Skip specific pod in specific namespace +export REUSE_IF_RESOURCE_EXISTS="{Pod: {my-pod: my-namespace}}" + +# Skip namespace and pod creation +export REUSE_IF_RESOURCE_EXISTS="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}" +``` + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +# If REUSE_IF_RESOURCE_EXISTS is set and a matching Pod exists, +# deploy() returns the existing resource without calling create() +pod = Pod( + name="my-pod", + namespace="my-namespace", + client=client, + containers=[{"name": "test", "image": "nginx"}], +) +pod.deploy() # Skips creation if resource matches the env var pattern +``` + +> **Note:** The resource must actually exist on the cluster for the skip to take effect. If the resource does not exist, `deploy()` proceeds with normal creation. + +--- + +## Skip Teardown + +### `SKIP_RESOURCE_TEARDOWN` + +| Property | Value | +|---|---| +| **Type** | `str` (YAML dict syntax) | +| **Default** | _(unset — no resources skipped)_ | +| **Used by** | `Resource.clean_up()` | +| **Source** | `ocp_resources/resource.py` | + +When set, `clean_up()` skips deletion for matching resources and returns `True` without calling `delete()`. This is intended for debugging — preserving resources on the cluster after tests finish. + +> **Warning:** Spaces are significant in the value syntax. The value is parsed as YAML. + +**Value format:** + +Uses the same YAML dict syntax as [`REUSE_IF_RESOURCE_EXISTS`](#reuse_if_resource_exists). + +**Matching rules:** + +| Pattern | Behavior | +|---|---| +| `{Pod: {}}` | Skip teardown for **all** Pods | +| `{Pod: {my-pod:}}` | Skip teardown for Pod named `my-pod` in **any** namespace | +| `{Pod: {my-pod: my-ns}}` | Skip teardown for Pod named `my-pod` in namespace `my-ns` only | +| `{Kind1: {}, Kind2: {name: ns}}` | Multiple resource patterns combined | + +**Examples:** + +```bash +# Keep all Namespaces after test run +export SKIP_RESOURCE_TEARDOWN="{Namespace: {}}" + +# Keep specific resources +export SKIP_RESOURCE_TEARDOWN="{Namespace: {test-ns:}, Pod: {debug-pod: test-ns}}" +``` + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() +pod = Pod( + name="debug-pod", + namespace="test-ns", + client=client, + containers=[{"name": "test", "image": "nginx"}], +) +pod.deploy() + +# If SKIP_RESOURCE_TEARDOWN is set with a matching pattern, +# clean_up() returns True without deleting the resource +pod.clean_up() # Resource remains on the cluster +``` + +> **Tip:** Use `REUSE_IF_RESOURCE_EXISTS` and `SKIP_RESOURCE_TEARDOWN` together for a fast debug loop: skip creation if a resource already exists, and skip teardown so it persists between runs. + +```bash +export REUSE_IF_RESOURCE_EXISTS="{Pod: {debug-pod: test-ns}}" +export SKIP_RESOURCE_TEARDOWN="{Pod: {debug-pod: test-ns}}" +``` + +--- + +## Combined Usage Patterns + +### Debug iteration loop + +Set both skip variables to avoid re-creating and tearing down resources between test runs: + +```bash +export REUSE_IF_RESOURCE_EXISTS="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}" +export SKIP_RESOURCE_TEARDOWN="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}" +``` + +### Full debug logging with unhashed data + +```bash +export OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG +export OPENSHIFT_PYTHON_WRAPPER_LOG_FILE=/tmp/debug.log +export OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA=false +``` + +> **Warning:** This combination logs sensitive data in plaintext. Do not use in production or shared environments. + +### Custom kubeconfig with proxy + +```bash +export KUBECONFIG=/path/to/custom/kubeconfig +export HTTPS_PROXY=http://corporate-proxy.example.com:3128 +``` + +--- + +## Programmatic Configuration Alternatives + +Several behaviors controlled by environment variables can also be set programmatically via constructor parameters or function arguments. See [Resource and NamespacedResource API](resource-api.html) for full constructor parameter documentation. + +| Environment Variable | Programmatic Alternative | +|---|---| +| `KUBECONFIG` | `get_client(config_file="/path/to/kubeconfig")` | +| `HTTPS_PROXY` / `HTTP_PROXY` | `client_configuration.proxy = "http://proxy:port"` | +| `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` | `Resource(hash_log_data=False, ...)` constructor parameter | + +> **Note:** `OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL`, `OPENSHIFT_PYTHON_WRAPPER_LOG_FILE`, `REUSE_IF_RESOURCE_EXISTS`, and `SKIP_RESOURCE_TEARDOWN` can **only** be configured via environment variables. + +## Related Pages + +- [Connecting to Clusters](connecting-to-clusters.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Resource and NamespacedResource API](resource-api.html) +- [Common Resource Patterns](common-patterns.html) +- [Error Handling and Exception Reference](error-handling.html) + +--- + +Source: resource-class-hierarchy.md + +Let me start by reading the exclusions file and pages manifest, then explore the codebase thoroughly. + +Now let me look at some generated resource subclasses to understand how they extend the base classes: + +Now let me look at some more examples - a cluster-scoped resource, the `ResourceConstants`, the class generator, and the `virtual_machine.py` for a more complex subclass: + +Now let me look at the `kind` property and how it's derived, plus some other resources for variety: + +Now let me look at how the `kind` property works (the `sub_resource_level` function) and the `conversion.py` for the kind name derivation: + +Now let me look at how resources discover their API version dynamically: + +Let me now look at one more important aspect - how the `get` method works differently in the two base classes, and how `_base_body` differs: + +Now let me check the `__init__.py` to see what's exported: + +Let me also check for examples: + +Now I have enough context to write the documentation. Let me draft it: + +# Understanding the Resource Class Hierarchy + +Every Kubernetes and OpenShift object you interact with through openshift-python-wrapper — whether it's a Pod, a Namespace, or a VirtualMachine — is a Python class that inherits from a small set of base classes. Understanding how these classes relate to each other helps you know **what capabilities any resource has**, **why some resources require a `namespace` argument and others don't**, and **how new resource types get added to the library**. + +This page explains the architecture so you can use existing resources effectively and extend the library when you need to work with a new custom resource. + +--- + +## The Big Picture + +The class hierarchy has three conceptual layers: + +| Layer | Classes | Purpose | +|---|---|---| +| **Constants** | `ResourceConstants` | Provides shared status strings, condition names, and type constants used across all resources | +| **Base classes** | `Resource`, `NamespacedResource` | Define all CRUD operations, lifecycle management, waiting, validation, and API discovery logic | +| **Concrete resource classes** | `Pod`, `Namespace`, `Deployment`, `VirtualMachine`, and 200+ others | Represent specific Kubernetes/OpenShift resource kinds, adding kind-specific parameters and behavior | + +The inheritance flows like this: + +``` +ResourceConstants + └── Resource ← cluster-scoped resources (Node, Namespace, StorageClass, ClusterRole, …) + └── NamespacedResource ← namespace-scoped resources (Pod, Deployment, ConfigMap, Secret, Route, …) +``` + +Every concrete resource class inherits from either `Resource` (for cluster-scoped resources) or `NamespacedResource` (for namespace-scoped resources). This single design decision controls whether the class requires a `namespace` argument and how it builds API requests. + +--- + +## Key Concepts + +### ResourceConstants: Shared Vocabulary + +At the root of the hierarchy sits `ResourceConstants`, which defines inner classes for common values: + +- **`Status`** — Strings like `RUNNING`, `SUCCEEDED`, `FAILED`, `PENDING`, `ACTIVE` +- **`Condition`** — Condition types like `READY`, `AVAILABLE`, `DEGRADED` and their status values (`TRUE`, `FALSE`, `UNKNOWN`) +- **`Type`** — Service types like `ClusterIP`, `NodePort`, `LoadBalancer` + +Because `Resource` inherits from `ResourceConstants`, every resource class in the library can reference these constants: + +```python +from ocp_resources.namespace import Namespace + +ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) +``` + +Concrete classes can extend these constants with kind-specific values. For example, `VirtualMachine` adds statuses like `MIGRATING`, `STOPPED`, and `PROVISIONING` to the base `Status` class. + +### Resource: The Foundation for Cluster-Scoped Resources + +`Resource` is the main base class. It provides everything needed to manage a Kubernetes resource: + +| Capability | Methods / Properties | +|---|---| +| **CRUD operations** | `create()`, `delete()`, `update()`, `update_replace()` | +| **Lifecycle management** | `deploy()`, `clean_up()`, context manager (`with` statement) | +| **Querying** | `exists`, `instance`, `status`, `labels` | +| **Waiting** | `wait()`, `wait_deleted()`, `wait_for_status()`, `wait_for_condition()` | +| **Listing** | `get()` class method — yields resource objects matching filters | +| **Validation** | `validate()`, `validate_dict()` | +| **Serialization** | `to_dict()`, `to_yaml()` | +| **API discovery** | Automatic `api_version` resolution from the cluster when only `api_group` is set | + +Resources that exist at the cluster level — not inside any namespace — inherit directly from `Resource`. Examples include `Namespace`, `Node`, `StorageClass`, `ClusterRole`, and `CustomResourceDefinition`. + +```python +from ocp_resources.namespace import Namespace + +# No namespace argument needed — Namespace is cluster-scoped +ns = Namespace(client=client, name="my-namespace") +``` + +#### Automatic Kind Detection + +You never set the `kind` field manually. The `kind` property is a class-level property that automatically derives the Kubernetes kind name from the **class name** using Python's method resolution order (MRO). When you define a class named `StorageClass`, its `kind` is automatically `"StorageClass"`. + +#### API Version Discovery + +Resources can specify their API identity in two ways: + +1. **`api_version`** — A fixed version string (e.g., `"v1"`), used for core Kubernetes resources +2. **`api_group`** — An API group string (e.g., `"apps"`, `"kubevirt.io"`), where the full `apiVersion` is discovered dynamically from the cluster + +When only `api_group` is set, the library queries the cluster at runtime to find the latest supported version for that resource kind. This means resource classes automatically work across cluster versions without code changes. + +```python +class Namespace(Resource): + # Core resource — fixed version, no group + api_version: str = Resource.ApiVersion.V1 + +class ClusterRole(Resource): + # Grouped resource — version discovered from cluster + api_group = Resource.ApiGroup.RBAC_AUTHORIZATION_K8S_IO +``` + +> **Note:** The `Resource.ApiGroup` and `Resource.ApiVersion` inner classes provide predefined constants for all known API groups and versions. Using these constants avoids typos and makes your code self-documenting. + +### NamespacedResource: Adding Namespace Awareness + +`NamespacedResource` extends `Resource` with one critical addition: **namespace handling**. It requires a `namespace` argument during construction and injects the namespace into all API calls. + +The differences from `Resource` are focused but important: + +| Behavior | `Resource` | `NamespacedResource` | +|---|---|---| +| `namespace` required? | No | Yes (unless using `yaml_file` or `kind_dict`) | +| `to_dict()` output | No namespace in metadata | Adds `metadata.namespace` | +| `get()` yields | Objects with `name` only | Objects with both `name` and `namespace` | +| `instance` property | Fetches by name only | Fetches by name and namespace | + +```python +from ocp_resources.pod import Pod + +# Namespace is required for namespaced resources +pod = Pod(client=client, name="my-pod", namespace="default", + containers=[{"name": "app", "image": "nginx"}]) +``` + +### Concrete Resource Classes: Where Specifics Live + +Concrete classes add three things on top of the base classes: + +1. **API identity** — Setting `api_group` or `api_version` to identify which Kubernetes API to call +2. **Constructor parameters** — Typed arguments for the resource's spec fields (like `containers` for Pod, `replicas` for Deployment) +3. **Custom `to_dict()` method** — Builds the Kubernetes resource dictionary from constructor arguments + +Here is how a typical generated class is structured: + +```python +class Deployment(NamespacedResource): + # 1. API identity + api_group: str = NamespacedResource.ApiGroup.APPS + + def __init__(self, replicas=None, selector=None, template=None, **kwargs): + # 2. Pass common args to base class, store kind-specific args + super().__init__(**kwargs) + self.replicas = replicas + self.selector = selector + self.template = template + + def to_dict(self): + # 3. Build the resource dictionary + super().to_dict() + if not self.kind_dict and not self.yaml_file: + self.res["spec"] = {} + _spec = self.res["spec"] + _spec["selector"] = self.selector + _spec["template"] = self.template + if self.replicas is not None: + _spec["replicas"] = self.replicas +``` + +> **Tip:** You can always bypass the typed constructor entirely by passing `yaml_file` or `kind_dict` to any resource class. When you do, the `to_dict()` logic is skipped and the resource is created from your raw definition instead. + +#### Adding Kind-Specific Behavior + +Many concrete classes go beyond what the generator produces by adding custom methods and properties. For example: + +- **Pod** adds `execute()` for running commands, `log()` for reading logs, and a `node` property +- **Deployment** adds `scale_replicas()` and `wait_for_replicas()` +- **Secret** overrides `keys_to_hash` to ensure sensitive data is masked in logs + +These additions are preserved across regeneration because the class generator recognizes the `# End of generated code` marker and keeps any code written below it. + +### How Generated Classes Are Created + +Most concrete resource classes in the library are **code-generated** from the cluster's OpenAPI schema using the `class-generator` tool. The generator: + +1. Reads the resource definition from the cluster's OpenAPI schema +2. Determines whether the resource is namespaced (→ `NamespacedResource`) or cluster-scoped (→ `Resource`) +3. Extracts spec fields with their types and descriptions +4. Renders a Python class from a Jinja2 template +5. Preserves any hand-written code below the `# End of generated code` marker + +This means the hierarchy is not just an architectural choice — it is **enforced by the code generation pipeline**. Every generated class correctly inherits from the appropriate base class based on the resource's actual scope in Kubernetes. + +See [Generating New Resource Classes with class-generator](generating-resource-classes.html) for details on generating classes for new resource types. + +--- + +## How It Affects You + +Understanding the hierarchy helps you in several practical ways: + +### Knowing What Methods Are Available + +Every resource — regardless of kind — inherits the full set of CRUD, waiting, and lifecycle methods from `Resource`. You don't need to check whether a particular resource supports `wait_for_condition()` or context managers; they all do. + +```python +# Works for any resource type +with SomeResource(client=client, name="example", **specific_args) as res: + res.wait_for_condition(condition="Ready", status="True") +``` + +See [Resource and NamespacedResource API](resource-api.html) for the complete method reference. + +### Understanding Constructor Requirements + +The base class determines what arguments are mandatory: + +| If you're using... | Required arguments | +|---|---| +| `Resource` subclass | `client`, `name` | +| `NamespacedResource` subclass | `client`, `name`, `namespace` | +| Any class with `yaml_file` | `client`, `yaml_file` | +| Any class with `kind_dict` | `client`, `kind_dict` | + +See [Creating and Managing Resources](creating-and-managing-resources.html) for full examples of all creation methods. + +### Using Status and Condition Constants + +The `Status` and `Condition` constants inherited from `ResourceConstants` are available on every resource class. Concrete classes may extend them: + +```python +from ocp_resources.virtual_machine import VirtualMachine + +# Base constants work on all resources +vm.wait_for_status(status=VirtualMachine.Status.RUNNING) + +# Kind-specific constants are added by the concrete class +vm.wait_for_status(status=VirtualMachine.Status.STOPPED) +``` + +### Extending the Library + +If you need to work with a CRD that isn't included in the library, you have two options: + +1. **Use the class generator** to scaffold a new class automatically — see [Generating New Resource Classes with class-generator](generating-resource-classes.html) +2. **Write a class manually** by inheriting from `Resource` or `NamespacedResource` and setting `api_group` or `api_version` + +A minimal hand-written resource class looks like this: + +```python +from ocp_resources.resource import NamespacedResource + +class MyCustomResource(NamespacedResource): + api_group = "example.com" + # That's it — you get full CRUD, waiting, and lifecycle support +``` + +> **Warning:** Your class name must match the Kubernetes `kind` exactly (in PascalCase). The `kind` property is derived from the class name automatically — a class named `MyCustomResource` will have `kind = "MyCustomResource"`. + +--- + +## Related Pages + +- [Resource and NamespacedResource API](resource-api.html) — Complete method and property reference for the base classes +- [Creating and Managing Resources](creating-and-managing-resources.html) — Practical guide to creating, updating, and deleting resources +- [Generating New Resource Classes with class-generator](generating-resource-classes.html) — Generate Python classes for any CRD +- [Querying and Watching Resources](querying-resources.html) — Use the `get()` class method and watchers to list and observe resources +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) — Use the waiting methods inherited from the base classes +- [Validating Resources Against OpenAPI Schemas](validating-resources.html) — Validate resources using the built-in schema validation +- [Environment Variables and Configuration](environment-variables.html) — Configure logging, resource reuse, and teardown behavior + +## Related Pages + +- [Resource and NamespacedResource API](resource-api.html) +- [Generating New Resource Classes with class-generator](generating-resource-classes.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) +- [Validating Resources Against OpenAPI Schemas](validating-resources.html) + +--- + +Source: schema-validation-internals.md + +Let me start by reading the knowledge graph and the pages manifest, then explore the relevant source files. + +Now let me look at some more key files - the explain parser, resource base class validation, archive utils, and some test files: + +Now let me look at the exceptions module for validation: + +Now let me check the CI/CD configuration to understand how schemas are updated: + +Let me look at a few generated resource files to understand the output of the code generation: + +Let me also check the parsers for status schema generation: + +Now let me check what data is actually in the schema file to understand the structure: + +Now I have enough information. Let me write the documentation page. + +# Schema Validation and Code Generation Architecture + +The openshift-python-wrapper uses OpenAPI schemas as the single source of truth for two critical capabilities: **validating resource definitions** before they reach the cluster, and **generating Python wrapper classes** for any Kubernetes or OpenShift resource type. Understanding this architecture helps you diagnose validation errors, generate classes for custom resources, and keep your schemas up to date as clusters evolve. + +## The Big Picture + +OpenAPI schemas flow through the system in two directions: they are fetched from a live cluster and stored locally, then consumed by the validation engine at runtime and by the code generator at development time. + +| Component | Location | Purpose | +|---|---|---| +| **Schema Fetcher** | `class_generator/core/schema.py` | Connects to a cluster, downloads OpenAPI v3 schemas, and writes them to disk | +| **Resource Mappings** | `class_generator/schema/__resources-mappings.json.gz` | Compressed archive mapping each resource kind (lowercase) to its schema(s) | +| **Definitions File** | `class_generator/schema/_definitions.json` | JSON file containing detailed schema definitions with `$ref` targets for nested types | +| **Schema Validator** | `ocp_resources/utils/schema_validator.py` | Runtime engine that loads schemas, resolves `$ref` references, and validates resource dicts | +| **Code Generator** | `class_generator/core/generator.py` | Reads schemas and generates Python class files from a Jinja2 template | +| **Explain Parser** | `class_generator/parsers/explain_parser.py` | Parses the resource mapping to extract fields, types, and group-version-kind metadata | +| **Cluster Version Tracker** | `class_generator/schema/__cluster_version__.txt` | Tracks the cluster version that last produced the schema, controlling update strategy | + +### Data Flow: Schema Fetch and Storage + +1. **Detect client binary** — The system finds `oc` or falls back to `kubectl` via `get_client_binary()`. +2. **Check cluster version** — `check_and_update_cluster_version()` compares the connected cluster's version against the stored version in `__cluster_version__.txt`. +3. **Determine update strategy** — If the cluster is the same version or newer, all schemas are fetched and existing entries are updated. If older, only missing resources are fetched and existing schemas are preserved. +4. **Fetch OpenAPI v3 index** — `GET /openapi/v3` returns an index of all API group paths (e.g., `api/v1`, `apis/apps/v1`). +5. **Fetch schemas in parallel** — `fetch_all_api_schemas()` downloads schemas from each API path using a thread pool (up to 10 workers). +6. **Build namespacing dictionary** — `build_namespacing_dict()` queries `api-resources` to determine which kinds are namespaced vs. cluster-scoped. +7. **Process definitions** — `process_schema_definitions()` extracts `x-kubernetes-group-version-kind` metadata, builds schema entries, and merges them into the resource mappings. Existing resources are **never deleted** — only added or updated. +8. **Supplement with `oc explain`** — Missing field descriptions, required-field markers, and `$ref` definitions are filled in by running `oc explain` commands in parallel. +9. **Write to disk** — The resource mappings are saved as a gzip-compressed JSON archive (`__resources-mappings.json.gz`), and definitions are written to `_definitions.json`. + +### Data Flow: Validation at Runtime + +1. **Load on first use** — `SchemaValidator.load_mappings_data()` decompresses and loads the mappings archive and definitions file into class-level caches. +2. **Look up by kind** — `SchemaValidator.load_schema(kind, api_group)` finds the schema for a resource kind (case-insensitive). When multiple API groups define the same kind (e.g., `Ingress` in `networking.k8s.io` and `config.openshift.io`), the `api_group` parameter disambiguates. +3. **Resolve `$ref` references** — The validator recursively resolves all `$ref` pointers against the definitions data, producing a self-contained schema. +4. **Cache resolved schemas** — Resolved schemas are stored in `_schema_cache` keyed by `api_group:kind`, so subsequent validations are fast. +5. **Validate with jsonschema** — `jsonschema.validate()` checks the resource dictionary against the resolved schema. +6. **Format errors** — If validation fails, `format_validation_error()` produces a human-readable message including the field path, error details, and schema context. + +### Data Flow: Code Generation + +1. **Read resource mappings** — `parse_explain()` loads the mapping file and finds all schema entries for the requested kind. +2. **Select latest API version** — When multiple versions exist within an API group, the latest version is selected using a priority ranking (`v2 > v1 > v1beta2 > v1beta1 > v1alpha2 > v1alpha1`). +3. **Extract fields and types** — The `type_parser` module converts OpenAPI types to Python type annotations and builds parameter dictionaries for the class constructor. +4. **Render Jinja2 template** — `render_jinja_template()` processes `class_generator_template.j2` with the extracted data, producing a complete Python class. +5. **Preserve user code** — If the target file already exists, `parse_user_code_from_file()` extracts any user-added code below the `# End of generated code` marker and re-inserts it after regeneration. +6. **Format and write** — `write_and_format_rendered()` writes the file, then runs `prek` (pre-commit hooks) or falls back to `ruff` for formatting. + +## Key Concepts + +### The Resource Mappings File + +The resource mappings file (`__resources-mappings.json.gz`) is the central schema store. It maps lowercase kind names to arrays of schema objects: + +```json +{ + "pod": [ + { + "description": "Pod is a collection of containers...", + "properties": { ... }, + "required": [], + "type": "object", + "x-kubernetes-group-version-kind": [ + { "group": "", "kind": "Pod", "version": "v1" } + ], + "namespaced": true + } + ], + "ingress": [ + { "x-kubernetes-group-version-kind": [{ "group": "networking.k8s.io", ... }], ... }, + { "x-kubernetes-group-version-kind": [{ "group": "config.openshift.io", ... }], ... } + ] +} +``` + +Resources with the same kind but different API groups (like `Ingress` above) are stored as separate entries in the array. This structure allows the validator and generator to handle API group disambiguation correctly. + +> **Note:** The mappings file is compressed with gzip to reduce repository size. The `archive_utils` module handles transparent compression and decompression via `save_json_archive()` and `load_json_archive()`. + +### The Definitions File + +The definitions file (`_definitions.json`) contains detailed schema definitions keyed by `group/version/Kind` paths (e.g., `apps/v1/Deployment`, `v1/Pod`). It also stores referenced sub-schemas like `io.k8s.api.core.v1.PodSpec` that are targets of `$ref` pointers. During validation, the `SchemaValidator` uses this file as the reference store for resolving nested types. + +### SchemaValidator + +`SchemaValidator` is a class with only class-level methods and caches — you never need to instantiate it. It serves as the shared validation engine used by both `Resource.validate()` and `Resource.validate_dict()`. + +```python +from ocp_resources.utils.schema_validator import SchemaValidator +``` + +| Method | Signature | Description | +|---|---|---| +| `load_mappings_data` | `(skip_cache: bool = False) -> bool` | Loads the mappings archive and definitions file. Returns `True` on success. | +| `get_mappings_data` | `(skip_cache: bool = False) -> dict[str, Any] \| None` | Returns loaded mappings data, loading first if needed. | +| `get_definitions_data` | `() -> dict[str, Any] \| None` | Returns loaded definitions data, loading first if needed. | +| `load_schema` | `(kind: str, api_group: str \| None = None) -> dict[str, Any] \| None` | Loads and resolves a complete schema for a kind. Handles API group disambiguation and caching. | +| `validate` | `(resource_dict: dict[str, Any], kind: str, api_group: str \| None = None) -> None` | Validates a resource dict. Raises `jsonschema.ValidationError` on failure. | +| `format_validation_error` | `(error, kind, name, api_group=None) -> str` | Formats a validation error into a user-friendly message with field path and context. | +| `clear_cache` | `() -> None` | Clears the resolved schema cache (not the raw mappings/definitions data). | + +> **Tip:** The `load_schema` method resolves `$ref` references recursively. For core Kubernetes types like `ObjectMeta` or `TypeMeta` that may be missing from definitions, it falls back to a permissive `{"type": "object", "additionalProperties": true}` schema rather than failing outright. + +### Resource Validation Methods + +The `Resource` base class exposes two validation methods that delegate to `SchemaValidator`: + +```python +from ocp_resources.resource import Resource + +# Instance-level validation — validates self.res +resource.validate() + +# Class-level validation — validates any dict against the class's schema +MyResource.validate_dict(resource_dict) +``` + +Both raise `ocp_resources.exceptions.ValidationError` (not the raw `jsonschema.ValidationError`) with formatted error messages that include the resource identifier, field path, and details. + +**Auto-validation** can be enabled per-instance by passing `schema_validation_enabled=True` to the constructor: + +```python +from ocp_resources.pod import Pod + +pod = Pod( + name="my-pod", + namespace="default", + containers=[{"name": "app", "image": "nginx"}], + client=client, + schema_validation_enabled=True, +) +# validation runs automatically before create() and update_replace() +pod.deploy() +``` + +When auto-validation is enabled: +- `create()` calls `self.validate()` after `to_dict()` builds the resource dictionary +- `update_replace()` calls `validate_dict()` on the replacement resource dictionary +- `update()` (patch operations) does **not** trigger validation because patches are partial documents that may not pass full schema validation + +### Schema Update Strategies + +The `update_kind_schema()` function employs a version-aware update strategy: + +| Cluster Version | Behavior | +|---|---| +| **Same or newer** than last recorded | Fetches **all** API schemas, updates existing resources, supplements with `oc explain` data | +| **Older** than last recorded | Identifies only **missing** resources via `identify_missing_resources()`, fetches only relevant API paths, does **not** modify existing schemas | + +This strategy prevents an older cluster from overwriting schemas that contain richer data from a newer cluster. + +> **Warning:** If you connect to an older cluster and need to update a specific CRD's schema (for example, after installing a new operator), use `update_single_resource_schema(kind)` or the CLI flag `--update-schema-for `. This bypasses the version guard for that one resource. + +### Single-Resource Schema Updates + +The `update_single_resource_schema()` function fetches the schema for exactly one resource kind without affecting other resources in the mapping: + +```python +from class_generator.core.schema import update_single_resource_schema + +update_single_resource_schema(kind="LlamaStackDistribution") +``` + +| Parameter | Type | Description | +|---|---|---| +| `kind` | `str` | The resource Kind (case-sensitive, e.g., `"LlamaStackDistribution"`) | +| `client` | `str \| None` | Client binary path. Auto-detected if `None`. | + +**Raises:** +- `ResourceNotFoundError` — if the kind is not found on the cluster +- `RuntimeError` — if schema fetching fails + +### Code Generation Pipeline + +The `class_generator()` function orchestrates the full generation pipeline: + +```python +from class_generator.core.generator import class_generator + +generated_files = class_generator(kind="Deployment", overwrite=True) +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `kind` | `str` | (required) | Kubernetes resource kind | +| `overwrite` | `bool` | `False` | Overwrite existing files | +| `dry_run` | `bool` | `False` | Print output without writing | +| `output_file` | `str` | `""` | Specific output file path | +| `output_dir` | `str` | `""` | Output directory (defaults to `ocp_resources`) | +| `add_tests` | `bool` | `False` | Generate test files in `class_generator/tests/manifests/` | + +**Returns:** `list[str]` — paths of generated files. + +**Raises:** +- `RuntimeError` — if the kind is not found in schema mappings +- `ValueError` — if the generated filename contains invalid patterns + +When a kind exists in multiple API groups (e.g., `Ingress` in both `networking.k8s.io` and `config.openshift.io`), the generator produces **separate files** with a group-based suffix (e.g., `ingress_networking_k8s_io.py`, `ingress_config_openshift_io.py`). + +### Generated Class Structure + +Every generated file follows a consistent structure produced by the Jinja2 template: + +```python +# Generated using https://github.com/RedHatQE/openshift-python-wrapper/blob/main/class_generator/README.md + +from typing import Any +from ocp_resources.resource import NamespacedResource + +class Pod(NamespacedResource): + """Pod is a collection of containers that can run on a host.""" + + api_version: str = NamespacedResource.ApiVersion.V1 + + def __init__(self, containers: list[Any] | None = None, ..., **kwargs: Any) -> None: + """Args: + containers (list[Any]): List of containers belonging to the pod. + ... + """ + super().__init__(**kwargs) + self.containers = containers + ... + + def to_dict(self) -> None: + super().to_dict() + if not self.kind_dict and not self.yaml_file: + # Required fields raise MissingRequiredArgumentError + # Optional fields added only if not None + ... + # End of generated code +``` + +User-added code placed **after** the `# End of generated code` marker is preserved across regenerations. + +### `$ref` Resolution + +OpenAPI schemas extensively use `$ref` pointers to reference shared type definitions (e.g., `"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"`). The `SchemaValidator._resolve_refs()` method handles this by: + +1. Parsing the reference path to extract the definition name +2. Looking up the definition in `_definitions_data` using multiple key formats (full dotted path, short name, or `version/Kind` format) +3. Recursively resolving any `$ref` pointers within the resolved definition +4. Falling back to a permissive object schema for well-known core types that may be missing + +### Schema Supplementation via `oc explain` + +The raw OpenAPI v3 schemas from the cluster sometimes lack field descriptions and required-field markers. The system supplements these gaps by: + +- Running `oc explain .` commands **in parallel** for critical resource types (Pod, Deployment, Service, etc.) +- Parsing the explain output to extract field descriptions, types, and required-field markers +- Merging descriptions into existing schema properties where they are missing +- Detecting missing `$ref` targets and fetching their definitions via `oc explain --recursive` + +This supplementation only runs during a **full update** (same or newer cluster version), not when only fetching missing resources. + +## How It Affects You + +| What you do | What happens under the hood | +|---|---| +| Call `resource.validate()` | `SchemaValidator` loads schemas from the compressed archive, resolves `$ref` references, validates with jsonschema, and raises `ValidationError` with a formatted message | +| Pass `schema_validation_enabled=True` | `create()` and `update_replace()` automatically validate before sending to the API server | +| Run `class-generator --update-schema` | Full schema fetch from cluster, version check, parallel API downloads, `oc explain` supplementation, and compressed archive write | +| Run `class-generator -k Pod` | Reads the mapping for `pod`, parses fields/types, renders Jinja2 template, preserves user code, writes and formats the Python file | +| Run `class-generator --update-schema-for MyResource` | Single-resource fetch that bypasses the cluster version guard | +| Run `class-generator --regenerate-all` | Discovers all generated files (by marker comment), regenerates each in parallel, preserving user code | +| Validation reports "No schema found" | The resource kind is not in the mappings archive — run `--update-schema` to fetch it | +| Multiple files generated for one kind | The kind exists in multiple API groups — each group gets its own file with a suffix | + +## Related Pages + +- See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for a hands-on guide to enabling and using validation. +- See [Generating New Resource Classes with class-generator](generating-resource-classes.html) for step-by-step class generation instructions. +- See [class-generator CLI Reference](class-generator-cli.html) for all CLI options including `--update-schema`, `--update-schema-for`, `--regenerate-all`, and `--coverage-report`. +- See [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) to learn how generated classes fit into the `Resource` / `NamespacedResource` inheritance tree. +- See [Resource and NamespacedResource API](resource-api.html) for complete documentation of the `validate()` and `validate_dict()` methods. +- See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for how the fake client uses schema-based status templates. + +## Related Pages + +- [Validating Resources Against OpenAPI Schemas](validating-resources.html) +- [Generating New Resource Classes with class-generator](generating-resource-classes.html) +- [class-generator CLI Reference](class-generator-cli.html) +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) +- [Resource and NamespacedResource API](resource-api.html) + +--- + +Source: working-with-events.md + +Let me start by exploring the repository structure and the knowledge graph. + +Now let me explore the Event-related source code: + +Now let me look at the `_build_event_field_selector` function and related event test files: + +Now let me check the `events` test files in the tests directory: + +Now let me check the resource.py import and the `events` method more carefully, and also look at how TIMEOUT_4MINUTES is defined: + +Let me check where TIMEOUT_4MINUTES is defined in resource.py: + +Now let me check the constants value for TIMEOUT_4MINUTES: + +Now let me check how the Event class is used in the fake client: + +Now let me look at the `_extract_event_info` and the `_process_watch_events` functions to understand the event data structure better: + +Now let me look at the event object structure from watch events: + +Now let me check how to properly import and use Event: + +Let me also look at the test file for events in more detail: + +Now let me check the watch method's return structure for events: + +Now I have a thorough understanding. Let me write the documentation. + +# Working with Kubernetes Events + +Monitor, retrieve, and clean up Kubernetes events to debug resource behavior and automate event-driven workflows using the `Event` class and the `resource.events()` method. + +## Prerequisites + +- A connected Kubernetes/OpenShift client — see [Connecting to Clusters](connecting-to-clusters.html) +- The `ocp_resources` package installed — see [Installing and Creating Your First Resource](quickstart.html) + +## Quick Example + +Stream events from a namespace in real-time: + +```python +from ocp_resources.event import Event + +for event in Event.get(client=client, namespace="my-namespace", timeout=30): + print(f"{event['type']}: {event.object.reason} — {event.object.message}") +``` + +## Streaming Events in Real-Time with `Event.get()` + +`Event.get()` opens a watch connection and yields events as they occur. It's a generator that blocks until the timeout expires or you break out of the loop. + +```python +from ocp_resources.event import Event + +for event in Event.get( + client=client, + namespace="my-namespace", + timeout=60, +): + obj = event.object + print(f"[{obj.type}] {obj.reason}: {obj.message}") +``` + +Each yielded event is a watch event dictionary with these keys: + +| Key | Description | +|----------------|--------------------------------------------------------------------| +| `type` | Watch event type: `ADDED`, `MODIFIED`, or `DELETED` | +| `object` | The Event resource object (access `.type`, `.reason`, `.message`, `.involvedObject`, etc.) | +| `raw_object` | The raw dictionary representation of the event | + +### `Event.get()` Parameters + +| Parameter | Type | Default | Description | +|--------------------|-------------------------|----------|-----------------------------------------------------------| +| `client` | `DynamicClient` | `None` | Kubernetes dynamic client (required) | +| `namespace` | `str \| None` | `None` | Filter events to a specific namespace | +| `name` | `str \| None` | `None` | Filter by event name | +| `label_selector` | `str \| None` | `None` | Filter by labels (e.g. `"app=nginx"`) | +| `field_selector` | `str \| None` | `None` | Filter by fields (e.g. `"type==Warning"`) | +| `resource_version` | `str \| None` | `None` | Start watching from a specific resource version | +| `timeout` | `int \| None` | `None` | Timeout in seconds; `None` watches indefinitely | + +**Returns:** `Generator` — yields watch event dictionaries. + +### Filtering with Field Selectors + +Field selectors let you narrow down events to exactly what you care about. Combine multiple selectors with commas: + +```python +# Only Warning events for ClusterServiceVersion resources +for event in Event.get( + client=client, + namespace="my-namespace", + field_selector="involvedObject.kind==ClusterServiceVersion,type==Warning,reason==AnEventReason", + timeout=10, +): + print(event.object.message) +``` + +Common field selector fields: + +| Field | Example | +|------------------------------|-------------------------------------------------| +| `type` | `type==Warning` or `type==Normal` | +| `reason` | `reason==FailedScheduling` | +| `involvedObject.kind` | `involvedObject.kind==Pod` | +| `involvedObject.name` | `involvedObject.name==my-pod` | +| `involvedObject.namespace` | `involvedObject.namespace==default` | + +## Listing Existing Events with `Event.list()` + +Unlike `Event.get()`, which streams events in real-time, `Event.list()` returns existing events immediately as a list. By default it only returns events from the last 5 minutes. + +```python +from ocp_resources.event import Event + +# List all Warning events from the last 5 minutes +events = Event.list( + client=client, + namespace="my-namespace", + field_selector="type==Warning", +) + +for event in events: + print(f"{event.reason}: {event.message}") +``` + +Results are sorted by `lastTimestamp` descending (most recent first). + +### `Event.list()` Parameters + +| Parameter | Type | Default | Description | +|------------------|---------------------|---------|-----------------------------------------------------------| +| `client` | `DynamicClient` | — | Kubernetes dynamic client (required) | +| `namespace` | `str \| None` | `None` | Filter events to a specific namespace | +| `field_selector` | `str \| None` | `None` | Filter by fields (e.g. `"type==Warning"`) | +| `label_selector` | `str \| None` | `None` | Filter by labels | +| `since_seconds` | `int` | `300` | Only return events from the last N seconds | + +**Returns:** `list` — event resource objects sorted by timestamp (most recent first). + +**Raises:** `ValueError` — if `since_seconds` is negative. + +```python +# Events from the last 30 minutes +events = Event.list(client=client, since_seconds=1800) + +# All events (no time filter — uses a very large window) +events = Event.list(client=client, since_seconds=999999) +``` + +### When to Use `Event.get()` vs `Event.list()` + +| Use case | Method | +|--------------------------------------------------|----------------| +| Watch for new events as they happen | `Event.get()` | +| Fetch events that already occurred | `Event.list()` | +| Collect events during a test run | `Event.get()` | +| Check recent events after a failure | `Event.list()` | +| Stream events with a timeout | `Event.get()` | +| Get a snapshot sorted by time | `Event.list()` | + +## Getting Events for a Specific Resource + +Every resource instance has an `.events()` method that automatically filters events to that specific resource by setting `involvedObject.name` in the field selector. + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") + +for event in pod.events(timeout=10): + print(f"{event.object.reason}: {event.object.message}") +``` + +You can add extra filters on top of the automatic resource filter: + +```python +# Only Warning events for this specific pod +for event in pod.events( + field_selector="type==Warning", + timeout=10, +): + print(event.object.message) +``` + +### `resource.events()` Parameters + +| Parameter | Type | Default | Description | +|--------------------|---------|---------|-----------------------------------------------------------| +| `name` | `str` | `""` | Filter by event name | +| `label_selector` | `str` | `""` | Filter by labels | +| `field_selector` | `str` | `""` | Additional field selectors (combined with `involvedObject.name` automatically) | +| `resource_version` | `str` | `""` | Start watching from a specific resource version | +| `timeout` | `int` | `240` | Timeout in seconds (default: 4 minutes) | + +**Returns:** `Generator` — yields watch event dictionaries, same format as `Event.get()`. + +> **Note:** The `field_selector` you provide is appended to the automatic `involvedObject.name==` filter. You don't need to specify the resource name yourself. + +## Deleting Events with `Event.delete_events()` + +Clean up events before a test run to avoid false positives from stale events: + +```python +from ocp_resources.event import Event + +# Delete all events in a namespace +Event.delete_events(client=client, namespace="my-namespace") + +# Delete events matching a specific reason +Event.delete_events( + client=client, + namespace="my-namespace", + field_selector="reason==AnEventReason", +) +``` + +### `Event.delete_events()` Parameters + +| Parameter | Type | Default | Description | +|--------------------|-------------------------|----------|-----------------------------------------------------------| +| `client` | `DynamicClient` | `None` | Kubernetes dynamic client (required) | +| `namespace` | `str \| None` | `None` | Target namespace | +| `name` | `str \| None` | `None` | Specific event name to delete | +| `label_selector` | `str \| None` | `None` | Filter by labels | +| `field_selector` | `str \| None` | `None` | Filter by fields | +| `resource_version` | `str \| None` | `None` | Filter by resource version | +| `timeout` | `int \| None` | `None` | Timeout in seconds | + +**Returns:** `None` + +## Advanced Usage + +### Test Setup: Clean Events Before Each Test + +A common pattern is deleting events before a test so that only events generated during the test are captured: + +```python +from ocp_resources.event import Event + + +def test_pod_scheduling(client, namespace): + # Clean slate — remove old events + Event.delete_events(client=client, namespace=namespace) + + # ... create resources and trigger the behavior under test ... + + # Verify expected events occurred + events = Event.list( + client=client, + namespace=namespace, + field_selector="reason==Scheduled", + since_seconds=60, + ) + assert len(events) > 0, "Pod was not scheduled" +``` + +### Collecting Events During an Operation + +Use `Event.get()` with a timeout to capture all events that occur during an operation: + +```python +from ocp_resources.event import Event + +events = [] +for event in Event.get( + client=client, + namespace="my-namespace", + field_selector="involvedObject.kind==Deployment", + timeout=30, +): + events.append(event.object) + if event.object.reason == "ScalingReplicaSet": + break # Found what we were looking for + +print(f"Captured {len(events)} events") +``` + +### Watching Cluster-Wide Events + +Omit the `namespace` parameter to watch events across all namespaces: + +```python +from ocp_resources.event import Event + +# Watch all Warning events cluster-wide +for event in Event.get( + client=client, + field_selector="type==Warning", + timeout=60, +): + ns = event.object.involvedObject.get("namespace", "cluster-scoped") + print(f"[{ns}] {event.object.reason}: {event.object.message}") +``` + +### Accessing Event Object Properties + +The `event.object` yielded by `Event.get()` and `resource.events()` is a Kubernetes `ResourceInstance` with these commonly used attributes: + +| Attribute | Description | +|-------------------------------|----------------------------------------------| +| `event.object.type` | `"Normal"` or `"Warning"` | +| `event.object.reason` | Short reason string (e.g. `"Scheduled"`) | +| `event.object.message` | Human-readable event message | +| `event.object.count` | Number of times this event occurred | +| `event.object.firstTimestamp` | When the event first occurred | +| `event.object.lastTimestamp` | When the event most recently occurred | +| `event.object.source` | Dict with `component` and `host` | +| `event.object.involvedObject` | Dict with `kind`, `name`, `namespace`, etc. | + +## Troubleshooting + +**`Event.get()` hangs indefinitely** +You likely omitted the `timeout` parameter. Always set a `timeout` value to avoid blocking forever: + +```python +# Bad — hangs if no events arrive +for event in Event.get(client=client, namespace="ns"): + ... + +# Good — stops after 30 seconds +for event in Event.get(client=client, namespace="ns", timeout=30): + ... +``` + +**`Event.list()` returns an empty list** +- Check the `since_seconds` value. The default is 300 (5 minutes). If the events are older, increase the value. +- Verify the `namespace` is correct. +- Ensure the `field_selector` syntax uses `==` (double equals), not `=`. + +**`resource.events()` returns events for other resources too** +This method filters only by `involvedObject.name`. If multiple resources share the same name across different kinds, add a `field_selector` to narrow it down: + +```python +for event in pod.events( + field_selector="involvedObject.kind==Pod", + timeout=10, +): + print(event.object.message) +``` + +> **Warning:** The `dyn_client` parameter on `Event.get()` and `Event.delete_events()` is deprecated and will be removed in the next major release. Use `client` instead. + +## Related Pages + +- [Querying and Watching Resources](querying-resources.html) +- [Common Resource Patterns](common-patterns.html) +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) + +--- + +Source: pod-execution-and-logs.md + +Let me explore the repository structure and understand the codebase first. + +Now let me check the `read_namespaced_pod_log` kwargs available, and look at how the MCP server uses log parameters: + +Now let me check the import paths and verify the `get_client` function: + +Now let me check test files for pod execution tests: + +Now let me also verify whether the Node class has a `name` property: + +Now I have all the information I need to write the documentation. Let me compose it: + +# Executing Commands in Pods and Retrieving Logs + +Run commands inside running pods and retrieve their logs to debug and interact with workloads on your OpenShift or Kubernetes cluster. + +## Prerequisites + +- A connected cluster client (see [Connecting to Clusters](connecting-to-clusters.html)) +- An existing, running pod you want to interact with + +## Quick Example + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() +pod = Pod(client=client, name="my-app", namespace="default") + +# Run a command inside the pod +output = pod.execute(command=["echo", "hello"]) +print(output) # "hello\n" + +# Get pod logs +logs = pod.log() +print(logs) +``` + +## Executing Commands with `Pod.execute()` + +`Pod.execute()` runs a command inside a pod container using the Kubernetes exec API and returns the standard output as a string. + +### Method Signature + +```python +Pod.execute( + command: list[str], + timeout: int = 60, + container: str = "", + ignore_rc: bool = False, +) -> str +``` + +| Parameter | Type | Default | Description | +|-------------|--------------|---------|---------------------------------------------------------------------------------------------| +| `command` | `list[str]` | — | The command to run, as a list of strings (e.g., `["ls", "-la", "/tmp"]`) | +| `timeout` | `int` | `60` | Maximum seconds to wait for the command to complete | +| `container` | `str` | `""` | Container name to execute in. If empty, uses the first container in the pod spec | +| `ignore_rc` | `bool` | `False` | When `True`, return stdout even if the command exits with a non-zero return code | + +**Returns:** `str` — the standard output of the command. + +**Raises:** `ExecOnPodError` — when the command fails (non-zero exit code) and `ignore_rc` is `False`. + +### Step-by-Step: Running a Simple Command + +1. Get a reference to the pod: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() +pod = Pod(client=client, name="my-app", namespace="my-namespace") +``` + +2. Execute a command: + +```python +output = pod.execute(command=["cat", "/etc/hostname"]) +print(output) +``` + +3. Use the result for further logic: + +```python +files = pod.execute(command=["ls", "/app/data"]) +for filename in files.strip().split("\n"): + print(f"Found: {filename}") +``` + +### Selecting a Container + +For multi-container pods, specify which container to run the command in. If you omit `container`, the first container defined in the pod spec is used. + +```python +# Execute in a specific container +output = pod.execute( + command=["cat", "/var/log/app.log"], + container="sidecar", +) +``` + +### Setting a Timeout + +Long-running commands may need a longer timeout. The default is 60 seconds. + +```python +# Allow up to 5 minutes for a heavy operation +output = pod.execute( + command=["pg_dump", "mydb"], + timeout=300, +) +``` + +When the timeout is exceeded, an `ExecOnPodError` is raised with the error message `"stream resp is closed"`. + +### Ignoring Non-Zero Exit Codes + +Some commands return a non-zero exit code as part of normal operation (e.g., `grep` returns 1 when no match is found). Use `ignore_rc=True` to get the output regardless: + +```python +# grep returns exit code 1 when there's no match — don't treat that as an error +output = pod.execute( + command=["grep", "ERROR", "/var/log/app.log"], + ignore_rc=True, +) +if output: + print("Errors found:", output) +else: + print("No errors in logs") +``` + +### Handling Errors with `ExecOnPodError` + +When a command fails, `ExecOnPodError` provides structured access to the failure details. + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ExecOnPodError + +try: + pod.execute(command=["ls", "/nonexistent"]) +except ExecOnPodError as e: + print(f"Command: {e.cmd}") # ['ls', '/nonexistent'] + print(f"Return code: {e.rc}") # Non-zero exit code (or -1 for stream errors) + print(f"Stdout: {e.out}") # Standard output captured before failure + print(f"Stderr: {e.err}") # Standard error or error channel details +``` + +`ExecOnPodError` attributes: + +| Attribute | Type | Description | +|-----------|-------------|----------------------------------------------------------------------| +| `cmd` | `list[str]` | The command that was executed | +| `rc` | `int` | Return code (`-1` for stream/timeout errors, otherwise the exit code)| +| `out` | `str` | Standard output captured from the command | +| `err` | `str` or `dict` | Standard error output, or the Kubernetes error channel response | + +> **Tip:** For a complete list of all custom exceptions, see [Error Handling and Exception Reference](error-handling.html). + +## Retrieving Logs with `Pod.log()` + +`Pod.log()` returns the logs from a pod container as a string. It passes keyword arguments directly to the Kubernetes `read_namespaced_pod_log` API. + +### Basic Usage + +```python +logs = pod.log() +print(logs) +``` + +### Common Keyword Arguments + +Pass any parameter supported by the Kubernetes `read_namespaced_pod_log` API: + +| Parameter | Type | Description | +|-----------------|--------|-----------------------------------------------------------| +| `container` | `str` | Container name to get logs from (required for multi-container pods) | +| `previous` | `bool` | Return logs from a previous terminated container instance | +| `tail_lines` | `int` | Number of lines from the end of the logs to return | +| `since_seconds` | `int` | Only return logs newer than this many seconds | + +### Examples + +```python +# Get logs from a specific container +logs = pod.log(container="nginx") + +# Get the last 50 lines +logs = pod.log(tail_lines=50) + +# Get logs from the last 5 minutes +logs = pod.log(since_seconds=300) + +# Get logs from a crashed container's previous instance +logs = pod.log(container="app", previous=True) +``` + +## Accessing Pod Properties + +Beyond executing commands and reading logs, the `Pod` class provides properties for inspecting the pod's runtime state. + +### Getting the Node + +The `node` property returns a `Node` object for the node where the pod is scheduled: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +for pod in Pod.get(client=client, label_selector="app=my-app"): + node = pod.node + print(f"Pod {pod.name} is running on node {node.name}") +``` + +> **Note:** The `node` property raises an `AssertionError` if the pod has not yet been scheduled to a node. + +### Getting the Pod IP Address + +The `ip` property returns the pod's IP address from its status: + +```python +pod_ip = pod.ip +print(f"Pod IP: {pod_ip}") +``` + +### Getting the Pod Status + +The `status` property (inherited from the base resource class) returns the pod's phase: + +```python +print(f"Pod status: {pod.status}") # e.g., "Running", "Pending", "Succeeded" +``` + +## Advanced Usage + +### Iterating Over Pods with Selectors + +Combine `Pod.get()` with `execute()` or `log()` to debug across multiple pods: + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ExecOnPodError +from ocp_resources.resource import get_client + +client = get_client() + +for pod in Pod.get(client=client, namespace="production", label_selector="app=web"): + try: + uptime = pod.execute(command=["uptime"]) + print(f"{pod.name} on {pod.node.name}: {uptime.strip()}") + except ExecOnPodError as e: + print(f"{pod.name}: command failed — {e}") +``` + +### Executing in Multi-Container Pods + +When pods contain sidecar containers (e.g., logging or proxy containers), always specify the target container explicitly: + +```python +# Main application container +app_output = pod.execute(command=["cat", "/app/config.yaml"], container="app") + +# Envoy sidecar +proxy_stats = pod.execute(command=["curl", "localhost:15000/stats"], container="istio-proxy") + +# Logs from each container +app_logs = pod.log(container="app", tail_lines=100) +proxy_logs = pod.log(container="istio-proxy", tail_lines=100) +``` + +### Collecting Debug Information + +A practical recipe for gathering debugging data from a running pod: + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ExecOnPodError +from ocp_resources.resource import get_client + +client = get_client() +pod = Pod(client=client, name="my-app", namespace="default") + +debug_info = { + "pod_name": pod.name, + "node": pod.node.name, + "ip": pod.ip, + "status": pod.status, + "logs_tail": pod.log(tail_lines=20), +} + +# Safely attempt commands that might fail +for cmd_name, cmd in [("env", ["env"]), ("df", ["df", "-h"]), ("ps", ["ps", "aux"])]: + try: + debug_info[cmd_name] = pod.execute(command=cmd, timeout=10) + except ExecOnPodError: + debug_info[cmd_name] = "command failed" + +for key, value in debug_info.items(): + print(f"--- {key} ---\n{value}\n") +``` + +### Using Pod Context Manager for Temporary Pods + +Create a pod, run commands, and clean up automatically: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +with Pod( + client=client, + name="debug-pod", + namespace="default", + containers=[{ + "name": "debug", + "image": "registry.access.redhat.com/ubi9/ubi:latest", + "command": ["sleep", "3600"], + }], +) as pod: + pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120) + output = pod.execute(command=["cat", "/etc/os-release"]) + print(output) +# Pod is automatically deleted when the context manager exits +``` + +> **Note:** See [Creating and Managing Resources](creating-and-managing-resources.html) for more about context managers and resource lifecycle. + +## Troubleshooting + +### `ExecOnPodError` with `"stream resp is closed"` + +This typically means the command timed out. Increase the `timeout` parameter: + +```python +pod.execute(command=["long-running-script.sh"], timeout=300) +``` + +### `AssertionError: Node not found for pod ` + +The pod is not yet scheduled. Wait for it to be running before accessing `pod.node`: + +```python +pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120) +node = pod.node +``` + +### `ExecOnPodError` with return code `-1` and an error channel dict + +This indicates a Kubernetes-level failure (e.g., container not found, pod not running). Check that: + +- The pod is in `Running` status +- The `container` name matches one defined in the pod spec +- The container has started and is not in `CrashLoopBackOff` + +### Logs return empty string + +The container may not have produced any output yet, or it may have restarted. Try: + +```python +# Check previous container instance +logs = pod.log(previous=True) + +# Or narrow to recent logs +logs = pod.log(since_seconds=60) +``` + +## Related Pages + +- [Common Resource Patterns](common-patterns.html) +- [Working with Kubernetes Events](working-with-events.html) +- [Error Handling and Exception Reference](error-handling.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) + +--- + +Source: error-handling.md + +Let me explore the repository structure and find all relevant exception definitions and usages. + +Now let me look at the `ConditionError` usage more closely and the `MissingTemplateVariables` and `MissingResourceError` exceptions: + +Now let me look at how the deploy method uses ValidationError: + +Now let me see where `validate` is called during `create`: + +Now let me look at the `Pod.execute` method more closely: + +Now let me look at how `client_with_basic_auth` works: + +Now let me see the NNCP methods in full context: + +Let me also look at `MissingTemplateVariables` usage more closely: + +Now let me check the `__init__` of Resource to see how `schema_validation_enabled` is handled: + +Now let me also check the `wait_for_condition` method signature more closely: + +Let me also look at `MissingResourceResError` usage: + +Now let me check the test for `ResourceTeardownError`: + +Let me see how SecretTestExit is defined: + +Let me check the import path for all exceptions: + +Now I have a thorough understanding of the codebase. Let me write the reference documentation. + +# Error Handling and Exception Reference + +All custom exceptions are defined in `ocp_resources.exceptions` and can be imported directly: + +```python +from ocp_resources.exceptions import ( + ExecOnPodError, + MissingRequiredArgumentError, + ResourceTeardownError, + ValidationError, + ConditionError, + NNCPConfigurationFailed, + ClientWithBasicAuthError, + MissingResourceError, + MissingTemplateVariables, +) +``` + +> **Tip:** All exceptions inherit from Python's built-in `Exception` class and can be caught with a bare `except Exception` if needed. + +--- + +## ExecOnPodError + +Raised when a command executed inside a pod via `Pod.execute()` fails. See [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) for full usage details. + +**Import:** + +```python +from ocp_resources.exceptions import ExecOnPodError +``` + +**Constructor Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `command` | `list[str]` | The command that was executed | +| `rc` | `int` | Return code (`-1` if the return code could not be determined) | +| `out` | `str` | Standard output captured from the command | +| `err` | `Any` | Standard error output or error channel details | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `cmd` | `list[str]` | The command that was executed | +| `rc` | `int` | The return code | +| `out` | `str` | Standard output | +| `err` | `Any` | Standard error or error channel dict | + +**Raised by:** `Pod.execute()` in the following scenarios: + +- Command times out (stream response closes before completion) — `rc=-1` +- Error channel returns no status — `rc=-1` +- Error channel status is `"Failure"` — `rc=-1`, `err` contains the full error channel dict +- Command exits with a non-zero exit code — `rc` contains the actual exit code + +**String representation:** + +``` +Command execution failure: ['ls', '/nonexistent'], RC: 2, OUT: , ERR: ls: cannot access '/nonexistent' +``` + +**Example:** + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ExecOnPodError + +pod = Pod(client=client, name="my-pod", namespace="default") + +try: + output = pod.execute(command=["ls", "/nonexistent"], timeout=30) +except ExecOnPodError as e: + print(f"Command: {e.cmd}") + print(f"Return code: {e.rc}") + print(f"Stdout: {e.out}") + print(f"Stderr: {e.err}") +``` + +**Handling non-zero exit codes without exceptions:** + +```python +# Use ignore_rc=True to suppress ExecOnPodError on non-zero exit codes +output = pod.execute(command=["grep", "pattern", "/var/log/messages"], ignore_rc=True) +``` + +--- + +## MissingRequiredArgumentError + +Raised when a resource is instantiated without providing required arguments and no `yaml_file` or `kind_dict` was supplied. This error is raised during `to_dict()`, which is called automatically by `create()` and `deploy()`. + +**Import:** + +```python +from ocp_resources.exceptions import MissingRequiredArgumentError +``` + +**Constructor Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `argument` | `str` | Name of the missing required argument(s) | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `argument` | `str` | The missing argument name | + +**String representation:** + +``` +Missing required argument/s. Either provide yaml_file, kind_dict or pass self.containers +``` + +**Raised by:** The `to_dict()` method of resource subclasses when required spec fields are not provided. Examples of resources that raise this exception: + +| Resource Class | Required Arguments | +|---|---| +| `Pod` | `containers` | +| `StorageClass` | `provisioner` | +| `ClusterRoleBinding` | `cluster_role` | +| `ClusterResourceQuota` | `quota`, `selector` | +| `CronJob` | `job_template`, `schedule` | +| `InferenceService` | `predictor` | +| `IPAddressPool` | `addresses` | +| `ResourceQuota` | `hard` | +| `UserDefinedNetwork` | `topology` | + +> **Note:** This exception is **not** raised if you construct the resource using `yaml_file` or `kind_dict`, since those bypass the `to_dict()` logic. + +**Example:** + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import MissingRequiredArgumentError + +try: + pod = Pod(client=client, name="my-pod", namespace="default") + pod.deploy() +except MissingRequiredArgumentError as e: + print(f"Missing: {e.argument}") + # Output: Missing required argument/s. Either provide yaml_file, kind_dict or pass self.containers +``` + +--- + +## ResourceTeardownError + +Raised when a resource's `clean_up()` method returns `False` during context manager exit. This indicates the resource could not be deleted. + +**Import:** + +```python +from ocp_resources.exceptions import ResourceTeardownError +``` + +**Constructor Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `resource` | `Any` | The resource object that failed to tear down | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `resource` | `Any` | The resource object that failed teardown | + +**String representation:** + +``` +Failed to execute teardown for resource +``` + +**Raised by:** `Resource.__exit__()` — the context manager exit handler. Specifically, this is raised when: + +1. The resource was created with `teardown=True` (the default). +2. The `clean_up()` method returns `False`. + +**Example:** + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ResourceTeardownError + +try: + with Pod( + client=client, + name="my-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + ) as pod: + # Use pod... + pass + # clean_up() is called automatically here +except ResourceTeardownError as e: + print(f"Could not delete: {e.resource}") +``` + +> **Tip:** Set `teardown=False` on the resource constructor if you do not want automatic cleanup on context manager exit, and thus never want this exception raised. + +--- + +## ValidationError + +Raised when a resource fails schema validation against the OpenAPI specification. See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for full validation guide. + +**Import:** + +```python +from ocp_resources.exceptions import ValidationError +``` + +**Constructor Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `message` | `str` | *(required)* | Human-readable error description | +| `path` | `str` | `""` | JSONPath to the invalid field (e.g., `"spec.containers[0].image"`) | +| `schema_error` | `Any` | `None` | Original `jsonschema` validation error for debugging | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `message` | `str` | Human-readable error message | +| `path` | `str` | JSONPath to the invalid field | +| `schema_error` | `Any` | Original `jsonschema.ValidationError` if available | + +**String representation:** + +``` +Validation error at 'spec.containers[0].image': Invalid type +``` + +When `path` is empty: + +``` +Validation error: Field is required +``` + +**Raised by:** + +| Method | Trigger | +|--------|---------| +| `resource.validate()` | Called explicitly by user | +| `resource.create()` | When `schema_validation_enabled=True` | +| `resource.update_replace()` | When `schema_validation_enabled=True` | +| `Resource.validate_dict()` | Class method for validating raw dicts | + +> **Note:** `resource.update()` does **not** trigger validation even when `schema_validation_enabled=True`, because updates send partial patches that would fail full schema validation. + +**Example — explicit validation:** + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ValidationError + +pod = Pod(client=client, name="my-pod", namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}]) + +try: + pod.validate() +except ValidationError as e: + print(f"Error: {e.message}") + print(f"Path: {e.path}") + if e.schema_error: + print(f"Original error: {e.schema_error}") +``` + +**Example — auto-validation on create:** + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ValidationError + +try: + pod = Pod( + client=client, + name="my-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + schema_validation_enabled=True, + ) + pod.deploy() +except ValidationError as e: + print(f"Invalid resource: {e}") +``` + +**Example — validate a raw dictionary:** + +```python +from ocp_resources.deployment import Deployment +from ocp_resources.exceptions import ValidationError + +deployment_dict = { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": {"name": "my-deploy"}, + "spec": {"replicas": "three"}, # Wrong type — should be int +} + +try: + Deployment.validate_dict(resource_dict=deployment_dict) +except ValidationError as e: + print(f"Validation failed: {e}") +``` + +--- + +## ConditionError + +Raised when a resource reaches an undesired stop condition during `wait_for_condition()`. See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for details. + +**Import:** + +```python +from ocp_resources.exceptions import ConditionError +``` + +**Constructor Parameters:** Standard `Exception` — accepts a single string message. + +**Raised by:** `Resource.wait_for_condition()` when the `stop_condition` parameter matches before the desired condition is met. + +**`wait_for_condition()` parameters relevant to `ConditionError`:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `condition` | `str` | *(required)* | Condition type to wait for | +| `status` | `str` | *(required)* | Expected status value | +| `timeout` | `int` | `300` | Maximum wait time in seconds | +| `sleep_time` | `int` | `1` | Polling interval in seconds | +| `stop_condition` | `str \| None` | `None` | Condition type that should abort the wait | +| `stop_status` | `str` | `"True"` | Status value for the stop condition | + +**String representation:** + +``` +Deployment my-deploy reached stop_condition 'Failed' in status 'True': +{'type': 'Failed', 'status': 'True', 'reason': 'DeadlineExceeded', 'message': '...'} +``` + +> **Note:** When `stop_condition` is `None` (the default), `ConditionError` is never raised. Instead, `TimeoutExpiredError` is raised if the desired condition is not met within the timeout. + +**Example:** + +```python +from ocp_resources.deployment import Deployment +from ocp_resources.exceptions import ConditionError +from timeout_sampler import TimeoutExpiredError + +deploy = Deployment(client=client, name="my-deploy", namespace="default") + +try: + deploy.wait_for_condition( + condition="Available", + status="True", + timeout=120, + stop_condition="Failed", + stop_status="True", + ) +except ConditionError as e: + print(f"Resource entered failure state: {e}") +except TimeoutExpiredError: + print("Timed out waiting for condition") +``` + +--- + +## NNCPConfigurationFailed + +Raised when a `NodeNetworkConfigurationPolicy` (NNCP) fails to configure on one or more nodes. + +**Import:** + +```python +from ocp_resources.exceptions import NNCPConfigurationFailed +``` + +**Constructor Parameters:** Standard `Exception` — accepts a single string message describing the failure reason and error details. + +**Raised by:** `NodeNetworkConfigurationPolicy.wait_for_status_success()` in two scenarios: + +| Scenario | Message Pattern | +|----------|-----------------| +| No matching node found | `"{name}. Reason: NoMatchingNode"` | +| Configuration failed on nodes | `"Reason: FailedToConfigure\n{error_details}"` | + +> **Note:** When `wait_for_status_success()` catches `TimeoutExpiredError` or `NNCPConfigurationFailed`, it logs the error with node details and re-raises the exception. + +**Example:** + +```python +from ocp_resources.node_network_configuration_policy import NodeNetworkConfigurationPolicy +from ocp_resources.exceptions import NNCPConfigurationFailed +from timeout_sampler import TimeoutExpiredError + +nncp = NodeNetworkConfigurationPolicy( + client=client, + name="my-nncp", + desired_state={"interfaces": [{"name": "eth1", "type": "ethernet", "state": "up"}]}, +) + +try: + nncp.deploy() + nncp.wait_for_status_success() +except NNCPConfigurationFailed as e: + print(f"NNCP configuration failed: {e}") +except TimeoutExpiredError: + print("NNCP configuration timed out") +``` + +--- + +## ClientWithBasicAuthError + +Raised during OAuth-based client authentication when using username/password credentials to connect to an OpenShift cluster. See [Connecting to Clusters](connecting-to-clusters.html) for connection methods. + +**Import:** + +```python +from ocp_resources.exceptions import ClientWithBasicAuthError +``` + +**Constructor Parameters:** Standard `Exception` — accepts a single string message. + +**Raised by:** `client_configuration_with_basic_auth()` in the following scenarios: + +| Scenario | Message | +|----------|---------| +| OAuth well-known endpoint not reachable | `"No well-known file found at endpoint"` | +| Authorization code not returned after login | `"No authorization code found"` | +| No `authorization_endpoint` in OAuth config | `"No authorization_endpoint found in well-known file"` | +| Token exchange fails | `"Failed to authenticate with basic auth"` | + +**Example:** + +```python +from ocp_resources.exceptions import ClientWithBasicAuthError +from ocp_resources.resource import client_configuration_with_basic_auth + +import kubernetes + +configuration = kubernetes.client.Configuration() +configuration.verify_ssl = False + +try: + api_client = client_configuration_with_basic_auth( + username="admin", + password="password", + host="https://api.cluster.example.com:6443", + configuration=configuration, + ) +except ClientWithBasicAuthError as e: + print(f"Authentication failed: {e}") +``` + +--- + +## MissingResourceError + +Raised when a resource object fails to generate its internal representation. + +**Import:** + +```python +from ocp_resources.exceptions import MissingResourceError +``` + +**Constructor Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `name` | `str` | Name of the resource that could not be generated | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `resource_name` | `str` | The resource name | + +**String representation:** + +``` +Failed to generate resource: my-resource +``` + +**Example:** + +```python +from ocp_resources.exceptions import MissingResourceError + +try: + # Operations that may fail to generate a resource + ... +except MissingResourceError as e: + print(f"Resource generation failed: {e.resource_name}") +``` + +--- + +## MissingTemplateVariables + +Raised when rendering a YAML template and not all required template variables have been provided. + +**Import:** + +```python +from ocp_resources.exceptions import MissingTemplateVariables +``` + +**Constructor Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `var` | `str` | Name of the missing template variable | +| `template` | `str` | Path to the template file | + +**Attributes on the caught exception:** + +| Attribute | Type | Description | +|-----------|------|-------------| +| `var` | `str` | The missing variable name | +| `template` | `str` | The template file path | + +**String representation:** + +``` +Missing variables image for template tests/manifests/vm.yaml +``` + +**Example:** + +```python +from ocp_resources.exceptions import MissingTemplateVariables + +try: + result = generate_yaml_from_template(name="my-vm") + # If template also requires 'image', raises MissingTemplateVariables +except MissingTemplateVariables as e: + print(f"Variable '{e.var}' missing in template '{e.template}'") +``` + +--- + +## Common Error-Handling Patterns + +### Catching Multiple Library Exceptions + +```python +from ocp_resources.exceptions import ( + ExecOnPodError, + MissingRequiredArgumentError, + ResourceTeardownError, + ValidationError, +) + +try: + with Pod(client=client, name="worker", namespace="default", + containers=[{"name": "app", "image": "myapp:latest"}], + schema_validation_enabled=True) as pod: + pod.execute(command=["python", "run_task.py"]) +except ValidationError as e: + print(f"Invalid resource spec: {e}") +except ExecOnPodError as e: + print(f"Task failed (rc={e.rc}): {e.err}") +except ResourceTeardownError as e: + print(f"Pod cleanup failed: {e}") +except MissingRequiredArgumentError as e: + print(f"Missing field: {e.argument}") +``` + +### Safe Condition Waiting with Early Abort + +```python +from ocp_resources.exceptions import ConditionError +from timeout_sampler import TimeoutExpiredError + +try: + resource.wait_for_condition( + condition="Ready", + status="True", + timeout=180, + stop_condition="Failed", + stop_status="True", + ) +except ConditionError: + # Resource entered a terminal failure state — no point waiting further + resource.clean_up() + raise +except TimeoutExpiredError: + # Condition not met within timeout — may be transient + print("Resource is taking too long, investigating...") +``` + +### Ignoring Non-Critical Command Failures + +```python +from ocp_resources.exceptions import ExecOnPodError + +# Method 1: Use ignore_rc=True +output = pod.execute(command=["grep", "ERROR", "/var/log/app.log"], ignore_rc=True) + +# Method 2: Catch and inspect the return code +try: + output = pod.execute(command=["test", "-f", "/tmp/lockfile"]) +except ExecOnPodError as e: + if e.rc == 1: + print("Lock file does not exist — proceeding") + else: + raise # Re-raise unexpected errors +``` + +### Validation Before Batch Deployment + +```python +from ocp_resources.exceptions import ValidationError + +resources = [pod1, pod2, deployment1, service1] + +# Pre-validate all resources before deploying any +errors = [] +for resource in resources: + try: + resource.validate() + except ValidationError as e: + errors.append((resource.name, str(e))) + +if errors: + for name, err in errors: + print(f" {name}: {err}") + raise SystemExit("Fix validation errors before deploying") + +for resource in resources: + resource.deploy() +``` + +--- + +## Exception Hierarchy Summary + +| Exception | Parent | Module | +|-----------|--------|--------| +| `ExecOnPodError` | `Exception` | `ocp_resources.exceptions` | +| `MissingRequiredArgumentError` | `Exception` | `ocp_resources.exceptions` | +| `ResourceTeardownError` | `Exception` | `ocp_resources.exceptions` | +| `ValidationError` | `Exception` | `ocp_resources.exceptions` | +| `ConditionError` | `Exception` | `ocp_resources.exceptions` | +| `NNCPConfigurationFailed` | `Exception` | `ocp_resources.exceptions` | +| `ClientWithBasicAuthError` | `Exception` | `ocp_resources.exceptions` | +| `MissingResourceError` | `Exception` | `ocp_resources.exceptions` | +| `MissingTemplateVariables` | `Exception` | `ocp_resources.exceptions` | + +> **Warning:** `MissingResourceResError` is deprecated and will be removed in a future release. Use `MissingResourceError` instead. + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Validating Resources Against OpenAPI Schemas](validating-resources.html) +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) +- [Connecting to Clusters](connecting-to-clusters.html) + +--- diff --git a/docs/llms.txt b/docs/llms.txt new file mode 100644 index 0000000000..36bb7516b6 --- /dev/null +++ b/docs/llms.txt @@ -0,0 +1,45 @@ +# openshift-python-wrapper + +> Manage OpenShift and Kubernetes resources with clean, Pythonic code instead of raw API calls + +## Getting Started + +- [Installing and Creating Your First Resource](quickstart.md): Install the package, connect to a cluster, and create, query, and delete your first Kubernetes resource in under 5 minutes + +## User Guides + +- [Connecting to Clusters](connecting-to-clusters.md): Set up client connections using kubeconfig files, tokens, basic auth, environment variables, or in-cluster config +- [Creating and Managing Resources](creating-and-managing-resources.md): Create, update, replace, and delete resources using deploy/clean_up, context managers, and YAML files +- [Querying and Watching Resources](querying-resources.md): List resources with label/field selectors, check existence, watch for changes, and retrieve resource status and conditions +- [Waiting for Resource Conditions and Status](waiting-for-conditions.md): Wait for resources to reach a desired status or condition using built-in polling with configurable timeouts +- [Validating Resources Against OpenAPI Schemas](validating-resources.md): Validate resource definitions before creating them, enable auto-validation, and troubleshoot schema validation errors +- [Managing Bulk Resources with ResourceList](managing-resource-lists.md): Create and manage multiple similar resources or deploy one resource per namespace using ResourceList and NamespacedResourceList +- [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.md): Apply temporary patches to resources during tests and automatically restore original values using ResourceEditor +- [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.md): Create, start, stop, restart, and migrate VirtualMachine resources and interact with VirtualMachineInstances +- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.md): Write unit tests for Kubernetes code using FakeDynamicClient with CRUD operations, status simulation, and watch events +- [Generating New Resource Classes with class-generator](generating-resource-classes.md): Use the class-generator CLI to scaffold Python wrapper classes for any Kubernetes or OpenShift CRD + +## Recipes + +- [Common Resource Patterns](common-patterns.md): Copy-paste recipes for pods, deployments, networking, RBAC roles, data volumes, and handling resources with duplicate API groups +- [Integrating with AI Assistants via MCP Server](mcp-server-integration.md): Set up and use the MCP server with Cursor, Claude Desktop, or other MCP-compatible AI tools to manage cluster resources + +## Reference + +- [Resource and NamespacedResource API](resource-api.md): Complete API reference for the Resource and NamespacedResource base classes including all methods, properties, and constructor parameters +- [class-generator CLI Reference](class-generator-cli.md): All CLI options for class-generator including kind generation, schema updates, coverage reports, batch regeneration, and test generation +- [Environment Variables and Configuration](environment-variables.md): All supported environment variables for logging, resource reuse, teardown skipping, and hash control + +## Internals + +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.md): Learn how Resource, NamespacedResource, and generated subclasses fit together to extend the library with new resource types +- [Schema Validation and Code Generation Architecture](schema-validation-internals.md): Understand how OpenAPI schemas are fetched, cached, and used for both resource validation and class generation + +## Using Resources + +- [Working with Kubernetes Events](working-with-events.md): How to use the standalone Event class to watch, list, and delete Kubernetes events. Covers Event.get() for streaming events in real-time, Event.list() for fetching existing events with time-based filtering, Event.delete_events() for cleanup, and the resource.events() method for getting events related to a specific resource. Includes field selectors, namespace filtering, and common debugging patterns. +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.md): How to use Pod.execute() to run commands inside pod containers with timeout handling, container selection, and error handling via ExecOnPodError. Covers Pod.log() for retrieving pod logs, accessing pod properties like node, IP address, and practical recipes for debugging running workloads. + +## API Reference + +- [Error Handling and Exception Reference](error-handling.md): Reference for all custom exceptions raised by the library including ExecOnPodError, MissingRequiredArgumentError, ResourceTeardownError, ValidationError, ConditionError, NNCPConfigurationFailed, and ClientWithBasicAuthError. Covers which operations raise each exception, how to catch and handle them, and common error-handling patterns for robust automation scripts. diff --git a/docs/managing-resource-lists.html b/docs/managing-resource-lists.html new file mode 100644 index 0000000000..3e5fbc2792 --- /dev/null +++ b/docs/managing-resource-lists.html @@ -0,0 +1,588 @@ + + + + + + Managing Bulk Resources with ResourceList - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me explore the codebase to understand ResourceList and NamespacedResourceList.

+

Now let me check the example file more carefully and also look at how the NamespacedResourceList actually works in the example vs the source code — I noticed the example passes namespaces=[ns.name for ns in namespaces] (a list of strings) but the source code expects a ResourceList object. Let me verify.

+

The example file seems to have an outdated API for NamespacedResourceList (passing a list of strings), but the actual source code expects a ResourceList object. The tests confirm it passes a ResourceList. Let me check if the example is outdated:

+

The example file is inconsistent with the actual source code. The source code validates that each element in namespaces has ns.kind == "Namespace", meaning it expects actual Namespace resource objects (from a ResourceList), not strings. The test confirms this by passing a ResourceList fixture. I'll document based on the actual source code, not the potentially outdated example.

+

Let me also check what methods and properties are available:

+

Now I have all the information I need. Let me write the documentation.

+

Managing Bulk Resources with ResourceList

+

When you need to create several copies of the same resource or deploy a resource across multiple namespaces, doing it one at a time is tedious and error-prone. ResourceList and NamespacedResourceList let you create, deploy, and clean up groups of resources in a single operation.

+

Prerequisites

+ +

Quick Example

+

Create three namespaces and deploy them in two lines:

+
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import ResourceList, get_client
+
+client = get_client()
+
+namespaces = ResourceList(resource_class=Namespace, num_resources=3, client=client, name="my-ns")
+namespaces.deploy()
+# Creates: my-ns-1, my-ns-2, my-ns-3
+
+ +

Or use a context manager for automatic cleanup:

+
with ResourceList(client=client, resource_class=Namespace, name="my-ns", num_resources=3) as namespaces:
+    print(namespaces[0].name)  # "my-ns-1"
+# All three namespaces are deleted when the block exits
+
+ +

Creating Multiple Copies of a Resource

+

ResourceList creates N copies of a resource type, each with an auto-generated name based on a base name you provide.

+

Step 1: Choose a resource class and specify how many copies you want:

+
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import ResourceList, get_client
+
+client = get_client()
+namespaces = ResourceList(
+    resource_class=Namespace,
+    num_resources=3,
+    client=client,
+    name="test-ns",
+)
+
+ +

This creates three Namespace objects named test-ns-1, test-ns-2, and test-ns-3. They are not yet deployed to the cluster.

+

Step 2: Deploy all resources:

+
namespaces.deploy()
+
+ +

Step 3: Work with individual resources by index or iteration:

+
# Access by index
+first_ns = namespaces[0]
+print(first_ns.name)  # "test-ns-1"
+
+# Iterate over all resources
+for ns in namespaces:
+    print(ns.name)
+
+# Check how many resources are in the list
+print(len(namespaces))  # 3
+
+ +

Step 4: Clean up all resources when done:

+
namespaces.clean_up()
+
+ +
+

Note: clean_up() deletes resources in reverse order. This ensures dependent resources are removed before the resources they depend on.

+
+

Deploying One Resource Per Namespace

+

NamespacedResourceList creates one instance of a namespaced resource in each namespace from a ResourceList.

+
from ocp_resources.namespace import Namespace
+from ocp_resources.pod import Pod
+from ocp_resources.resource import NamespacedResourceList, ResourceList, get_client
+
+client = get_client()
+
+# First, create the namespaces
+namespaces = ResourceList(resource_class=Namespace, num_resources=3, client=client, name="app-ns")
+namespaces.deploy()
+
+# Then create one pod in each namespace
+pods = NamespacedResourceList(
+    client=client,
+    resource_class=Pod,
+    namespaces=namespaces,
+    name="web-server",
+    containers=[{"name": "nginx", "image": "nginx:latest"}],
+)
+pods.deploy()
+
+ +

This creates three pods, all named web-server, one in each namespace (app-ns-1, app-ns-2, app-ns-3).

+
for pod in pods:
+    print(f"{pod.name} in {pod.namespace}")
+# web-server in app-ns-1
+# web-server in app-ns-2
+# web-server in app-ns-3
+
+ +
+

Warning: The namespaces parameter must be a ResourceList containing only Namespace resources. Passing any other resource type raises a TypeError.

+
+

Passing Extra Resource Parameters

+

Any additional keyword arguments you pass are forwarded to each resource's constructor. This lets you configure resources beyond just the name:

+
from ocp_resources.role import Role
+from ocp_resources.resource import NamespacedResourceList
+
+roles = NamespacedResourceList(
+    client=client,
+    resource_class=Role,
+    namespaces=namespaces,
+    name="viewer-role",
+    rules=[
+        {
+            "apiGroups": [""],
+            "resources": ["pods"],
+            "verbs": ["get", "list", "watch"],
+        }
+    ],
+)
+roles.deploy()
+
+ +

Advanced Usage

+

Context Managers for Automatic Lifecycle

+

Both ResourceList and NamespacedResourceList support context managers. Resources are deployed on entry and cleaned up on exit — ideal for tests:

+
with ResourceList(client=client, resource_class=Namespace, name="test-ns", num_resources=3) as namespaces:
+    with NamespacedResourceList(
+        client=client,
+        resource_class=Pod,
+        namespaces=namespaces,
+        name="test-pod",
+        containers=[{"name": "app", "image": "busybox"}],
+    ) as pods:
+        # All namespaces and pods are deployed
+        assert len(pods) == 3
+    # Pods are cleaned up here
+# Namespaces are cleaned up here
+
+ +
+

Tip: Nest the context managers so that namespaced resources are cleaned up before their namespaces are deleted.

+
+

Waiting During Deploy and Cleanup

+

Pass wait=True to deploy() to wait for each resource to reach a ready state. By default, clean_up() already waits for deletion to complete.

+
namespaces = ResourceList(resource_class=Namespace, num_resources=2, client=client, name="ns")
+namespaces.deploy(wait=True)
+
+# Later...
+namespaces.clean_up(wait=True)   # wait=True is the default
+namespaces.clean_up(wait=False)  # Skip waiting for deletion
+
+ +

For more on waiting for specific conditions, see Waiting for Resource Conditions and Status.

+

Comparison: ResourceList vs NamespacedResourceList

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureResourceListNamespacedResourceList
PurposeCreate N copies of any resourceCreate one resource per namespace
NamingAuto-generates {name}-1, {name}-2, …Same name in each namespace
Required paramsresource_class, num_resources, nameresource_class, namespaces, name
Use caseMultiple namespaces, nodes, cluster-scoped resourcesPods, services, roles — anything namespace-scoped
Context manager
+

Troubleshooting

+

TypeError: "All the resources in namespaces should be namespaces"

+

You passed a ResourceList that contains non-Namespace resources to NamespacedResourceList. Make sure the namespaces parameter is a ResourceList created with resource_class=Namespace.

+

Resources not appearing on the cluster

+

Creating a ResourceList or NamespacedResourceList only instantiates the Python objects — it does not deploy them. Call .deploy() or use a context manager (with ... as) to actually create the resources on the cluster.

+

Cleanup order issues

+

Always clean up namespaced resources before their namespaces. When using context managers, nest them so the inner block (namespaced resources) exits first. When calling clean_up() manually, clean up dependent resources before their parents.

+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/managing-resource-lists.md b/docs/managing-resource-lists.md new file mode 100644 index 0000000000..d7fccfb23b --- /dev/null +++ b/docs/managing-resource-lists.md @@ -0,0 +1,225 @@ +Let me explore the codebase to understand ResourceList and NamespacedResourceList. + +Now let me check the example file more carefully and also look at how the `NamespacedResourceList` actually works in the example vs the source code — I noticed the example passes `namespaces=[ns.name for ns in namespaces]` (a list of strings) but the source code expects a `ResourceList` object. Let me verify. + +The example file seems to have an outdated API for `NamespacedResourceList` (passing a list of strings), but the actual source code expects a `ResourceList` object. The tests confirm it passes a `ResourceList`. Let me check if the example is outdated: + +The example file is inconsistent with the actual source code. The source code validates that each element in `namespaces` has `ns.kind == "Namespace"`, meaning it expects actual Namespace resource objects (from a ResourceList), not strings. The test confirms this by passing a `ResourceList` fixture. I'll document based on the actual source code, not the potentially outdated example. + +Let me also check what methods and properties are available: + +Now I have all the information I need. Let me write the documentation. + +# Managing Bulk Resources with ResourceList + +When you need to create several copies of the same resource or deploy a resource across multiple namespaces, doing it one at a time is tedious and error-prone. `ResourceList` and `NamespacedResourceList` let you create, deploy, and clean up groups of resources in a single operation. + +## Prerequisites + +- A working cluster connection (see [Connecting to Clusters](connecting-to-clusters.html)) +- Familiarity with creating individual resources (see [Creating and Managing Resources](creating-and-managing-resources.html)) + +## Quick Example + +Create three namespaces and deploy them in two lines: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import ResourceList, get_client + +client = get_client() + +namespaces = ResourceList(resource_class=Namespace, num_resources=3, client=client, name="my-ns") +namespaces.deploy() +# Creates: my-ns-1, my-ns-2, my-ns-3 +``` + +Or use a context manager for automatic cleanup: + +```python +with ResourceList(client=client, resource_class=Namespace, name="my-ns", num_resources=3) as namespaces: + print(namespaces[0].name) # "my-ns-1" +# All three namespaces are deleted when the block exits +``` + +## Creating Multiple Copies of a Resource + +`ResourceList` creates N copies of a resource type, each with an auto-generated name based on a base name you provide. + +**Step 1:** Choose a resource class and specify how many copies you want: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import ResourceList, get_client + +client = get_client() +namespaces = ResourceList( + resource_class=Namespace, + num_resources=3, + client=client, + name="test-ns", +) +``` + +This creates three `Namespace` objects named `test-ns-1`, `test-ns-2`, and `test-ns-3`. They are not yet deployed to the cluster. + +**Step 2:** Deploy all resources: + +```python +namespaces.deploy() +``` + +**Step 3:** Work with individual resources by index or iteration: + +```python +# Access by index +first_ns = namespaces[0] +print(first_ns.name) # "test-ns-1" + +# Iterate over all resources +for ns in namespaces: + print(ns.name) + +# Check how many resources are in the list +print(len(namespaces)) # 3 +``` + +**Step 4:** Clean up all resources when done: + +```python +namespaces.clean_up() +``` + +> **Note:** `clean_up()` deletes resources in reverse order. This ensures dependent resources are removed before the resources they depend on. + +## Deploying One Resource Per Namespace + +`NamespacedResourceList` creates one instance of a namespaced resource in each namespace from a `ResourceList`. + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.pod import Pod +from ocp_resources.resource import NamespacedResourceList, ResourceList, get_client + +client = get_client() + +# First, create the namespaces +namespaces = ResourceList(resource_class=Namespace, num_resources=3, client=client, name="app-ns") +namespaces.deploy() + +# Then create one pod in each namespace +pods = NamespacedResourceList( + client=client, + resource_class=Pod, + namespaces=namespaces, + name="web-server", + containers=[{"name": "nginx", "image": "nginx:latest"}], +) +pods.deploy() +``` + +This creates three pods, all named `web-server`, one in each namespace (`app-ns-1`, `app-ns-2`, `app-ns-3`). + +```python +for pod in pods: + print(f"{pod.name} in {pod.namespace}") +# web-server in app-ns-1 +# web-server in app-ns-2 +# web-server in app-ns-3 +``` + +> **Warning:** The `namespaces` parameter must be a `ResourceList` containing only Namespace resources. Passing any other resource type raises a `TypeError`. + +## Passing Extra Resource Parameters + +Any additional keyword arguments you pass are forwarded to each resource's constructor. This lets you configure resources beyond just the name: + +```python +from ocp_resources.role import Role +from ocp_resources.resource import NamespacedResourceList + +roles = NamespacedResourceList( + client=client, + resource_class=Role, + namespaces=namespaces, + name="viewer-role", + rules=[ + { + "apiGroups": [""], + "resources": ["pods"], + "verbs": ["get", "list", "watch"], + } + ], +) +roles.deploy() +``` + +## Advanced Usage + +### Context Managers for Automatic Lifecycle + +Both `ResourceList` and `NamespacedResourceList` support context managers. Resources are deployed on entry and cleaned up on exit — ideal for tests: + +```python +with ResourceList(client=client, resource_class=Namespace, name="test-ns", num_resources=3) as namespaces: + with NamespacedResourceList( + client=client, + resource_class=Pod, + namespaces=namespaces, + name="test-pod", + containers=[{"name": "app", "image": "busybox"}], + ) as pods: + # All namespaces and pods are deployed + assert len(pods) == 3 + # Pods are cleaned up here +# Namespaces are cleaned up here +``` + +> **Tip:** Nest the context managers so that namespaced resources are cleaned up before their namespaces are deleted. + +### Waiting During Deploy and Cleanup + +Pass `wait=True` to `deploy()` to wait for each resource to reach a ready state. By default, `clean_up()` already waits for deletion to complete. + +```python +namespaces = ResourceList(resource_class=Namespace, num_resources=2, client=client, name="ns") +namespaces.deploy(wait=True) + +# Later... +namespaces.clean_up(wait=True) # wait=True is the default +namespaces.clean_up(wait=False) # Skip waiting for deletion +``` + +For more on waiting for specific conditions, see [Waiting for Resource Conditions and Status](waiting-for-conditions.html). + +### Comparison: ResourceList vs NamespacedResourceList + +| Feature | `ResourceList` | `NamespacedResourceList` | +|---|---|---| +| **Purpose** | Create N copies of any resource | Create one resource per namespace | +| **Naming** | Auto-generates `{name}-1`, `{name}-2`, … | Same name in each namespace | +| **Required params** | `resource_class`, `num_resources`, `name` | `resource_class`, `namespaces`, `name` | +| **Use case** | Multiple namespaces, nodes, cluster-scoped resources | Pods, services, roles — anything namespace-scoped | +| **Context manager** | ✅ | ✅ | + +## Troubleshooting + +**TypeError: "All the resources in namespaces should be namespaces"** + +You passed a `ResourceList` that contains non-Namespace resources to `NamespacedResourceList`. Make sure the `namespaces` parameter is a `ResourceList` created with `resource_class=Namespace`. + +**Resources not appearing on the cluster** + +Creating a `ResourceList` or `NamespacedResourceList` only instantiates the Python objects — it does not deploy them. Call `.deploy()` or use a context manager (`with ... as`) to actually create the resources on the cluster. + +**Cleanup order issues** + +Always clean up namespaced resources before their namespaces. When using context managers, nest them so the inner block (namespaced resources) exits first. When calling `clean_up()` manually, clean up dependent resources before their parents. + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) +- [Common Resource Patterns](common-patterns.html) +- [Resource and NamespacedResource API](resource-api.html) diff --git a/docs/mcp-server-integration.html b/docs/mcp-server-integration.html new file mode 100644 index 0000000000..29ba1bdb45 --- /dev/null +++ b/docs/mcp-server-integration.html @@ -0,0 +1,799 @@ + + + + + + Integrating with AI Assistants via MCP Server - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Integrating with AI Assistants via MCP Server

+

The openshift-python-wrapper ships with a built-in Model Context Protocol (MCP) server that lets AI assistants like Cursor, Claude Desktop, and other MCP-compatible tools directly manage your OpenShift/Kubernetes cluster resources.

+

Configure the MCP Server for Cursor

+

Connect your Cursor IDE to your cluster so the AI assistant can list, create, update, and delete resources on your behalf.

+
// ~/.cursor/mcp.json
+{
+  "mcpServers": {
+    "openshift-python-wrapper": {
+      "command": "openshift-mcp-server"
+    }
+  }
+}
+
+ +

After saving, restart Cursor and reference the server with @openshift-python-wrapper in your prompts. The server uses your active kubeconfig context automatically.

+
+

Note: The openshift-mcp-server command is installed as a script entry point when you install the package. See Installing and Creating Your First Resource for installation instructions.

+
+

Configure the MCP Server for Claude Desktop

+

Set up Claude Desktop to interact with your cluster through natural language.

+
// macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
+// Windows: %APPDATA%\Claude\claude_desktop_config.json
+// Linux: ~/.config/Claude/claude_desktop_config.json
+{
+  "mcpServers": {
+    "openshift-python-wrapper": {
+      "command": "openshift-mcp-server"
+    }
+  }
+}
+
+ +

Claude Desktop will connect to the server on startup. You can then ask it to list pods, create resources, check logs, and more using natural language.

+

Configure a Custom Kubeconfig Path

+

Point the MCP server at a specific kubeconfig when your default location doesn't apply.

+
{
+  "mcpServers": {
+    "openshift-python-wrapper": {
+      "command": "openshift-mcp-server",
+      "env": {
+        "KUBECONFIG": "/home/user/.kube/my-cluster-config"
+      }
+    }
+  }
+}
+
+ +

The KUBECONFIG environment variable is passed to the server process at startup. This is useful when managing multiple clusters or when your kubeconfig is in a non-standard location.

+
+

Tip: See Connecting to Clusters for all authentication methods supported by openshift-python-wrapper, including tokens, basic auth, and in-cluster config.

+
+

Run the MCP Server from Source (Development)

+

Run the server directly from a cloned repository for development or testing.

+
git clone https://github.com/RedHatQE/openshift-python-wrapper.git
+cd openshift-python-wrapper
+uv run mcp_server/server.py
+
+ +

For MCP client configuration when running from source:

+
{
+  "mcpServers": {
+    "openshift-python-wrapper": {
+      "command": "uv",
+      "args": ["run", "mcp_server/server.py"],
+      "cwd": "/path/to/openshift-python-wrapper"
+    }
+  }
+}
+
+ +

This bypasses the installed entry point and runs the server module directly, which is useful when testing local changes.

+

List Cluster Resources via AI Chat

+

Ask your AI assistant to list resources — the MCP server exposes a list_resources tool that supports type filtering, namespaces, label selectors, and limits.

+
Prompt: "List all pods in the openshift-monitoring namespace with label app=prometheus"
+
+ +

The AI will call the list_resources tool, which maps to:

+
list_resources(
+    resource_type="pod",
+    namespace="openshift-monitoring",
+    label_selector="app=prometheus",
+    limit=20
+)
+
+ +

Returned data includes name, namespace, UID, creation timestamp, labels, phase, and conditions for each matched resource.

+
+

Tip: Use get_resource_types to discover all available resource types. The server dynamically scans every module in ocp_resources/ at startup, so any resource supported by the library — including OpenShift-specific types like route, virtualmachine, or clusterserviceversion — is available.

+
+

Get Detailed Resource Information

+

Retrieve a single resource with full metadata, status, and optionally the raw YAML or JSON.

+
Prompt: "Show me the deployment 'api-server' in namespace 'production' as YAML"
+
+ +

The AI calls:

+
get_resource(
+    resource_type="deployment",
+    name="api-server",
+    namespace="production",
+    output_format="yaml"    # also accepts "json" or "info"
+)
+
+ +

The info format (default) returns structured metadata plus resource-specific details: for pods you get container statuses and node placement; for deployments you get replica counts; for services you get ports and cluster IP.

+

Create Resources Through Natural Language

+

Ask the AI to create resources — it can use either YAML content or structured spec parameters.

+
Prompt: "Create a ConfigMap called 'app-settings' in the 'staging' namespace with keys
+database_host=db.staging.svc and log_level=debug"
+
+ +

The AI calls:

+
create_resource(
+    resource_type="configmap",
+    name="app-settings",
+    namespace="staging",
+    spec={"data": {"database_host": "db.staging.svc", "log_level": "debug"}},
+    labels={"managed-by": "mcp-server"}
+)
+
+ +

You can also provide full YAML:

+
create_resource(
+    resource_type="pod",
+    name="nginx",
+    yaml_content="""
+apiVersion: v1
+kind: Pod
+metadata:
+  name: nginx
+  namespace: default
+spec:
+  containers:
+  - name: nginx
+    image: nginx:1.27
+    ports:
+    - containerPort: 80
+"""
+)
+
+ +
+

Warning: The MCP server creates real resources on your cluster. Make sure your AI assistant is targeting the correct cluster and namespace before confirming creation operations.

+
+

Apply Multi-Document YAML Manifests

+

Deploy a complete application stack from a single YAML block containing multiple resources.

+
Prompt: "Apply this manifest to create a namespace, deployment, and service for my app"
+
+ +

The AI calls:

+
apply_yaml(yaml_content="""
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: my-app
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: backend
+  namespace: my-app
+spec:
+  replicas: 3
+  selector:
+    matchLabels:
+      app: backend
+  template:
+    metadata:
+      labels:
+        app: backend
+    spec:
+      containers:
+      - name: api
+        image: myapp/backend:v1.0
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: backend
+  namespace: my-app
+spec:
+  selector:
+    app: backend
+  ports:
+  - port: 80
+    targetPort: 8080
+""")
+
+ +

The response includes a summary with total_resources, successful, and failed counts, plus per-resource results. Each YAML document is parsed and deployed independently.

+

Update Resources with Patches

+

Modify existing resources using merge or strategic-merge patches.

+
Prompt: "Scale the 'web-frontend' deployment in 'production' to 5 replicas"
+
+ +

The AI calls:

+
update_resource(
+    resource_type="deployment",
+    name="web-frontend",
+    namespace="production",
+    patch={"spec": {"replicas": 5}},
+    patch_type="merge"       # or "strategic"
+)
+
+ +

The patch_type parameter controls the content type: "merge" uses application/merge-patch+json, while "strategic" uses application/strategic-merge-patch+json.

+

Delete Resources

+

Remove resources with optional wait-for-deletion behavior.

+
Prompt: "Delete the pod 'debug-pod' in namespace 'default' and wait for it to be gone"
+
+ +

The AI calls:

+
delete_resource(
+    resource_type="pod",
+    name="debug-pod",
+    namespace="default",
+    wait=True,
+    timeout=60
+)
+
+ +

If the resource doesn't exist, the server returns a success response with a warning rather than an error, making the operation idempotent.

+

Retrieve Pod Logs

+

Fetch logs from running or crashed pods with filtering options.

+
Prompt: "Show me the last 200 lines of logs from pod 'api-server-7f8b9c' container 'app'
+in namespace 'production'"
+
+ +

The AI calls:

+
get_pod_logs(
+    name="api-server-7f8b9c",
+    namespace="production",
+    container="app",
+    tail_lines=200
+)
+
+ +
    +
  • Use since_seconds=3600 to get logs from the last hour
  • +
  • Use previous=True to get logs from a crashed container's previous instance
  • +
  • Omit container for single-container pods
  • +
+

Execute Commands in Pods

+

Run diagnostic commands inside a running pod container.

+
Prompt: "Check the nginx config in pod 'web-server' namespace 'production'"
+
+ +

The AI calls:

+
exec_in_pod(
+    name="web-server",
+    namespace="production",
+    command=["nginx", "-t"],
+    container="nginx"
+)
+
+ +

The response includes stdout, stderr, and returncode. If the command fails, the exit code and error output are captured rather than raising an exception.

+
+

Warning: Pod exec gives shell-level access to your containers. Ensure your RBAC policies restrict which service accounts and users can perform exec operations.

+
+

Get Resource Events

+

Retrieve Kubernetes events related to a specific resource for troubleshooting.

+
Prompt: "Show me the events for pod 'crashloop-pod' in 'default' namespace"
+
+ +

The AI calls:

+
get_resource_events(
+    resource_type="pod",
+    name="crashloop-pod",
+    namespace="default",
+    limit=10
+)
+
+ +

Events are filtered using field selectors on involvedObject.name, involvedObject.namespace, and involvedObject.kind. Each event includes type (Normal/Warning), reason, message, count, timestamps, and source component.

+

Troubleshoot a Failing Pod (End-to-End Workflow)

+

Combine multiple MCP tools in sequence to diagnose pod issues — the AI assistant will chain these calls automatically.

+
Prompt: "Pod 'payment-svc-6d4f8' in namespace 'checkout' keeps crashing. Help me figure out why."
+
+ +

The AI will typically execute this sequence:

+
    +
  1. Check status: get_resource(resource_type="pod", name="payment-svc-6d4f8", namespace="checkout") — gets phase, container statuses, restart count
  2. +
  3. Review events: get_resource_events(resource_type="pod", name="payment-svc-6d4f8", namespace="checkout") — shows scheduling, pulling, crash events
  4. +
  5. Read current logs: get_pod_logs(name="payment-svc-6d4f8", namespace="checkout", tail_lines=100) — application output before crash
  6. +
  7. Read previous crash logs: get_pod_logs(name="payment-svc-6d4f8", namespace="checkout", previous=True) — logs from the last terminated container
  8. +
  9. Inspect config: exec_in_pod(name="payment-svc-6d4f8", namespace="checkout", command=["cat", "/etc/app/config.yaml"]) — verify mounted configuration
  10. +
+

The AI then synthesizes all the data into a diagnosis with suggested fixes.

+

Write a Programmatic MCP Client

+

Connect to the MCP server programmatically using FastMCPClient for automation scripts.

+
import asyncio
+from fastmcp import FastMCPClient
+
+
+async def main():
+    async with FastMCPClient() as client:
+        await client.connect_stdio(cmd=["openshift-mcp-server"])
+
+        # Discover available resource types
+        types = await client.call_tool(
+            name="get_resource_types",
+            arguments={"random_string": "x"},
+        )
+        print(f"Available types: {types['total_count']}")
+
+        # List pods in a namespace
+        pods = await client.call_tool(
+            name="list_resources",
+            arguments={"resource_type": "pod", "namespace": "default", "limit": 5},
+        )
+        for pod in pods["resources"]:
+            print(f"  {pod['name']}{pod.get('phase', 'Unknown')}")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
+
+ +

This uses the same stdio transport that AI assistants use. The FastMCPClient is provided by the fastmcp library, which is already a dependency of openshift-python-wrapper.

+

Debug MCP Server Issues

+

Enable debug logging and inspect the server log file when tools aren't behaving as expected.

+
# Check the MCP server log file
+tail -f /tmp/mcp_server_debug.log
+
+ +
# Run with debug-level logging from the simple-logger
+SIMPLE_LOGGER_LEVEL=DEBUG openshift-mcp-server
+
+ +

The server writes debug logs to /tmp/mcp_server_debug.log by default, including every resource scan at startup, client creation events, and detailed error tracebacks.

+
    +
  • Verify cluster connectivity independently: kubectl cluster-info
  • +
  • Check your permissions: kubectl auth can-i --list
  • +
  • Confirm the entry point is installed: which openshift-mcp-server
  • +
+
+

Tip: See Environment Variables and Configuration for other environment variables that affect library behavior.

+
+

Available MCP Tools Reference

+

The MCP server exposes ten tools. All resource type names are case-insensitive (e.g., "Pod", "pod", and "POD" all work).

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ToolPurposeRequired Parameters
list_resourcesList resources by type with optional filtersresource_type
get_resourceGet a single resource by nameresource_type, name
create_resourceCreate a resource from spec or YAMLresource_type, name, plus spec or yaml_content
update_resourcePatch an existing resourceresource_type, name, patch
delete_resourceDelete a resourceresource_type, name
get_pod_logsRetrieve pod container logsname, namespace
exec_in_podExecute a command in a podname, namespace, command
get_resource_eventsGet events for a resourceresource_type, name
apply_yamlApply one or more YAML documentsyaml_content
get_resource_typesList all discovered resource typesrandom_string (any value)
+
+

Note: Namespaced resources (pods, deployments, configmaps, etc.) require a namespace parameter. Cluster-scoped resources (nodes, namespaces, cluster roles) do not. The server validates this automatically and returns a clear error if a namespace is missing.

+
+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/mcp-server-integration.md b/docs/mcp-server-integration.md new file mode 100644 index 0000000000..7fee3656fc --- /dev/null +++ b/docs/mcp-server-integration.md @@ -0,0 +1,446 @@ +# Integrating with AI Assistants via MCP Server + +The openshift-python-wrapper ships with a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that lets AI assistants like Cursor, Claude Desktop, and other MCP-compatible tools directly manage your OpenShift/Kubernetes cluster resources. + +## Configure the MCP Server for Cursor + +Connect your Cursor IDE to your cluster so the AI assistant can list, create, update, and delete resources on your behalf. + +```json +// ~/.cursor/mcp.json +{ + "mcpServers": { + "openshift-python-wrapper": { + "command": "openshift-mcp-server" + } + } +} +``` + +After saving, restart Cursor and reference the server with `@openshift-python-wrapper` in your prompts. The server uses your active kubeconfig context automatically. + +> **Note:** The `openshift-mcp-server` command is installed as a script entry point when you install the package. See [Installing and Creating Your First Resource](quickstart.html) for installation instructions. + +## Configure the MCP Server for Claude Desktop + +Set up Claude Desktop to interact with your cluster through natural language. + +```json +// macOS: ~/Library/Application Support/Claude/claude_desktop_config.json +// Windows: %APPDATA%\Claude\claude_desktop_config.json +// Linux: ~/.config/Claude/claude_desktop_config.json +{ + "mcpServers": { + "openshift-python-wrapper": { + "command": "openshift-mcp-server" + } + } +} +``` + +Claude Desktop will connect to the server on startup. You can then ask it to list pods, create resources, check logs, and more using natural language. + +## Configure a Custom Kubeconfig Path + +Point the MCP server at a specific kubeconfig when your default location doesn't apply. + +```json +{ + "mcpServers": { + "openshift-python-wrapper": { + "command": "openshift-mcp-server", + "env": { + "KUBECONFIG": "/home/user/.kube/my-cluster-config" + } + } + } +} +``` + +The `KUBECONFIG` environment variable is passed to the server process at startup. This is useful when managing multiple clusters or when your kubeconfig is in a non-standard location. + +> **Tip:** See [Connecting to Clusters](connecting-to-clusters.html) for all authentication methods supported by openshift-python-wrapper, including tokens, basic auth, and in-cluster config. + +## Run the MCP Server from Source (Development) + +Run the server directly from a cloned repository for development or testing. + +```bash +git clone https://github.com/RedHatQE/openshift-python-wrapper.git +cd openshift-python-wrapper +uv run mcp_server/server.py +``` + +For MCP client configuration when running from source: + +```json +{ + "mcpServers": { + "openshift-python-wrapper": { + "command": "uv", + "args": ["run", "mcp_server/server.py"], + "cwd": "/path/to/openshift-python-wrapper" + } + } +} +``` + +This bypasses the installed entry point and runs the server module directly, which is useful when testing local changes. + +## List Cluster Resources via AI Chat + +Ask your AI assistant to list resources — the MCP server exposes a `list_resources` tool that supports type filtering, namespaces, label selectors, and limits. + +``` +Prompt: "List all pods in the openshift-monitoring namespace with label app=prometheus" +``` + +The AI will call the `list_resources` tool, which maps to: + +```python +list_resources( + resource_type="pod", + namespace="openshift-monitoring", + label_selector="app=prometheus", + limit=20 +) +``` + +Returned data includes name, namespace, UID, creation timestamp, labels, phase, and conditions for each matched resource. + +> **Tip:** Use `get_resource_types` to discover all available resource types. The server dynamically scans every module in `ocp_resources/` at startup, so any resource supported by the library — including OpenShift-specific types like `route`, `virtualmachine`, or `clusterserviceversion` — is available. + +## Get Detailed Resource Information + +Retrieve a single resource with full metadata, status, and optionally the raw YAML or JSON. + +``` +Prompt: "Show me the deployment 'api-server' in namespace 'production' as YAML" +``` + +The AI calls: + +```python +get_resource( + resource_type="deployment", + name="api-server", + namespace="production", + output_format="yaml" # also accepts "json" or "info" +) +``` + +The `info` format (default) returns structured metadata plus resource-specific details: for pods you get container statuses and node placement; for deployments you get replica counts; for services you get ports and cluster IP. + +## Create Resources Through Natural Language + +Ask the AI to create resources — it can use either YAML content or structured spec parameters. + +``` +Prompt: "Create a ConfigMap called 'app-settings' in the 'staging' namespace with keys +database_host=db.staging.svc and log_level=debug" +``` + +The AI calls: + +```python +create_resource( + resource_type="configmap", + name="app-settings", + namespace="staging", + spec={"data": {"database_host": "db.staging.svc", "log_level": "debug"}}, + labels={"managed-by": "mcp-server"} +) +``` + +You can also provide full YAML: + +```python +create_resource( + resource_type="pod", + name="nginx", + yaml_content=""" +apiVersion: v1 +kind: Pod +metadata: + name: nginx + namespace: default +spec: + containers: + - name: nginx + image: nginx:1.27 + ports: + - containerPort: 80 +""" +) +``` + +> **Warning:** The MCP server creates real resources on your cluster. Make sure your AI assistant is targeting the correct cluster and namespace before confirming creation operations. + +## Apply Multi-Document YAML Manifests + +Deploy a complete application stack from a single YAML block containing multiple resources. + +``` +Prompt: "Apply this manifest to create a namespace, deployment, and service for my app" +``` + +The AI calls: + +```python +apply_yaml(yaml_content=""" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: my-app +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + namespace: my-app +spec: + replicas: 3 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + spec: + containers: + - name: api + image: myapp/backend:v1.0 + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: backend + namespace: my-app +spec: + selector: + app: backend + ports: + - port: 80 + targetPort: 8080 +""") +``` + +The response includes a summary with `total_resources`, `successful`, and `failed` counts, plus per-resource results. Each YAML document is parsed and deployed independently. + +## Update Resources with Patches + +Modify existing resources using merge or strategic-merge patches. + +``` +Prompt: "Scale the 'web-frontend' deployment in 'production' to 5 replicas" +``` + +The AI calls: + +```python +update_resource( + resource_type="deployment", + name="web-frontend", + namespace="production", + patch={"spec": {"replicas": 5}}, + patch_type="merge" # or "strategic" +) +``` + +The `patch_type` parameter controls the content type: `"merge"` uses `application/merge-patch+json`, while `"strategic"` uses `application/strategic-merge-patch+json`. + +## Delete Resources + +Remove resources with optional wait-for-deletion behavior. + +``` +Prompt: "Delete the pod 'debug-pod' in namespace 'default' and wait for it to be gone" +``` + +The AI calls: + +```python +delete_resource( + resource_type="pod", + name="debug-pod", + namespace="default", + wait=True, + timeout=60 +) +``` + +If the resource doesn't exist, the server returns a success response with a warning rather than an error, making the operation idempotent. + +## Retrieve Pod Logs + +Fetch logs from running or crashed pods with filtering options. + +``` +Prompt: "Show me the last 200 lines of logs from pod 'api-server-7f8b9c' container 'app' +in namespace 'production'" +``` + +The AI calls: + +```python +get_pod_logs( + name="api-server-7f8b9c", + namespace="production", + container="app", + tail_lines=200 +) +``` + +- Use `since_seconds=3600` to get logs from the last hour +- Use `previous=True` to get logs from a crashed container's previous instance +- Omit `container` for single-container pods + +## Execute Commands in Pods + +Run diagnostic commands inside a running pod container. + +``` +Prompt: "Check the nginx config in pod 'web-server' namespace 'production'" +``` + +The AI calls: + +```python +exec_in_pod( + name="web-server", + namespace="production", + command=["nginx", "-t"], + container="nginx" +) +``` + +The response includes `stdout`, `stderr`, and `returncode`. If the command fails, the exit code and error output are captured rather than raising an exception. + +> **Warning:** Pod exec gives shell-level access to your containers. Ensure your RBAC policies restrict which service accounts and users can perform `exec` operations. + +## Get Resource Events + +Retrieve Kubernetes events related to a specific resource for troubleshooting. + +``` +Prompt: "Show me the events for pod 'crashloop-pod' in 'default' namespace" +``` + +The AI calls: + +```python +get_resource_events( + resource_type="pod", + name="crashloop-pod", + namespace="default", + limit=10 +) +``` + +Events are filtered using field selectors on `involvedObject.name`, `involvedObject.namespace`, and `involvedObject.kind`. Each event includes type (Normal/Warning), reason, message, count, timestamps, and source component. + +## Troubleshoot a Failing Pod (End-to-End Workflow) + +Combine multiple MCP tools in sequence to diagnose pod issues — the AI assistant will chain these calls automatically. + +``` +Prompt: "Pod 'payment-svc-6d4f8' in namespace 'checkout' keeps crashing. Help me figure out why." +``` + +The AI will typically execute this sequence: + +1. **Check status:** `get_resource(resource_type="pod", name="payment-svc-6d4f8", namespace="checkout")` — gets phase, container statuses, restart count +2. **Review events:** `get_resource_events(resource_type="pod", name="payment-svc-6d4f8", namespace="checkout")` — shows scheduling, pulling, crash events +3. **Read current logs:** `get_pod_logs(name="payment-svc-6d4f8", namespace="checkout", tail_lines=100)` — application output before crash +4. **Read previous crash logs:** `get_pod_logs(name="payment-svc-6d4f8", namespace="checkout", previous=True)` — logs from the last terminated container +5. **Inspect config:** `exec_in_pod(name="payment-svc-6d4f8", namespace="checkout", command=["cat", "/etc/app/config.yaml"])` — verify mounted configuration + +The AI then synthesizes all the data into a diagnosis with suggested fixes. + +## Write a Programmatic MCP Client + +Connect to the MCP server programmatically using `FastMCPClient` for automation scripts. + +```python +import asyncio +from fastmcp import FastMCPClient + + +async def main(): + async with FastMCPClient() as client: + await client.connect_stdio(cmd=["openshift-mcp-server"]) + + # Discover available resource types + types = await client.call_tool( + name="get_resource_types", + arguments={"random_string": "x"}, + ) + print(f"Available types: {types['total_count']}") + + # List pods in a namespace + pods = await client.call_tool( + name="list_resources", + arguments={"resource_type": "pod", "namespace": "default", "limit": 5}, + ) + for pod in pods["resources"]: + print(f" {pod['name']} — {pod.get('phase', 'Unknown')}") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +This uses the same stdio transport that AI assistants use. The `FastMCPClient` is provided by the `fastmcp` library, which is already a dependency of openshift-python-wrapper. + +## Debug MCP Server Issues + +Enable debug logging and inspect the server log file when tools aren't behaving as expected. + +```bash +# Check the MCP server log file +tail -f /tmp/mcp_server_debug.log +``` + +```bash +# Run with debug-level logging from the simple-logger +SIMPLE_LOGGER_LEVEL=DEBUG openshift-mcp-server +``` + +The server writes debug logs to `/tmp/mcp_server_debug.log` by default, including every resource scan at startup, client creation events, and detailed error tracebacks. + +- Verify cluster connectivity independently: `kubectl cluster-info` +- Check your permissions: `kubectl auth can-i --list` +- Confirm the entry point is installed: `which openshift-mcp-server` + +> **Tip:** See [Environment Variables and Configuration](environment-variables.html) for other environment variables that affect library behavior. + +## Available MCP Tools Reference + +The MCP server exposes ten tools. All resource type names are case-insensitive (e.g., `"Pod"`, `"pod"`, and `"POD"` all work). + +| Tool | Purpose | Required Parameters | +|------|---------|-------------------| +| `list_resources` | List resources by type with optional filters | `resource_type` | +| `get_resource` | Get a single resource by name | `resource_type`, `name` | +| `create_resource` | Create a resource from spec or YAML | `resource_type`, `name`, plus `spec` or `yaml_content` | +| `update_resource` | Patch an existing resource | `resource_type`, `name`, `patch` | +| `delete_resource` | Delete a resource | `resource_type`, `name` | +| `get_pod_logs` | Retrieve pod container logs | `name`, `namespace` | +| `exec_in_pod` | Execute a command in a pod | `name`, `namespace`, `command` | +| `get_resource_events` | Get events for a resource | `resource_type`, `name` | +| `apply_yaml` | Apply one or more YAML documents | `yaml_content` | +| `get_resource_types` | List all discovered resource types | `random_string` (any value) | + +> **Note:** Namespaced resources (pods, deployments, configmaps, etc.) require a `namespace` parameter. Cluster-scoped resources (nodes, namespaces, cluster roles) do not. The server validates this automatically and returns a clear error if a namespace is missing. + +## Related Pages + +- [Connecting to Clusters](connecting-to-clusters.html) +- [Installing and Creating Your First Resource](quickstart.html) +- [Environment Variables and Configuration](environment-variables.html) +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) +- [Querying and Watching Resources](querying-resources.html) diff --git a/docs/pod-execution-and-logs.html b/docs/pod-execution-and-logs.html new file mode 100644 index 0000000000..b17487a9fe --- /dev/null +++ b/docs/pod-execution-and-logs.html @@ -0,0 +1,791 @@ + + + + + + Executing Commands in Pods and Retrieving Logs - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me explore the repository structure and understand the codebase first.

+

Now let me check the read_namespaced_pod_log kwargs available, and look at how the MCP server uses log parameters:

+

Now let me check the import paths and verify the get_client function:

+

Now let me check test files for pod execution tests:

+

Now let me also verify whether the Node class has a name property:

+

Now I have all the information I need to write the documentation. Let me compose it:

+

Executing Commands in Pods and Retrieving Logs

+

Run commands inside running pods and retrieve their logs to debug and interact with workloads on your OpenShift or Kubernetes cluster.

+

Prerequisites

+
    +
  • A connected cluster client (see Connecting to Clusters)
  • +
  • An existing, running pod you want to interact with
  • +
+

Quick Example

+
from ocp_resources.pod import Pod
+from ocp_resources.resource import get_client
+
+client = get_client()
+pod = Pod(client=client, name="my-app", namespace="default")
+
+# Run a command inside the pod
+output = pod.execute(command=["echo", "hello"])
+print(output)  # "hello\n"
+
+# Get pod logs
+logs = pod.log()
+print(logs)
+
+ +

Executing Commands with Pod.execute()

+

Pod.execute() runs a command inside a pod container using the Kubernetes exec API and returns the standard output as a string.

+

Method Signature

+
Pod.execute(
+    command: list[str],
+    timeout: int = 60,
+    container: str = "",
+    ignore_rc: bool = False,
+) -> str
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
commandlist[str]The command to run, as a list of strings (e.g., ["ls", "-la", "/tmp"])
timeoutint60Maximum seconds to wait for the command to complete
containerstr""Container name to execute in. If empty, uses the first container in the pod spec
ignore_rcboolFalseWhen True, return stdout even if the command exits with a non-zero return code
+

Returns: str — the standard output of the command.

+

Raises: ExecOnPodError — when the command fails (non-zero exit code) and ignore_rc is False.

+

Step-by-Step: Running a Simple Command

+
    +
  1. Get a reference to the pod:
  2. +
+
from ocp_resources.pod import Pod
+from ocp_resources.resource import get_client
+
+client = get_client()
+pod = Pod(client=client, name="my-app", namespace="my-namespace")
+
+ +
    +
  1. Execute a command:
  2. +
+
output = pod.execute(command=["cat", "/etc/hostname"])
+print(output)
+
+ +
    +
  1. Use the result for further logic:
  2. +
+
files = pod.execute(command=["ls", "/app/data"])
+for filename in files.strip().split("\n"):
+    print(f"Found: {filename}")
+
+ +

Selecting a Container

+

For multi-container pods, specify which container to run the command in. If you omit container, the first container defined in the pod spec is used.

+
# Execute in a specific container
+output = pod.execute(
+    command=["cat", "/var/log/app.log"],
+    container="sidecar",
+)
+
+ +

Setting a Timeout

+

Long-running commands may need a longer timeout. The default is 60 seconds.

+
# Allow up to 5 minutes for a heavy operation
+output = pod.execute(
+    command=["pg_dump", "mydb"],
+    timeout=300,
+)
+
+ +

When the timeout is exceeded, an ExecOnPodError is raised with the error message "stream resp is closed".

+

Ignoring Non-Zero Exit Codes

+

Some commands return a non-zero exit code as part of normal operation (e.g., grep returns 1 when no match is found). Use ignore_rc=True to get the output regardless:

+
# grep returns exit code 1 when there's no match — don't treat that as an error
+output = pod.execute(
+    command=["grep", "ERROR", "/var/log/app.log"],
+    ignore_rc=True,
+)
+if output:
+    print("Errors found:", output)
+else:
+    print("No errors in logs")
+
+ +

Handling Errors with ExecOnPodError

+

When a command fails, ExecOnPodError provides structured access to the failure details.

+
from ocp_resources.pod import Pod
+from ocp_resources.exceptions import ExecOnPodError
+
+try:
+    pod.execute(command=["ls", "/nonexistent"])
+except ExecOnPodError as e:
+    print(f"Command: {e.cmd}")      # ['ls', '/nonexistent']
+    print(f"Return code: {e.rc}")   # Non-zero exit code (or -1 for stream errors)
+    print(f"Stdout: {e.out}")       # Standard output captured before failure
+    print(f"Stderr: {e.err}")       # Standard error or error channel details
+
+ +

ExecOnPodError attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeTypeDescription
cmdlist[str]The command that was executed
rcintReturn code (-1 for stream/timeout errors, otherwise the exit code)
outstrStandard output captured from the command
errstr or dictStandard error output, or the Kubernetes error channel response
+
+

Tip: For a complete list of all custom exceptions, see Error Handling and Exception Reference.

+
+

Retrieving Logs with Pod.log()

+

Pod.log() returns the logs from a pod container as a string. It passes keyword arguments directly to the Kubernetes read_namespaced_pod_log API.

+

Basic Usage

+
logs = pod.log()
+print(logs)
+
+ +

Common Keyword Arguments

+

Pass any parameter supported by the Kubernetes read_namespaced_pod_log API:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
containerstrContainer name to get logs from (required for multi-container pods)
previousboolReturn logs from a previous terminated container instance
tail_linesintNumber of lines from the end of the logs to return
since_secondsintOnly return logs newer than this many seconds
+

Examples

+
# Get logs from a specific container
+logs = pod.log(container="nginx")
+
+# Get the last 50 lines
+logs = pod.log(tail_lines=50)
+
+# Get logs from the last 5 minutes
+logs = pod.log(since_seconds=300)
+
+# Get logs from a crashed container's previous instance
+logs = pod.log(container="app", previous=True)
+
+ +

Accessing Pod Properties

+

Beyond executing commands and reading logs, the Pod class provides properties for inspecting the pod's runtime state.

+

Getting the Node

+

The node property returns a Node object for the node where the pod is scheduled:

+
from ocp_resources.pod import Pod
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+for pod in Pod.get(client=client, label_selector="app=my-app"):
+    node = pod.node
+    print(f"Pod {pod.name} is running on node {node.name}")
+
+ +
+

Note: The node property raises an AssertionError if the pod has not yet been scheduled to a node.

+
+

Getting the Pod IP Address

+

The ip property returns the pod's IP address from its status:

+
pod_ip = pod.ip
+print(f"Pod IP: {pod_ip}")
+
+ +

Getting the Pod Status

+

The status property (inherited from the base resource class) returns the pod's phase:

+
print(f"Pod status: {pod.status}")  # e.g., "Running", "Pending", "Succeeded"
+
+ +

Advanced Usage

+

Iterating Over Pods with Selectors

+

Combine Pod.get() with execute() or log() to debug across multiple pods:

+
from ocp_resources.pod import Pod
+from ocp_resources.exceptions import ExecOnPodError
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+for pod in Pod.get(client=client, namespace="production", label_selector="app=web"):
+    try:
+        uptime = pod.execute(command=["uptime"])
+        print(f"{pod.name} on {pod.node.name}: {uptime.strip()}")
+    except ExecOnPodError as e:
+        print(f"{pod.name}: command failed — {e}")
+
+ +

Executing in Multi-Container Pods

+

When pods contain sidecar containers (e.g., logging or proxy containers), always specify the target container explicitly:

+
# Main application container
+app_output = pod.execute(command=["cat", "/app/config.yaml"], container="app")
+
+# Envoy sidecar
+proxy_stats = pod.execute(command=["curl", "localhost:15000/stats"], container="istio-proxy")
+
+# Logs from each container
+app_logs = pod.log(container="app", tail_lines=100)
+proxy_logs = pod.log(container="istio-proxy", tail_lines=100)
+
+ +

Collecting Debug Information

+

A practical recipe for gathering debugging data from a running pod:

+
from ocp_resources.pod import Pod
+from ocp_resources.exceptions import ExecOnPodError
+from ocp_resources.resource import get_client
+
+client = get_client()
+pod = Pod(client=client, name="my-app", namespace="default")
+
+debug_info = {
+    "pod_name": pod.name,
+    "node": pod.node.name,
+    "ip": pod.ip,
+    "status": pod.status,
+    "logs_tail": pod.log(tail_lines=20),
+}
+
+# Safely attempt commands that might fail
+for cmd_name, cmd in [("env", ["env"]), ("df", ["df", "-h"]), ("ps", ["ps", "aux"])]:
+    try:
+        debug_info[cmd_name] = pod.execute(command=cmd, timeout=10)
+    except ExecOnPodError:
+        debug_info[cmd_name] = "command failed"
+
+for key, value in debug_info.items():
+    print(f"--- {key} ---\n{value}\n")
+
+ +

Using Pod Context Manager for Temporary Pods

+

Create a pod, run commands, and clean up automatically:

+
from ocp_resources.pod import Pod
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+with Pod(
+    client=client,
+    name="debug-pod",
+    namespace="default",
+    containers=[{
+        "name": "debug",
+        "image": "registry.access.redhat.com/ubi9/ubi:latest",
+        "command": ["sleep", "3600"],
+    }],
+) as pod:
+    pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120)
+    output = pod.execute(command=["cat", "/etc/os-release"])
+    print(output)
+# Pod is automatically deleted when the context manager exits
+
+ +
+

Note: See Creating and Managing Resources for more about context managers and resource lifecycle.

+
+

Troubleshooting

+

ExecOnPodError with "stream resp is closed"

+

This typically means the command timed out. Increase the timeout parameter:

+
pod.execute(command=["long-running-script.sh"], timeout=300)
+
+ +

AssertionError: Node not found for pod <name>

+

The pod is not yet scheduled. Wait for it to be running before accessing pod.node:

+
pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120)
+node = pod.node
+
+ +

ExecOnPodError with return code -1 and an error channel dict

+

This indicates a Kubernetes-level failure (e.g., container not found, pod not running). Check that:

+
    +
  • The pod is in Running status
  • +
  • The container name matches one defined in the pod spec
  • +
  • The container has started and is not in CrashLoopBackOff
  • +
+

Logs return empty string

+

The container may not have produced any output yet, or it may have restarted. Try:

+
# Check previous container instance
+logs = pod.log(previous=True)
+
+# Or narrow to recent logs
+logs = pod.log(since_seconds=60)
+
+ + + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/pod-execution-and-logs.md b/docs/pod-execution-and-logs.md new file mode 100644 index 0000000000..5107fa45e8 --- /dev/null +++ b/docs/pod-execution-and-logs.md @@ -0,0 +1,380 @@ +Let me explore the repository structure and understand the codebase first. + +Now let me check the `read_namespaced_pod_log` kwargs available, and look at how the MCP server uses log parameters: + +Now let me check the import paths and verify the `get_client` function: + +Now let me check test files for pod execution tests: + +Now let me also verify whether the Node class has a `name` property: + +Now I have all the information I need to write the documentation. Let me compose it: + +# Executing Commands in Pods and Retrieving Logs + +Run commands inside running pods and retrieve their logs to debug and interact with workloads on your OpenShift or Kubernetes cluster. + +## Prerequisites + +- A connected cluster client (see [Connecting to Clusters](connecting-to-clusters.html)) +- An existing, running pod you want to interact with + +## Quick Example + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() +pod = Pod(client=client, name="my-app", namespace="default") + +# Run a command inside the pod +output = pod.execute(command=["echo", "hello"]) +print(output) # "hello\n" + +# Get pod logs +logs = pod.log() +print(logs) +``` + +## Executing Commands with `Pod.execute()` + +`Pod.execute()` runs a command inside a pod container using the Kubernetes exec API and returns the standard output as a string. + +### Method Signature + +```python +Pod.execute( + command: list[str], + timeout: int = 60, + container: str = "", + ignore_rc: bool = False, +) -> str +``` + +| Parameter | Type | Default | Description | +|-------------|--------------|---------|---------------------------------------------------------------------------------------------| +| `command` | `list[str]` | — | The command to run, as a list of strings (e.g., `["ls", "-la", "/tmp"]`) | +| `timeout` | `int` | `60` | Maximum seconds to wait for the command to complete | +| `container` | `str` | `""` | Container name to execute in. If empty, uses the first container in the pod spec | +| `ignore_rc` | `bool` | `False` | When `True`, return stdout even if the command exits with a non-zero return code | + +**Returns:** `str` — the standard output of the command. + +**Raises:** `ExecOnPodError` — when the command fails (non-zero exit code) and `ignore_rc` is `False`. + +### Step-by-Step: Running a Simple Command + +1. Get a reference to the pod: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() +pod = Pod(client=client, name="my-app", namespace="my-namespace") +``` + +2. Execute a command: + +```python +output = pod.execute(command=["cat", "/etc/hostname"]) +print(output) +``` + +3. Use the result for further logic: + +```python +files = pod.execute(command=["ls", "/app/data"]) +for filename in files.strip().split("\n"): + print(f"Found: {filename}") +``` + +### Selecting a Container + +For multi-container pods, specify which container to run the command in. If you omit `container`, the first container defined in the pod spec is used. + +```python +# Execute in a specific container +output = pod.execute( + command=["cat", "/var/log/app.log"], + container="sidecar", +) +``` + +### Setting a Timeout + +Long-running commands may need a longer timeout. The default is 60 seconds. + +```python +# Allow up to 5 minutes for a heavy operation +output = pod.execute( + command=["pg_dump", "mydb"], + timeout=300, +) +``` + +When the timeout is exceeded, an `ExecOnPodError` is raised with the error message `"stream resp is closed"`. + +### Ignoring Non-Zero Exit Codes + +Some commands return a non-zero exit code as part of normal operation (e.g., `grep` returns 1 when no match is found). Use `ignore_rc=True` to get the output regardless: + +```python +# grep returns exit code 1 when there's no match — don't treat that as an error +output = pod.execute( + command=["grep", "ERROR", "/var/log/app.log"], + ignore_rc=True, +) +if output: + print("Errors found:", output) +else: + print("No errors in logs") +``` + +### Handling Errors with `ExecOnPodError` + +When a command fails, `ExecOnPodError` provides structured access to the failure details. + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ExecOnPodError + +try: + pod.execute(command=["ls", "/nonexistent"]) +except ExecOnPodError as e: + print(f"Command: {e.cmd}") # ['ls', '/nonexistent'] + print(f"Return code: {e.rc}") # Non-zero exit code (or -1 for stream errors) + print(f"Stdout: {e.out}") # Standard output captured before failure + print(f"Stderr: {e.err}") # Standard error or error channel details +``` + +`ExecOnPodError` attributes: + +| Attribute | Type | Description | +|-----------|-------------|----------------------------------------------------------------------| +| `cmd` | `list[str]` | The command that was executed | +| `rc` | `int` | Return code (`-1` for stream/timeout errors, otherwise the exit code)| +| `out` | `str` | Standard output captured from the command | +| `err` | `str` or `dict` | Standard error output, or the Kubernetes error channel response | + +> **Tip:** For a complete list of all custom exceptions, see [Error Handling and Exception Reference](error-handling.html). + +## Retrieving Logs with `Pod.log()` + +`Pod.log()` returns the logs from a pod container as a string. It passes keyword arguments directly to the Kubernetes `read_namespaced_pod_log` API. + +### Basic Usage + +```python +logs = pod.log() +print(logs) +``` + +### Common Keyword Arguments + +Pass any parameter supported by the Kubernetes `read_namespaced_pod_log` API: + +| Parameter | Type | Description | +|-----------------|--------|-----------------------------------------------------------| +| `container` | `str` | Container name to get logs from (required for multi-container pods) | +| `previous` | `bool` | Return logs from a previous terminated container instance | +| `tail_lines` | `int` | Number of lines from the end of the logs to return | +| `since_seconds` | `int` | Only return logs newer than this many seconds | + +### Examples + +```python +# Get logs from a specific container +logs = pod.log(container="nginx") + +# Get the last 50 lines +logs = pod.log(tail_lines=50) + +# Get logs from the last 5 minutes +logs = pod.log(since_seconds=300) + +# Get logs from a crashed container's previous instance +logs = pod.log(container="app", previous=True) +``` + +## Accessing Pod Properties + +Beyond executing commands and reading logs, the `Pod` class provides properties for inspecting the pod's runtime state. + +### Getting the Node + +The `node` property returns a `Node` object for the node where the pod is scheduled: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +for pod in Pod.get(client=client, label_selector="app=my-app"): + node = pod.node + print(f"Pod {pod.name} is running on node {node.name}") +``` + +> **Note:** The `node` property raises an `AssertionError` if the pod has not yet been scheduled to a node. + +### Getting the Pod IP Address + +The `ip` property returns the pod's IP address from its status: + +```python +pod_ip = pod.ip +print(f"Pod IP: {pod_ip}") +``` + +### Getting the Pod Status + +The `status` property (inherited from the base resource class) returns the pod's phase: + +```python +print(f"Pod status: {pod.status}") # e.g., "Running", "Pending", "Succeeded" +``` + +## Advanced Usage + +### Iterating Over Pods with Selectors + +Combine `Pod.get()` with `execute()` or `log()` to debug across multiple pods: + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ExecOnPodError +from ocp_resources.resource import get_client + +client = get_client() + +for pod in Pod.get(client=client, namespace="production", label_selector="app=web"): + try: + uptime = pod.execute(command=["uptime"]) + print(f"{pod.name} on {pod.node.name}: {uptime.strip()}") + except ExecOnPodError as e: + print(f"{pod.name}: command failed — {e}") +``` + +### Executing in Multi-Container Pods + +When pods contain sidecar containers (e.g., logging or proxy containers), always specify the target container explicitly: + +```python +# Main application container +app_output = pod.execute(command=["cat", "/app/config.yaml"], container="app") + +# Envoy sidecar +proxy_stats = pod.execute(command=["curl", "localhost:15000/stats"], container="istio-proxy") + +# Logs from each container +app_logs = pod.log(container="app", tail_lines=100) +proxy_logs = pod.log(container="istio-proxy", tail_lines=100) +``` + +### Collecting Debug Information + +A practical recipe for gathering debugging data from a running pod: + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ExecOnPodError +from ocp_resources.resource import get_client + +client = get_client() +pod = Pod(client=client, name="my-app", namespace="default") + +debug_info = { + "pod_name": pod.name, + "node": pod.node.name, + "ip": pod.ip, + "status": pod.status, + "logs_tail": pod.log(tail_lines=20), +} + +# Safely attempt commands that might fail +for cmd_name, cmd in [("env", ["env"]), ("df", ["df", "-h"]), ("ps", ["ps", "aux"])]: + try: + debug_info[cmd_name] = pod.execute(command=cmd, timeout=10) + except ExecOnPodError: + debug_info[cmd_name] = "command failed" + +for key, value in debug_info.items(): + print(f"--- {key} ---\n{value}\n") +``` + +### Using Pod Context Manager for Temporary Pods + +Create a pod, run commands, and clean up automatically: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +with Pod( + client=client, + name="debug-pod", + namespace="default", + containers=[{ + "name": "debug", + "image": "registry.access.redhat.com/ubi9/ubi:latest", + "command": ["sleep", "3600"], + }], +) as pod: + pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120) + output = pod.execute(command=["cat", "/etc/os-release"]) + print(output) +# Pod is automatically deleted when the context manager exits +``` + +> **Note:** See [Creating and Managing Resources](creating-and-managing-resources.html) for more about context managers and resource lifecycle. + +## Troubleshooting + +### `ExecOnPodError` with `"stream resp is closed"` + +This typically means the command timed out. Increase the `timeout` parameter: + +```python +pod.execute(command=["long-running-script.sh"], timeout=300) +``` + +### `AssertionError: Node not found for pod ` + +The pod is not yet scheduled. Wait for it to be running before accessing `pod.node`: + +```python +pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120) +node = pod.node +``` + +### `ExecOnPodError` with return code `-1` and an error channel dict + +This indicates a Kubernetes-level failure (e.g., container not found, pod not running). Check that: + +- The pod is in `Running` status +- The `container` name matches one defined in the pod spec +- The container has started and is not in `CrashLoopBackOff` + +### Logs return empty string + +The container may not have produced any output yet, or it may have restarted. Try: + +```python +# Check previous container instance +logs = pod.log(previous=True) + +# Or narrow to recent logs +logs = pod.log(since_seconds=60) +``` + +## Related Pages + +- [Common Resource Patterns](common-patterns.html) +- [Working with Kubernetes Events](working-with-events.html) +- [Error Handling and Exception Reference](error-handling.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) diff --git a/docs/querying-resources.html b/docs/querying-resources.html new file mode 100644 index 0000000000..063e40c11c --- /dev/null +++ b/docs/querying-resources.html @@ -0,0 +1,1063 @@ + + + + + + Querying and Watching Resources - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by reading the knowledge graph and understanding the codebase structure.

+

Now let me explore the key source files related to querying, listing, watching, existence checks, and status retrieval.

+

Now let me look at the _prepare_resources method and the get method more closely to understand the filtering parameters:

+

Now let me look at the event module and test files for querying:

+

Now let me look at the test files for get/list/watch operations:

+

Now let me check the conftest to understand fake_client and look at the Condition attribute and Status attribute patterns:

+

Now let me check how get is called with selectors:

+

Let me look at how get passes field_selector, label_selector, and namespace through kwargs:

+

Now let me look at more examples of how get() is used with various kwargs, and check what other files show:

+

Let me check how _build_event_field_selector and events work:

+

Now let me check the namespace.py to see how get() is used in a simple resource:

+

Now let me look at the node.py for a cluster-scoped resource usage of get:

+

Now let me check the exceptions module for ConditionError:

+

Let me also check ensure_exists:

+

Let me also look for properties that relate to querying the resource (uuid, labels):

+

Now let me check the test for events and any resource-specific tests about querying:

+

Let me also look at the fake_client to understand what kwargs get passes through:

+

Now I have a thorough understanding of the codebase. Let me write the documentation page.

+

Querying and Watching Resources

+

Find, filter, and monitor Kubernetes and OpenShift resources in your cluster — whether you need to list all pods by label, check if a resource exists, watch for real-time changes, or inspect status and conditions.

+

Prerequisites

+ +

Quick Example

+
from ocp_resources.pod import Pod
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+# List all pods in a namespace
+for pod in Pod.get(client=client, namespace="my-namespace"):
+    print(f"{pod.name}{pod.status}")
+
+ +

Listing Resources

+

Every resource class provides a get() class method that returns a generator of resource objects. The method accepts standard Kubernetes query parameters as keyword arguments.

+

List all resources of a type

+
from ocp_resources.namespace import Namespace
+
+for ns in Namespace.get(client=client):
+    print(ns.name)
+
+ +

Filter by namespace

+

Namespaced resources (like Pod, Service, ConfigMap) accept a namespace keyword:

+
from ocp_resources.pod import Pod
+
+for pod in Pod.get(client=client, namespace="default"):
+    print(pod.name)
+
+ +

Filter by label selector

+

Use label_selector with standard Kubernetes label selector syntax:

+
from ocp_resources.pod import Pod
+
+# Single label
+for pod in Pod.get(client=client, namespace="my-app", label_selector="app=nginx"):
+    print(pod.name)
+
+# Multiple labels (comma-separated)
+for pod in Pod.get(client=client, label_selector="app=nginx,tier=frontend"):
+    print(pod.name)
+
+ +

Filter by field selector

+

Use field_selector with standard Kubernetes field selector syntax:

+
from ocp_resources.pod import Pod
+
+# Find pods on a specific node
+for pod in Pod.get(client=client, field_selector="spec.nodeName=worker-1"):
+    print(pod.name)
+
+# Find pods by status phase
+for pod in Pod.get(client=client, namespace="default", field_selector="status.phase=Running"):
+    print(pod.name)
+
+ +

Get raw resource objects

+

By default, get() returns wrapper objects. Pass raw=True to get the underlying ResourceInstance or ResourceField objects directly:

+
from ocp_resources.pod import Pod
+
+for pod in Pod.get(client=client, namespace="default", raw=True):
+    # Returns raw ResourceField objects instead of Pod instances
+    print(pod.metadata.name, pod.status.phase)
+
+ +

List all cluster resources

+

To iterate over every resource type in the cluster:

+
from ocp_resources.resource import Resource
+
+for resource in Resource.get_all_cluster_resources(client=client):
+    print(f"{resource.kind}: {resource.metadata.name}")
+
+# Filter with label selector
+for resource in Resource.get_all_cluster_resources(client=client, label_selector="my-label=value"):
+    print(f"{resource.kind}: {resource.metadata.name}")
+
+ +

get() Method Reference

+
@classmethod
+def get(
+    cls,
+    client: DynamicClient | None = None,
+    singular_name: str = "",
+    exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
+    raw: bool = False,
+    *args,
+    **kwargs,
+) -> Generator
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
clientDynamicClientKubernetes dynamic client (required)
singular_namestrResource kind in lowercase; used when multiple resources match the same kind
rawboolIf True, return raw ResourceInstance/ResourceField objects instead of wrapper instances
exceptions_dictdictExceptions to retry on during API calls
**kwargsPassed through to the Kubernetes API: namespace, label_selector, field_selector, limit, resource_version, timeout_seconds, etc.
+

Returns: A generator yielding resource objects.

+

Checking Resource Existence

+

The exists property queries the API server and returns the resource instance if it exists, or None if it does not.

+
from ocp_resources.namespace import Namespace
+
+ns = Namespace(client=client, name="my-namespace")
+
+if ns.exists:
+    print("Namespace exists")
+else:
+    print("Namespace not found")
+
+ +

Require existence at initialization

+

Use ensure_exists=True to raise ResourceNotFoundError immediately if the resource is not found:

+
from ocp_resources.pod import Pod
+
+# Raises ResourceNotFoundError if the pod doesn't exist
+pod = Pod(client=client, name="my-pod", namespace="default", ensure_exists=True)
+
+ +
+

Tip: Use ensure_exists=True when you need a reference to a resource that must already be on the cluster, such as a pre-existing ConfigMap or Secret.

+
+

Getting the Resource Instance

+

The instance property fetches the full live resource from the API server:

+
pod = Pod(client=client, name="my-pod", namespace="default")
+
+# Get the full resource instance
+inst = pod.instance
+print(inst.metadata.name)
+print(inst.metadata.uid)
+print(inst.metadata.resourceVersion)
+
+# Convert to a plain dictionary
+resource_dict = pod.instance.to_dict()
+
+ +
+

Note: Each access to .instance makes an API call. Cache the result in a local variable if you need multiple fields from the same snapshot.

+
+

Retrieving Labels

+

The labels property returns the resource's labels:

+
ns = Namespace(client=client, name="my-namespace")
+print(ns.labels)  # {'kubernetes.io/metadata.name': 'my-namespace', ...}
+
+# Check a specific label
+if "app" in ns.labels:
+    print(f"App label: {ns.labels['app']}")
+
+ +

Retrieving Resource Status

+

Status phase

+

The status property returns the current phase (e.g., Running, Pending, Active):

+
from ocp_resources.namespace import Namespace
+
+ns = Namespace(client=client, name="my-namespace")
+print(ns.status)  # "Active"
+
+ +

Each resource class defines status constants you can compare against:

+
from ocp_resources.pod import Pod
+
+pod = Pod(client=client, name="my-pod", namespace="default")
+if pod.status == Pod.Status.RUNNING:
+    print("Pod is running")
+
+ +

Common status constants available on all resources:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConstantValue
Status.RUNNING"Running"
Status.PENDING"Pending"
Status.SUCCEEDED"Succeeded"
Status.FAILED"Failed"
Status.ACTIVE"Active"
Status.TERMINATING"Terminating"
Status.COMPLETED"Completed"
Status.ERROR"Error"
+

Condition messages

+

Use get_condition_message() to retrieve the message for a specific condition type:

+
pod = Pod(client=client, name="my-pod", namespace="default")
+
+# Get message for a condition type
+msg = pod.get_condition_message(condition_type="Ready")
+print(msg)
+
+# Optionally filter by condition status
+msg = pod.get_condition_message(
+    condition_type="Ready",
+    condition_status="False",
+)
+print(msg)  # Returns empty string if status doesn't match
+
+ +

Signature:

+
def get_condition_message(
+    self,
+    condition_type: str,
+    condition_status: str = "",
+) -> str
+
+ + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
condition_typestrThe condition type to look up (e.g., "Ready", "Available")
condition_statusstrIf provided, only return the message when the condition has this status
+

Returns: The condition message string, or "" if not found or status doesn't match.

+

Condition constants

+

Resources provide built-in condition constants:

+
from ocp_resources.pod import Pod
+
+pod = Pod(client=client, name="my-pod", namespace="default")
+
+# Use condition constants
+msg = pod.get_condition_message(
+    condition_type=Pod.Condition.READY,
+    condition_status=Pod.Condition.Status.TRUE,
+)
+
+ +

Common condition constants:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConstantValue
Condition.READY"Ready"
Condition.AVAILABLE"Available"
Condition.PROGRESSING"Progressing"
Condition.DEGRADED"Degraded"
Condition.UPGRADEABLE"Upgradeable"
Condition.Status.TRUE"True"
Condition.Status.FALSE"False"
Condition.Status.UNKNOWN"Unknown"
+

Watching for Changes

+

Watch a specific resource

+

The watcher() method streams events for a specific resource instance:

+
from ocp_resources.pod import Pod
+
+pod = Pod(client=client, name="my-pod", namespace="default")
+
+for event in pod.watcher(timeout=60):
+    print(f"Event type: {event['type']}")       # ADDED, MODIFIED, DELETED
+    print(f"Object: {event['object'].metadata.name}")
+    print(f"Raw: {event['raw_object']}")
+
+ +

Signature:

+
def watcher(
+    self,
+    timeout: int,
+    resource_version: str = "",
+) -> Generator[dict[str, Any], None, None]
+
+ + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
timeoutintHow long to watch (in seconds)
resource_versionstrOnly receive events newer than this version. Defaults to the resource version at creation time.
+

Each yielded event is a dict with these keys:

+ + + + + + + + + + + + + + + + + + + + + +
KeyDescription
typeEvent type: "ADDED", "MODIFIED", or "DELETED"
objectA ResourceInstance wrapping the resource
raw_objectA plain dict representing the resource
+

Querying Events

+

Stream events for a resource

+

The events() method watches Kubernetes events associated with a resource:

+
from ocp_resources.pod import Pod
+
+pod = Pod(client=client, name="my-pod", namespace="default")
+
+for event in pod.events(timeout=30):
+    print(f"Reason: {event.object.reason}")
+    print(f"Message: {event.object.message}")
+
+ +

Signature:

+
def events(
+    self,
+    name: str = "",
+    label_selector: str = "",
+    field_selector: str = "",
+    resource_version: str = "",
+    timeout: int = 240,
+) -> Generator
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
namestrFilter by event name
label_selectorstrFilter by labels (comma-separated key=value)
field_selectorstrAdditional field filters (merged with auto-generated involvedObject.name filter)
resource_versionstrOnly return events newer than this version
timeoutintWatch timeout in seconds (default: 240)
+
# Example: filter for Warning events with a specific reason
+for event in pod.events(
+    field_selector="type==Warning,reason==BackOff",
+    timeout=10,
+):
+    print(event.object.message)
+
+ +

List existing events (non-streaming)

+

Use Event.list() to get a snapshot of existing events without streaming:

+
from ocp_resources.event import Event
+
+# List Warning events from the last 5 minutes
+events = Event.list(
+    client=client,
+    namespace="my-namespace",
+    field_selector="type==Warning",
+)
+for event in events:
+    print(f"{event.reason}: {event.message}")
+
+# List events from the last 10 minutes
+events = Event.list(
+    client=client,
+    namespace="my-namespace",
+    since_seconds=600,
+)
+
+ +

Signature:

+
@classmethod
+def Event.list(
+    cls,
+    client: DynamicClient,
+    namespace: str | None = None,
+    field_selector: str | None = None,
+    label_selector: str | None = None,
+    since_seconds: int = 300,
+) -> list
+
+ +

Events are returned sorted by timestamp (most recent first).

+

Delete events

+

Clean up events before tests to avoid false positives:

+
from ocp_resources.event import Event
+
+Event.delete_events(
+    client=client,
+    namespace="my-namespace",
+    field_selector="reason=AnEventReason",
+)
+
+ +

Advanced Usage

+

Handling resources with duplicate API groups

+

Some resource kinds exist in multiple API groups. Use singular_name to disambiguate:

+
from ocp_resources.dns import DNS
+
+# When a kind exists in multiple API groups, pass singular_name
+for dns in DNS.get(client=client, singular_name="dns"):
+    print(dns.name)
+
+ +

Using full_api() for low-level access

+

The full_api() method returns the raw Kubernetes API resource object, giving you direct access to the API:

+
ns = Namespace(client=client, name="my-namespace")
+api = ns.full_api()
+
+# Use the API directly for custom queries
+result = api.get(
+    label_selector="app=nginx",
+    field_selector="status.phase=Active",
+    limit=10,
+)
+
+ +

full_api() accepts these keyword arguments:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescription
prettyPretty-print output
_continueContinuation token for paginated results
field_selectorFilter by fields
label_selectorFilter by labels
limitMaximum number of results
resource_versionFilter by resource version
timeout_secondsServer-side timeout
watchEnable watch mode
+

Converting resources to YAML

+
ns = Namespace(client=client, name="my-namespace")
+print(ns.to_yaml())
+
+ +

Wait for conditions and status

+

For polling-based waiting (e.g., wait until a pod is Running or a condition is True), see Waiting for Resource Conditions and Status.

+

Querying during tests with the fake client

+

You can use all querying methods with the fake client for unit testing:

+
from ocp_resources.resource import get_client
+from ocp_resources.namespace import Namespace
+
+client = get_client(fake=True)
+ns = Namespace(client=client, name="test-ns")
+ns.deploy()
+
+# All query methods work the same way
+assert ns.exists
+assert ns.instance
+assert ns.status == Namespace.Status.ACTIVE
+
+# Listing works too
+for ns in Namespace.get(client=client):
+    print(ns.name)
+
+ +

See Testing Without a Cluster Using the Fake Client for details.

+

Troubleshooting

+

ResourceNotFoundError when using ensure_exists=True +The resource does not exist on the cluster. Verify the name, namespace, and that the resource has been created before initializing the wrapper.

+

NotImplementedError: Couldn't find <Kind> in <api_group> +The resource kind is not available on your cluster. This typically means the CRD has not been installed or the API group is incorrect.

+

get() returns no results

+
    +
  • Verify the namespace is correct (omit it for cluster-scoped resources).
  • +
  • Check your label_selector and field_selector syntax — use == for equality (e.g., field_selector="status.phase==Running").
  • +
  • Ensure the client has permissions to list the resource type.
  • +
+

status property raises AttributeError +Not all resources have a status.phase field. Access instance.status directly to inspect what status fields are available for your resource type.

+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/querying-resources.md b/docs/querying-resources.md new file mode 100644 index 0000000000..d0f1595cbd --- /dev/null +++ b/docs/querying-resources.md @@ -0,0 +1,564 @@ +Let me start by reading the knowledge graph and understanding the codebase structure. + +Now let me explore the key source files related to querying, listing, watching, existence checks, and status retrieval. + +Now let me look at the `_prepare_resources` method and the `get` method more closely to understand the filtering parameters: + +Now let me look at the event module and test files for querying: + +Now let me look at the test files for get/list/watch operations: + +Now let me check the conftest to understand fake_client and look at the `Condition` attribute and `Status` attribute patterns: + +Now let me check how `get` is called with selectors: + +Let me look at how `get` passes `field_selector`, `label_selector`, and `namespace` through kwargs: + +Now let me look at more examples of how get() is used with various kwargs, and check what other files show: + +Let me check how `_build_event_field_selector` and events work: + +Now let me check the namespace.py to see how get() is used in a simple resource: + +Now let me look at the node.py for a cluster-scoped resource usage of get: + +Now let me check the exceptions module for ConditionError: + +Let me also check `ensure_exists`: + +Let me also look for properties that relate to querying the resource (uuid, labels): + +Now let me check the test for events and any resource-specific tests about querying: + +Let me also look at the fake_client to understand what kwargs `get` passes through: + +Now I have a thorough understanding of the codebase. Let me write the documentation page. + +# Querying and Watching Resources + +Find, filter, and monitor Kubernetes and OpenShift resources in your cluster — whether you need to list all pods by label, check if a resource exists, watch for real-time changes, or inspect status and conditions. + +## Prerequisites + +- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html)) +- A connected `DynamicClient` instance (see [Connecting to Clusters](connecting-to-clusters.html)) + +## Quick Example + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +# List all pods in a namespace +for pod in Pod.get(client=client, namespace="my-namespace"): + print(f"{pod.name} — {pod.status}") +``` + +## Listing Resources + +Every resource class provides a `get()` class method that returns a generator of resource objects. The method accepts standard Kubernetes query parameters as keyword arguments. + +### List all resources of a type + +```python +from ocp_resources.namespace import Namespace + +for ns in Namespace.get(client=client): + print(ns.name) +``` + +### Filter by namespace + +Namespaced resources (like `Pod`, `Service`, `ConfigMap`) accept a `namespace` keyword: + +```python +from ocp_resources.pod import Pod + +for pod in Pod.get(client=client, namespace="default"): + print(pod.name) +``` + +### Filter by label selector + +Use `label_selector` with standard Kubernetes label selector syntax: + +```python +from ocp_resources.pod import Pod + +# Single label +for pod in Pod.get(client=client, namespace="my-app", label_selector="app=nginx"): + print(pod.name) + +# Multiple labels (comma-separated) +for pod in Pod.get(client=client, label_selector="app=nginx,tier=frontend"): + print(pod.name) +``` + +### Filter by field selector + +Use `field_selector` with standard Kubernetes field selector syntax: + +```python +from ocp_resources.pod import Pod + +# Find pods on a specific node +for pod in Pod.get(client=client, field_selector="spec.nodeName=worker-1"): + print(pod.name) + +# Find pods by status phase +for pod in Pod.get(client=client, namespace="default", field_selector="status.phase=Running"): + print(pod.name) +``` + +### Get raw resource objects + +By default, `get()` returns wrapper objects. Pass `raw=True` to get the underlying `ResourceInstance` or `ResourceField` objects directly: + +```python +from ocp_resources.pod import Pod + +for pod in Pod.get(client=client, namespace="default", raw=True): + # Returns raw ResourceField objects instead of Pod instances + print(pod.metadata.name, pod.status.phase) +``` + +### List all cluster resources + +To iterate over every resource type in the cluster: + +```python +from ocp_resources.resource import Resource + +for resource in Resource.get_all_cluster_resources(client=client): + print(f"{resource.kind}: {resource.metadata.name}") + +# Filter with label selector +for resource in Resource.get_all_cluster_resources(client=client, label_selector="my-label=value"): + print(f"{resource.kind}: {resource.metadata.name}") +``` + +## `get()` Method Reference + +```python +@classmethod +def get( + cls, + client: DynamicClient | None = None, + singular_name: str = "", + exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS, + raw: bool = False, + *args, + **kwargs, +) -> Generator +``` + +| Parameter | Type | Description | +|---|---|---| +| `client` | `DynamicClient` | Kubernetes dynamic client (required) | +| `singular_name` | `str` | Resource kind in lowercase; used when multiple resources match the same kind | +| `raw` | `bool` | If `True`, return raw `ResourceInstance`/`ResourceField` objects instead of wrapper instances | +| `exceptions_dict` | `dict` | Exceptions to retry on during API calls | +| `**kwargs` | | Passed through to the Kubernetes API: `namespace`, `label_selector`, `field_selector`, `limit`, `resource_version`, `timeout_seconds`, etc. | + +**Returns:** A generator yielding resource objects. + +## Checking Resource Existence + +The `exists` property queries the API server and returns the resource instance if it exists, or `None` if it does not. + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-namespace") + +if ns.exists: + print("Namespace exists") +else: + print("Namespace not found") +``` + +### Require existence at initialization + +Use `ensure_exists=True` to raise `ResourceNotFoundError` immediately if the resource is not found: + +```python +from ocp_resources.pod import Pod + +# Raises ResourceNotFoundError if the pod doesn't exist +pod = Pod(client=client, name="my-pod", namespace="default", ensure_exists=True) +``` + +> **Tip:** Use `ensure_exists=True` when you need a reference to a resource that must already be on the cluster, such as a pre-existing ConfigMap or Secret. + +## Getting the Resource Instance + +The `instance` property fetches the full live resource from the API server: + +```python +pod = Pod(client=client, name="my-pod", namespace="default") + +# Get the full resource instance +inst = pod.instance +print(inst.metadata.name) +print(inst.metadata.uid) +print(inst.metadata.resourceVersion) + +# Convert to a plain dictionary +resource_dict = pod.instance.to_dict() +``` + +> **Note:** Each access to `.instance` makes an API call. Cache the result in a local variable if you need multiple fields from the same snapshot. + +## Retrieving Labels + +The `labels` property returns the resource's labels: + +```python +ns = Namespace(client=client, name="my-namespace") +print(ns.labels) # {'kubernetes.io/metadata.name': 'my-namespace', ...} + +# Check a specific label +if "app" in ns.labels: + print(f"App label: {ns.labels['app']}") +``` + +## Retrieving Resource Status + +### Status phase + +The `status` property returns the current phase (e.g., `Running`, `Pending`, `Active`): + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-namespace") +print(ns.status) # "Active" +``` + +Each resource class defines status constants you can compare against: + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") +if pod.status == Pod.Status.RUNNING: + print("Pod is running") +``` + +Common status constants available on all resources: + +| Constant | Value | +|---|---| +| `Status.RUNNING` | `"Running"` | +| `Status.PENDING` | `"Pending"` | +| `Status.SUCCEEDED` | `"Succeeded"` | +| `Status.FAILED` | `"Failed"` | +| `Status.ACTIVE` | `"Active"` | +| `Status.TERMINATING` | `"Terminating"` | +| `Status.COMPLETED` | `"Completed"` | +| `Status.ERROR` | `"Error"` | + +### Condition messages + +Use `get_condition_message()` to retrieve the message for a specific condition type: + +```python +pod = Pod(client=client, name="my-pod", namespace="default") + +# Get message for a condition type +msg = pod.get_condition_message(condition_type="Ready") +print(msg) + +# Optionally filter by condition status +msg = pod.get_condition_message( + condition_type="Ready", + condition_status="False", +) +print(msg) # Returns empty string if status doesn't match +``` + +**Signature:** +```python +def get_condition_message( + self, + condition_type: str, + condition_status: str = "", +) -> str +``` + +| Parameter | Type | Description | +|---|---|---| +| `condition_type` | `str` | The condition type to look up (e.g., `"Ready"`, `"Available"`) | +| `condition_status` | `str` | If provided, only return the message when the condition has this status | + +**Returns:** The condition message string, or `""` if not found or status doesn't match. + +### Condition constants + +Resources provide built-in condition constants: + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") + +# Use condition constants +msg = pod.get_condition_message( + condition_type=Pod.Condition.READY, + condition_status=Pod.Condition.Status.TRUE, +) +``` + +Common condition constants: + +| Constant | Value | +|---|---| +| `Condition.READY` | `"Ready"` | +| `Condition.AVAILABLE` | `"Available"` | +| `Condition.PROGRESSING` | `"Progressing"` | +| `Condition.DEGRADED` | `"Degraded"` | +| `Condition.UPGRADEABLE` | `"Upgradeable"` | +| `Condition.Status.TRUE` | `"True"` | +| `Condition.Status.FALSE` | `"False"` | +| `Condition.Status.UNKNOWN` | `"Unknown"` | + +## Watching for Changes + +### Watch a specific resource + +The `watcher()` method streams events for a specific resource instance: + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") + +for event in pod.watcher(timeout=60): + print(f"Event type: {event['type']}") # ADDED, MODIFIED, DELETED + print(f"Object: {event['object'].metadata.name}") + print(f"Raw: {event['raw_object']}") +``` + +**Signature:** +```python +def watcher( + self, + timeout: int, + resource_version: str = "", +) -> Generator[dict[str, Any], None, None] +``` + +| Parameter | Type | Description | +|---|---|---| +| `timeout` | `int` | How long to watch (in seconds) | +| `resource_version` | `str` | Only receive events newer than this version. Defaults to the resource version at creation time. | + +Each yielded event is a dict with these keys: + +| Key | Description | +|---|---| +| `type` | Event type: `"ADDED"`, `"MODIFIED"`, or `"DELETED"` | +| `object` | A `ResourceInstance` wrapping the resource | +| `raw_object` | A plain dict representing the resource | + +## Querying Events + +### Stream events for a resource + +The `events()` method watches Kubernetes events associated with a resource: + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") + +for event in pod.events(timeout=30): + print(f"Reason: {event.object.reason}") + print(f"Message: {event.object.message}") +``` + +**Signature:** +```python +def events( + self, + name: str = "", + label_selector: str = "", + field_selector: str = "", + resource_version: str = "", + timeout: int = 240, +) -> Generator +``` + +| Parameter | Type | Description | +|---|---|---| +| `name` | `str` | Filter by event name | +| `label_selector` | `str` | Filter by labels (comma-separated `key=value`) | +| `field_selector` | `str` | Additional field filters (merged with auto-generated `involvedObject.name` filter) | +| `resource_version` | `str` | Only return events newer than this version | +| `timeout` | `int` | Watch timeout in seconds (default: 240) | + +```python +# Example: filter for Warning events with a specific reason +for event in pod.events( + field_selector="type==Warning,reason==BackOff", + timeout=10, +): + print(event.object.message) +``` + +### List existing events (non-streaming) + +Use `Event.list()` to get a snapshot of existing events without streaming: + +```python +from ocp_resources.event import Event + +# List Warning events from the last 5 minutes +events = Event.list( + client=client, + namespace="my-namespace", + field_selector="type==Warning", +) +for event in events: + print(f"{event.reason}: {event.message}") + +# List events from the last 10 minutes +events = Event.list( + client=client, + namespace="my-namespace", + since_seconds=600, +) +``` + +**Signature:** +```python +@classmethod +def Event.list( + cls, + client: DynamicClient, + namespace: str | None = None, + field_selector: str | None = None, + label_selector: str | None = None, + since_seconds: int = 300, +) -> list +``` + +Events are returned sorted by timestamp (most recent first). + +### Delete events + +Clean up events before tests to avoid false positives: + +```python +from ocp_resources.event import Event + +Event.delete_events( + client=client, + namespace="my-namespace", + field_selector="reason=AnEventReason", +) +``` + +## Advanced Usage + +### Handling resources with duplicate API groups + +Some resource kinds exist in multiple API groups. Use `singular_name` to disambiguate: + +```python +from ocp_resources.dns import DNS + +# When a kind exists in multiple API groups, pass singular_name +for dns in DNS.get(client=client, singular_name="dns"): + print(dns.name) +``` + +### Using `full_api()` for low-level access + +The `full_api()` method returns the raw Kubernetes API resource object, giving you direct access to the API: + +```python +ns = Namespace(client=client, name="my-namespace") +api = ns.full_api() + +# Use the API directly for custom queries +result = api.get( + label_selector="app=nginx", + field_selector="status.phase=Active", + limit=10, +) +``` + +`full_api()` accepts these keyword arguments: + +| Parameter | Description | +|---|---| +| `pretty` | Pretty-print output | +| `_continue` | Continuation token for paginated results | +| `field_selector` | Filter by fields | +| `label_selector` | Filter by labels | +| `limit` | Maximum number of results | +| `resource_version` | Filter by resource version | +| `timeout_seconds` | Server-side timeout | +| `watch` | Enable watch mode | + +### Converting resources to YAML + +```python +ns = Namespace(client=client, name="my-namespace") +print(ns.to_yaml()) +``` + +### Wait for conditions and status + +For polling-based waiting (e.g., wait until a pod is `Running` or a condition is `True`), see [Waiting for Resource Conditions and Status](waiting-for-conditions.html). + +### Querying during tests with the fake client + +You can use all querying methods with the fake client for unit testing: + +```python +from ocp_resources.resource import get_client +from ocp_resources.namespace import Namespace + +client = get_client(fake=True) +ns = Namespace(client=client, name="test-ns") +ns.deploy() + +# All query methods work the same way +assert ns.exists +assert ns.instance +assert ns.status == Namespace.Status.ACTIVE + +# Listing works too +for ns in Namespace.get(client=client): + print(ns.name) +``` + +See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for details. + +## Troubleshooting + +**`ResourceNotFoundError` when using `ensure_exists=True`** +The resource does not exist on the cluster. Verify the name, namespace, and that the resource has been created before initializing the wrapper. + +**`NotImplementedError: Couldn't find in `** +The resource kind is not available on your cluster. This typically means the CRD has not been installed or the API group is incorrect. + +**`get()` returns no results** +- Verify the `namespace` is correct (omit it for cluster-scoped resources). +- Check your `label_selector` and `field_selector` syntax — use `==` for equality (e.g., `field_selector="status.phase==Running"`). +- Ensure the client has permissions to list the resource type. + +**`status` property raises `AttributeError`** +Not all resources have a `status.phase` field. Access `instance.status` directly to inspect what status fields are available for your resource type. + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Working with Kubernetes Events](working-with-events.html) +- [Common Resource Patterns](common-patterns.html) +- [Resource and NamespacedResource API](resource-api.html) diff --git a/docs/quickstart.html b/docs/quickstart.html new file mode 100644 index 0000000000..5a6c0fa524 --- /dev/null +++ b/docs/quickstart.html @@ -0,0 +1,679 @@ + + + + + + Installing and Creating Your First Resource - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me explore the repository structure and key files to understand the codebase before writing the documentation.

+

Now let me look at the NamespacedResource class and the get classmethod for listing resources:

+

Now let me check the get_client with the utils/client_config.py for understanding connection options:

+

Now I have a thorough understanding of the codebase. Let me write the documentation:

+

Now I have a comprehensive understanding of the codebase. Let me write the guide:

+

Installing and Creating Your First Resource

+

Get up and running with openshift-python-wrapper so you can programmatically create, query, and delete Kubernetes and OpenShift resources from Python. This guide takes you from installation to your first working resource in under 5 minutes.

+

Prerequisites

+
    +
  • Python 3.10 or later
  • +
  • Access to a Kubernetes or OpenShift cluster (with a valid kubeconfig)
  • +
  • pip or another Python package manager
  • +
+

Quick Example

+
pip install openshift-python-wrapper
+
+ +
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+with Namespace(client=client, name="my-first-namespace") as ns:
+    ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
+    print(f"Namespace {ns.name} is active: {ns.exists}")
+# Namespace is automatically deleted when the block exits
+
+ +

That's it — the namespace is created when entering the with block and cleaned up when leaving it.

+

Step 1: Install the Package

+
pip install openshift-python-wrapper
+
+ +
+

Tip: For isolated environments, use a virtual environment: +bash +python -m venv .venv && source .venv/bin/activate +pip install openshift-python-wrapper

+
+

Step 2: Connect to Your Cluster

+

The get_client() function returns a dynamic client connected to your cluster. By default it reads your kubeconfig from the KUBECONFIG environment variable or ~/.kube/config:

+
from ocp_resources.resource import get_client
+
+client = get_client()
+
+ +

You can also point to a specific kubeconfig file:

+
client = get_client(config_file="/path/to/kubeconfig")
+
+ +

Or connect with a token and host directly:

+
client = get_client(host="https://api.mycluster.example.com:6443", token="sha256~...")
+
+ +
+

Note: For the full set of connection options — including basic auth, in-cluster config, and environment variables — see Connecting to Clusters.

+
+

Step 3: Create a Resource

+

There are two ways to create a resource: explicitly with deploy()/clean_up(), or automatically with a context manager (with statement).

+ +

The context manager automatically deletes the resource when the block exits, which prevents leftover resources:

+
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+with Namespace(client=client, name="namespace-example-2") as ns:
+    ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
+    assert ns.exists
+# Resource is automatically cleaned up here
+
+ +

Option B: Explicit Deploy and Clean Up

+

Use deploy() and clean_up() when you need more control over the lifecycle:

+
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+ns = Namespace(client=client, name="namespace-example-1")
+ns.deploy()
+assert ns.exists
+ns.clean_up()
+
+ +
+

Tip: Set teardown=False on the resource to prevent clean_up() from being called when using a context manager. This is useful when you want the resource to persist after the block exits.

+
+

Step 4: Create a Namespaced Resource

+

Most Kubernetes resources (Pods, ConfigMaps, Deployments, etc.) live inside a namespace. These require both name and namespace:

+
from ocp_resources.config_map import ConfigMap
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+with ConfigMap(
+    client=client,
+    name="app-config",
+    namespace="default",
+    data={"app.properties": "debug=true\nport=8080"},
+) as cm:
+    assert cm.exists
+    print(f"ConfigMap {cm.name} created in namespace {cm.namespace}")
+
+ +

Step 5: Query Resources

+

Use the get() class method to list resources from the cluster. It returns a generator you can iterate over:

+
from ocp_resources.pod import Pod
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+for pod in Pod.get(client=client, namespace="default"):
+    print(pod.name)
+
+ +

Filter by labels using label_selector:

+
for pod in Pod.get(client=client, label_selector="app=nginx"):
+    node = pod.node
+    print(f"Pod {pod.name} is running on node {node.name}")
+
+ +

Check if a specific resource exists:

+
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+ns = Namespace(client=client, name="default")
+if ns.exists:
+    print("Namespace exists!")
+
+ +
+

Note: For more on querying, filtering, and watching resources, see Querying and Watching Resources.

+
+

Step 6: Delete a Resource

+

If you didn't use a context manager, call clean_up() to delete the resource:

+
ns = Namespace(client=client, name="my-temp-namespace")
+ns.deploy()
+# ... do work ...
+ns.clean_up()
+
+ +

The clean_up() method waits for the resource to be fully deleted by default. Pass wait=False to return immediately:

+
ns.clean_up(wait=False)
+
+ +
+

Note: For full details on resource lifecycle management, see Creating and Managing Resources.

+
+

Putting It All Together

+

Here's a complete script that creates a namespace, deploys a ConfigMap inside it, reads it back, and cleans everything up:

+
from ocp_resources.config_map import ConfigMap
+from ocp_resources.namespace import Namespace
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+with Namespace(client=client, name="quickstart-demo") as ns:
+    ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
+
+    with ConfigMap(
+        client=client,
+        name="demo-config",
+        namespace="quickstart-demo",
+        data={"greeting": "hello from openshift-python-wrapper"},
+    ) as cm:
+        assert cm.exists
+        print(f"Created ConfigMap '{cm.name}' in namespace '{ns.name}'")
+
+        # Query it back
+        for config_map in ConfigMap.get(client=client, namespace="quickstart-demo"):
+            print(f"  Found: {config_map.name}")
+    # ConfigMap cleaned up here
+# Namespace cleaned up here
+
+ +

Advanced Usage

+

Creating Resources from YAML

+

You can create any resource from an existing YAML file instead of specifying parameters in Python:

+
from ocp_resources.config_map import ConfigMap
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+cm = ConfigMap(client=client, yaml_file="my-configmap.yaml")
+cm.deploy()
+
+ +

Using a Fake Client for Testing

+

You can run your code without a live cluster by using a fake client — useful for unit tests and local development:

+
from ocp_resources.config_map import ConfigMap
+from ocp_resources.resource import get_client
+
+client = get_client(fake=True)
+
+with ConfigMap(
+    client=client,
+    name="test-config",
+    namespace="default",
+    data={"key": "value"},
+) as cm:
+    cm.create()
+    print(f"Created (fake): {cm.name}")
+
+ +
+

Note: See Testing Without a Cluster Using the Fake Client for full details on the fake client.

+
+

Schema Validation Before Creating

+

Enable schema validation to catch configuration errors before sending requests to the cluster:

+
from ocp_resources.pod import Pod
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+pod = Pod(
+    client=client,
+    name="validated-pod",
+    namespace="default",
+    containers=[{"name": "nginx", "image": "nginx:latest"}],
+    schema_validation_enabled=True,  # Validates on create
+)
+pod.deploy()
+
+ +

You can also validate manually without creating:

+
pod = Pod(
+    name="check-me",
+    namespace="default",
+    containers=[{"name": "nginx", "image": "nginx:latest"}],
+)
+pod.validate()  # Raises ValidationError if invalid
+
+ +
+

Note: See Validating Resources Against OpenAPI Schemas for more validation options.

+
+

Waiting for Resource Conditions

+

After creating a resource, you can wait for it to reach a specific status or condition:

+
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import get_client
+
+client = get_client()
+
+with Namespace(client=client, name="wait-demo") as ns:
+    ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
+
+ +
+

Note: See Waiting for Resource Conditions and Status for advanced waiting patterns.

+
+

Troubleshooting

+

"KUBECONFIG not found" or connection errors

+

Make sure your kubeconfig is in the expected location or explicitly set the path:

+
export KUBECONFIG=/path/to/kubeconfig
+
+ +

Or pass it directly:

+
client = get_client(config_file="/path/to/kubeconfig")
+
+ +

Resource not found after creation

+

Some resources take time to become available. Use wait_for_status() or wait() to confirm the resource is ready before interacting with it:

+
ns.deploy()
+ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
+
+ +

Missing name or namespace errors

+
    +
  • Cluster-scoped resources (like Namespace) require only name.
  • +
  • Namespaced resources (like Pod, ConfigMap, Deployment) require both name and namespace.
  • +
+
# Cluster-scoped — only name needed
+Namespace(client=client, name="my-ns")
+
+# Namespaced — both name and namespace required
+ConfigMap(client=client, name="my-cm", namespace="my-ns")
+
+ + + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000000..3facbdc914 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,352 @@ +Let me explore the repository structure and key files to understand the codebase before writing the documentation. + +Now let me look at the `NamespacedResource` class and the `get` classmethod for listing resources: + +Now let me check the `get_client` with the `utils/client_config.py` for understanding connection options: + +Now I have a thorough understanding of the codebase. Let me write the documentation: + +Now I have a comprehensive understanding of the codebase. Let me write the guide: + +# Installing and Creating Your First Resource + +Get up and running with openshift-python-wrapper so you can programmatically create, query, and delete Kubernetes and OpenShift resources from Python. This guide takes you from installation to your first working resource in under 5 minutes. + +## Prerequisites + +- Python 3.10 or later +- Access to a Kubernetes or OpenShift cluster (with a valid kubeconfig) +- `pip` or another Python package manager + +## Quick Example + +```bash +pip install openshift-python-wrapper +``` + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +with Namespace(client=client, name="my-first-namespace") as ns: + ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) + print(f"Namespace {ns.name} is active: {ns.exists}") +# Namespace is automatically deleted when the block exits +``` + +That's it — the namespace is created when entering the `with` block and cleaned up when leaving it. + +## Step 1: Install the Package + +```bash +pip install openshift-python-wrapper +``` + +> **Tip:** For isolated environments, use a virtual environment: +> ```bash +> python -m venv .venv && source .venv/bin/activate +> pip install openshift-python-wrapper +> ``` + +## Step 2: Connect to Your Cluster + +The `get_client()` function returns a dynamic client connected to your cluster. By default it reads your kubeconfig from the `KUBECONFIG` environment variable or `~/.kube/config`: + +```python +from ocp_resources.resource import get_client + +client = get_client() +``` + +You can also point to a specific kubeconfig file: + +```python +client = get_client(config_file="/path/to/kubeconfig") +``` + +Or connect with a token and host directly: + +```python +client = get_client(host="https://api.mycluster.example.com:6443", token="sha256~...") +``` + +> **Note:** For the full set of connection options — including basic auth, in-cluster config, and environment variables — see [Connecting to Clusters](connecting-to-clusters.html). + +## Step 3: Create a Resource + +There are two ways to create a resource: explicitly with `deploy()`/`clean_up()`, or automatically with a context manager (`with` statement). + +### Option A: Context Manager (Recommended) + +The context manager automatically deletes the resource when the block exits, which prevents leftover resources: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +with Namespace(client=client, name="namespace-example-2") as ns: + ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) + assert ns.exists +# Resource is automatically cleaned up here +``` + +### Option B: Explicit Deploy and Clean Up + +Use `deploy()` and `clean_up()` when you need more control over the lifecycle: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +ns = Namespace(client=client, name="namespace-example-1") +ns.deploy() +assert ns.exists +ns.clean_up() +``` + +> **Tip:** Set `teardown=False` on the resource to prevent `clean_up()` from being called when using a context manager. This is useful when you want the resource to persist after the block exits. + +## Step 4: Create a Namespaced Resource + +Most Kubernetes resources (Pods, ConfigMaps, Deployments, etc.) live inside a namespace. These require both `name` and `namespace`: + +```python +from ocp_resources.config_map import ConfigMap +from ocp_resources.resource import get_client + +client = get_client() + +with ConfigMap( + client=client, + name="app-config", + namespace="default", + data={"app.properties": "debug=true\nport=8080"}, +) as cm: + assert cm.exists + print(f"ConfigMap {cm.name} created in namespace {cm.namespace}") +``` + +## Step 5: Query Resources + +Use the `get()` class method to list resources from the cluster. It returns a generator you can iterate over: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +for pod in Pod.get(client=client, namespace="default"): + print(pod.name) +``` + +Filter by labels using `label_selector`: + +```python +for pod in Pod.get(client=client, label_selector="app=nginx"): + node = pod.node + print(f"Pod {pod.name} is running on node {node.name}") +``` + +Check if a specific resource exists: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +ns = Namespace(client=client, name="default") +if ns.exists: + print("Namespace exists!") +``` + +> **Note:** For more on querying, filtering, and watching resources, see [Querying and Watching Resources](querying-resources.html). + +## Step 6: Delete a Resource + +If you didn't use a context manager, call `clean_up()` to delete the resource: + +```python +ns = Namespace(client=client, name="my-temp-namespace") +ns.deploy() +# ... do work ... +ns.clean_up() +``` + +The `clean_up()` method waits for the resource to be fully deleted by default. Pass `wait=False` to return immediately: + +```python +ns.clean_up(wait=False) +``` + +> **Note:** For full details on resource lifecycle management, see [Creating and Managing Resources](creating-and-managing-resources.html). + +## Putting It All Together + +Here's a complete script that creates a namespace, deploys a ConfigMap inside it, reads it back, and cleans everything up: + +```python +from ocp_resources.config_map import ConfigMap +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +with Namespace(client=client, name="quickstart-demo") as ns: + ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) + + with ConfigMap( + client=client, + name="demo-config", + namespace="quickstart-demo", + data={"greeting": "hello from openshift-python-wrapper"}, + ) as cm: + assert cm.exists + print(f"Created ConfigMap '{cm.name}' in namespace '{ns.name}'") + + # Query it back + for config_map in ConfigMap.get(client=client, namespace="quickstart-demo"): + print(f" Found: {config_map.name}") + # ConfigMap cleaned up here +# Namespace cleaned up here +``` + +## Advanced Usage + +### Creating Resources from YAML + +You can create any resource from an existing YAML file instead of specifying parameters in Python: + +```python +from ocp_resources.config_map import ConfigMap +from ocp_resources.resource import get_client + +client = get_client() + +cm = ConfigMap(client=client, yaml_file="my-configmap.yaml") +cm.deploy() +``` + +### Using a Fake Client for Testing + +You can run your code without a live cluster by using a fake client — useful for unit tests and local development: + +```python +from ocp_resources.config_map import ConfigMap +from ocp_resources.resource import get_client + +client = get_client(fake=True) + +with ConfigMap( + client=client, + name="test-config", + namespace="default", + data={"key": "value"}, +) as cm: + cm.create() + print(f"Created (fake): {cm.name}") +``` + +> **Note:** See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for full details on the fake client. + +### Schema Validation Before Creating + +Enable schema validation to catch configuration errors before sending requests to the cluster: + +```python +from ocp_resources.pod import Pod +from ocp_resources.resource import get_client + +client = get_client() + +pod = Pod( + client=client, + name="validated-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + schema_validation_enabled=True, # Validates on create +) +pod.deploy() +``` + +You can also validate manually without creating: + +```python +pod = Pod( + name="check-me", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], +) +pod.validate() # Raises ValidationError if invalid +``` + +> **Note:** See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for more validation options. + +### Waiting for Resource Conditions + +After creating a resource, you can wait for it to reach a specific status or condition: + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import get_client + +client = get_client() + +with Namespace(client=client, name="wait-demo") as ns: + ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) +``` + +> **Note:** See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for advanced waiting patterns. + +## Troubleshooting + +### "KUBECONFIG not found" or connection errors + +Make sure your kubeconfig is in the expected location or explicitly set the path: + +```bash +export KUBECONFIG=/path/to/kubeconfig +``` + +Or pass it directly: + +```python +client = get_client(config_file="/path/to/kubeconfig") +``` + +### Resource not found after creation + +Some resources take time to become available. Use `wait_for_status()` or `wait()` to confirm the resource is ready before interacting with it: + +```python +ns.deploy() +ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) +``` + +### Missing `name` or `namespace` errors + +- Cluster-scoped resources (like `Namespace`) require only `name`. +- Namespaced resources (like `Pod`, `ConfigMap`, `Deployment`) require both `name` and `namespace`. + +```python +# Cluster-scoped — only name needed +Namespace(client=client, name="my-ns") + +# Namespaced — both name and namespace required +ConfigMap(client=client, name="my-cm", namespace="my-ns") +``` + +## Related Pages + +- [Connecting to Clusters](connecting-to-clusters.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Querying and Watching Resources](querying-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) diff --git a/docs/resource-api.html b/docs/resource-api.html new file mode 100644 index 0000000000..0fb3c2a55e --- /dev/null +++ b/docs/resource-api.html @@ -0,0 +1,2544 @@ + + + + + + Resource and NamespacedResource API - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by reading the knowledge graph and the pages manifest, then explore the source code.

+

Now let me examine the constants and exceptions:

+

Now let me check the apply_yaml function and the get_client function signature, plus see what ResourceConstants inherits:

+

Let me look for key public methods in resource.py that I might have missed (the file was truncated before):

+

Let me also check for import paths:

+

Now let me look at the NamespacedResource._base_body more carefully and check for anything more:

+

Now let me check what the Event class looks like for the events method reference:

+

Let me also check for generate_yaml_from_template:

+

That's a test utility. Let me check for the skip_existing_resource_creation_teardown function:

+

Now I have all the information needed. Let me write the comprehensive API reference page.

+

Resource and NamespacedResource API

+

Complete API reference for the Resource and NamespacedResource base classes, the get_client factory function, and supporting classes (ResourceEditor, ResourceList, NamespacedResourceList, KubeAPIVersion).

+

Import path:

+
from ocp_resources.resource import (
+    Resource,
+    NamespacedResource,
+    ResourceEditor,
+    ResourceList,
+    NamespacedResourceList,
+    get_client,
+)
+
+ +
+

get_client

+

Factory function to obtain a Kubernetes DynamicClient.

+
from ocp_resources.resource import get_client
+
+client = get_client(config_file="~/.kube/config")
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
config_filestr \| NoneNonePath to a kubeconfig file. Falls back to KUBECONFIG env var or ~/.kube/config.
config_dictdict[str, Any] \| NoneNoneDict with kubeconfig configuration. Mutually exclusive with config_file.
contextstr \| NoneNoneName of the kubeconfig context to use.
client_configurationkubernetes.client.Configuration \| NoneNonePre-built Kubernetes client configuration object.
persist_configboolTrueWhether to persist config file changes.
temp_file_pathstr \| NoneNonePath to a temporary kubeconfig file.
try_refresh_tokenboolTrueTry to refresh the authentication token.
usernamestr \| NoneNoneUsername for basic auth. Requires password and host.
passwordstr \| NoneNonePassword for basic auth. Requires username and host.
hoststr \| NoneNoneCluster host URL.
verify_sslbool \| NoneNoneWhether to verify SSL certificates.
tokenstr \| NoneNoneBearer token for authentication. Requires host.
fakeboolFalseReturn a FakeDynamicClient for testing without a cluster.
generate_kubeconfigboolFalseSave the resolved kubeconfig to a temp file and attach the path to the client.
+

Returns: DynamicClient | FakeDynamicClient

+
# Default kubeconfig
+client = get_client()
+
+# With explicit config file and context
+client = get_client(config_file="/path/to/kubeconfig", context="my-cluster")
+
+# Token-based auth
+client = get_client(host="https://api.cluster.example.com:6443", token="sha256~abc123")
+
+# Basic auth
+client = get_client(host="https://api.cluster.example.com:6443", username="admin", password="secret")
+
+# Fake client for unit testing
+client = get_client(fake=True)
+
+ +
+

Note: If neither config_file nor config_dict is provided, the client falls back to the KUBECONFIG environment variable, then ~/.kube/config, and finally in-cluster configuration.

+
+

See Connecting to Clusters for connection patterns. See Testing Without a Cluster Using the Fake Client for fake=True usage.

+
+

Resource

+
from ocp_resources.resource import Resource
+
+ +

Base class for cluster-scoped Kubernetes/OpenShift resources (e.g., Namespace, Node, ClusterRole). Inherits from ResourceConstants. All concrete resource classes inherit from either Resource or NamespacedResource.

+

See Understanding the Resource Class Hierarchy for the inheritance model.

+

Class Attributes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeTypeDefaultDescription
api_groupstr""API group for the resource (e.g., "apps", "batch").
api_versionstr""Full API version string (e.g., "v1", "apps/v1"). Resolved automatically if api_group is set.
singular_namestr""Singular resource name for API calls. Used to disambiguate when multiple resources match the same kind.
timeout_secondsint60Default timeout for API list/watch operations.
+

Constructor

+
Resource(
+    client=client,
+    name="my-resource",
+)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
clientDynamicClient \| NoneNoneKubernetes dynamic client. Will be mandatory in the next major release.
namestr \| NoneNoneResource name. Required unless yaml_file or kind_dict is provided.
teardownboolTrueWhether this resource should be deleted when used as a context manager.
yaml_filestr \| NoneNonePath to a YAML file defining the resource. Mutually exclusive with kind_dict.
delete_timeoutint240Timeout in seconds for delete operations.
dry_runboolFalseIf True, create operations use dry-run mode.
node_selectordict[str, Any] \| NoneNoneNode selector for scheduling.
node_selector_labelsdict[str, str] \| NoneNoneNode selector labels for scheduling.
config_filestr \| NoneNonePath to kubeconfig. Deprecated; pass client instead.
config_dictdict[str, Any] \| NoneNoneKubeconfig dict.
contextstr \| NoneNoneKubeconfig context name. Deprecated; pass client instead.
labeldict[str, str] \| NoneNoneLabels to set on the resource.
annotationsdict[str, str] \| NoneNoneAnnotations to set on the resource.
api_groupstr""Override the class-level api_group.
hash_log_databoolTrueHash sensitive fields (defined by keys_to_hash) in log output.
ensure_existsboolFalseCheck that the resource already exists on the cluster at init time. Raises ResourceNotFoundError if not.
kind_dictdict[Any, Any] \| NoneNoneComplete resource dict. Mutually exclusive with yaml_file. Bypasses to_dict() logic.
wait_for_resourceboolFalseWhen used as a context manager, wait for the resource to exist after creation.
schema_validation_enabledboolFalseEnable automatic OpenAPI schema validation on create() and update_replace().
+

Raises:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ExceptionCondition
ValueErrorBoth yaml_file and kind_dict are provided.
NotImplementedErrorNeither api_group nor api_version is defined on the class.
MissingRequiredArgumentErrorNone of name, yaml_file, or kind_dict is provided.
ResourceNotFoundErrorensure_exists=True and the resource does not exist.
+
from ocp_resources.namespace import Namespace
+
+# Basic creation
+ns = Namespace(client=client, name="my-namespace")
+
+# From a YAML file
+ns = Namespace(client=client, yaml_file="namespace.yaml")
+
+# From a dict
+ns = Namespace(client=client, kind_dict={"apiVersion": "v1", "kind": "Namespace", "metadata": {"name": "test"}})
+
+# With labels and annotations
+ns = Namespace(client=client, name="my-ns", label={"env": "test"}, annotations={"owner": "team-a"})
+
+# Verify it already exists
+ns = Namespace(client=client, name="default", ensure_exists=True)
+
+ +
+

Context Manager Protocol

+

Resource supports the Python context manager protocol for automatic creation and cleanup.

+
from ocp_resources.namespace import Namespace
+
+with Namespace(client=client, name="temp-ns") as ns:
+    # Resource is created on __enter__
+    print(ns.name)
+# Resource is deleted on __exit__ (if teardown=True)
+
+ + + + + + + + + + + + + + + + + + +
MethodDescription
__enter__()Calls deploy(wait=self.wait_for_resource). Registers a SIGINT handler on the main thread to ensure cleanup on Ctrl+C. Returns self.
__exit__(...)Calls clean_up() if teardown=True. Raises ResourceTeardownError if cleanup fails.
+
+

Tip: Set teardown=False to prevent automatic deletion on context exit.

+
+
+

CRUD Methods

+

create

+
def create(
+    self,
+    wait: bool = False,
+    exceptions_dict: dict[type[Exception], list[str]] = ...,
+) -> ResourceInstance | None
+
+ +

Create the resource on the cluster.

+ + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
waitboolFalseWait for the resource to exist after creation.
exceptions_dictdict[type[Exception], list[str]]DEFAULT_CLUSTER_RETRY_EXCEPTIONS \| PROTOCOL_ERROR_EXCEPTION_DICTExceptions to retry on.
+

Returns: ResourceInstance | None

+

Raises: ValidationError if schema_validation_enabled=True and the resource dict is invalid.

+
ns = Namespace(client=client, name="my-ns")
+ns.create(wait=True)
+
+ +

delete

+
def delete(
+    self,
+    wait: bool = False,
+    timeout: int = 240,
+    body: dict[str, Any] | None = None,
+) -> bool
+
+ +

Delete the resource from the cluster.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
waitboolFalseWait for the resource to be fully deleted.
timeoutint240Timeout in seconds when wait=True.
bodydict[str, Any] \| NoneNoneOptional delete options body.
+

Returns: boolTrue if deleted or not found; False only if wait timed out.

+
ns.delete(wait=True, timeout=120)
+
+ +

update

+
def update(self, resource_dict: dict[str, Any]) -> None
+
+ +

Patch the resource with a merge-patch (application/merge-patch+json). Only updates the fields present in resource_dict.

+ + + + + + + + + + + + + + + +
ParameterTypeDescription
resource_dictdict[str, Any]Partial resource dictionary with fields to update.
+
+

Note: Schema validation is not applied on update() because patches are partial and would fail full-schema validation.

+
+
ns.update(resource_dict={"metadata": {"labels": {"env": "staging"}}})
+
+ +

update_replace

+
def update_replace(self, resource_dict: dict[str, Any]) -> None
+
+ +

Replace the full resource. Use this to remove existing fields (unlike update(), which only adds/modifies).

+ + + + + + + + + + + + + + + +
ParameterTypeDescription
resource_dictdict[str, Any]Complete resource dictionary to replace with.
+

Raises: ValidationError if schema_validation_enabled=True and the dict is invalid.

+
full_dict = ns.instance.to_dict()
+full_dict["metadata"]["labels"] = {"new-label": "only"}
+ns.update_replace(resource_dict=full_dict)
+
+ +
+

Deploy / Clean Up

+

deploy

+
def deploy(self, wait: bool = False) -> Self
+
+ +

Create the resource (respects REUSE_IF_RESOURCE_EXISTS environment variable).

+ + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
waitboolFalseWait for the resource after creation.
+

Returns: Self

+

See Environment Variables and Configuration for REUSE_IF_RESOURCE_EXISTS details.

+

clean_up

+
def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool
+
+ +

Delete the resource (respects SKIP_RESOURCE_TEARDOWN environment variable).

+ + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
waitboolTrueWait for deletion to complete.
timeoutint \| NoneNoneTimeout in seconds. Defaults to delete_timeout.
+

Returns: boolTrue if deleted successfully.

+

See Environment Variables and Configuration for SKIP_RESOURCE_TEARDOWN details.

+
+

Query Methods and Properties

+

exists

+
@property
+def exists(self) -> ResourceInstance | None
+
+ +

Check if the resource exists on the cluster.

+

Returns: ResourceInstance if found, None if not.

+
if ns.exists:
+    print("Namespace exists")
+
+ +

instance

+
@property
+def instance(self) -> ResourceInstance
+
+ +

Get the live resource instance from the cluster. Retries on transient cluster errors.

+

Returns: ResourceInstance

+

Raises: NotFoundError if the resource does not exist.

+
resource_version = ns.instance.metadata.resourceVersion
+
+ +

status

+
@property
+def status(self) -> str
+
+ +

Get status.phase from the resource instance.

+

Returns: str — The status phase string (e.g., "Running", "Active", "Pending").

+
print(ns.status)  # "Active"
+
+ +

labels

+
@property
+def labels(self) -> ResourceField
+
+ +

Get resource labels from metadata.labels.

+

Returns: ResourceField

+
for key, value in ns.labels.items():
+    print(f"{key}={value}")
+
+ +

kind

+
@ClassProperty
+def kind(cls) -> str | None
+
+ +

Get the resource kind name derived from the class hierarchy. This is a class-level property — accessible on both the class and instances.

+

Returns: str | None

+
from ocp_resources.namespace import Namespace
+print(Namespace.kind)  # "Namespace"
+
+ +

api

+
@property
+def api(self) -> ResourceInstance
+
+ +

Get the resource API object (shortcut for full_api() with no extra kwargs).

+

Returns: ResourceInstance

+

full_api

+
def full_api(self, **kwargs: Any) -> ResourceInstance
+
+ +

Get the resource API object with optional filtering kwargs.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Keyword ArgumentDescription
prettyPretty-print output.
_continueContinuation token for list pagination.
field_selectorFilter by field.
label_selectorFilter by label.
limitMaximum number of results.
resource_versionFilter by resource version.
timeout_secondsRequest timeout.
watchEnable watch mode.
+

Returns: ResourceInstance

+
+

Class Method: get

+
@classmethod
+def get(
+    cls,
+    client: DynamicClient | None = None,
+    dyn_client: DynamicClient | None = None,
+    config_file: str = "",
+    singular_name: str = "",
+    exceptions_dict: dict[type[Exception], list[str]] = ...,
+    raw: bool = False,
+    context: str | None = None,
+    *args: Any,
+    **kwargs: Any,
+) -> Generator[Any, None, None]
+
+ +

List resources of this kind from the cluster.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
clientDynamicClient \| NoneNoneKubernetes client.
dyn_clientDynamicClient \| NoneNoneDeprecated alias for client.
config_filestr""Path to kubeconfig. Deprecated; pass client.
singular_namestr""Singular resource name for disambiguation.
exceptions_dictdictDEFAULT_CLUSTER_RETRY_EXCEPTIONSExceptions to retry.
rawboolFalseIf True, yield raw ResourceInstance objects instead of wrapper instances.
contextstr \| NoneNoneKubeconfig context. Deprecated; pass client.
**kwargsPassed through to the API (e.g., label_selector, field_selector).
+

Returns: Generator of resource instances.

+
+

Note: For Resource (cluster-scoped), yielded objects are constructed with name only. For NamespacedResource, yielded objects include both name and namespace.

+
+
from ocp_resources.namespace import Namespace
+
+for ns in Namespace.get(client=client):
+    print(ns.name)
+
+# With label selector
+for ns in Namespace.get(client=client, label_selector="env=production"):
+    print(ns.name)
+
+# Raw mode
+for raw_ns in Namespace.get(client=client, raw=True):
+    print(raw_ns.metadata.name, raw_ns.status.phase)
+
+ +

See Querying and Watching Resources for advanced list/filter patterns.

+
+

Static Method: get_all_cluster_resources

+
@staticmethod
+def get_all_cluster_resources(
+    client: DynamicClient | None = None,
+    config_file: str = "",
+    context: str | None = None,
+    config_dict: dict[str, Any] | None = None,
+    *args: Any,
+    **kwargs: Any,
+) -> Generator[ResourceField, None, None]
+
+ +

Yield all resources across all API groups in the cluster.

+ + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
clientDynamicClient \| NoneNoneKubernetes client.
**kwargsPassed to the API (e.g., label_selector).
+

Yields: ResourceField

+
for resource in Resource.get_all_cluster_resources(client=client, label_selector="app=myapp"):
+    print(f"{resource.kind}: {resource.metadata.name}")
+
+ +
+

Wait Methods

+

wait

+
def wait(self, timeout: int = 240, sleep: int = 1) -> None
+
+ +

Wait until the resource exists on the cluster.

+ + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
timeoutint240Maximum wait time in seconds.
sleepint1Sleep interval between retries.
+

Raises: TimeoutExpiredError if the resource does not exist within the timeout.

+

wait_deleted

+
def wait_deleted(self, timeout: int = 240) -> bool
+
+ +

Wait until the resource no longer exists.

+ + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
timeoutint240Maximum wait time in seconds.
+

Returns: boolTrue if deleted, False if timed out.

+

wait_for_status

+
def wait_for_status(
+    self,
+    status: str,
+    timeout: int = 240,
+    stop_status: str | None = None,
+    sleep: int = 1,
+    exceptions_dict: dict[type[Exception], list[str]] = ...,
+) -> None
+
+ +

Wait for status.phase to reach the expected value.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
statusstrExpected status phase string.
timeoutint240Maximum wait time in seconds.
stop_statusstr \| NoneNoneStop and fail immediately if this status is reached. Defaults to Status.FAILED.
sleepint1Sleep interval between retries.
exceptions_dictdictPROTOCOL_ERROR_EXCEPTION_DICT \| DEFAULT_CLUSTER_RETRY_EXCEPTIONSExceptions to retry.
+

Raises: TimeoutExpiredError if the desired status is not reached.

+
from ocp_resources.pod import Pod
+
+pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120)
+
+ +

wait_for_condition

+
def wait_for_condition(
+    self,
+    condition: str,
+    status: str,
+    timeout: int = 300,
+    sleep_time: int = 1,
+    reason: str | None = None,
+    message: str = "",
+    stop_condition: str | None = None,
+    stop_status: str = "True",
+) -> None
+
+ +

Wait for a specific condition to reach the desired state.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
conditionstrCondition type name (e.g., "Ready", "Available").
statusstrExpected condition status (e.g., "True", "False").
timeoutint300Maximum wait time in seconds.
sleep_timeint1Interval between retries.
reasonstr \| NoneNoneExpected condition reason. If set, must match exactly.
messagestr""Expected substring within the condition message.
stop_conditionstr \| NoneNoneCondition type that, if matched, stops the wait and fails immediately.
stop_statusstr"True"Status for stop_condition matching.
+

Raises:

+ + + + + + + + + + + + + + + + + +
ExceptionCondition
TimeoutExpiredErrorCondition not met within timeout.
ConditionErrorstop_condition is detected with matching stop_status.
+
ns.wait_for_condition(
+    condition="Ready",
+    status="True",
+    timeout=60,
+)
+
+ +

See Waiting for Resource Conditions and Status for more patterns.

+

wait_for_conditions

+
def wait_for_conditions(self) -> None
+
+ +

Wait for the resource to have any conditions populated in its status. Uses a 30-second timeout.

+
+

Serialization

+

to_dict

+
def to_dict(self) -> None
+
+ +

Populate self.res with the intended dict representation of the resource. Called automatically before create(). Override this in subclasses to add resource-specific fields.

+
+

Note: If kind_dict or yaml_file was provided, to_dict() uses those directly instead of building from individual parameters.

+
+

to_yaml

+
def to_yaml(self) -> str
+
+ +

Returns: str — YAML string representation of the resource dict.

+
print(ns.to_yaml())
+
+ +
+

Watch / Events

+

watcher

+
def watcher(
+    self,
+    timeout: int,
+    resource_version: str = "",
+) -> Generator[dict[str, Any], None, None]
+
+ +

Watch for changes to this specific resource.

+ + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
timeoutintWatch duration in seconds.
resource_versionstr""Only return events after this version. Defaults to the version at resource creation time.
+

Yields: Event dicts with keys type ("ADDED", "MODIFIED", "DELETED"), raw_object, and object.

+
for event in ns.watcher(timeout=30):
+    print(event["type"], event["object"].metadata.name)
+
+ +

events

+
def events(
+    self,
+    name: str = "",
+    label_selector: str = "",
+    field_selector: str = "",
+    resource_version: str = "",
+    timeout: int = 240,
+) -> Generator[Any, Any, None]
+
+ +

Get Kubernetes events related to this resource.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
namestr""Filter by event name.
label_selectorstr""Comma-separated key=value label filters.
field_selectorstr""Additional field selectors (auto-prefixed with involvedObject.name==<resource_name>).
resource_versionstr""Filter events by resource version.
timeoutint240Timeout in seconds.
+

Yields: Event objects.

+
for event in pod.events(field_selector="type==Warning", timeout=10):
+    print(event.object)
+
+ +
+

Validation

+

validate

+
def validate(self) -> None
+
+ +

Validate self.res against the OpenAPI schema for this resource kind. Called automatically during create() and update_replace() when schema_validation_enabled=True.

+

Raises: ValidationError if the resource dict is invalid.

+
pod = Pod(client=client, name="test", namespace="default")
+pod.to_dict()
+pod.validate()  # Raises ValidationError on invalid spec
+
+ +

validate_dict (classmethod)

+
@classmethod
+def validate_dict(cls, resource_dict: dict[str, Any]) -> None
+
+ +

Validate an arbitrary resource dictionary against the schema without creating a resource instance.

+ + + + + + + + + + + + + + + +
ParameterTypeDescription
resource_dictdict[str, Any]Complete resource dictionary.
+

Raises: ValidationError if validation fails.

+
from ocp_resources.pod import Pod
+
+Pod.validate_dict({
+    "apiVersion": "v1",
+    "kind": "Pod",
+    "metadata": {"name": "test"},
+    "spec": {"containers": [{"name": "web", "image": "nginx"}]},
+})
+
+ +

See Validating Resources Against OpenAPI Schemas for full validation guide.

+
+

API Request

+

api_request

+
def api_request(
+    self,
+    method: str,
+    action: str,
+    url: str,
+    retry_params: dict[str, int] | None = None,
+    **params: Any,
+) -> dict[str, Any]
+
+ +

Send a raw HTTP request to the resource's API endpoint. Used internally for subresource actions (e.g., VirtualMachine start/stop).

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
methodstrHTTP method ("GET", "PUT", "POST", etc.).
actionstrSubresource action to append to the URL (e.g., "start", "stop").
urlstrBase URL of the resource.
retry_paramsdict[str, int] \| NoneNoneDict with timeout and sleep_time keys for retry behavior.
**paramsAdditional params passed to the HTTP request.
+

Returns: dict[str, Any] — Parsed JSON response, or raw response data if not valid JSON.

+
+

Utility Methods

+

retry_cluster_exceptions (static)

+
@staticmethod
+def retry_cluster_exceptions(
+    func: Callable,
+    exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
+    timeout: int = 10,
+    sleep_time: int = 1,
+    **kwargs: Any,
+) -> Any
+
+ +

Retry a callable on transient cluster errors.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
funcCallableFunction to call.
exceptions_dictdictDEFAULT_CLUSTER_RETRY_EXCEPTIONSMap of exception types to message substrings to match.
timeoutint10Total retry timeout in seconds.
sleep_timeint1Sleep between retries.
**kwargsPassed to func.
+

Returns: The return value of func.

+

Raises: The last exception if timeout is reached.

+

get_condition_message

+
def get_condition_message(
+    self,
+    condition_type: str,
+    condition_status: str = "",
+) -> str
+
+ +

Get the message for a specific condition.

+ + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
condition_typestrCondition type name.
condition_statusstr""If set, only return the message when the condition status matches.
+

Returns: str — The condition message, or "" if not found or status doesn't match.

+

hash_resource_dict

+
def hash_resource_dict(self, resource_dict: dict[Any, Any]) -> dict[Any, Any]
+
+ +

Replace sensitive fields (defined by keys_to_hash) with "*******" for logging.

+ + + + + + + + + + + + + + + +
ParameterTypeDescription
resource_dictdictThe resource dict to hash.
+

Returns: dict — A copy with sensitive values replaced.

+
+

Tip: Override the keys_to_hash property in subclasses to define which fields to mask.

+
+

keys_to_hash

+
@property
+def keys_to_hash(self) -> list[str]
+
+ +

List of key paths to mask in logs. Uses > as separator, [] for list elements.

+

Returns: list[str] — Default: [] (no hashing).

+
# Example override in a Secret subclass:
+@property
+def keys_to_hash(self):
+    return ["data", "stringData"]
+
+ +
+

Inner Classes

+

Resource.Status

+

Pre-defined status phase constants (inherited from ResourceConstants).

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConstantValue
Status.SUCCEEDED"Succeeded"
Status.FAILED"Failed"
Status.DELETING"Deleting"
Status.DEPLOYED"Deployed"
Status.PENDING"Pending"
Status.COMPLETED"Completed"
Status.RUNNING"Running"
Status.READY"Ready"
Status.TERMINATING"Terminating"
Status.ERROR"Error"
Status.ACTIVE"Active"
Status.SCHEDULING"Scheduling"
Status.CRASH_LOOPBACK_OFF"CrashLoopBackOff"
Status.IMAGE_PULL_BACK_OFF"ImagePullBackOff"
+
pod.wait_for_status(status=Pod.Status.RUNNING)
+
+ +

Resource.Condition

+

Pre-defined condition type and status constants.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConstantValue
Condition.READY"Ready"
Condition.AVAILABLE"Available"
Condition.DEGRADED"Degraded"
Condition.PROGRESSING"Progressing"
Condition.UPGRADEABLE"Upgradeable"
Condition.Status.TRUE"True"
Condition.Status.FALSE"False"
Condition.Status.UNKNOWN"Unknown"
+
ns.wait_for_condition(
+    condition=Resource.Condition.READY,
+    status=Resource.Condition.Status.TRUE,
+)
+
+ +

Resource.ApiGroup

+

Pre-defined API group string constants. A selection of commonly used values:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ConstantValue
ApiGroup.APPS"apps"
ApiGroup.BATCH"batch"
ApiGroup.NETWORKING_K8S_IO"networking.k8s.io"
ApiGroup.RBAC_AUTHORIZATION_K8S_IO"rbac.authorization.k8s.io"
ApiGroup.STORAGE_K8S_IO"storage.k8s.io"
ApiGroup.CONFIG_OPENSHIFT_IO"config.openshift.io"
ApiGroup.KUBEVIRT_IO"kubevirt.io"
ApiGroup.CDI_KUBEVIRT_IO"cdi.kubevirt.io"
ApiGroup.ROUTE_OPENSHIFT_IO"route.openshift.io"
ApiGroup.MACHINE_OPENSHIFT_IO"machine.openshift.io"
+
+

Note: Over 100 API group constants are available. Use IDE auto-complete to discover all values.

+
+

Resource.ApiVersion

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ConstantValue
ApiVersion.V1"v1"
ApiVersion.V1BETA1"v1beta1"
ApiVersion.V1ALPHA1"v1alpha1"
ApiVersion.V1ALPHA3"v1alpha3"
+
+

NamespacedResource

+
from ocp_resources.resource import NamespacedResource
+
+ +

Base class for namespace-scoped resources (e.g., Pod, Deployment, Service, ConfigMap). Extends Resource with namespace awareness.

+

Constructor

+
NamespacedResource(
+    client=client,
+    name="my-pod",
+    namespace="my-namespace",
+)
+
+ +

All parameters from Resource are accepted, plus:

+ + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
namespacestr \| NoneNoneKubernetes namespace. Required unless yaml_file or kind_dict is provided.
+

Raises: MissingRequiredArgumentError if neither (name + namespace) nor yaml_file / kind_dict is provided.

+
from ocp_resources.pod import Pod
+
+pod = Pod(
+    client=client,
+    name="nginx",
+    namespace="default",
+    label={"app": "web"},
+)
+
+ +

Overridden Methods

+

instance

+
@property
+def instance(self) -> ResourceInstance
+
+ +

Get the live resource instance, scoped to self.namespace.

+

Returns: ResourceInstance

+

to_dict

+
def to_dict(self) -> None
+
+ +

Populates self.res and sets metadata.namespace. If using yaml_file or kind_dict, reads the namespace from the YAML/dict.

+

Raises: MissingRequiredArgumentError if namespace is still not set after processing.

+

get (classmethod)

+

Behaves like Resource.get(), but yields instances constructed with both name and namespace.

+
from ocp_resources.pod import Pod
+
+for pod in Pod.get(client=client, namespace="default"):
+    print(f"{pod.namespace}/{pod.name}")
+
+# With label selector
+for pod in Pod.get(client=client, namespace="default", label_selector="app=web"):
+    print(pod.name)
+
+ +
+

ResourceEditor

+
from ocp_resources.resource import ResourceEditor
+
+ +

Temporarily patch resources and automatically restore original values. Designed for test scenarios.

+

Constructor

+
ResourceEditor(
+    patches: dict[Any, Any],
+    action: str = "update",
+    user_backups: dict[Any, Any] | None = None,
+)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
patchesdictMap of {Resource: patch_dict}.
actionstr"update""update" for merge-patch, "replace" for full replacement.
user_backupsdict \| NoneNonePre-computed backup dicts. If provided, skips automatic backup creation.
+

Context Manager Usage

+
from ocp_resources.resource import ResourceEditor
+from ocp_resources.namespace import Namespace
+
+ns = Namespace(client=client, name="my-ns", ensure_exists=True)
+
+with ResourceEditor(
+    patches={ns: {"metadata": {"labels": {"temporary": "true"}}}}
+) as editor:
+    # Labels are patched
+    assert ns.instance.metadata.labels.temporary == "true"
+# Labels are restored to original values
+
+ +

Methods

+ + + + + + + + + + + + + + + + + + + + +
MethodSignatureDescription
updateupdate(backup_resources: bool = False) -> NoneApply patches. If backup_resources=True, back up original values first.
restorerestore() -> NoneRestore all backed-up values.
+

Properties

+ + + + + + + + + + + + + + + + + + + + +
PropertyTypeDescription
backupsdict[Any, Any]Backed-up original values for each patched resource.
patchesdict[Any, Any]The patches dict from the constructor.
+
+

Warning: The DynamicClient used to obtain the resource objects must have sufficient privileges to patch and replace resources.

+
+

See Editing Resources Temporarily with ResourceEditor for detailed patterns.

+
+

ResourceList

+
from ocp_resources.resource import ResourceList
+
+ +

Create and manage N copies of a cluster-scoped resource with indexed names.

+

Constructor

+
ResourceList(
+    resource_class: type[Resource],
+    num_resources: int,
+    client: DynamicClient,
+    **kwargs: Any,
+)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
resource_classtype[Resource]The resource class to instantiate.
num_resourcesintNumber of resource copies to create.
clientDynamicClientKubernetes client.
**kwargsPassed to each resource constructor. Must include name (used as base name).
+

Resources are named {name}-1, {name}-2, ..., {name}-N.

+
from ocp_resources.namespace import Namespace
+from ocp_resources.resource import ResourceList
+
+namespaces = ResourceList(
+    resource_class=Namespace,
+    num_resources=3,
+    client=client,
+    name="test-ns",
+)
+
+with namespaces:
+    # Creates test-ns-1, test-ns-2, test-ns-3
+    for ns in namespaces:
+        print(ns.name)
+# All three namespaces are deleted
+
+ +

Methods (inherited from BaseResourceList)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodSignatureReturnsDescription
deploydeploy(wait=False)list[Resource]Deploy all resources.
clean_upclean_up(wait=True)boolDelete all resources in reverse order.
__len____len__()intNumber of resources.
__getitem____getitem__(index)ResourceAccess by index.
__iter____iter__()GeneratorIterate over resources.
+
+

NamespacedResourceList

+
from ocp_resources.resource import NamespacedResourceList
+
+ +

Create one instance of a namespaced resource in each namespace from a ResourceList.

+

Constructor

+
NamespacedResourceList(
+    resource_class: type[NamespacedResource],
+    namespaces: ResourceList,
+    client: DynamicClient,
+    **kwargs: Any,
+)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
resource_classtype[NamespacedResource]The namespaced resource class (e.g., Pod).
namespacesResourceListA ResourceList of Namespace resources.
clientDynamicClientKubernetes client.
**kwargsPassed to each resource constructor. Must include name.
+

Raises: TypeError if any resource in namespaces is not a Namespace.

+
from ocp_resources.namespace import Namespace
+from ocp_resources.pod import Pod
+from ocp_resources.resource import ResourceList, NamespacedResourceList
+
+namespaces = ResourceList(
+    resource_class=Namespace, num_resources=2, client=client, name="test-ns"
+)
+
+pods = NamespacedResourceList(
+    resource_class=Pod,
+    namespaces=namespaces,
+    client=client,
+    name="nginx",
+)
+
+with namespaces:
+    with pods:
+        # Creates nginx in test-ns-1 and test-ns-2
+        for pod in pods:
+            print(f"{pod.namespace}/{pod.name}")
+
+ +

See Managing Bulk Resources with ResourceList for full usage patterns.

+
+

KubeAPIVersion

+
from ocp_resources.resource import KubeAPIVersion
+
+ +

Implements Kubernetes API versioning with comparison operators. Extends packaging.version.Version.

+

Constructor

+
KubeAPIVersion(vstring: str)
+
+ + + + + + + + + + + + + + + + +
ParameterTypeDescription
vstringstrVersion string (e.g., "v1", "v1beta1", "v1alpha2").
+

Raises: ValueError if the version string does not conform to Kubernetes versioning.

+

Ordering

+

Versions are ordered: v1alpha1 < v1alpha2 < v1beta1 < v1beta2 < v1 < v2.

+
assert KubeAPIVersion("v1") > KubeAPIVersion("v1beta1")
+assert KubeAPIVersion("v1beta1") > KubeAPIVersion("v1alpha1")
+assert KubeAPIVersion("v1") == KubeAPIVersion("v1")
+
+ +
+

Exceptions

+

All exceptions are importable from ocp_resources.exceptions.

+
from ocp_resources.exceptions import (
+    MissingRequiredArgumentError,
+    ResourceTeardownError,
+    ValidationError,
+    ConditionError,
+)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExceptionRaised ByDescription
MissingRequiredArgumentErrorResource.__init__, NamespacedResource.__init__, NamespacedResource._base_bodyRequired parameters (name, namespace) not provided.
ResourceTeardownErrorResource.__exit__clean_up() returned False during context manager exit.
ValidationErrorvalidate(), validate_dict(), create(), update_replace()Resource dict fails OpenAPI schema validation. Has message, path, and schema_error attributes.
ConditionErrorwait_for_condition()A stop_condition was detected during condition waiting.
MissingResourceResErrorResource._base_bodyDeprecated. self.res is empty after _base_body().
+
+

Timeout Constants

+

Available from ocp_resources.utils.constants:

+
from ocp_resources.utils.constants import (
+    TIMEOUT_1SEC,      # 1
+    TIMEOUT_5SEC,      # 5
+    TIMEOUT_10SEC,     # 10
+    TIMEOUT_30SEC,     # 30
+    TIMEOUT_1MINUTE,   # 60
+    TIMEOUT_2MINUTES,  # 120
+    TIMEOUT_4MINUTES,  # 240
+    TIMEOUT_10MINUTES, # 600
+)
+
+ +
+

Module-Level Functions

+

replace_key_with_hashed_value

+
from ocp_resources.resource import replace_key_with_hashed_value
+
+replace_key_with_hashed_value(resource_dict: dict, key_name: str) -> dict
+
+ +

Recursively replace the value at a key path with "*******". Supports dict paths (a>b>c) and list paths (a>b[]>c).

+ + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
resource_dictdictThe nested dictionary.
key_namestrKey path using > as separator, [] for list elements.
+

Returns: dict — A deep copy with the value replaced.

+
data = {"spec": {"secret": {"token": "sensitive"}}}
+hashed = replace_key_with_hashed_value(data, "spec>secret>token")
+# {"spec": {"secret": {"token": "*******"}}}
+
+# With list path
+data = {"spec": {"containers": [{"env": [{"value": "secret"}]}]}}
+hashed = replace_key_with_hashed_value(data, "spec>containers[]>env[]>value")
+
+ + + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/resource-api.md b/docs/resource-api.md new file mode 100644 index 0000000000..30d17cbb90 --- /dev/null +++ b/docs/resource-api.md @@ -0,0 +1,1325 @@ +Let me start by reading the knowledge graph and the pages manifest, then explore the source code. + +Now let me examine the constants and exceptions: + +Now let me check the `apply_yaml` function and the `get_client` function signature, plus see what `ResourceConstants` inherits: + +Let me look for key public methods in resource.py that I might have missed (the file was truncated before): + +Let me also check for import paths: + +Now let me look at the `NamespacedResource._base_body` more carefully and check for anything more: + +Now let me check what the `Event` class looks like for the events method reference: + +Let me also check for `generate_yaml_from_template`: + +That's a test utility. Let me check for the `skip_existing_resource_creation_teardown` function: + +Now I have all the information needed. Let me write the comprehensive API reference page. + +# Resource and NamespacedResource API + +Complete API reference for the `Resource` and `NamespacedResource` base classes, the `get_client` factory function, and supporting classes (`ResourceEditor`, `ResourceList`, `NamespacedResourceList`, `KubeAPIVersion`). + +**Import path:** + +```python +from ocp_resources.resource import ( + Resource, + NamespacedResource, + ResourceEditor, + ResourceList, + NamespacedResourceList, + get_client, +) +``` + +--- + +## `get_client` + +Factory function to obtain a Kubernetes `DynamicClient`. + +```python +from ocp_resources.resource import get_client + +client = get_client(config_file="~/.kube/config") +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `config_file` | `str \| None` | `None` | Path to a kubeconfig file. Falls back to `KUBECONFIG` env var or `~/.kube/config`. | +| `config_dict` | `dict[str, Any] \| None` | `None` | Dict with kubeconfig configuration. Mutually exclusive with `config_file`. | +| `context` | `str \| None` | `None` | Name of the kubeconfig context to use. | +| `client_configuration` | `kubernetes.client.Configuration \| None` | `None` | Pre-built Kubernetes client configuration object. | +| `persist_config` | `bool` | `True` | Whether to persist config file changes. | +| `temp_file_path` | `str \| None` | `None` | Path to a temporary kubeconfig file. | +| `try_refresh_token` | `bool` | `True` | Try to refresh the authentication token. | +| `username` | `str \| None` | `None` | Username for basic auth. Requires `password` and `host`. | +| `password` | `str \| None` | `None` | Password for basic auth. Requires `username` and `host`. | +| `host` | `str \| None` | `None` | Cluster host URL. | +| `verify_ssl` | `bool \| None` | `None` | Whether to verify SSL certificates. | +| `token` | `str \| None` | `None` | Bearer token for authentication. Requires `host`. | +| `fake` | `bool` | `False` | Return a `FakeDynamicClient` for testing without a cluster. | +| `generate_kubeconfig` | `bool` | `False` | Save the resolved kubeconfig to a temp file and attach the path to the client. | + +**Returns:** `DynamicClient | FakeDynamicClient` + +```python +# Default kubeconfig +client = get_client() + +# With explicit config file and context +client = get_client(config_file="/path/to/kubeconfig", context="my-cluster") + +# Token-based auth +client = get_client(host="https://api.cluster.example.com:6443", token="sha256~abc123") + +# Basic auth +client = get_client(host="https://api.cluster.example.com:6443", username="admin", password="secret") + +# Fake client for unit testing +client = get_client(fake=True) +``` + +> **Note:** If neither `config_file` nor `config_dict` is provided, the client falls back to the `KUBECONFIG` environment variable, then `~/.kube/config`, and finally in-cluster configuration. + +See [Connecting to Clusters](connecting-to-clusters.html) for connection patterns. See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for `fake=True` usage. + +--- + +## `Resource` + +```python +from ocp_resources.resource import Resource +``` + +Base class for **cluster-scoped** Kubernetes/OpenShift resources (e.g., `Namespace`, `Node`, `ClusterRole`). Inherits from `ResourceConstants`. All concrete resource classes inherit from either `Resource` or `NamespacedResource`. + +See [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) for the inheritance model. + +### Class Attributes + +| Attribute | Type | Default | Description | +|---|---|---|---| +| `api_group` | `str` | `""` | API group for the resource (e.g., `"apps"`, `"batch"`). | +| `api_version` | `str` | `""` | Full API version string (e.g., `"v1"`, `"apps/v1"`). Resolved automatically if `api_group` is set. | +| `singular_name` | `str` | `""` | Singular resource name for API calls. Used to disambiguate when multiple resources match the same kind. | +| `timeout_seconds` | `int` | `60` | Default timeout for API list/watch operations. | + +### Constructor + +```python +Resource( + client=client, + name="my-resource", +) +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `client` | `DynamicClient \| None` | `None` | Kubernetes dynamic client. Will be mandatory in the next major release. | +| `name` | `str \| None` | `None` | Resource name. Required unless `yaml_file` or `kind_dict` is provided. | +| `teardown` | `bool` | `True` | Whether this resource should be deleted when used as a context manager. | +| `yaml_file` | `str \| None` | `None` | Path to a YAML file defining the resource. Mutually exclusive with `kind_dict`. | +| `delete_timeout` | `int` | `240` | Timeout in seconds for delete operations. | +| `dry_run` | `bool` | `False` | If `True`, create operations use dry-run mode. | +| `node_selector` | `dict[str, Any] \| None` | `None` | Node selector for scheduling. | +| `node_selector_labels` | `dict[str, str] \| None` | `None` | Node selector labels for scheduling. | +| `config_file` | `str \| None` | `None` | Path to kubeconfig. Deprecated; pass `client` instead. | +| `config_dict` | `dict[str, Any] \| None` | `None` | Kubeconfig dict. | +| `context` | `str \| None` | `None` | Kubeconfig context name. Deprecated; pass `client` instead. | +| `label` | `dict[str, str] \| None` | `None` | Labels to set on the resource. | +| `annotations` | `dict[str, str] \| None` | `None` | Annotations to set on the resource. | +| `api_group` | `str` | `""` | Override the class-level `api_group`. | +| `hash_log_data` | `bool` | `True` | Hash sensitive fields (defined by `keys_to_hash`) in log output. | +| `ensure_exists` | `bool` | `False` | Check that the resource already exists on the cluster at init time. Raises `ResourceNotFoundError` if not. | +| `kind_dict` | `dict[Any, Any] \| None` | `None` | Complete resource dict. Mutually exclusive with `yaml_file`. Bypasses `to_dict()` logic. | +| `wait_for_resource` | `bool` | `False` | When used as a context manager, wait for the resource to exist after creation. | +| `schema_validation_enabled` | `bool` | `False` | Enable automatic OpenAPI schema validation on `create()` and `update_replace()`. | + +**Raises:** + +| Exception | Condition | +|---|---| +| `ValueError` | Both `yaml_file` and `kind_dict` are provided. | +| `NotImplementedError` | Neither `api_group` nor `api_version` is defined on the class. | +| `MissingRequiredArgumentError` | None of `name`, `yaml_file`, or `kind_dict` is provided. | +| `ResourceNotFoundError` | `ensure_exists=True` and the resource does not exist. | + +```python +from ocp_resources.namespace import Namespace + +# Basic creation +ns = Namespace(client=client, name="my-namespace") + +# From a YAML file +ns = Namespace(client=client, yaml_file="namespace.yaml") + +# From a dict +ns = Namespace(client=client, kind_dict={"apiVersion": "v1", "kind": "Namespace", "metadata": {"name": "test"}}) + +# With labels and annotations +ns = Namespace(client=client, name="my-ns", label={"env": "test"}, annotations={"owner": "team-a"}) + +# Verify it already exists +ns = Namespace(client=client, name="default", ensure_exists=True) +``` + +--- + +### Context Manager Protocol + +`Resource` supports the Python context manager protocol for automatic creation and cleanup. + +```python +from ocp_resources.namespace import Namespace + +with Namespace(client=client, name="temp-ns") as ns: + # Resource is created on __enter__ + print(ns.name) +# Resource is deleted on __exit__ (if teardown=True) +``` + +| Method | Description | +|---|---| +| `__enter__()` | Calls `deploy(wait=self.wait_for_resource)`. Registers a `SIGINT` handler on the main thread to ensure cleanup on Ctrl+C. Returns `self`. | +| `__exit__(...)` | Calls `clean_up()` if `teardown=True`. Raises `ResourceTeardownError` if cleanup fails. | + +> **Tip:** Set `teardown=False` to prevent automatic deletion on context exit. + +--- + +### CRUD Methods + +#### `create` + +```python +def create( + self, + wait: bool = False, + exceptions_dict: dict[type[Exception], list[str]] = ..., +) -> ResourceInstance | None +``` + +Create the resource on the cluster. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `wait` | `bool` | `False` | Wait for the resource to exist after creation. | +| `exceptions_dict` | `dict[type[Exception], list[str]]` | `DEFAULT_CLUSTER_RETRY_EXCEPTIONS \| PROTOCOL_ERROR_EXCEPTION_DICT` | Exceptions to retry on. | + +**Returns:** `ResourceInstance | None` + +**Raises:** `ValidationError` if `schema_validation_enabled=True` and the resource dict is invalid. + +```python +ns = Namespace(client=client, name="my-ns") +ns.create(wait=True) +``` + +#### `delete` + +```python +def delete( + self, + wait: bool = False, + timeout: int = 240, + body: dict[str, Any] | None = None, +) -> bool +``` + +Delete the resource from the cluster. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `wait` | `bool` | `False` | Wait for the resource to be fully deleted. | +| `timeout` | `int` | `240` | Timeout in seconds when `wait=True`. | +| `body` | `dict[str, Any] \| None` | `None` | Optional delete options body. | + +**Returns:** `bool` — `True` if deleted or not found; `False` only if wait timed out. + +```python +ns.delete(wait=True, timeout=120) +``` + +#### `update` + +```python +def update(self, resource_dict: dict[str, Any]) -> None +``` + +Patch the resource with a merge-patch (`application/merge-patch+json`). Only updates the fields present in `resource_dict`. + +| Parameter | Type | Description | +|---|---|---| +| `resource_dict` | `dict[str, Any]` | Partial resource dictionary with fields to update. | + +> **Note:** Schema validation is **not** applied on `update()` because patches are partial and would fail full-schema validation. + +```python +ns.update(resource_dict={"metadata": {"labels": {"env": "staging"}}}) +``` + +#### `update_replace` + +```python +def update_replace(self, resource_dict: dict[str, Any]) -> None +``` + +Replace the full resource. Use this to **remove** existing fields (unlike `update()`, which only adds/modifies). + +| Parameter | Type | Description | +|---|---|---| +| `resource_dict` | `dict[str, Any]` | Complete resource dictionary to replace with. | + +**Raises:** `ValidationError` if `schema_validation_enabled=True` and the dict is invalid. + +```python +full_dict = ns.instance.to_dict() +full_dict["metadata"]["labels"] = {"new-label": "only"} +ns.update_replace(resource_dict=full_dict) +``` + +--- + +### Deploy / Clean Up + +#### `deploy` + +```python +def deploy(self, wait: bool = False) -> Self +``` + +Create the resource (respects `REUSE_IF_RESOURCE_EXISTS` environment variable). + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `wait` | `bool` | `False` | Wait for the resource after creation. | + +**Returns:** `Self` + +See [Environment Variables and Configuration](environment-variables.html) for `REUSE_IF_RESOURCE_EXISTS` details. + +#### `clean_up` + +```python +def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool +``` + +Delete the resource (respects `SKIP_RESOURCE_TEARDOWN` environment variable). + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `wait` | `bool` | `True` | Wait for deletion to complete. | +| `timeout` | `int \| None` | `None` | Timeout in seconds. Defaults to `delete_timeout`. | + +**Returns:** `bool` — `True` if deleted successfully. + +See [Environment Variables and Configuration](environment-variables.html) for `SKIP_RESOURCE_TEARDOWN` details. + +--- + +### Query Methods and Properties + +#### `exists` + +```python +@property +def exists(self) -> ResourceInstance | None +``` + +Check if the resource exists on the cluster. + +**Returns:** `ResourceInstance` if found, `None` if not. + +```python +if ns.exists: + print("Namespace exists") +``` + +#### `instance` + +```python +@property +def instance(self) -> ResourceInstance +``` + +Get the live resource instance from the cluster. Retries on transient cluster errors. + +**Returns:** `ResourceInstance` + +**Raises:** `NotFoundError` if the resource does not exist. + +```python +resource_version = ns.instance.metadata.resourceVersion +``` + +#### `status` + +```python +@property +def status(self) -> str +``` + +Get `status.phase` from the resource instance. + +**Returns:** `str` — The status phase string (e.g., `"Running"`, `"Active"`, `"Pending"`). + +```python +print(ns.status) # "Active" +``` + +#### `labels` + +```python +@property +def labels(self) -> ResourceField +``` + +Get resource labels from `metadata.labels`. + +**Returns:** `ResourceField` + +```python +for key, value in ns.labels.items(): + print(f"{key}={value}") +``` + +#### `kind` + +```python +@ClassProperty +def kind(cls) -> str | None +``` + +Get the resource kind name derived from the class hierarchy. This is a **class-level property** — accessible on both the class and instances. + +**Returns:** `str | None` + +```python +from ocp_resources.namespace import Namespace +print(Namespace.kind) # "Namespace" +``` + +#### `api` + +```python +@property +def api(self) -> ResourceInstance +``` + +Get the resource API object (shortcut for `full_api()` with no extra kwargs). + +**Returns:** `ResourceInstance` + +#### `full_api` + +```python +def full_api(self, **kwargs: Any) -> ResourceInstance +``` + +Get the resource API object with optional filtering kwargs. + +| Keyword Argument | Description | +|---|---| +| `pretty` | Pretty-print output. | +| `_continue` | Continuation token for list pagination. | +| `field_selector` | Filter by field. | +| `label_selector` | Filter by label. | +| `limit` | Maximum number of results. | +| `resource_version` | Filter by resource version. | +| `timeout_seconds` | Request timeout. | +| `watch` | Enable watch mode. | + +**Returns:** `ResourceInstance` + +--- + +### Class Method: `get` + +```python +@classmethod +def get( + cls, + client: DynamicClient | None = None, + dyn_client: DynamicClient | None = None, + config_file: str = "", + singular_name: str = "", + exceptions_dict: dict[type[Exception], list[str]] = ..., + raw: bool = False, + context: str | None = None, + *args: Any, + **kwargs: Any, +) -> Generator[Any, None, None] +``` + +List resources of this kind from the cluster. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `client` | `DynamicClient \| None` | `None` | Kubernetes client. | +| `dyn_client` | `DynamicClient \| None` | `None` | Deprecated alias for `client`. | +| `config_file` | `str` | `""` | Path to kubeconfig. Deprecated; pass `client`. | +| `singular_name` | `str` | `""` | Singular resource name for disambiguation. | +| `exceptions_dict` | `dict` | `DEFAULT_CLUSTER_RETRY_EXCEPTIONS` | Exceptions to retry. | +| `raw` | `bool` | `False` | If `True`, yield raw `ResourceInstance` objects instead of wrapper instances. | +| `context` | `str \| None` | `None` | Kubeconfig context. Deprecated; pass `client`. | +| `**kwargs` | | | Passed through to the API (e.g., `label_selector`, `field_selector`). | + +**Returns:** `Generator` of resource instances. + +> **Note:** For `Resource` (cluster-scoped), yielded objects are constructed with `name` only. For `NamespacedResource`, yielded objects include both `name` and `namespace`. + +```python +from ocp_resources.namespace import Namespace + +for ns in Namespace.get(client=client): + print(ns.name) + +# With label selector +for ns in Namespace.get(client=client, label_selector="env=production"): + print(ns.name) + +# Raw mode +for raw_ns in Namespace.get(client=client, raw=True): + print(raw_ns.metadata.name, raw_ns.status.phase) +``` + +See [Querying and Watching Resources](querying-resources.html) for advanced list/filter patterns. + +--- + +### Static Method: `get_all_cluster_resources` + +```python +@staticmethod +def get_all_cluster_resources( + client: DynamicClient | None = None, + config_file: str = "", + context: str | None = None, + config_dict: dict[str, Any] | None = None, + *args: Any, + **kwargs: Any, +) -> Generator[ResourceField, None, None] +``` + +Yield all resources across **all** API groups in the cluster. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `client` | `DynamicClient \| None` | `None` | Kubernetes client. | +| `**kwargs` | | | Passed to the API (e.g., `label_selector`). | + +**Yields:** `ResourceField` + +```python +for resource in Resource.get_all_cluster_resources(client=client, label_selector="app=myapp"): + print(f"{resource.kind}: {resource.metadata.name}") +``` + +--- + +### Wait Methods + +#### `wait` + +```python +def wait(self, timeout: int = 240, sleep: int = 1) -> None +``` + +Wait until the resource exists on the cluster. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `timeout` | `int` | `240` | Maximum wait time in seconds. | +| `sleep` | `int` | `1` | Sleep interval between retries. | + +**Raises:** `TimeoutExpiredError` if the resource does not exist within the timeout. + +#### `wait_deleted` + +```python +def wait_deleted(self, timeout: int = 240) -> bool +``` + +Wait until the resource no longer exists. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `timeout` | `int` | `240` | Maximum wait time in seconds. | + +**Returns:** `bool` — `True` if deleted, `False` if timed out. + +#### `wait_for_status` + +```python +def wait_for_status( + self, + status: str, + timeout: int = 240, + stop_status: str | None = None, + sleep: int = 1, + exceptions_dict: dict[type[Exception], list[str]] = ..., +) -> None +``` + +Wait for `status.phase` to reach the expected value. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `status` | `str` | — | Expected status phase string. | +| `timeout` | `int` | `240` | Maximum wait time in seconds. | +| `stop_status` | `str \| None` | `None` | Stop and fail immediately if this status is reached. Defaults to `Status.FAILED`. | +| `sleep` | `int` | `1` | Sleep interval between retries. | +| `exceptions_dict` | `dict` | `PROTOCOL_ERROR_EXCEPTION_DICT \| DEFAULT_CLUSTER_RETRY_EXCEPTIONS` | Exceptions to retry. | + +**Raises:** `TimeoutExpiredError` if the desired status is not reached. + +```python +from ocp_resources.pod import Pod + +pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120) +``` + +#### `wait_for_condition` + +```python +def wait_for_condition( + self, + condition: str, + status: str, + timeout: int = 300, + sleep_time: int = 1, + reason: str | None = None, + message: str = "", + stop_condition: str | None = None, + stop_status: str = "True", +) -> None +``` + +Wait for a specific condition to reach the desired state. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `condition` | `str` | — | Condition type name (e.g., `"Ready"`, `"Available"`). | +| `status` | `str` | — | Expected condition status (e.g., `"True"`, `"False"`). | +| `timeout` | `int` | `300` | Maximum wait time in seconds. | +| `sleep_time` | `int` | `1` | Interval between retries. | +| `reason` | `str \| None` | `None` | Expected condition reason. If set, must match exactly. | +| `message` | `str` | `""` | Expected substring within the condition message. | +| `stop_condition` | `str \| None` | `None` | Condition type that, if matched, stops the wait and fails immediately. | +| `stop_status` | `str` | `"True"` | Status for `stop_condition` matching. | + +**Raises:** + +| Exception | Condition | +|---|---| +| `TimeoutExpiredError` | Condition not met within timeout. | +| `ConditionError` | `stop_condition` is detected with matching `stop_status`. | + +```python +ns.wait_for_condition( + condition="Ready", + status="True", + timeout=60, +) +``` + +See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for more patterns. + +#### `wait_for_conditions` + +```python +def wait_for_conditions(self) -> None +``` + +Wait for the resource to have any conditions populated in its status. Uses a 30-second timeout. + +--- + +### Serialization + +#### `to_dict` + +```python +def to_dict(self) -> None +``` + +Populate `self.res` with the intended dict representation of the resource. Called automatically before `create()`. Override this in subclasses to add resource-specific fields. + +> **Note:** If `kind_dict` or `yaml_file` was provided, `to_dict()` uses those directly instead of building from individual parameters. + +#### `to_yaml` + +```python +def to_yaml(self) -> str +``` + +**Returns:** `str` — YAML string representation of the resource dict. + +```python +print(ns.to_yaml()) +``` + +--- + +### Watch / Events + +#### `watcher` + +```python +def watcher( + self, + timeout: int, + resource_version: str = "", +) -> Generator[dict[str, Any], None, None] +``` + +Watch for changes to this specific resource. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `timeout` | `int` | — | Watch duration in seconds. | +| `resource_version` | `str` | `""` | Only return events after this version. Defaults to the version at resource creation time. | + +**Yields:** Event dicts with keys `type` (`"ADDED"`, `"MODIFIED"`, `"DELETED"`), `raw_object`, and `object`. + +```python +for event in ns.watcher(timeout=30): + print(event["type"], event["object"].metadata.name) +``` + +#### `events` + +```python +def events( + self, + name: str = "", + label_selector: str = "", + field_selector: str = "", + resource_version: str = "", + timeout: int = 240, +) -> Generator[Any, Any, None] +``` + +Get Kubernetes events related to this resource. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `name` | `str` | `""` | Filter by event name. | +| `label_selector` | `str` | `""` | Comma-separated `key=value` label filters. | +| `field_selector` | `str` | `""` | Additional field selectors (auto-prefixed with `involvedObject.name==`). | +| `resource_version` | `str` | `""` | Filter events by resource version. | +| `timeout` | `int` | `240` | Timeout in seconds. | + +**Yields:** `Event` objects. + +```python +for event in pod.events(field_selector="type==Warning", timeout=10): + print(event.object) +``` + +--- + +### Validation + +#### `validate` + +```python +def validate(self) -> None +``` + +Validate `self.res` against the OpenAPI schema for this resource kind. Called automatically during `create()` and `update_replace()` when `schema_validation_enabled=True`. + +**Raises:** `ValidationError` if the resource dict is invalid. + +```python +pod = Pod(client=client, name="test", namespace="default") +pod.to_dict() +pod.validate() # Raises ValidationError on invalid spec +``` + +#### `validate_dict` (classmethod) + +```python +@classmethod +def validate_dict(cls, resource_dict: dict[str, Any]) -> None +``` + +Validate an arbitrary resource dictionary against the schema without creating a resource instance. + +| Parameter | Type | Description | +|---|---|---| +| `resource_dict` | `dict[str, Any]` | Complete resource dictionary. | + +**Raises:** `ValidationError` if validation fails. + +```python +from ocp_resources.pod import Pod + +Pod.validate_dict({ + "apiVersion": "v1", + "kind": "Pod", + "metadata": {"name": "test"}, + "spec": {"containers": [{"name": "web", "image": "nginx"}]}, +}) +``` + +See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for full validation guide. + +--- + +### API Request + +#### `api_request` + +```python +def api_request( + self, + method: str, + action: str, + url: str, + retry_params: dict[str, int] | None = None, + **params: Any, +) -> dict[str, Any] +``` + +Send a raw HTTP request to the resource's API endpoint. Used internally for subresource actions (e.g., VirtualMachine start/stop). + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `method` | `str` | — | HTTP method (`"GET"`, `"PUT"`, `"POST"`, etc.). | +| `action` | `str` | — | Subresource action to append to the URL (e.g., `"start"`, `"stop"`). | +| `url` | `str` | — | Base URL of the resource. | +| `retry_params` | `dict[str, int] \| None` | `None` | Dict with `timeout` and `sleep_time` keys for retry behavior. | +| `**params` | | | Additional params passed to the HTTP request. | + +**Returns:** `dict[str, Any]` — Parsed JSON response, or raw response data if not valid JSON. + +--- + +### Utility Methods + +#### `retry_cluster_exceptions` (static) + +```python +@staticmethod +def retry_cluster_exceptions( + func: Callable, + exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS, + timeout: int = 10, + sleep_time: int = 1, + **kwargs: Any, +) -> Any +``` + +Retry a callable on transient cluster errors. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `func` | `Callable` | — | Function to call. | +| `exceptions_dict` | `dict` | `DEFAULT_CLUSTER_RETRY_EXCEPTIONS` | Map of exception types to message substrings to match. | +| `timeout` | `int` | `10` | Total retry timeout in seconds. | +| `sleep_time` | `int` | `1` | Sleep between retries. | +| `**kwargs` | | | Passed to `func`. | + +**Returns:** The return value of `func`. + +**Raises:** The last exception if timeout is reached. + +#### `get_condition_message` + +```python +def get_condition_message( + self, + condition_type: str, + condition_status: str = "", +) -> str +``` + +Get the message for a specific condition. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `condition_type` | `str` | — | Condition type name. | +| `condition_status` | `str` | `""` | If set, only return the message when the condition status matches. | + +**Returns:** `str` — The condition message, or `""` if not found or status doesn't match. + +#### `hash_resource_dict` + +```python +def hash_resource_dict(self, resource_dict: dict[Any, Any]) -> dict[Any, Any] +``` + +Replace sensitive fields (defined by `keys_to_hash`) with `"*******"` for logging. + +| Parameter | Type | Description | +|---|---|---| +| `resource_dict` | `dict` | The resource dict to hash. | + +**Returns:** `dict` — A copy with sensitive values replaced. + +> **Tip:** Override the `keys_to_hash` property in subclasses to define which fields to mask. + +#### `keys_to_hash` + +```python +@property +def keys_to_hash(self) -> list[str] +``` + +List of key paths to mask in logs. Uses `>` as separator, `[]` for list elements. + +**Returns:** `list[str]` — Default: `[]` (no hashing). + +```python +# Example override in a Secret subclass: +@property +def keys_to_hash(self): + return ["data", "stringData"] +``` + +--- + +### Inner Classes + +#### `Resource.Status` + +Pre-defined status phase constants (inherited from `ResourceConstants`). + +| Constant | Value | +|---|---| +| `Status.SUCCEEDED` | `"Succeeded"` | +| `Status.FAILED` | `"Failed"` | +| `Status.DELETING` | `"Deleting"` | +| `Status.DEPLOYED` | `"Deployed"` | +| `Status.PENDING` | `"Pending"` | +| `Status.COMPLETED` | `"Completed"` | +| `Status.RUNNING` | `"Running"` | +| `Status.READY` | `"Ready"` | +| `Status.TERMINATING` | `"Terminating"` | +| `Status.ERROR` | `"Error"` | +| `Status.ACTIVE` | `"Active"` | +| `Status.SCHEDULING` | `"Scheduling"` | +| `Status.CRASH_LOOPBACK_OFF` | `"CrashLoopBackOff"` | +| `Status.IMAGE_PULL_BACK_OFF` | `"ImagePullBackOff"` | + +```python +pod.wait_for_status(status=Pod.Status.RUNNING) +``` + +#### `Resource.Condition` + +Pre-defined condition type and status constants. + +| Constant | Value | +|---|---| +| `Condition.READY` | `"Ready"` | +| `Condition.AVAILABLE` | `"Available"` | +| `Condition.DEGRADED` | `"Degraded"` | +| `Condition.PROGRESSING` | `"Progressing"` | +| `Condition.UPGRADEABLE` | `"Upgradeable"` | +| `Condition.Status.TRUE` | `"True"` | +| `Condition.Status.FALSE` | `"False"` | +| `Condition.Status.UNKNOWN` | `"Unknown"` | + +```python +ns.wait_for_condition( + condition=Resource.Condition.READY, + status=Resource.Condition.Status.TRUE, +) +``` + +#### `Resource.ApiGroup` + +Pre-defined API group string constants. A selection of commonly used values: + +| Constant | Value | +|---|---| +| `ApiGroup.APPS` | `"apps"` | +| `ApiGroup.BATCH` | `"batch"` | +| `ApiGroup.NETWORKING_K8S_IO` | `"networking.k8s.io"` | +| `ApiGroup.RBAC_AUTHORIZATION_K8S_IO` | `"rbac.authorization.k8s.io"` | +| `ApiGroup.STORAGE_K8S_IO` | `"storage.k8s.io"` | +| `ApiGroup.CONFIG_OPENSHIFT_IO` | `"config.openshift.io"` | +| `ApiGroup.KUBEVIRT_IO` | `"kubevirt.io"` | +| `ApiGroup.CDI_KUBEVIRT_IO` | `"cdi.kubevirt.io"` | +| `ApiGroup.ROUTE_OPENSHIFT_IO` | `"route.openshift.io"` | +| `ApiGroup.MACHINE_OPENSHIFT_IO` | `"machine.openshift.io"` | + +> **Note:** Over 100 API group constants are available. Use IDE auto-complete to discover all values. + +#### `Resource.ApiVersion` + +| Constant | Value | +|---|---| +| `ApiVersion.V1` | `"v1"` | +| `ApiVersion.V1BETA1` | `"v1beta1"` | +| `ApiVersion.V1ALPHA1` | `"v1alpha1"` | +| `ApiVersion.V1ALPHA3` | `"v1alpha3"` | + +--- + +## `NamespacedResource` + +```python +from ocp_resources.resource import NamespacedResource +``` + +Base class for **namespace-scoped** resources (e.g., `Pod`, `Deployment`, `Service`, `ConfigMap`). Extends `Resource` with namespace awareness. + +### Constructor + +```python +NamespacedResource( + client=client, + name="my-pod", + namespace="my-namespace", +) +``` + +All parameters from [`Resource`](#constructor) are accepted, plus: + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `namespace` | `str \| None` | `None` | Kubernetes namespace. Required unless `yaml_file` or `kind_dict` is provided. | + +**Raises:** `MissingRequiredArgumentError` if neither (`name` + `namespace`) nor `yaml_file` / `kind_dict` is provided. + +```python +from ocp_resources.pod import Pod + +pod = Pod( + client=client, + name="nginx", + namespace="default", + label={"app": "web"}, +) +``` + +### Overridden Methods + +#### `instance` + +```python +@property +def instance(self) -> ResourceInstance +``` + +Get the live resource instance, scoped to `self.namespace`. + +**Returns:** `ResourceInstance` + +#### `to_dict` + +```python +def to_dict(self) -> None +``` + +Populates `self.res` and sets `metadata.namespace`. If using `yaml_file` or `kind_dict`, reads the namespace from the YAML/dict. + +**Raises:** `MissingRequiredArgumentError` if namespace is still not set after processing. + +#### `get` (classmethod) + +Behaves like `Resource.get()`, but yields instances constructed with both `name` and `namespace`. + +```python +from ocp_resources.pod import Pod + +for pod in Pod.get(client=client, namespace="default"): + print(f"{pod.namespace}/{pod.name}") + +# With label selector +for pod in Pod.get(client=client, namespace="default", label_selector="app=web"): + print(pod.name) +``` + +--- + +## `ResourceEditor` + +```python +from ocp_resources.resource import ResourceEditor +``` + +Temporarily patch resources and automatically restore original values. Designed for test scenarios. + +### Constructor + +```python +ResourceEditor( + patches: dict[Any, Any], + action: str = "update", + user_backups: dict[Any, Any] | None = None, +) +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `patches` | `dict` | — | Map of `{Resource: patch_dict}`. | +| `action` | `str` | `"update"` | `"update"` for merge-patch, `"replace"` for full replacement. | +| `user_backups` | `dict \| None` | `None` | Pre-computed backup dicts. If provided, skips automatic backup creation. | + +### Context Manager Usage + +```python +from ocp_resources.resource import ResourceEditor +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-ns", ensure_exists=True) + +with ResourceEditor( + patches={ns: {"metadata": {"labels": {"temporary": "true"}}}} +) as editor: + # Labels are patched + assert ns.instance.metadata.labels.temporary == "true" +# Labels are restored to original values +``` + +### Methods + +| Method | Signature | Description | +|---|---|---| +| `update` | `update(backup_resources: bool = False) -> None` | Apply patches. If `backup_resources=True`, back up original values first. | +| `restore` | `restore() -> None` | Restore all backed-up values. | + +### Properties + +| Property | Type | Description | +|---|---|---| +| `backups` | `dict[Any, Any]` | Backed-up original values for each patched resource. | +| `patches` | `dict[Any, Any]` | The patches dict from the constructor. | + +> **Warning:** The `DynamicClient` used to obtain the resource objects must have sufficient privileges to patch and replace resources. + +See [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html) for detailed patterns. + +--- + +## `ResourceList` + +```python +from ocp_resources.resource import ResourceList +``` + +Create and manage N copies of a cluster-scoped resource with indexed names. + +### Constructor + +```python +ResourceList( + resource_class: type[Resource], + num_resources: int, + client: DynamicClient, + **kwargs: Any, +) +``` + +| Parameter | Type | Description | +|---|---|---| +| `resource_class` | `type[Resource]` | The resource class to instantiate. | +| `num_resources` | `int` | Number of resource copies to create. | +| `client` | `DynamicClient` | Kubernetes client. | +| `**kwargs` | | Passed to each resource constructor. Must include `name` (used as base name). | + +Resources are named `{name}-1`, `{name}-2`, ..., `{name}-N`. + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.resource import ResourceList + +namespaces = ResourceList( + resource_class=Namespace, + num_resources=3, + client=client, + name="test-ns", +) + +with namespaces: + # Creates test-ns-1, test-ns-2, test-ns-3 + for ns in namespaces: + print(ns.name) +# All three namespaces are deleted +``` + +### Methods (inherited from `BaseResourceList`) + +| Method | Signature | Returns | Description | +|---|---|---|---| +| `deploy` | `deploy(wait=False)` | `list[Resource]` | Deploy all resources. | +| `clean_up` | `clean_up(wait=True)` | `bool` | Delete all resources in reverse order. | +| `__len__` | `__len__()` | `int` | Number of resources. | +| `__getitem__` | `__getitem__(index)` | `Resource` | Access by index. | +| `__iter__` | `__iter__()` | `Generator` | Iterate over resources. | + +--- + +## `NamespacedResourceList` + +```python +from ocp_resources.resource import NamespacedResourceList +``` + +Create one instance of a namespaced resource in each namespace from a `ResourceList`. + +### Constructor + +```python +NamespacedResourceList( + resource_class: type[NamespacedResource], + namespaces: ResourceList, + client: DynamicClient, + **kwargs: Any, +) +``` + +| Parameter | Type | Description | +|---|---|---| +| `resource_class` | `type[NamespacedResource]` | The namespaced resource class (e.g., `Pod`). | +| `namespaces` | `ResourceList` | A `ResourceList` of `Namespace` resources. | +| `client` | `DynamicClient` | Kubernetes client. | +| `**kwargs` | | Passed to each resource constructor. Must include `name`. | + +**Raises:** `TypeError` if any resource in `namespaces` is not a `Namespace`. + +```python +from ocp_resources.namespace import Namespace +from ocp_resources.pod import Pod +from ocp_resources.resource import ResourceList, NamespacedResourceList + +namespaces = ResourceList( + resource_class=Namespace, num_resources=2, client=client, name="test-ns" +) + +pods = NamespacedResourceList( + resource_class=Pod, + namespaces=namespaces, + client=client, + name="nginx", +) + +with namespaces: + with pods: + # Creates nginx in test-ns-1 and test-ns-2 + for pod in pods: + print(f"{pod.namespace}/{pod.name}") +``` + +See [Managing Bulk Resources with ResourceList](managing-resource-lists.html) for full usage patterns. + +--- + +## `KubeAPIVersion` + +```python +from ocp_resources.resource import KubeAPIVersion +``` + +Implements [Kubernetes API versioning](https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-versioning) with comparison operators. Extends `packaging.version.Version`. + +### Constructor + +```python +KubeAPIVersion(vstring: str) +``` + +| Parameter | Type | Description | +|---|---|---| +| `vstring` | `str` | Version string (e.g., `"v1"`, `"v1beta1"`, `"v1alpha2"`). | + +**Raises:** `ValueError` if the version string does not conform to Kubernetes versioning. + +### Ordering + +Versions are ordered: `v1alpha1 < v1alpha2 < v1beta1 < v1beta2 < v1 < v2`. + +```python +assert KubeAPIVersion("v1") > KubeAPIVersion("v1beta1") +assert KubeAPIVersion("v1beta1") > KubeAPIVersion("v1alpha1") +assert KubeAPIVersion("v1") == KubeAPIVersion("v1") +``` + +--- + +## Exceptions + +All exceptions are importable from `ocp_resources.exceptions`. + +```python +from ocp_resources.exceptions import ( + MissingRequiredArgumentError, + ResourceTeardownError, + ValidationError, + ConditionError, +) +``` + +| Exception | Raised By | Description | +|---|---|---| +| `MissingRequiredArgumentError` | `Resource.__init__`, `NamespacedResource.__init__`, `NamespacedResource._base_body` | Required parameters (`name`, `namespace`) not provided. | +| `ResourceTeardownError` | `Resource.__exit__` | `clean_up()` returned `False` during context manager exit. | +| `ValidationError` | `validate()`, `validate_dict()`, `create()`, `update_replace()` | Resource dict fails OpenAPI schema validation. Has `message`, `path`, and `schema_error` attributes. | +| `ConditionError` | `wait_for_condition()` | A `stop_condition` was detected during condition waiting. | +| `MissingResourceResError` | `Resource._base_body` | Deprecated. `self.res` is empty after `_base_body()`. | + +--- + +## Timeout Constants + +Available from `ocp_resources.utils.constants`: + +```python +from ocp_resources.utils.constants import ( + TIMEOUT_1SEC, # 1 + TIMEOUT_5SEC, # 5 + TIMEOUT_10SEC, # 10 + TIMEOUT_30SEC, # 30 + TIMEOUT_1MINUTE, # 60 + TIMEOUT_2MINUTES, # 120 + TIMEOUT_4MINUTES, # 240 + TIMEOUT_10MINUTES, # 600 +) +``` + +--- + +## Module-Level Functions + +### `replace_key_with_hashed_value` + +```python +from ocp_resources.resource import replace_key_with_hashed_value + +replace_key_with_hashed_value(resource_dict: dict, key_name: str) -> dict +``` + +Recursively replace the value at a key path with `"*******"`. Supports dict paths (`a>b>c`) and list paths (`a>b[]>c`). + +| Parameter | Type | Description | +|---|---|---| +| `resource_dict` | `dict` | The nested dictionary. | +| `key_name` | `str` | Key path using `>` as separator, `[]` for list elements. | + +**Returns:** `dict` — A deep copy with the value replaced. + +```python +data = {"spec": {"secret": {"token": "sensitive"}}} +hashed = replace_key_with_hashed_value(data, "spec>secret>token") +# {"spec": {"secret": {"token": "*******"}}} + +# With list path +data = {"spec": {"containers": [{"env": [{"value": "secret"}]}]}} +hashed = replace_key_with_hashed_value(data, "spec>containers[]>env[]>value") +``` + +## Related Pages + +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Querying and Watching Resources](querying-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Environment Variables and Configuration](environment-variables.html) diff --git a/docs/resource-class-hierarchy.html b/docs/resource-class-hierarchy.html new file mode 100644 index 0000000000..3e2d426616 --- /dev/null +++ b/docs/resource-class-hierarchy.html @@ -0,0 +1,702 @@ + + + + + + Understanding the Resource Class Hierarchy - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by reading the exclusions file and pages manifest, then explore the codebase thoroughly.

+

Now let me look at some generated resource subclasses to understand how they extend the base classes:

+

Now let me look at some more examples - a cluster-scoped resource, the ResourceConstants, the class generator, and the virtual_machine.py for a more complex subclass:

+

Now let me look at the kind property and how it's derived, plus some other resources for variety:

+

Now let me look at how the kind property works (the sub_resource_level function) and the conversion.py for the kind name derivation:

+

Now let me look at how resources discover their API version dynamically:

+

Let me now look at one more important aspect - how the get method works differently in the two base classes, and how _base_body differs:

+

Now let me check the __init__.py to see what's exported:

+

Let me also check for examples:

+

Now I have enough context to write the documentation. Let me draft it:

+

Understanding the Resource Class Hierarchy

+

Every Kubernetes and OpenShift object you interact with through openshift-python-wrapper — whether it's a Pod, a Namespace, or a VirtualMachine — is a Python class that inherits from a small set of base classes. Understanding how these classes relate to each other helps you know what capabilities any resource has, why some resources require a namespace argument and others don't, and how new resource types get added to the library.

+

This page explains the architecture so you can use existing resources effectively and extend the library when you need to work with a new custom resource.

+
+

The Big Picture

+

The class hierarchy has three conceptual layers:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
LayerClassesPurpose
ConstantsResourceConstantsProvides shared status strings, condition names, and type constants used across all resources
Base classesResource, NamespacedResourceDefine all CRUD operations, lifecycle management, waiting, validation, and API discovery logic
Concrete resource classesPod, Namespace, Deployment, VirtualMachine, and 200+ othersRepresent specific Kubernetes/OpenShift resource kinds, adding kind-specific parameters and behavior
+

The inheritance flows like this:

+
ResourceConstants
+  └── Resource                  ← cluster-scoped resources (Node, Namespace, StorageClass, ClusterRole, …)
+        └── NamespacedResource  ← namespace-scoped resources (Pod, Deployment, ConfigMap, Secret, Route, …)
+
+ +

Every concrete resource class inherits from either Resource (for cluster-scoped resources) or NamespacedResource (for namespace-scoped resources). This single design decision controls whether the class requires a namespace argument and how it builds API requests.

+
+

Key Concepts

+

ResourceConstants: Shared Vocabulary

+

At the root of the hierarchy sits ResourceConstants, which defines inner classes for common values:

+
    +
  • Status — Strings like RUNNING, SUCCEEDED, FAILED, PENDING, ACTIVE
  • +
  • Condition — Condition types like READY, AVAILABLE, DEGRADED and their status values (TRUE, FALSE, UNKNOWN)
  • +
  • Type — Service types like ClusterIP, NodePort, LoadBalancer
  • +
+

Because Resource inherits from ResourceConstants, every resource class in the library can reference these constants:

+
from ocp_resources.namespace import Namespace
+
+ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
+
+ +

Concrete classes can extend these constants with kind-specific values. For example, VirtualMachine adds statuses like MIGRATING, STOPPED, and PROVISIONING to the base Status class.

+

Resource: The Foundation for Cluster-Scoped Resources

+

Resource is the main base class. It provides everything needed to manage a Kubernetes resource:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CapabilityMethods / Properties
CRUD operationscreate(), delete(), update(), update_replace()
Lifecycle managementdeploy(), clean_up(), context manager (with statement)
Queryingexists, instance, status, labels
Waitingwait(), wait_deleted(), wait_for_status(), wait_for_condition()
Listingget() class method — yields resource objects matching filters
Validationvalidate(), validate_dict()
Serializationto_dict(), to_yaml()
API discoveryAutomatic api_version resolution from the cluster when only api_group is set
+

Resources that exist at the cluster level — not inside any namespace — inherit directly from Resource. Examples include Namespace, Node, StorageClass, ClusterRole, and CustomResourceDefinition.

+
from ocp_resources.namespace import Namespace
+
+# No namespace argument needed — Namespace is cluster-scoped
+ns = Namespace(client=client, name="my-namespace")
+
+ +

Automatic Kind Detection

+

You never set the kind field manually. The kind property is a class-level property that automatically derives the Kubernetes kind name from the class name using Python's method resolution order (MRO). When you define a class named StorageClass, its kind is automatically "StorageClass".

+

API Version Discovery

+

Resources can specify their API identity in two ways:

+
    +
  1. api_version — A fixed version string (e.g., "v1"), used for core Kubernetes resources
  2. +
  3. api_group — An API group string (e.g., "apps", "kubevirt.io"), where the full apiVersion is discovered dynamically from the cluster
  4. +
+

When only api_group is set, the library queries the cluster at runtime to find the latest supported version for that resource kind. This means resource classes automatically work across cluster versions without code changes.

+
class Namespace(Resource):
+    # Core resource — fixed version, no group
+    api_version: str = Resource.ApiVersion.V1
+
+class ClusterRole(Resource):
+    # Grouped resource — version discovered from cluster
+    api_group = Resource.ApiGroup.RBAC_AUTHORIZATION_K8S_IO
+
+ +
+

Note: The Resource.ApiGroup and Resource.ApiVersion inner classes provide predefined constants for all known API groups and versions. Using these constants avoids typos and makes your code self-documenting.

+
+

NamespacedResource: Adding Namespace Awareness

+

NamespacedResource extends Resource with one critical addition: namespace handling. It requires a namespace argument during construction and injects the namespace into all API calls.

+

The differences from Resource are focused but important:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BehaviorResourceNamespacedResource
namespace required?NoYes (unless using yaml_file or kind_dict)
to_dict() outputNo namespace in metadataAdds metadata.namespace
get() yieldsObjects with name onlyObjects with both name and namespace
instance propertyFetches by name onlyFetches by name and namespace
+
from ocp_resources.pod import Pod
+
+# Namespace is required for namespaced resources
+pod = Pod(client=client, name="my-pod", namespace="default",
+          containers=[{"name": "app", "image": "nginx"}])
+
+ +

Concrete Resource Classes: Where Specifics Live

+

Concrete classes add three things on top of the base classes:

+
    +
  1. API identity — Setting api_group or api_version to identify which Kubernetes API to call
  2. +
  3. Constructor parameters — Typed arguments for the resource's spec fields (like containers for Pod, replicas for Deployment)
  4. +
  5. Custom to_dict() method — Builds the Kubernetes resource dictionary from constructor arguments
  6. +
+

Here is how a typical generated class is structured:

+
class Deployment(NamespacedResource):
+    # 1. API identity
+    api_group: str = NamespacedResource.ApiGroup.APPS
+
+    def __init__(self, replicas=None, selector=None, template=None, **kwargs):
+        # 2. Pass common args to base class, store kind-specific args
+        super().__init__(**kwargs)
+        self.replicas = replicas
+        self.selector = selector
+        self.template = template
+
+    def to_dict(self):
+        # 3. Build the resource dictionary
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            self.res["spec"] = {}
+            _spec = self.res["spec"]
+            _spec["selector"] = self.selector
+            _spec["template"] = self.template
+            if self.replicas is not None:
+                _spec["replicas"] = self.replicas
+
+ +
+

Tip: You can always bypass the typed constructor entirely by passing yaml_file or kind_dict to any resource class. When you do, the to_dict() logic is skipped and the resource is created from your raw definition instead.

+
+

Adding Kind-Specific Behavior

+

Many concrete classes go beyond what the generator produces by adding custom methods and properties. For example:

+
    +
  • Pod adds execute() for running commands, log() for reading logs, and a node property
  • +
  • Deployment adds scale_replicas() and wait_for_replicas()
  • +
  • Secret overrides keys_to_hash to ensure sensitive data is masked in logs
  • +
+

These additions are preserved across regeneration because the class generator recognizes the # End of generated code marker and keeps any code written below it.

+

How Generated Classes Are Created

+

Most concrete resource classes in the library are code-generated from the cluster's OpenAPI schema using the class-generator tool. The generator:

+
    +
  1. Reads the resource definition from the cluster's OpenAPI schema
  2. +
  3. Determines whether the resource is namespaced (→ NamespacedResource) or cluster-scoped (→ Resource)
  4. +
  5. Extracts spec fields with their types and descriptions
  6. +
  7. Renders a Python class from a Jinja2 template
  8. +
  9. Preserves any hand-written code below the # End of generated code marker
  10. +
+

This means the hierarchy is not just an architectural choice — it is enforced by the code generation pipeline. Every generated class correctly inherits from the appropriate base class based on the resource's actual scope in Kubernetes.

+

See Generating New Resource Classes with class-generator for details on generating classes for new resource types.

+
+

How It Affects You

+

Understanding the hierarchy helps you in several practical ways:

+

Knowing What Methods Are Available

+

Every resource — regardless of kind — inherits the full set of CRUD, waiting, and lifecycle methods from Resource. You don't need to check whether a particular resource supports wait_for_condition() or context managers; they all do.

+
# Works for any resource type
+with SomeResource(client=client, name="example", **specific_args) as res:
+    res.wait_for_condition(condition="Ready", status="True")
+
+ +

See Resource and NamespacedResource API for the complete method reference.

+

Understanding Constructor Requirements

+

The base class determines what arguments are mandatory:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
If you're using...Required arguments
Resource subclassclient, name
NamespacedResource subclassclient, name, namespace
Any class with yaml_fileclient, yaml_file
Any class with kind_dictclient, kind_dict
+

See Creating and Managing Resources for full examples of all creation methods.

+

Using Status and Condition Constants

+

The Status and Condition constants inherited from ResourceConstants are available on every resource class. Concrete classes may extend them:

+
from ocp_resources.virtual_machine import VirtualMachine
+
+# Base constants work on all resources
+vm.wait_for_status(status=VirtualMachine.Status.RUNNING)
+
+# Kind-specific constants are added by the concrete class
+vm.wait_for_status(status=VirtualMachine.Status.STOPPED)
+
+ +

Extending the Library

+

If you need to work with a CRD that isn't included in the library, you have two options:

+
    +
  1. Use the class generator to scaffold a new class automatically — see Generating New Resource Classes with class-generator
  2. +
  3. Write a class manually by inheriting from Resource or NamespacedResource and setting api_group or api_version
  4. +
+

A minimal hand-written resource class looks like this:

+
from ocp_resources.resource import NamespacedResource
+
+class MyCustomResource(NamespacedResource):
+    api_group = "example.com"
+    # That's it — you get full CRUD, waiting, and lifecycle support
+
+ +
+

Warning: Your class name must match the Kubernetes kind exactly (in PascalCase). The kind property is derived from the class name automatically — a class named MyCustomResource will have kind = "MyCustomResource".

+
+
+ + + + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/resource-class-hierarchy.md b/docs/resource-class-hierarchy.md new file mode 100644 index 0000000000..e5a2dd3c4b --- /dev/null +++ b/docs/resource-class-hierarchy.md @@ -0,0 +1,283 @@ +Let me start by reading the exclusions file and pages manifest, then explore the codebase thoroughly. + +Now let me look at some generated resource subclasses to understand how they extend the base classes: + +Now let me look at some more examples - a cluster-scoped resource, the `ResourceConstants`, the class generator, and the `virtual_machine.py` for a more complex subclass: + +Now let me look at the `kind` property and how it's derived, plus some other resources for variety: + +Now let me look at how the `kind` property works (the `sub_resource_level` function) and the `conversion.py` for the kind name derivation: + +Now let me look at how resources discover their API version dynamically: + +Let me now look at one more important aspect - how the `get` method works differently in the two base classes, and how `_base_body` differs: + +Now let me check the `__init__.py` to see what's exported: + +Let me also check for examples: + +Now I have enough context to write the documentation. Let me draft it: + +# Understanding the Resource Class Hierarchy + +Every Kubernetes and OpenShift object you interact with through openshift-python-wrapper — whether it's a Pod, a Namespace, or a VirtualMachine — is a Python class that inherits from a small set of base classes. Understanding how these classes relate to each other helps you know **what capabilities any resource has**, **why some resources require a `namespace` argument and others don't**, and **how new resource types get added to the library**. + +This page explains the architecture so you can use existing resources effectively and extend the library when you need to work with a new custom resource. + +--- + +## The Big Picture + +The class hierarchy has three conceptual layers: + +| Layer | Classes | Purpose | +|---|---|---| +| **Constants** | `ResourceConstants` | Provides shared status strings, condition names, and type constants used across all resources | +| **Base classes** | `Resource`, `NamespacedResource` | Define all CRUD operations, lifecycle management, waiting, validation, and API discovery logic | +| **Concrete resource classes** | `Pod`, `Namespace`, `Deployment`, `VirtualMachine`, and 200+ others | Represent specific Kubernetes/OpenShift resource kinds, adding kind-specific parameters and behavior | + +The inheritance flows like this: + +``` +ResourceConstants + └── Resource ← cluster-scoped resources (Node, Namespace, StorageClass, ClusterRole, …) + └── NamespacedResource ← namespace-scoped resources (Pod, Deployment, ConfigMap, Secret, Route, …) +``` + +Every concrete resource class inherits from either `Resource` (for cluster-scoped resources) or `NamespacedResource` (for namespace-scoped resources). This single design decision controls whether the class requires a `namespace` argument and how it builds API requests. + +--- + +## Key Concepts + +### ResourceConstants: Shared Vocabulary + +At the root of the hierarchy sits `ResourceConstants`, which defines inner classes for common values: + +- **`Status`** — Strings like `RUNNING`, `SUCCEEDED`, `FAILED`, `PENDING`, `ACTIVE` +- **`Condition`** — Condition types like `READY`, `AVAILABLE`, `DEGRADED` and their status values (`TRUE`, `FALSE`, `UNKNOWN`) +- **`Type`** — Service types like `ClusterIP`, `NodePort`, `LoadBalancer` + +Because `Resource` inherits from `ResourceConstants`, every resource class in the library can reference these constants: + +```python +from ocp_resources.namespace import Namespace + +ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120) +``` + +Concrete classes can extend these constants with kind-specific values. For example, `VirtualMachine` adds statuses like `MIGRATING`, `STOPPED`, and `PROVISIONING` to the base `Status` class. + +### Resource: The Foundation for Cluster-Scoped Resources + +`Resource` is the main base class. It provides everything needed to manage a Kubernetes resource: + +| Capability | Methods / Properties | +|---|---| +| **CRUD operations** | `create()`, `delete()`, `update()`, `update_replace()` | +| **Lifecycle management** | `deploy()`, `clean_up()`, context manager (`with` statement) | +| **Querying** | `exists`, `instance`, `status`, `labels` | +| **Waiting** | `wait()`, `wait_deleted()`, `wait_for_status()`, `wait_for_condition()` | +| **Listing** | `get()` class method — yields resource objects matching filters | +| **Validation** | `validate()`, `validate_dict()` | +| **Serialization** | `to_dict()`, `to_yaml()` | +| **API discovery** | Automatic `api_version` resolution from the cluster when only `api_group` is set | + +Resources that exist at the cluster level — not inside any namespace — inherit directly from `Resource`. Examples include `Namespace`, `Node`, `StorageClass`, `ClusterRole`, and `CustomResourceDefinition`. + +```python +from ocp_resources.namespace import Namespace + +# No namespace argument needed — Namespace is cluster-scoped +ns = Namespace(client=client, name="my-namespace") +``` + +#### Automatic Kind Detection + +You never set the `kind` field manually. The `kind` property is a class-level property that automatically derives the Kubernetes kind name from the **class name** using Python's method resolution order (MRO). When you define a class named `StorageClass`, its `kind` is automatically `"StorageClass"`. + +#### API Version Discovery + +Resources can specify their API identity in two ways: + +1. **`api_version`** — A fixed version string (e.g., `"v1"`), used for core Kubernetes resources +2. **`api_group`** — An API group string (e.g., `"apps"`, `"kubevirt.io"`), where the full `apiVersion` is discovered dynamically from the cluster + +When only `api_group` is set, the library queries the cluster at runtime to find the latest supported version for that resource kind. This means resource classes automatically work across cluster versions without code changes. + +```python +class Namespace(Resource): + # Core resource — fixed version, no group + api_version: str = Resource.ApiVersion.V1 + +class ClusterRole(Resource): + # Grouped resource — version discovered from cluster + api_group = Resource.ApiGroup.RBAC_AUTHORIZATION_K8S_IO +``` + +> **Note:** The `Resource.ApiGroup` and `Resource.ApiVersion` inner classes provide predefined constants for all known API groups and versions. Using these constants avoids typos and makes your code self-documenting. + +### NamespacedResource: Adding Namespace Awareness + +`NamespacedResource` extends `Resource` with one critical addition: **namespace handling**. It requires a `namespace` argument during construction and injects the namespace into all API calls. + +The differences from `Resource` are focused but important: + +| Behavior | `Resource` | `NamespacedResource` | +|---|---|---| +| `namespace` required? | No | Yes (unless using `yaml_file` or `kind_dict`) | +| `to_dict()` output | No namespace in metadata | Adds `metadata.namespace` | +| `get()` yields | Objects with `name` only | Objects with both `name` and `namespace` | +| `instance` property | Fetches by name only | Fetches by name and namespace | + +```python +from ocp_resources.pod import Pod + +# Namespace is required for namespaced resources +pod = Pod(client=client, name="my-pod", namespace="default", + containers=[{"name": "app", "image": "nginx"}]) +``` + +### Concrete Resource Classes: Where Specifics Live + +Concrete classes add three things on top of the base classes: + +1. **API identity** — Setting `api_group` or `api_version` to identify which Kubernetes API to call +2. **Constructor parameters** — Typed arguments for the resource's spec fields (like `containers` for Pod, `replicas` for Deployment) +3. **Custom `to_dict()` method** — Builds the Kubernetes resource dictionary from constructor arguments + +Here is how a typical generated class is structured: + +```python +class Deployment(NamespacedResource): + # 1. API identity + api_group: str = NamespacedResource.ApiGroup.APPS + + def __init__(self, replicas=None, selector=None, template=None, **kwargs): + # 2. Pass common args to base class, store kind-specific args + super().__init__(**kwargs) + self.replicas = replicas + self.selector = selector + self.template = template + + def to_dict(self): + # 3. Build the resource dictionary + super().to_dict() + if not self.kind_dict and not self.yaml_file: + self.res["spec"] = {} + _spec = self.res["spec"] + _spec["selector"] = self.selector + _spec["template"] = self.template + if self.replicas is not None: + _spec["replicas"] = self.replicas +``` + +> **Tip:** You can always bypass the typed constructor entirely by passing `yaml_file` or `kind_dict` to any resource class. When you do, the `to_dict()` logic is skipped and the resource is created from your raw definition instead. + +#### Adding Kind-Specific Behavior + +Many concrete classes go beyond what the generator produces by adding custom methods and properties. For example: + +- **Pod** adds `execute()` for running commands, `log()` for reading logs, and a `node` property +- **Deployment** adds `scale_replicas()` and `wait_for_replicas()` +- **Secret** overrides `keys_to_hash` to ensure sensitive data is masked in logs + +These additions are preserved across regeneration because the class generator recognizes the `# End of generated code` marker and keeps any code written below it. + +### How Generated Classes Are Created + +Most concrete resource classes in the library are **code-generated** from the cluster's OpenAPI schema using the `class-generator` tool. The generator: + +1. Reads the resource definition from the cluster's OpenAPI schema +2. Determines whether the resource is namespaced (→ `NamespacedResource`) or cluster-scoped (→ `Resource`) +3. Extracts spec fields with their types and descriptions +4. Renders a Python class from a Jinja2 template +5. Preserves any hand-written code below the `# End of generated code` marker + +This means the hierarchy is not just an architectural choice — it is **enforced by the code generation pipeline**. Every generated class correctly inherits from the appropriate base class based on the resource's actual scope in Kubernetes. + +See [Generating New Resource Classes with class-generator](generating-resource-classes.html) for details on generating classes for new resource types. + +--- + +## How It Affects You + +Understanding the hierarchy helps you in several practical ways: + +### Knowing What Methods Are Available + +Every resource — regardless of kind — inherits the full set of CRUD, waiting, and lifecycle methods from `Resource`. You don't need to check whether a particular resource supports `wait_for_condition()` or context managers; they all do. + +```python +# Works for any resource type +with SomeResource(client=client, name="example", **specific_args) as res: + res.wait_for_condition(condition="Ready", status="True") +``` + +See [Resource and NamespacedResource API](resource-api.html) for the complete method reference. + +### Understanding Constructor Requirements + +The base class determines what arguments are mandatory: + +| If you're using... | Required arguments | +|---|---| +| `Resource` subclass | `client`, `name` | +| `NamespacedResource` subclass | `client`, `name`, `namespace` | +| Any class with `yaml_file` | `client`, `yaml_file` | +| Any class with `kind_dict` | `client`, `kind_dict` | + +See [Creating and Managing Resources](creating-and-managing-resources.html) for full examples of all creation methods. + +### Using Status and Condition Constants + +The `Status` and `Condition` constants inherited from `ResourceConstants` are available on every resource class. Concrete classes may extend them: + +```python +from ocp_resources.virtual_machine import VirtualMachine + +# Base constants work on all resources +vm.wait_for_status(status=VirtualMachine.Status.RUNNING) + +# Kind-specific constants are added by the concrete class +vm.wait_for_status(status=VirtualMachine.Status.STOPPED) +``` + +### Extending the Library + +If you need to work with a CRD that isn't included in the library, you have two options: + +1. **Use the class generator** to scaffold a new class automatically — see [Generating New Resource Classes with class-generator](generating-resource-classes.html) +2. **Write a class manually** by inheriting from `Resource` or `NamespacedResource` and setting `api_group` or `api_version` + +A minimal hand-written resource class looks like this: + +```python +from ocp_resources.resource import NamespacedResource + +class MyCustomResource(NamespacedResource): + api_group = "example.com" + # That's it — you get full CRUD, waiting, and lifecycle support +``` + +> **Warning:** Your class name must match the Kubernetes `kind` exactly (in PascalCase). The `kind` property is derived from the class name automatically — a class named `MyCustomResource` will have `kind = "MyCustomResource"`. + +--- + +## Related Pages + +- [Resource and NamespacedResource API](resource-api.html) — Complete method and property reference for the base classes +- [Creating and Managing Resources](creating-and-managing-resources.html) — Practical guide to creating, updating, and deleting resources +- [Generating New Resource Classes with class-generator](generating-resource-classes.html) — Generate Python classes for any CRD +- [Querying and Watching Resources](querying-resources.html) — Use the `get()` class method and watchers to list and observe resources +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) — Use the waiting methods inherited from the base classes +- [Validating Resources Against OpenAPI Schemas](validating-resources.html) — Validate resources using the built-in schema validation +- [Environment Variables and Configuration](environment-variables.html) — Configure logging, resource reuse, and teardown behavior + +## Related Pages + +- [Resource and NamespacedResource API](resource-api.html) +- [Generating New Resource Classes with class-generator](generating-resource-classes.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) +- [Validating Resources Against OpenAPI Schemas](validating-resources.html) diff --git a/docs/schema-validation-internals.html b/docs/schema-validation-internals.html new file mode 100644 index 0000000000..a4cb533c5c --- /dev/null +++ b/docs/schema-validation-internals.html @@ -0,0 +1,818 @@ + + + + + + Schema Validation and Code Generation Architecture - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by reading the knowledge graph and the pages manifest, then explore the relevant source files.

+

Now let me look at some more key files - the explain parser, resource base class validation, archive utils, and some test files:

+

Now let me look at the exceptions module for validation:

+

Now let me check the CI/CD configuration to understand how schemas are updated:

+

Let me look at a few generated resource files to understand the output of the code generation:

+

Let me also check the parsers for status schema generation:

+

Now let me check what data is actually in the schema file to understand the structure:

+

Now I have enough information. Let me write the documentation page.

+

Schema Validation and Code Generation Architecture

+

The openshift-python-wrapper uses OpenAPI schemas as the single source of truth for two critical capabilities: validating resource definitions before they reach the cluster, and generating Python wrapper classes for any Kubernetes or OpenShift resource type. Understanding this architecture helps you diagnose validation errors, generate classes for custom resources, and keep your schemas up to date as clusters evolve.

+

The Big Picture

+

OpenAPI schemas flow through the system in two directions: they are fetched from a live cluster and stored locally, then consumed by the validation engine at runtime and by the code generator at development time.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentLocationPurpose
Schema Fetcherclass_generator/core/schema.pyConnects to a cluster, downloads OpenAPI v3 schemas, and writes them to disk
Resource Mappingsclass_generator/schema/__resources-mappings.json.gzCompressed archive mapping each resource kind (lowercase) to its schema(s)
Definitions Fileclass_generator/schema/_definitions.jsonJSON file containing detailed schema definitions with $ref targets for nested types
Schema Validatorocp_resources/utils/schema_validator.pyRuntime engine that loads schemas, resolves $ref references, and validates resource dicts
Code Generatorclass_generator/core/generator.pyReads schemas and generates Python class files from a Jinja2 template
Explain Parserclass_generator/parsers/explain_parser.pyParses the resource mapping to extract fields, types, and group-version-kind metadata
Cluster Version Trackerclass_generator/schema/__cluster_version__.txtTracks the cluster version that last produced the schema, controlling update strategy
+

Data Flow: Schema Fetch and Storage

+
    +
  1. Detect client binary — The system finds oc or falls back to kubectl via get_client_binary().
  2. +
  3. Check cluster versioncheck_and_update_cluster_version() compares the connected cluster's version against the stored version in __cluster_version__.txt.
  4. +
  5. Determine update strategy — If the cluster is the same version or newer, all schemas are fetched and existing entries are updated. If older, only missing resources are fetched and existing schemas are preserved.
  6. +
  7. Fetch OpenAPI v3 indexGET /openapi/v3 returns an index of all API group paths (e.g., api/v1, apis/apps/v1).
  8. +
  9. Fetch schemas in parallelfetch_all_api_schemas() downloads schemas from each API path using a thread pool (up to 10 workers).
  10. +
  11. Build namespacing dictionarybuild_namespacing_dict() queries api-resources to determine which kinds are namespaced vs. cluster-scoped.
  12. +
  13. Process definitionsprocess_schema_definitions() extracts x-kubernetes-group-version-kind metadata, builds schema entries, and merges them into the resource mappings. Existing resources are never deleted — only added or updated.
  14. +
  15. Supplement with oc explain — Missing field descriptions, required-field markers, and $ref definitions are filled in by running oc explain commands in parallel.
  16. +
  17. Write to disk — The resource mappings are saved as a gzip-compressed JSON archive (__resources-mappings.json.gz), and definitions are written to _definitions.json.
  18. +
+

Data Flow: Validation at Runtime

+
    +
  1. Load on first useSchemaValidator.load_mappings_data() decompresses and loads the mappings archive and definitions file into class-level caches.
  2. +
  3. Look up by kindSchemaValidator.load_schema(kind, api_group) finds the schema for a resource kind (case-insensitive). When multiple API groups define the same kind (e.g., Ingress in networking.k8s.io and config.openshift.io), the api_group parameter disambiguates.
  4. +
  5. Resolve $ref references — The validator recursively resolves all $ref pointers against the definitions data, producing a self-contained schema.
  6. +
  7. Cache resolved schemas — Resolved schemas are stored in _schema_cache keyed by api_group:kind, so subsequent validations are fast.
  8. +
  9. Validate with jsonschemajsonschema.validate() checks the resource dictionary against the resolved schema.
  10. +
  11. Format errors — If validation fails, format_validation_error() produces a human-readable message including the field path, error details, and schema context.
  12. +
+

Data Flow: Code Generation

+
    +
  1. Read resource mappingsparse_explain() loads the mapping file and finds all schema entries for the requested kind.
  2. +
  3. Select latest API version — When multiple versions exist within an API group, the latest version is selected using a priority ranking (v2 > v1 > v1beta2 > v1beta1 > v1alpha2 > v1alpha1).
  4. +
  5. Extract fields and types — The type_parser module converts OpenAPI types to Python type annotations and builds parameter dictionaries for the class constructor.
  6. +
  7. Render Jinja2 templaterender_jinja_template() processes class_generator_template.j2 with the extracted data, producing a complete Python class.
  8. +
  9. Preserve user code — If the target file already exists, parse_user_code_from_file() extracts any user-added code below the # End of generated code marker and re-inserts it after regeneration.
  10. +
  11. Format and writewrite_and_format_rendered() writes the file, then runs prek (pre-commit hooks) or falls back to ruff for formatting.
  12. +
+

Key Concepts

+

The Resource Mappings File

+

The resource mappings file (__resources-mappings.json.gz) is the central schema store. It maps lowercase kind names to arrays of schema objects:

+
{
+  "pod": [
+    {
+      "description": "Pod is a collection of containers...",
+      "properties": { ... },
+      "required": [],
+      "type": "object",
+      "x-kubernetes-group-version-kind": [
+        { "group": "", "kind": "Pod", "version": "v1" }
+      ],
+      "namespaced": true
+    }
+  ],
+  "ingress": [
+    { "x-kubernetes-group-version-kind": [{ "group": "networking.k8s.io", ... }], ... },
+    { "x-kubernetes-group-version-kind": [{ "group": "config.openshift.io", ... }], ... }
+  ]
+}
+
+ +

Resources with the same kind but different API groups (like Ingress above) are stored as separate entries in the array. This structure allows the validator and generator to handle API group disambiguation correctly.

+
+

Note: The mappings file is compressed with gzip to reduce repository size. The archive_utils module handles transparent compression and decompression via save_json_archive() and load_json_archive().

+
+

The Definitions File

+

The definitions file (_definitions.json) contains detailed schema definitions keyed by group/version/Kind paths (e.g., apps/v1/Deployment, v1/Pod). It also stores referenced sub-schemas like io.k8s.api.core.v1.PodSpec that are targets of $ref pointers. During validation, the SchemaValidator uses this file as the reference store for resolving nested types.

+

SchemaValidator

+

SchemaValidator is a class with only class-level methods and caches — you never need to instantiate it. It serves as the shared validation engine used by both Resource.validate() and Resource.validate_dict().

+
from ocp_resources.utils.schema_validator import SchemaValidator
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodSignatureDescription
load_mappings_data(skip_cache: bool = False) -> boolLoads the mappings archive and definitions file. Returns True on success.
get_mappings_data(skip_cache: bool = False) -> dict[str, Any] \| NoneReturns loaded mappings data, loading first if needed.
get_definitions_data() -> dict[str, Any] \| NoneReturns loaded definitions data, loading first if needed.
load_schema(kind: str, api_group: str \| None = None) -> dict[str, Any] \| NoneLoads and resolves a complete schema for a kind. Handles API group disambiguation and caching.
validate(resource_dict: dict[str, Any], kind: str, api_group: str \| None = None) -> NoneValidates a resource dict. Raises jsonschema.ValidationError on failure.
format_validation_error(error, kind, name, api_group=None) -> strFormats a validation error into a user-friendly message with field path and context.
clear_cache() -> NoneClears the resolved schema cache (not the raw mappings/definitions data).
+
+

Tip: The load_schema method resolves $ref references recursively. For core Kubernetes types like ObjectMeta or TypeMeta that may be missing from definitions, it falls back to a permissive {"type": "object", "additionalProperties": true} schema rather than failing outright.

+
+

Resource Validation Methods

+

The Resource base class exposes two validation methods that delegate to SchemaValidator:

+
from ocp_resources.resource import Resource
+
+# Instance-level validation — validates self.res
+resource.validate()
+
+# Class-level validation — validates any dict against the class's schema
+MyResource.validate_dict(resource_dict)
+
+ +

Both raise ocp_resources.exceptions.ValidationError (not the raw jsonschema.ValidationError) with formatted error messages that include the resource identifier, field path, and details.

+

Auto-validation can be enabled per-instance by passing schema_validation_enabled=True to the constructor:

+
from ocp_resources.pod import Pod
+
+pod = Pod(
+    name="my-pod",
+    namespace="default",
+    containers=[{"name": "app", "image": "nginx"}],
+    client=client,
+    schema_validation_enabled=True,
+)
+# validation runs automatically before create() and update_replace()
+pod.deploy()
+
+ +

When auto-validation is enabled:

+
    +
  • create() calls self.validate() after to_dict() builds the resource dictionary
  • +
  • update_replace() calls validate_dict() on the replacement resource dictionary
  • +
  • update() (patch operations) does not trigger validation because patches are partial documents that may not pass full schema validation
  • +
+

Schema Update Strategies

+

The update_kind_schema() function employs a version-aware update strategy:

+ + + + + + + + + + + + + + + + + +
Cluster VersionBehavior
Same or newer than last recordedFetches all API schemas, updates existing resources, supplements with oc explain data
Older than last recordedIdentifies only missing resources via identify_missing_resources(), fetches only relevant API paths, does not modify existing schemas
+

This strategy prevents an older cluster from overwriting schemas that contain richer data from a newer cluster.

+
+

Warning: If you connect to an older cluster and need to update a specific CRD's schema (for example, after installing a new operator), use update_single_resource_schema(kind) or the CLI flag --update-schema-for <Kind>. This bypasses the version guard for that one resource.

+
+

Single-Resource Schema Updates

+

The update_single_resource_schema() function fetches the schema for exactly one resource kind without affecting other resources in the mapping:

+
from class_generator.core.schema import update_single_resource_schema
+
+update_single_resource_schema(kind="LlamaStackDistribution")
+
+ + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
kindstrThe resource Kind (case-sensitive, e.g., "LlamaStackDistribution")
clientstr \| NoneClient binary path. Auto-detected if None.
+

Raises:

+
    +
  • ResourceNotFoundError — if the kind is not found on the cluster
  • +
  • RuntimeError — if schema fetching fails
  • +
+

Code Generation Pipeline

+

The class_generator() function orchestrates the full generation pipeline:

+
from class_generator.core.generator import class_generator
+
+generated_files = class_generator(kind="Deployment", overwrite=True)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
kindstr(required)Kubernetes resource kind
overwriteboolFalseOverwrite existing files
dry_runboolFalsePrint output without writing
output_filestr""Specific output file path
output_dirstr""Output directory (defaults to ocp_resources)
add_testsboolFalseGenerate test files in class_generator/tests/manifests/
+

Returns: list[str] — paths of generated files.

+

Raises:

+
    +
  • RuntimeError — if the kind is not found in schema mappings
  • +
  • ValueError — if the generated filename contains invalid patterns
  • +
+

When a kind exists in multiple API groups (e.g., Ingress in both networking.k8s.io and config.openshift.io), the generator produces separate files with a group-based suffix (e.g., ingress_networking_k8s_io.py, ingress_config_openshift_io.py).

+

Generated Class Structure

+

Every generated file follows a consistent structure produced by the Jinja2 template:

+
# Generated using https://github.com/RedHatQE/openshift-python-wrapper/blob/main/class_generator/README.md
+
+from typing import Any
+from ocp_resources.resource import NamespacedResource
+
+class Pod(NamespacedResource):
+    """Pod is a collection of containers that can run on a host."""
+
+    api_version: str = NamespacedResource.ApiVersion.V1
+
+    def __init__(self, containers: list[Any] | None = None, ..., **kwargs: Any) -> None:
+        """Args:
+            containers (list[Any]): List of containers belonging to the pod.
+            ...
+        """
+        super().__init__(**kwargs)
+        self.containers = containers
+        ...
+
+    def to_dict(self) -> None:
+        super().to_dict()
+        if not self.kind_dict and not self.yaml_file:
+            # Required fields raise MissingRequiredArgumentError
+            # Optional fields added only if not None
+            ...
+    # End of generated code
+
+ +

User-added code placed after the # End of generated code marker is preserved across regenerations.

+

$ref Resolution

+

OpenAPI schemas extensively use $ref pointers to reference shared type definitions (e.g., "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"). The SchemaValidator._resolve_refs() method handles this by:

+
    +
  1. Parsing the reference path to extract the definition name
  2. +
  3. Looking up the definition in _definitions_data using multiple key formats (full dotted path, short name, or version/Kind format)
  4. +
  5. Recursively resolving any $ref pointers within the resolved definition
  6. +
  7. Falling back to a permissive object schema for well-known core types that may be missing
  8. +
+

Schema Supplementation via oc explain

+

The raw OpenAPI v3 schemas from the cluster sometimes lack field descriptions and required-field markers. The system supplements these gaps by:

+
    +
  • Running oc explain <resource>.<field> commands in parallel for critical resource types (Pod, Deployment, Service, etc.)
  • +
  • Parsing the explain output to extract field descriptions, types, and required-field markers
  • +
  • Merging descriptions into existing schema properties where they are missing
  • +
  • Detecting missing $ref targets and fetching their definitions via oc explain --recursive
  • +
+

This supplementation only runs during a full update (same or newer cluster version), not when only fetching missing resources.

+

How It Affects You

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
What you doWhat happens under the hood
Call resource.validate()SchemaValidator loads schemas from the compressed archive, resolves $ref references, validates with jsonschema, and raises ValidationError with a formatted message
Pass schema_validation_enabled=Truecreate() and update_replace() automatically validate before sending to the API server
Run class-generator --update-schemaFull schema fetch from cluster, version check, parallel API downloads, oc explain supplementation, and compressed archive write
Run class-generator -k PodReads the mapping for pod, parses fields/types, renders Jinja2 template, preserves user code, writes and formats the Python file
Run class-generator --update-schema-for MyResourceSingle-resource fetch that bypasses the cluster version guard
Run class-generator --regenerate-allDiscovers all generated files (by marker comment), regenerates each in parallel, preserving user code
Validation reports "No schema found"The resource kind is not in the mappings archive — run --update-schema to fetch it
Multiple files generated for one kindThe kind exists in multiple API groups — each group gets its own file with a suffix
+ + + + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/schema-validation-internals.md b/docs/schema-validation-internals.md new file mode 100644 index 0000000000..0e847a50d9 --- /dev/null +++ b/docs/schema-validation-internals.md @@ -0,0 +1,299 @@ +Let me start by reading the knowledge graph and the pages manifest, then explore the relevant source files. + +Now let me look at some more key files - the explain parser, resource base class validation, archive utils, and some test files: + +Now let me look at the exceptions module for validation: + +Now let me check the CI/CD configuration to understand how schemas are updated: + +Let me look at a few generated resource files to understand the output of the code generation: + +Let me also check the parsers for status schema generation: + +Now let me check what data is actually in the schema file to understand the structure: + +Now I have enough information. Let me write the documentation page. + +# Schema Validation and Code Generation Architecture + +The openshift-python-wrapper uses OpenAPI schemas as the single source of truth for two critical capabilities: **validating resource definitions** before they reach the cluster, and **generating Python wrapper classes** for any Kubernetes or OpenShift resource type. Understanding this architecture helps you diagnose validation errors, generate classes for custom resources, and keep your schemas up to date as clusters evolve. + +## The Big Picture + +OpenAPI schemas flow through the system in two directions: they are fetched from a live cluster and stored locally, then consumed by the validation engine at runtime and by the code generator at development time. + +| Component | Location | Purpose | +|---|---|---| +| **Schema Fetcher** | `class_generator/core/schema.py` | Connects to a cluster, downloads OpenAPI v3 schemas, and writes them to disk | +| **Resource Mappings** | `class_generator/schema/__resources-mappings.json.gz` | Compressed archive mapping each resource kind (lowercase) to its schema(s) | +| **Definitions File** | `class_generator/schema/_definitions.json` | JSON file containing detailed schema definitions with `$ref` targets for nested types | +| **Schema Validator** | `ocp_resources/utils/schema_validator.py` | Runtime engine that loads schemas, resolves `$ref` references, and validates resource dicts | +| **Code Generator** | `class_generator/core/generator.py` | Reads schemas and generates Python class files from a Jinja2 template | +| **Explain Parser** | `class_generator/parsers/explain_parser.py` | Parses the resource mapping to extract fields, types, and group-version-kind metadata | +| **Cluster Version Tracker** | `class_generator/schema/__cluster_version__.txt` | Tracks the cluster version that last produced the schema, controlling update strategy | + +### Data Flow: Schema Fetch and Storage + +1. **Detect client binary** — The system finds `oc` or falls back to `kubectl` via `get_client_binary()`. +2. **Check cluster version** — `check_and_update_cluster_version()` compares the connected cluster's version against the stored version in `__cluster_version__.txt`. +3. **Determine update strategy** — If the cluster is the same version or newer, all schemas are fetched and existing entries are updated. If older, only missing resources are fetched and existing schemas are preserved. +4. **Fetch OpenAPI v3 index** — `GET /openapi/v3` returns an index of all API group paths (e.g., `api/v1`, `apis/apps/v1`). +5. **Fetch schemas in parallel** — `fetch_all_api_schemas()` downloads schemas from each API path using a thread pool (up to 10 workers). +6. **Build namespacing dictionary** — `build_namespacing_dict()` queries `api-resources` to determine which kinds are namespaced vs. cluster-scoped. +7. **Process definitions** — `process_schema_definitions()` extracts `x-kubernetes-group-version-kind` metadata, builds schema entries, and merges them into the resource mappings. Existing resources are **never deleted** — only added or updated. +8. **Supplement with `oc explain`** — Missing field descriptions, required-field markers, and `$ref` definitions are filled in by running `oc explain` commands in parallel. +9. **Write to disk** — The resource mappings are saved as a gzip-compressed JSON archive (`__resources-mappings.json.gz`), and definitions are written to `_definitions.json`. + +### Data Flow: Validation at Runtime + +1. **Load on first use** — `SchemaValidator.load_mappings_data()` decompresses and loads the mappings archive and definitions file into class-level caches. +2. **Look up by kind** — `SchemaValidator.load_schema(kind, api_group)` finds the schema for a resource kind (case-insensitive). When multiple API groups define the same kind (e.g., `Ingress` in `networking.k8s.io` and `config.openshift.io`), the `api_group` parameter disambiguates. +3. **Resolve `$ref` references** — The validator recursively resolves all `$ref` pointers against the definitions data, producing a self-contained schema. +4. **Cache resolved schemas** — Resolved schemas are stored in `_schema_cache` keyed by `api_group:kind`, so subsequent validations are fast. +5. **Validate with jsonschema** — `jsonschema.validate()` checks the resource dictionary against the resolved schema. +6. **Format errors** — If validation fails, `format_validation_error()` produces a human-readable message including the field path, error details, and schema context. + +### Data Flow: Code Generation + +1. **Read resource mappings** — `parse_explain()` loads the mapping file and finds all schema entries for the requested kind. +2. **Select latest API version** — When multiple versions exist within an API group, the latest version is selected using a priority ranking (`v2 > v1 > v1beta2 > v1beta1 > v1alpha2 > v1alpha1`). +3. **Extract fields and types** — The `type_parser` module converts OpenAPI types to Python type annotations and builds parameter dictionaries for the class constructor. +4. **Render Jinja2 template** — `render_jinja_template()` processes `class_generator_template.j2` with the extracted data, producing a complete Python class. +5. **Preserve user code** — If the target file already exists, `parse_user_code_from_file()` extracts any user-added code below the `# End of generated code` marker and re-inserts it after regeneration. +6. **Format and write** — `write_and_format_rendered()` writes the file, then runs `prek` (pre-commit hooks) or falls back to `ruff` for formatting. + +## Key Concepts + +### The Resource Mappings File + +The resource mappings file (`__resources-mappings.json.gz`) is the central schema store. It maps lowercase kind names to arrays of schema objects: + +```json +{ + "pod": [ + { + "description": "Pod is a collection of containers...", + "properties": { ... }, + "required": [], + "type": "object", + "x-kubernetes-group-version-kind": [ + { "group": "", "kind": "Pod", "version": "v1" } + ], + "namespaced": true + } + ], + "ingress": [ + { "x-kubernetes-group-version-kind": [{ "group": "networking.k8s.io", ... }], ... }, + { "x-kubernetes-group-version-kind": [{ "group": "config.openshift.io", ... }], ... } + ] +} +``` + +Resources with the same kind but different API groups (like `Ingress` above) are stored as separate entries in the array. This structure allows the validator and generator to handle API group disambiguation correctly. + +> **Note:** The mappings file is compressed with gzip to reduce repository size. The `archive_utils` module handles transparent compression and decompression via `save_json_archive()` and `load_json_archive()`. + +### The Definitions File + +The definitions file (`_definitions.json`) contains detailed schema definitions keyed by `group/version/Kind` paths (e.g., `apps/v1/Deployment`, `v1/Pod`). It also stores referenced sub-schemas like `io.k8s.api.core.v1.PodSpec` that are targets of `$ref` pointers. During validation, the `SchemaValidator` uses this file as the reference store for resolving nested types. + +### SchemaValidator + +`SchemaValidator` is a class with only class-level methods and caches — you never need to instantiate it. It serves as the shared validation engine used by both `Resource.validate()` and `Resource.validate_dict()`. + +```python +from ocp_resources.utils.schema_validator import SchemaValidator +``` + +| Method | Signature | Description | +|---|---|---| +| `load_mappings_data` | `(skip_cache: bool = False) -> bool` | Loads the mappings archive and definitions file. Returns `True` on success. | +| `get_mappings_data` | `(skip_cache: bool = False) -> dict[str, Any] \| None` | Returns loaded mappings data, loading first if needed. | +| `get_definitions_data` | `() -> dict[str, Any] \| None` | Returns loaded definitions data, loading first if needed. | +| `load_schema` | `(kind: str, api_group: str \| None = None) -> dict[str, Any] \| None` | Loads and resolves a complete schema for a kind. Handles API group disambiguation and caching. | +| `validate` | `(resource_dict: dict[str, Any], kind: str, api_group: str \| None = None) -> None` | Validates a resource dict. Raises `jsonschema.ValidationError` on failure. | +| `format_validation_error` | `(error, kind, name, api_group=None) -> str` | Formats a validation error into a user-friendly message with field path and context. | +| `clear_cache` | `() -> None` | Clears the resolved schema cache (not the raw mappings/definitions data). | + +> **Tip:** The `load_schema` method resolves `$ref` references recursively. For core Kubernetes types like `ObjectMeta` or `TypeMeta` that may be missing from definitions, it falls back to a permissive `{"type": "object", "additionalProperties": true}` schema rather than failing outright. + +### Resource Validation Methods + +The `Resource` base class exposes two validation methods that delegate to `SchemaValidator`: + +```python +from ocp_resources.resource import Resource + +# Instance-level validation — validates self.res +resource.validate() + +# Class-level validation — validates any dict against the class's schema +MyResource.validate_dict(resource_dict) +``` + +Both raise `ocp_resources.exceptions.ValidationError` (not the raw `jsonschema.ValidationError`) with formatted error messages that include the resource identifier, field path, and details. + +**Auto-validation** can be enabled per-instance by passing `schema_validation_enabled=True` to the constructor: + +```python +from ocp_resources.pod import Pod + +pod = Pod( + name="my-pod", + namespace="default", + containers=[{"name": "app", "image": "nginx"}], + client=client, + schema_validation_enabled=True, +) +# validation runs automatically before create() and update_replace() +pod.deploy() +``` + +When auto-validation is enabled: +- `create()` calls `self.validate()` after `to_dict()` builds the resource dictionary +- `update_replace()` calls `validate_dict()` on the replacement resource dictionary +- `update()` (patch operations) does **not** trigger validation because patches are partial documents that may not pass full schema validation + +### Schema Update Strategies + +The `update_kind_schema()` function employs a version-aware update strategy: + +| Cluster Version | Behavior | +|---|---| +| **Same or newer** than last recorded | Fetches **all** API schemas, updates existing resources, supplements with `oc explain` data | +| **Older** than last recorded | Identifies only **missing** resources via `identify_missing_resources()`, fetches only relevant API paths, does **not** modify existing schemas | + +This strategy prevents an older cluster from overwriting schemas that contain richer data from a newer cluster. + +> **Warning:** If you connect to an older cluster and need to update a specific CRD's schema (for example, after installing a new operator), use `update_single_resource_schema(kind)` or the CLI flag `--update-schema-for `. This bypasses the version guard for that one resource. + +### Single-Resource Schema Updates + +The `update_single_resource_schema()` function fetches the schema for exactly one resource kind without affecting other resources in the mapping: + +```python +from class_generator.core.schema import update_single_resource_schema + +update_single_resource_schema(kind="LlamaStackDistribution") +``` + +| Parameter | Type | Description | +|---|---|---| +| `kind` | `str` | The resource Kind (case-sensitive, e.g., `"LlamaStackDistribution"`) | +| `client` | `str \| None` | Client binary path. Auto-detected if `None`. | + +**Raises:** +- `ResourceNotFoundError` — if the kind is not found on the cluster +- `RuntimeError` — if schema fetching fails + +### Code Generation Pipeline + +The `class_generator()` function orchestrates the full generation pipeline: + +```python +from class_generator.core.generator import class_generator + +generated_files = class_generator(kind="Deployment", overwrite=True) +``` + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `kind` | `str` | (required) | Kubernetes resource kind | +| `overwrite` | `bool` | `False` | Overwrite existing files | +| `dry_run` | `bool` | `False` | Print output without writing | +| `output_file` | `str` | `""` | Specific output file path | +| `output_dir` | `str` | `""` | Output directory (defaults to `ocp_resources`) | +| `add_tests` | `bool` | `False` | Generate test files in `class_generator/tests/manifests/` | + +**Returns:** `list[str]` — paths of generated files. + +**Raises:** +- `RuntimeError` — if the kind is not found in schema mappings +- `ValueError` — if the generated filename contains invalid patterns + +When a kind exists in multiple API groups (e.g., `Ingress` in both `networking.k8s.io` and `config.openshift.io`), the generator produces **separate files** with a group-based suffix (e.g., `ingress_networking_k8s_io.py`, `ingress_config_openshift_io.py`). + +### Generated Class Structure + +Every generated file follows a consistent structure produced by the Jinja2 template: + +```python +# Generated using https://github.com/RedHatQE/openshift-python-wrapper/blob/main/class_generator/README.md + +from typing import Any +from ocp_resources.resource import NamespacedResource + +class Pod(NamespacedResource): + """Pod is a collection of containers that can run on a host.""" + + api_version: str = NamespacedResource.ApiVersion.V1 + + def __init__(self, containers: list[Any] | None = None, ..., **kwargs: Any) -> None: + """Args: + containers (list[Any]): List of containers belonging to the pod. + ... + """ + super().__init__(**kwargs) + self.containers = containers + ... + + def to_dict(self) -> None: + super().to_dict() + if not self.kind_dict and not self.yaml_file: + # Required fields raise MissingRequiredArgumentError + # Optional fields added only if not None + ... + # End of generated code +``` + +User-added code placed **after** the `# End of generated code` marker is preserved across regenerations. + +### `$ref` Resolution + +OpenAPI schemas extensively use `$ref` pointers to reference shared type definitions (e.g., `"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"`). The `SchemaValidator._resolve_refs()` method handles this by: + +1. Parsing the reference path to extract the definition name +2. Looking up the definition in `_definitions_data` using multiple key formats (full dotted path, short name, or `version/Kind` format) +3. Recursively resolving any `$ref` pointers within the resolved definition +4. Falling back to a permissive object schema for well-known core types that may be missing + +### Schema Supplementation via `oc explain` + +The raw OpenAPI v3 schemas from the cluster sometimes lack field descriptions and required-field markers. The system supplements these gaps by: + +- Running `oc explain .` commands **in parallel** for critical resource types (Pod, Deployment, Service, etc.) +- Parsing the explain output to extract field descriptions, types, and required-field markers +- Merging descriptions into existing schema properties where they are missing +- Detecting missing `$ref` targets and fetching their definitions via `oc explain --recursive` + +This supplementation only runs during a **full update** (same or newer cluster version), not when only fetching missing resources. + +## How It Affects You + +| What you do | What happens under the hood | +|---|---| +| Call `resource.validate()` | `SchemaValidator` loads schemas from the compressed archive, resolves `$ref` references, validates with jsonschema, and raises `ValidationError` with a formatted message | +| Pass `schema_validation_enabled=True` | `create()` and `update_replace()` automatically validate before sending to the API server | +| Run `class-generator --update-schema` | Full schema fetch from cluster, version check, parallel API downloads, `oc explain` supplementation, and compressed archive write | +| Run `class-generator -k Pod` | Reads the mapping for `pod`, parses fields/types, renders Jinja2 template, preserves user code, writes and formats the Python file | +| Run `class-generator --update-schema-for MyResource` | Single-resource fetch that bypasses the cluster version guard | +| Run `class-generator --regenerate-all` | Discovers all generated files (by marker comment), regenerates each in parallel, preserving user code | +| Validation reports "No schema found" | The resource kind is not in the mappings archive — run `--update-schema` to fetch it | +| Multiple files generated for one kind | The kind exists in multiple API groups — each group gets its own file with a suffix | + +## Related Pages + +- See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for a hands-on guide to enabling and using validation. +- See [Generating New Resource Classes with class-generator](generating-resource-classes.html) for step-by-step class generation instructions. +- See [class-generator CLI Reference](class-generator-cli.html) for all CLI options including `--update-schema`, `--update-schema-for`, `--regenerate-all`, and `--coverage-report`. +- See [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) to learn how generated classes fit into the `Resource` / `NamespacedResource` inheritance tree. +- See [Resource and NamespacedResource API](resource-api.html) for complete documentation of the `validate()` and `validate_dict()` methods. +- See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for how the fake client uses schema-based status templates. + +## Related Pages + +- [Validating Resources Against OpenAPI Schemas](validating-resources.html) +- [Generating New Resource Classes with class-generator](generating-resource-classes.html) +- [class-generator CLI Reference](class-generator-cli.html) +- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) +- [Resource and NamespacedResource API](resource-api.html) diff --git a/docs/search-index.json b/docs/search-index.json new file mode 100644 index 0000000000..3281872219 --- /dev/null +++ b/docs/search-index.json @@ -0,0 +1 @@ +[{"slug": "quickstart", "title": "Installing and Creating Your First Resource", "content": "Let me explore the repository structure and key files to understand the codebase before writing the documentation.\n\nNow let me look at the `NamespacedResource` class and the `get` classmethod for listing resources:\n\nNow let me check the `get_client` with the `utils/client_config.py` for understanding connection options:\n\nNow I have a thorough understanding of the codebase. Let me write the documentation:\n\nNow I have a comprehensive understanding of the codebase. Let me write the guide:\n\n# Installing and Creating Your First Resource\n\nGet up and running with openshift-python-wrapper so you can programmatically create, query, and delete Kubernetes and OpenShift resources from Python. This guide takes you from installation to your first working resource in under 5 minutes.\n\n## Prerequisites\n\n- Python 3.10 or later\n- Access to a Kubernetes or OpenShift cluster (with a valid kubeconfig)\n- `pip` or another Python package manager\n\n## Quick Example\n\n```bash\npip install openshift-python-wrapper\n```\n\n```python\nfrom ocp_resources.namespace import Namespace\nfrom ocp_resources.resource import get_client\n\nclient = get_client()\n\nwith Namespace(client=client, name=\"my-first-namespace\") as ns:\n ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)\n print(f\"Namespace {ns.name} is active: {ns.exists}\")\n# Namespace is automatically deleted when the block exits\n```\n\nThat's it \u2014 the namespace is created when entering the `with` block and cleaned up when leaving it.\n\n## Step 1: Install the Package\n\n```bash\npip install openshift-python-wrapper\n```\n\n> **Tip:** For isolated environments, use a virtual environment:\n> ```bash\n> python -m venv .venv && source .venv/bin/activate\n> pip install openshift-python-wrapper\n> ```\n\n## Step 2: Connect to Your Cluster\n\nThe `get_client()` function returns a dynamic client connected to your cluster. By default it reads your kubeconfig from the `KUBECONFIG` environment variable or `~/.kube/config`:\n\n```python\nfrom ocp_resources.resource import"}, {"slug": "connecting-to-clusters", "title": "Connecting to Clusters", "content": "Let me start by exploring the repository structure and the knowledge graph.\n\nNow let me explore the client configuration and authentication code:\n\nNow let me look at how the `client` parameter is used in the Resource class:\n\nNow let me look at how resources accept the client parameter:\n\nNow let me look at the NamespacedResource and how it handles client:\n\nNow let me look at the tests for the client configuration:\n\nLet me also check how environment variables relate to client configuration:\n\nNow let me check how `get_client` is used with `generate_kubeconfig`:\n\nNow let me look at the `conftest.py` for the tests to understand how fake_client is set up:\n\nLet me also check the test file for more patterns of client usage:\n\nNow let me check what environment variables are documented:\n\nNow let me check the `generate_kubeconfig` related tests more carefully and how `DynamicClientWithKubeconfig` is used:\n\nNow let me also look at how `get_client` is imported/used in other contexts to understand its full interface:\n\nLet me look at some example files to see real usage patterns:\n\nNow let me check the `get` classmethod for how client is used:\n\nNow let me check for the `client_configuration` parameter and how `verify_ssl` is used:\n\nNow I have all the information I need. Let me write the documentation page.\n\n# Connecting to Clusters\n\nEstablish authenticated connections to Kubernetes and OpenShift clusters so you can create, query, and manage resources with openshift-python-wrapper.\n\n## Prerequisites\n\n- openshift-python-wrapper installed (see [Installing and Creating Your First Resource](quickstart.html))\n- Access to a Kubernetes or OpenShift cluster (kubeconfig file, token, or in-cluster credentials)\n\n## Quick Example\n\n```python\nfrom ocp_resources.resource import get_client\nfrom ocp_resources.namespace import Namespace\n\n# Connect using default kubeconfig (~/.kube/config or $KUBECONFIG)\nclient = get_client()\n\n# Use the client with any resource\nfor ns in Namespace.get(client=client):\n "}, {"slug": "creating-and-managing-resources", "title": "Creating and Managing Resources", "content": "Let me start by exploring the repository structure and the knowledge graph.\n\nNow let me look at the YAML-related functions and some example resource files:\n\nNow let me look at some concrete resource examples and tests:\n\nNow let me look at tests for create, update, delete operations and the exceptions:\n\nNow let me look at the exceptions module and the project resource:\n\nNow let me look at how `to_yaml`, `yaml_file`, and `kind_dict` usage works, and the `dry_run` option:\n\nNow let me check how `ensure_exists` and `wait_for_resource` work:\n\nNow let me look at how the `service.py` and `route.py` look to understand namespaced resources better:\n\nLet me also look at the `node_network_configuration_policy.py` for a more complex `deploy/clean_up` example:\n\nNow let me look at the `to_yaml` method and some environment variable usage:\n\nLet me also check the `dry_run` behavior and check for how `label` and `annotations` work:\n\nNow I have a thorough understanding of the codebase. Let me write the documentation.\n\n# Creating and Managing Resources\n\nDeploy Kubernetes and OpenShift resources to your cluster, update them in place, and clean them up reliably using the openshift-python-wrapper's resource lifecycle methods.\n\n## Prerequisites\n\n- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html))\n- A connected client \u2014 either a real cluster connection or a fake client for testing (see [Connecting to Clusters](connecting-to-clusters.html))\n\n## Quick Example\n\n```python\nfrom ocp_resources.namespace import Namespace\nfrom ocp_resources.resource import get_client\n\nclient = get_client()\n\n# Create, use, and automatically delete a Namespace\nwith Namespace(client=client, name=\"my-app\") as ns:\n print(f\"{ns.name} is ready\")\n# Namespace is deleted when the block exits\n```\n\n## Creating Resources\n\nEvery resource provides three ways to create it: calling `deploy()` directly, using a context manager, or loading from a YAML file.\n\n### Using `deploy()`"}, {"slug": "querying-resources", "title": "Querying and Watching Resources", "content": "Let me start by reading the knowledge graph and understanding the codebase structure.\n\nNow let me explore the key source files related to querying, listing, watching, existence checks, and status retrieval.\n\nNow let me look at the `_prepare_resources` method and the `get` method more closely to understand the filtering parameters:\n\nNow let me look at the event module and test files for querying:\n\nNow let me look at the test files for get/list/watch operations:\n\nNow let me check the conftest to understand fake_client and look at the `Condition` attribute and `Status` attribute patterns:\n\nNow let me check how `get` is called with selectors:\n\nLet me look at how `get` passes `field_selector`, `label_selector`, and `namespace` through kwargs:\n\nNow let me look at more examples of how get() is used with various kwargs, and check what other files show:\n\nLet me check how `_build_event_field_selector` and events work:\n\nNow let me check the namespace.py to see how get() is used in a simple resource:\n\nNow let me look at the node.py for a cluster-scoped resource usage of get:\n\nNow let me check the exceptions module for ConditionError:\n\nLet me also check `ensure_exists`:\n\nLet me also look for properties that relate to querying the resource (uuid, labels):\n\nNow let me check the test for events and any resource-specific tests about querying:\n\nLet me also look at the fake_client to understand what kwargs `get` passes through:\n\nNow I have a thorough understanding of the codebase. Let me write the documentation page.\n\n# Querying and Watching Resources\n\nFind, filter, and monitor Kubernetes and OpenShift resources in your cluster \u2014 whether you need to list all pods by label, check if a resource exists, watch for real-time changes, or inspect status and conditions.\n\n## Prerequisites\n\n- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html))\n- A connected `DynamicClient` instance (see [Connecting to Clusters](connecting-to-clusters.html))\n"}, {"slug": "waiting-for-conditions", "title": "Waiting for Resource Conditions and Status", "content": "Let me start by reading the knowledge graph and page manifest, then explore the relevant source code.\n\nNow let me examine the resource-specific wait methods, the `wait_for_condition` more closely, and the constants used:\n\nNow let me look at the `Status` class, `Condition` class, and how `create` integrates with `wait`:\n\nNow let me look at how Status/Condition are exposed on the base Resource class and how `deploy` / `create` uses wait:\n\nNow let me see the `deploy` method more fully and how `wait` is passed through:\n\nNow let me check the `sriov_network_node_state.py` and `virtual_machine_restore.py` for more wait patterns, and also look at the `TimeoutExpiredError` import:\n\nNow let me check the `cdi_config.py` wait method and look at the `wait_for_resource` parameter in the constructor:\n\nNow let me look at how `create` uses `wait`:\n\nNow let me look at more of the `status` property:\n\nNow I have all the information I need. Let me also check the `TimeoutSampler` import to understand the exception:\n\nLet me check if there are any virtual machine import wait patterns with more stop_status details:\n\nNow let me look at the `node_network_state.py` wait patterns:\n\nI now have comprehensive understanding of all the wait methods. Let me compile the documentation.\n\n# Waiting for Resource Conditions and Status\n\nWait for Kubernetes and OpenShift resources to reach a desired status, condition, or lifecycle state using built-in polling methods with configurable timeouts and early exit on failure.\n\n## Prerequisites\n\n- `openshift-python-wrapper` installed in your project\n- A connected client (see [Connecting to Clusters](connecting-to-clusters.html))\n- A resource instance to wait on (see [Creating and Managing Resources](creating-and-managing-resources.html))\n\n## Quick Example\n\n```python\nfrom ocp_resources.pod import Pod\n\npod = Pod(client=client, name=\"my-pod\", namespace=\"default\")\npod.create(wait=True) # Creates the pod and waits for it to exist\n\n# Wait for the pod to reach \"Running\" "}, {"slug": "validating-resources", "title": "Validating Resources Against OpenAPI Schemas", "content": "Let me start by reading the exclusions file and pages manifest, then explore the codebase.\n\nNow let me look at how the Resource base class integrates validation:\n\nNow let me check the exceptions module:\n\nNow let me check how `deploy` and context managers interact with validation:\n\nNow I have a comprehensive understanding of the validation feature. Let me write the documentation.\n\n# Validating Resources Against OpenAPI Schemas\n\nCatch configuration mistakes before they reach the Kubernetes API by validating resource definitions against OpenAPI schemas. This saves debugging time and prevents failed deployments caused by typos, missing fields, or incorrect types.\n\n## Prerequisites\n\n- openshift-python-wrapper installed (see [Installing and Creating Your First Resource](quickstart.html))\n- A connection to a cluster, or use the fake client for offline validation (see [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html))\n\n## Quick Example\n\n```python\nfrom ocp_resources.pod import Pod\n\npod = Pod(\n name=\"nginx-pod\",\n namespace=\"default\",\n containers=[{\"name\": \"nginx\", \"image\": \"nginx:latest\"}],\n)\n\n# Validate against the OpenAPI schema \u2014 raises ValidationError if invalid\npod.validate()\n```\n\nIf the resource is valid, `validate()` returns silently. If something is wrong, it raises a `ValidationError` with a clear message pointing to the problematic field.\n\n## Manual Validation\n\nCall `.validate()` on any resource instance to check it against the OpenAPI schema for its kind:\n\n```python\nfrom ocp_resources.exceptions import ValidationError\nfrom ocp_resources.service import Service\n\nservice = Service(\n name=\"nginx-service\",\n namespace=\"default\",\n selector={\"app\": \"nginx\"},\n ports=[{\"port\": 80, \"targetPort\": 80, \"protocol\": \"TCP\"}],\n)\n\ntry:\n service.validate()\n print(\"Service configuration is valid\")\nexcept ValidationError as e:\n print(f\"Validation failed: {e}\")\n```\n\n> **Tip:** Use manual validation in CI pipelines or pre-commit "}, {"slug": "managing-resource-lists", "title": "Managing Bulk Resources with ResourceList", "content": "Let me explore the codebase to understand ResourceList and NamespacedResourceList.\n\nNow let me check the example file more carefully and also look at how the `NamespacedResourceList` actually works in the example vs the source code \u2014 I noticed the example passes `namespaces=[ns.name for ns in namespaces]` (a list of strings) but the source code expects a `ResourceList` object. Let me verify.\n\nThe example file seems to have an outdated API for `NamespacedResourceList` (passing a list of strings), but the actual source code expects a `ResourceList` object. The tests confirm it passes a `ResourceList`. Let me check if the example is outdated:\n\nThe example file is inconsistent with the actual source code. The source code validates that each element in `namespaces` has `ns.kind == \"Namespace\"`, meaning it expects actual Namespace resource objects (from a ResourceList), not strings. The test confirms this by passing a `ResourceList` fixture. I'll document based on the actual source code, not the potentially outdated example.\n\nLet me also check what methods and properties are available:\n\nNow I have all the information I need. Let me write the documentation.\n\n# Managing Bulk Resources with ResourceList\n\nWhen you need to create several copies of the same resource or deploy a resource across multiple namespaces, doing it one at a time is tedious and error-prone. `ResourceList` and `NamespacedResourceList` let you create, deploy, and clean up groups of resources in a single operation.\n\n## Prerequisites\n\n- A working cluster connection (see [Connecting to Clusters](connecting-to-clusters.html))\n- Familiarity with creating individual resources (see [Creating and Managing Resources](creating-and-managing-resources.html))\n\n## Quick Example\n\nCreate three namespaces and deploy them in two lines:\n\n```python\nfrom ocp_resources.namespace import Namespace\nfrom ocp_resources.resource import ResourceList, get_client\n\nclient = get_client()\n\nnamespaces = ResourceList(resource_class=Namespace"}, {"slug": "editing-resources-temporarily", "title": "Editing Resources Temporarily with ResourceEditor", "content": "Let me start by reading the knowledge graph and understanding the repository structure.\n\nNow let me find the ResourceEditor source code.\n\nNow let me look at how ResourceEditor is used in the NNCP file and any tests:\n\nLet me look at the import section and any related constants:\n\nNow let me look at the `update` and `update_replace` methods on the Resource class to understand the two actions:\n\nNow let me look at the imports to verify how ResourceEditor is exported:\n\nNow let me check for any additional examples or documentation:\n\nLet me look at the full ResourceEditor code once more to make sure I've captured everything, and also check the `_apply_patches` method details and what `action` values are supported:\n\nNow I have a complete picture of the ResourceEditor class. Let me write the documentation.\n\n# Editing Resources Temporarily with ResourceEditor\n\nTemporarily modify Kubernetes and OpenShift resources during tests and have their original values automatically restored when you're done. This is essential for test isolation\u2014ensuring that changes made during a test don't leak into other tests or persist in the cluster.\n\n## Prerequisites\n\n- `openshift-python-wrapper` installed in your project (see [Installing and Creating Your First Resource](quickstart.html))\n- A cluster connection established with a privileged client (see [Connecting to Clusters](connecting-to-clusters.html))\n- One or more existing resources on the cluster that you want to temporarily modify\n\n> **Warning:** The client used to retrieve resources must have sufficient privileges. Do not use an unprivileged user client with `ResourceEditor`.\n\n## Quick Example\n\n```python\nfrom ocp_resources.resource import ResourceEditor\nfrom ocp_resources.node import Node\n\nnode = Node(name=\"worker-0\")\n\nwith ResourceEditor(\n patches={node: {\"metadata\": {\"labels\": {\"test-label\": \"true\"}}}}\n):\n # The node now has the label \"test-label\": \"true\"\n # Run your test logic here...\n pass\n\n# The label is automatically remo"}, {"slug": "working-with-virtual-machines", "title": "Working with Virtual Machines (KubeVirt)", "content": "Let me start by exploring the knowledge graph and understanding the repository structure.\n\nNow let me look at the remaining VM-related resources and check the `RunStrategy` and `Status` constants, as well as the `data_volume.py`:\n\nNow let me check how the `api_request` method works in the base `Resource` class:\n\nNow let me look at the `data_volume.py` to check any VM-related interactions:\n\nNow let me check the `RunStrategy` constants in VirtualMachineInstance:\n\nNow let me look at the `Status` class in the base `Resource` for context and the VMI's `RunStrategy`:\n\nLet me find the Status class definition:\n\nNow let me check the `ApiGroup` constants for KubeVirt:\n\nNow let me check for any tests related to VMs:\n\nNow let me check the `deploy` and `clean_up` method signatures:\n\nNow I have a thorough understanding of the VM-related API. Let me write the documentation:\n\n# Working with Virtual Machines (KubeVirt)\n\nManage the full lifecycle of KubeVirt virtual machines on OpenShift \u2014 create VMs, start and stop them, live-migrate between nodes, take snapshots, and interact with their running instances \u2014 all from Python.\n\n## Prerequisites\n\n- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html))\n- A cluster connection configured (see [Connecting to Clusters](connecting-to-clusters.html))\n- KubeVirt / OpenShift Virtualization installed on your cluster\n\n## Quick Example\n\n```python\nfrom ocp_resources.virtual_machine import VirtualMachine\n\nvm = VirtualMachine(\n name=\"my-vm\",\n namespace=\"my-namespace\",\n body={\n \"spec\": {\n \"domain\": {\n \"devices\": {\"disks\": [{\"disk\": {\"bus\": \"virtio\"}, \"name\": \"rootdisk\"}]},\n \"resources\": {\"requests\": {\"memory\": \"1Gi\"}},\n },\n \"volumes\": [{\"name\": \"rootdisk\", \"containerDisk\": {\"image\": \"quay.io/containerdisks/fedora\"}}],\n }\n },\n)\n\nwith vm:\n vm.start(wait=True)\n print(f\"VM ready: {vm.ready}\") # True\n pr"}, {"slug": "testing-without-cluster", "title": "Testing Without a Cluster Using the Fake Client", "content": "Let me start by exploring the repository structure and the knowledge graph.\n\nNow let me look at the test files for the fake client:\n\nLet me look at a few more test files to understand patterns used:\n\nNow let me look at how `get_client` works with `fake=True` and how resource classes use the client:\n\nNow let me look at a few more test examples to understand patterns:\n\nNow let me check how resources use the fake client and how `deploy`, `clean_up`, `update` work:\n\nLet me look at additional test files for more patterns - ConfigMap, events, and the configmap resource:\n\nNow let me check how watch and events are used, and how to use the lower-level API:\n\nNow let me check the `events` and `watch` implementation in the Resource class:\n\nNow let me look at a few more resource test files and the low-level API usage:\n\nLet me check for import paths and the `ocp_resources.resource` module to understand the public API:\n\nLet me check how the `wait_for_condition` and other resource methods work with the fake client:\n\nNow I have enough information to write the documentation page. Let me check a couple more things:\n\nNow I have all the information I need. Let me write the documentation page.\n\n# Testing Without a Cluster Using the Fake Client\n\nWrite unit tests that create, read, update, and delete Kubernetes resources \u2014 including status checks, watch events, and custom CRDs \u2014 without connecting to a real cluster.\n\n## Prerequisites\n\n- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html))\n- `pytest` installed for running tests\n\n## Quick Example\n\n```python\nimport pytest\nfrom ocp_resources.resource import get_client\nfrom ocp_resources.namespace import Namespace\n\n@pytest.fixture(scope=\"class\")\ndef fake_client():\n return get_client(fake=True)\n\nclass TestMyFeature:\n def test_create_namespace(self, fake_client):\n ns = Namespace(client=fake_client, name=\"my-namespace\")\n ns.deploy()\n\n assert ns.exists\n assert ns"}, {"slug": "generating-resource-classes", "title": "Generating New Resource Classes with class-generator", "content": "Let me start by exploring the repository structure and the knowledge graph.\n\nNow let me check the generated output example and the test generation flow:\n\nNow let me look at a generated file with a spec section, and check one with duplicate kinds:\n\nLet me also check the `--help` output structure and look at a few more details:\n\nNow let me check a dry-run example and verify what shell completion setup looks like:\n\nNow I have a thorough understanding of the codebase. Let me write the documentation:\n\n# Generating New Resource Classes with class-generator\n\nScaffold Python wrapper classes for any Kubernetes or OpenShift custom resource definition (CRD) so you can manage those resources using the same patterns as built-in resources. The `class-generator` CLI introspects your cluster's OpenAPI schemas and produces ready-to-use Python files complete with typed constructors, docstrings, and `to_dict()` serialization.\n\n## Prerequisites\n\n- **openshift-python-wrapper** installed (see [Installing and Creating Your First Resource](quickstart.html))\n- `oc` or `kubectl` CLI available on your `PATH`\n- An active connection to a Kubernetes or OpenShift cluster with admin privileges (for `--kind` generation and schema updates)\n\n## Quick Example\n\nGenerate a wrapper class for the `Deployment` resource:\n\n```bash\nclass-generator -k Deployment\n```\n\nThis creates a Python file in `ocp_resources/` with a fully typed class you can import and use immediately:\n\n```python\nfrom ocp_resources.deployment import Deployment\n\ndep = Deployment(\n name=\"my-app\",\n namespace=\"default\",\n replicas=3,\n selector={\"matchLabels\": {\"app\": \"my-app\"}},\n template={...},\n)\ndep.deploy()\n```\n\n## Step-by-Step: Generating a Resource Class\n\n### 1. Generate the class file\n\nPass the CRD's `Kind` (case-sensitive) to `--kind` / `-k`:\n\n```bash\nclass-generator -k StorageCluster\n```\n\nThe output file is automatically named using snake_case \u2014 `ocp_resources/storage_cluster.py`.\n\n### 2. Preview without writing (dry run)"}, {"slug": "common-patterns", "title": "Common Resource Patterns", "content": "Let me start by reading the knowledge graph and the pages manifest, then explore the codebase.\n\nNow let me look at more resource files for networking, RBAC, and DataVolume:\n\nNow let me look at the secret, network policy, network attachment definition, and the duplicate API groups handling:\n\nNow let me look at the duplicate API groups handling:\n\nNow let me look at some files with duplicate kinds to understand the pattern:\n\nNow let me look at how to exec in pods and also look at the user_defined_network and apply_yaml approach:\n\nNow let me look at how the `yaml_file` parameter works and look at the event-related code:\n\nNow let me check some gateway files to see the pattern for duplicate API groups:\n\nNow let me look at the `user_defined_network.py` for newer networking resources:\n\nNow I have enough information. Let me write the documentation page:\n\n# Common Resource Patterns\n\nCopy-paste recipes for the most common Kubernetes and OpenShift resource operations using openshift-python-wrapper.\n\n> **Note:** All recipes assume you already have a connected client. See [Connecting to Clusters](connecting-to-clusters.html) for setup instructions.\n\n```python\n# Preamble used by all recipes below\nfrom ocp_resources.resource import get_client\nclient = get_client()\n```\n\n---\n\n## Create and Run a Pod\n\nCreate a pod with a single container and wait for it to start.\n\n```python\nfrom ocp_resources.pod import Pod\n\npod = Pod(\n client=client,\n name=\"my-nginx\",\n namespace=\"default\",\n containers=[{\n \"name\": \"nginx\",\n \"image\": \"nginx:1.25\",\n \"ports\": [{\"containerPort\": 80}],\n }],\n restart_policy=\"Never\",\n)\npod.deploy()\npod.wait_for_status(status=Pod.Status.RUNNING, timeout=120)\n```\n\nThe `deploy()` method creates the pod on the cluster. `wait_for_status` polls until the pod reaches `Running`. Use `restart_policy=\"Always\"` for long-running pods.\n\n---\n\n## Create a Pod as a Context Manager\n\nAutomatically clean up a pod when the block exits \u2014 ideal for tests.\n\n`"}, {"slug": "mcp-server-integration", "title": "Integrating with AI Assistants via MCP Server", "content": "# Integrating with AI Assistants via MCP Server\n\nThe openshift-python-wrapper ships with a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that lets AI assistants like Cursor, Claude Desktop, and other MCP-compatible tools directly manage your OpenShift/Kubernetes cluster resources.\n\n## Configure the MCP Server for Cursor\n\nConnect your Cursor IDE to your cluster so the AI assistant can list, create, update, and delete resources on your behalf.\n\n```json\n// ~/.cursor/mcp.json\n{\n \"mcpServers\": {\n \"openshift-python-wrapper\": {\n \"command\": \"openshift-mcp-server\"\n }\n }\n}\n```\n\nAfter saving, restart Cursor and reference the server with `@openshift-python-wrapper` in your prompts. The server uses your active kubeconfig context automatically.\n\n> **Note:** The `openshift-mcp-server` command is installed as a script entry point when you install the package. See [Installing and Creating Your First Resource](quickstart.html) for installation instructions.\n\n## Configure the MCP Server for Claude Desktop\n\nSet up Claude Desktop to interact with your cluster through natural language.\n\n```json\n// macOS: ~/Library/Application Support/Claude/claude_desktop_config.json\n// Windows: %APPDATA%\\Claude\\claude_desktop_config.json\n// Linux: ~/.config/Claude/claude_desktop_config.json\n{\n \"mcpServers\": {\n \"openshift-python-wrapper\": {\n \"command\": \"openshift-mcp-server\"\n }\n }\n}\n```\n\nClaude Desktop will connect to the server on startup. You can then ask it to list pods, create resources, check logs, and more using natural language.\n\n## Configure a Custom Kubeconfig Path\n\nPoint the MCP server at a specific kubeconfig when your default location doesn't apply.\n\n```json\n{\n \"mcpServers\": {\n \"openshift-python-wrapper\": {\n \"command\": \"openshift-mcp-server\",\n \"env\": {\n \"KUBECONFIG\": \"/home/user/.kube/my-cluster-config\"\n }\n }\n }\n}\n```\n\nThe `KUBECONFIG` environment variable is passed to the server process at startup. Thi"}, {"slug": "resource-api", "title": "Resource and NamespacedResource API", "content": "Let me start by reading the knowledge graph and the pages manifest, then explore the source code.\n\nNow let me examine the constants and exceptions:\n\nNow let me check the `apply_yaml` function and the `get_client` function signature, plus see what `ResourceConstants` inherits:\n\nLet me look for key public methods in resource.py that I might have missed (the file was truncated before):\n\nLet me also check for import paths:\n\nNow let me look at the `NamespacedResource._base_body` more carefully and check for anything more:\n\nNow let me check what the `Event` class looks like for the events method reference:\n\nLet me also check for `generate_yaml_from_template`:\n\nThat's a test utility. Let me check for the `skip_existing_resource_creation_teardown` function:\n\nNow I have all the information needed. Let me write the comprehensive API reference page.\n\n# Resource and NamespacedResource API\n\nComplete API reference for the `Resource` and `NamespacedResource` base classes, the `get_client` factory function, and supporting classes (`ResourceEditor`, `ResourceList`, `NamespacedResourceList`, `KubeAPIVersion`).\n\n**Import path:**\n\n```python\nfrom ocp_resources.resource import (\n Resource,\n NamespacedResource,\n ResourceEditor,\n ResourceList,\n NamespacedResourceList,\n get_client,\n)\n```\n\n---\n\n## `get_client`\n\nFactory function to obtain a Kubernetes `DynamicClient`.\n\n```python\nfrom ocp_resources.resource import get_client\n\nclient = get_client(config_file=\"~/.kube/config\")\n```\n\n| Parameter | Type | Default | Description |\n|---|---|---|---|\n| `config_file` | `str \\| None` | `None` | Path to a kubeconfig file. Falls back to `KUBECONFIG` env var or `~/.kube/config`. |\n| `config_dict` | `dict[str, Any] \\| None` | `None` | Dict with kubeconfig configuration. Mutually exclusive with `config_file`. |\n| `context` | `str \\| None` | `None` | Name of the kubeconfig context to use. |\n| `client_configuration` | `kubernetes.client.Configuration \\| None` | `None` | Pre-built Kubernetes cl"}, {"slug": "class-generator-cli", "title": "class-generator CLI Reference", "content": "I'll start by reading the knowledge graph and page manifest, then explore the class-generator source code in detail.\n\nNow let me check the entry point configuration and any additional schema-related functions:\n\nNow let me check the test files for CLI tests and the entry point:\n\nNow let me check the parsers and any additional CLI-relevant components:\n\nNow I have all the information I need. Let me compile the comprehensive CLI reference page.\n\n# class-generator CLI Reference\n\nThe `class-generator` command generates Python wrapper classes for Kubernetes and OpenShift resources. It is installed as a console script entry point from the `openshift-python-wrapper` package.\n\n```\nclass-generator = \"class_generator.cli:main\"\n```\n\n```bash\npip install openshift-python-wrapper\n# or\nuv tool install openshift-python-wrapper\n```\n\n> **Tip:** Enable shell completion by adding to `~/.bashrc` or `~/.zshrc`:\n> ```bash\n> if type class-generator > /dev/null; then eval \"$(_CLASS_GENERATOR_COMPLETE=zsh_source class-generator)\"; fi\n> ```\n\n## Synopsis\n\n```\nclass-generator [OPTIONS]\n```\n\nAt least one action option must be specified: `--kind`, `--update-schema`, `--update-schema-for`, `--discover-missing`, `--coverage-report`, `--generate-missing`, or `--regenerate-all`.\n\n---\n\n## Options Reference\n\n### Kind Generation\n\n#### `-k`, `--kind`\n\n| Property | Value |\n|----------|-------|\n| **Type** | `STRING` |\n| **Default** | `None` |\n| **Required** | No (but at least one action must be specified) |\n| **Requires** | Connected cluster with admin privileges |\n\nGenerate Python wrapper classes for the specified Kubernetes resource Kind(s). Multiple kinds can be comma-separated (no spaces).\n\nWhen a kind is not found in the local schema mapping file, the CLI interactively prompts to run `--update-schema` (only in interactive CLI mode).\n\n```bash\n# Single kind\nclass-generator -k Pod\n\n# Multiple kinds (processed in parallel)\nclass-generator -k Deployment,Pod,ConfigMap\n```\n\nWhen multiple kinds share the same K"}, {"slug": "environment-variables", "title": "Environment Variables and Configuration", "content": "Let me start by reading the knowledge graph and page manifest, then explore the codebase for environment variables.\n\nNow let me look at the `keys_to_hash` and hash-related functionality more closely:\n\nNow let me look at the `get_logger` import and how log levels work:\n\nNow let me check for any CI/CD or config-related environment variables:\n\nLet me also look for any validation-related environment variables and enable_validation_by_default:\n\nNow let me look at the `get_logger` usage and the `simple_logger` format:\n\nNow let me check the deploy() and clean_up() methods more carefully, and also the hash_resource_dict method:\n\nLet me also check how `replace_key_with_hashed_value` is defined:\n\nNow let me check the MCP server for environment variables:\n\nNow let me check if there are any other environment variable usages in the class_generator:\n\nLet me verify there are no other env vars in tests:\n\nLet me also check the full `get_client` function and the `save_kubeconfig` function:\n\nNow let me look for any additional environment variables in fake_kubernetes_client:\n\nLet me look at how the deploy and clean_up methods use the skip logic:\n\nNow I have all the information needed. Let me write the documentation:\n\n# Environment Variables and Configuration\n\nThis page documents all environment variables recognized by `openshift-python-wrapper` at runtime. These variables control logging behavior, resource lifecycle operations, proxy configuration, cluster connectivity, and sensitive data hashing.\n\n## Quick Reference\n\n| Environment Variable | Purpose | Default |\n|---|---|---|\n| `KUBECONFIG` | Path to kubeconfig file | `~/.kube/config` |\n| `HTTPS_PROXY` | HTTPS proxy URL for cluster connections | _(unset)_ |\n| `HTTP_PROXY` | HTTP proxy URL for cluster connections | _(unset)_ |\n| `OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL` | Logging verbosity level | `INFO` |\n| `OPENSHIFT_PYTHON_WRAPPER_LOG_FILE` | Path to log output file | `\"\"` (stdout) |\n| `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` | Enable/dis"}, {"slug": "resource-class-hierarchy", "title": "Understanding the Resource Class Hierarchy", "content": "Let me start by reading the exclusions file and pages manifest, then explore the codebase thoroughly.\n\nNow let me look at some generated resource subclasses to understand how they extend the base classes:\n\nNow let me look at some more examples - a cluster-scoped resource, the `ResourceConstants`, the class generator, and the `virtual_machine.py` for a more complex subclass:\n\nNow let me look at the `kind` property and how it's derived, plus some other resources for variety:\n\nNow let me look at how the `kind` property works (the `sub_resource_level` function) and the `conversion.py` for the kind name derivation:\n\nNow let me look at how resources discover their API version dynamically:\n\nLet me now look at one more important aspect - how the `get` method works differently in the two base classes, and how `_base_body` differs:\n\nNow let me check the `__init__.py` to see what's exported:\n\nLet me also check for examples:\n\nNow I have enough context to write the documentation. Let me draft it:\n\n# Understanding the Resource Class Hierarchy\n\nEvery Kubernetes and OpenShift object you interact with through openshift-python-wrapper \u2014 whether it's a Pod, a Namespace, or a VirtualMachine \u2014 is a Python class that inherits from a small set of base classes. Understanding how these classes relate to each other helps you know **what capabilities any resource has**, **why some resources require a `namespace` argument and others don't**, and **how new resource types get added to the library**.\n\nThis page explains the architecture so you can use existing resources effectively and extend the library when you need to work with a new custom resource.\n\n---\n\n## The Big Picture\n\nThe class hierarchy has three conceptual layers:\n\n| Layer | Classes | Purpose |\n|---|---|---|\n| **Constants** | `ResourceConstants` | Provides shared status strings, condition names, and type constants used across all resources |\n| **Base classes** | `Resource`, `NamespacedResource` | Define all CRUD operations, lifecycle"}, {"slug": "schema-validation-internals", "title": "Schema Validation and Code Generation Architecture", "content": "Let me start by reading the knowledge graph and the pages manifest, then explore the relevant source files.\n\nNow let me look at some more key files - the explain parser, resource base class validation, archive utils, and some test files:\n\nNow let me look at the exceptions module for validation:\n\nNow let me check the CI/CD configuration to understand how schemas are updated:\n\nLet me look at a few generated resource files to understand the output of the code generation:\n\nLet me also check the parsers for status schema generation:\n\nNow let me check what data is actually in the schema file to understand the structure:\n\nNow I have enough information. Let me write the documentation page.\n\n# Schema Validation and Code Generation Architecture\n\nThe openshift-python-wrapper uses OpenAPI schemas as the single source of truth for two critical capabilities: **validating resource definitions** before they reach the cluster, and **generating Python wrapper classes** for any Kubernetes or OpenShift resource type. Understanding this architecture helps you diagnose validation errors, generate classes for custom resources, and keep your schemas up to date as clusters evolve.\n\n## The Big Picture\n\nOpenAPI schemas flow through the system in two directions: they are fetched from a live cluster and stored locally, then consumed by the validation engine at runtime and by the code generator at development time.\n\n| Component | Location | Purpose |\n|---|---|---|\n| **Schema Fetcher** | `class_generator/core/schema.py` | Connects to a cluster, downloads OpenAPI v3 schemas, and writes them to disk |\n| **Resource Mappings** | `class_generator/schema/__resources-mappings.json.gz` | Compressed archive mapping each resource kind (lowercase) to its schema(s) |\n| **Definitions File** | `class_generator/schema/_definitions.json` | JSON file containing detailed schema definitions with `$ref` targets for nested types |\n| **Schema Validator** | `ocp_resources/utils/schema_validator.py` | Runtime engine tha"}, {"slug": "working-with-events", "title": "Working with Kubernetes Events", "content": "Let me start by exploring the repository structure and the knowledge graph.\n\nNow let me explore the Event-related source code:\n\nNow let me look at the `_build_event_field_selector` function and related event test files:\n\nNow let me check the `events` test files in the tests directory:\n\nNow let me check the resource.py import and the `events` method more carefully, and also look at how TIMEOUT_4MINUTES is defined:\n\nLet me check where TIMEOUT_4MINUTES is defined in resource.py:\n\nNow let me check the constants value for TIMEOUT_4MINUTES:\n\nNow let me check how the Event class is used in the fake client:\n\nNow let me look at the `_extract_event_info` and the `_process_watch_events` functions to understand the event data structure better:\n\nNow let me look at the event object structure from watch events:\n\nNow let me check how to properly import and use Event:\n\nLet me also look at the test file for events in more detail:\n\nNow let me check the watch method's return structure for events:\n\nNow I have a thorough understanding. Let me write the documentation.\n\n# Working with Kubernetes Events\n\nMonitor, retrieve, and clean up Kubernetes events to debug resource behavior and automate event-driven workflows using the `Event` class and the `resource.events()` method.\n\n## Prerequisites\n\n- A connected Kubernetes/OpenShift client \u2014 see [Connecting to Clusters](connecting-to-clusters.html)\n- The `ocp_resources` package installed \u2014 see [Installing and Creating Your First Resource](quickstart.html)\n\n## Quick Example\n\nStream events from a namespace in real-time:\n\n```python\nfrom ocp_resources.event import Event\n\nfor event in Event.get(client=client, namespace=\"my-namespace\", timeout=30):\n print(f\"{event['type']}: {event.object.reason} \u2014 {event.object.message}\")\n```\n\n## Streaming Events in Real-Time with `Event.get()`\n\n`Event.get()` opens a watch connection and yields events as they occur. It's a generator that blocks until the timeout expires or you break out of the loop.\n\n```python\nfrom "}, {"slug": "pod-execution-and-logs", "title": "Executing Commands in Pods and Retrieving Logs", "content": "Let me explore the repository structure and understand the codebase first.\n\nNow let me check the `read_namespaced_pod_log` kwargs available, and look at how the MCP server uses log parameters:\n\nNow let me check the import paths and verify the `get_client` function:\n\nNow let me check test files for pod execution tests:\n\nNow let me also verify whether the Node class has a `name` property:\n\nNow I have all the information I need to write the documentation. Let me compose it:\n\n# Executing Commands in Pods and Retrieving Logs\n\nRun commands inside running pods and retrieve their logs to debug and interact with workloads on your OpenShift or Kubernetes cluster.\n\n## Prerequisites\n\n- A connected cluster client (see [Connecting to Clusters](connecting-to-clusters.html))\n- An existing, running pod you want to interact with\n\n## Quick Example\n\n```python\nfrom ocp_resources.pod import Pod\nfrom ocp_resources.resource import get_client\n\nclient = get_client()\npod = Pod(client=client, name=\"my-app\", namespace=\"default\")\n\n# Run a command inside the pod\noutput = pod.execute(command=[\"echo\", \"hello\"])\nprint(output) # \"hello\\n\"\n\n# Get pod logs\nlogs = pod.log()\nprint(logs)\n```\n\n## Executing Commands with `Pod.execute()`\n\n`Pod.execute()` runs a command inside a pod container using the Kubernetes exec API and returns the standard output as a string.\n\n### Method Signature\n\n```python\nPod.execute(\n command: list[str],\n timeout: int = 60,\n container: str = \"\",\n ignore_rc: bool = False,\n) -> str\n```\n\n| Parameter | Type | Default | Description |\n|-------------|--------------|---------|---------------------------------------------------------------------------------------------|\n| `command` | `list[str]` | \u2014 | The command to run, as a list of strings (e.g., `[\"ls\", \"-la\", \"/tmp\"]`) |\n| `timeout` | `int` | `60` | Maximum seconds to wait for the command to comp"}, {"slug": "error-handling", "title": "Error Handling and Exception Reference", "content": "Let me explore the repository structure and find all relevant exception definitions and usages.\n\nNow let me look at the `ConditionError` usage more closely and the `MissingTemplateVariables` and `MissingResourceError` exceptions:\n\nNow let me look at how the deploy method uses ValidationError:\n\nNow let me see where `validate` is called during `create`:\n\nNow let me look at the `Pod.execute` method more closely:\n\nNow let me look at how `client_with_basic_auth` works:\n\nNow let me see the NNCP methods in full context:\n\nLet me also look at `MissingTemplateVariables` usage more closely:\n\nNow let me check the `__init__` of Resource to see how `schema_validation_enabled` is handled:\n\nNow let me also check the `wait_for_condition` method signature more closely:\n\nLet me also look at `MissingResourceResError` usage:\n\nNow let me check the test for `ResourceTeardownError`:\n\nLet me see how SecretTestExit is defined:\n\nLet me check the import path for all exceptions:\n\nNow I have a thorough understanding of the codebase. Let me write the reference documentation.\n\n# Error Handling and Exception Reference\n\nAll custom exceptions are defined in `ocp_resources.exceptions` and can be imported directly:\n\n```python\nfrom ocp_resources.exceptions import (\n ExecOnPodError,\n MissingRequiredArgumentError,\n ResourceTeardownError,\n ValidationError,\n ConditionError,\n NNCPConfigurationFailed,\n ClientWithBasicAuthError,\n MissingResourceError,\n MissingTemplateVariables,\n)\n```\n\n> **Tip:** All exceptions inherit from Python's built-in `Exception` class and can be caught with a bare `except Exception` if needed.\n\n---\n\n## ExecOnPodError\n\nRaised when a command executed inside a pod via `Pod.execute()` fails. See [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) for full usage details.\n\n**Import:**\n\n```python\nfrom ocp_resources.exceptions import ExecOnPodError\n```\n\n**Constructor Parameters:**\n\n| Parameter | Type | Description |\n|-----------|------|------"}] diff --git a/docs/testing-without-cluster.html b/docs/testing-without-cluster.html new file mode 100644 index 0000000000..9f47a53186 --- /dev/null +++ b/docs/testing-without-cluster.html @@ -0,0 +1,915 @@ + + + + + + Testing Without a Cluster Using the Fake Client - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by exploring the repository structure and the knowledge graph.

+

Now let me look at the test files for the fake client:

+

Let me look at a few more test files to understand patterns used:

+

Now let me look at how get_client works with fake=True and how resource classes use the client:

+

Now let me look at a few more test examples to understand patterns:

+

Now let me check how resources use the fake client and how deploy, clean_up, update work:

+

Let me look at additional test files for more patterns - ConfigMap, events, and the configmap resource:

+

Now let me check how watch and events are used, and how to use the lower-level API:

+

Now let me check the events and watch implementation in the Resource class:

+

Now let me look at a few more resource test files and the low-level API usage:

+

Let me check for import paths and the ocp_resources.resource module to understand the public API:

+

Let me check how the wait_for_condition and other resource methods work with the fake client:

+

Now I have enough information to write the documentation page. Let me check a couple more things:

+

Now I have all the information I need. Let me write the documentation page.

+

Testing Without a Cluster Using the Fake Client

+

Write unit tests that create, read, update, and delete Kubernetes resources — including status checks, watch events, and custom CRDs — without connecting to a real cluster.

+

Prerequisites

+ +

Quick Example

+
import pytest
+from ocp_resources.resource import get_client
+from ocp_resources.namespace import Namespace
+
+@pytest.fixture(scope="class")
+def fake_client():
+    return get_client(fake=True)
+
+class TestMyFeature:
+    def test_create_namespace(self, fake_client):
+        ns = Namespace(client=fake_client, name="my-namespace")
+        ns.deploy()
+
+        assert ns.exists
+        assert ns.kind == "Namespace"
+        assert ns.status == Namespace.Status.ACTIVE
+
+        ns.clean_up(wait=False)
+        assert not ns.exists
+
+ +

Run it with pytest — no cluster, no kubeconfig, no network calls.

+

How It Works

+

Calling get_client(fake=True) returns an in-memory client that simulates the Kubernetes API. It supports all standard resources (Pods, Deployments, Services, Namespaces, etc.) and OpenShift resources (Routes, Projects, etc.) out of the box.

+

All resource classes (Pod, Deployment, Namespace, etc.) accept this fake client through their client parameter, and all CRUD methods work identically to a real cluster.

+

Step-by-Step: CRUD Operations

+

1. Set up a shared fixture

+

Create a conftest.py in your test directory:

+
import pytest
+from ocp_resources.resource import get_client
+
+@pytest.fixture(scope="class")
+def fake_client():
+    """Provides a fake Kubernetes client for all tests."""
+    return get_client(fake=True)
+
+ +
+

Tip: Use scope="class" so resources created in one test method persist for subsequent methods in the same class.

+
+

2. Create a resource

+
from ocp_resources.pod import Pod
+
+def test_create_pod(fake_client):
+    pod = Pod(
+        client=fake_client,
+        name="my-pod",
+        namespace="default",
+        containers=[{"name": "nginx", "image": "nginx:latest"}],
+    )
+    result = pod.deploy()
+
+    assert result.name == "my-pod"
+    assert pod.exists
+
+ +

The fake client automatically generates metadata (uid, resourceVersion, creationTimestamp) and a realistic status section for the resource.

+

3. Read and query resources

+
from ocp_resources.namespace import Namespace
+
+def test_list_resources(fake_client):
+    # Create some namespaces first
+    Namespace(client=fake_client, name="ns-a").deploy()
+    Namespace(client=fake_client, name="ns-b").deploy()
+
+    # Iterate over all Namespace resources
+    for ns in Namespace.get(client=fake_client):
+        assert ns.name
+
+ +

Access individual resource data via the instance property:

+
def test_get_instance(fake_client):
+    ns = Namespace(client=fake_client, name="test-ns")
+    ns.deploy()
+
+    assert ns.instance
+    assert ns.kind == "Namespace"
+
+ +

4. Update a resource

+

Use update() to patch a resource or update_replace() to replace it entirely:

+
def test_update_labels(fake_client):
+    ns = Namespace(client=fake_client, name="labeled-ns")
+    ns.deploy()
+
+    # Patch: add a label
+    resource_dict = ns.instance.to_dict()
+    resource_dict["metadata"]["labels"]["env"] = "staging"
+    ns.update(resource_dict=resource_dict)
+
+    assert ns.labels["env"] == "staging"
+
+ +
def test_replace_labels(fake_client):
+    ns = Namespace(client=fake_client, name="replace-ns")
+    ns.deploy()
+
+    resource_dict = ns.instance.to_dict()
+    resource_dict["metadata"]["labels"] = {"new-label": "value"}
+    ns.update_replace(resource_dict=resource_dict)
+
+    assert "new-label" in ns.labels.keys()
+
+ +

5. Delete a resource

+
def test_delete_resource(fake_client):
+    ns = Namespace(client=fake_client, name="temp-ns")
+    ns.deploy()
+    assert ns.exists
+
+    ns.clean_up(wait=False)
+    assert not ns.exists
+
+ +

6. Use context managers for automatic cleanup

+

Resources support Python's with statement. The resource deploys on entry and is cleaned up on exit:

+
from ocp_resources.secret import Secret
+
+def test_context_manager(fake_client):
+    with Secret(name="my-secret", namespace="default", client=fake_client) as sec:
+        assert sec.exists
+
+    # Automatically cleaned up
+    assert not sec.exists
+
+ +

Testing Status and Conditions

+

The fake client generates realistic status objects so you can test code that reads resource status.

+

Checking resource status

+
def test_namespace_status(fake_client):
+    ns = Namespace(client=fake_client, name="status-ns")
+    ns.deploy()
+
+    assert ns.status == Namespace.Status.ACTIVE
+
+ +

Waiting for conditions

+
from ocp_resources.pod import Pod
+
+def test_wait_for_condition(fake_client):
+    pod = Pod(
+        client=fake_client,
+        name="ready-pod",
+        namespace="default",
+        containers=[{"name": "app", "image": "nginx:latest"}],
+    )
+    pod.deploy()
+
+    pod.wait_for_status(status=Pod.Status.RUNNING, timeout=5)
+
+ +

Simulating "not ready" resources

+

Control a resource's readiness state with the fake-client.io/ready annotation:

+
def test_not_ready_pod(fake_client):
+    pod = Pod(
+        client=fake_client,
+        name="failing-pod",
+        namespace="default",
+        containers=[{"name": "app", "image": "nginx:latest"}],
+        annotations={"fake-client.io/ready": "false"},
+    )
+    pod.deploy()
+
+    # The pod will have Ready condition set to False
+    pod.wait_for_condition(
+        condition=pod.Condition.READY,
+        status=pod.Condition.Status.FALSE,
+        timeout=5,
+    )
+
+    message = pod.get_condition_message(
+        condition_type=pod.Condition.READY,
+        condition_status=pod.Condition.Status.FALSE,
+    )
+    assert message
+
+ +

Resource-specific status behavior when marked "not ready":

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ResourceReady behaviorNot-ready behavior
Podphase: Running, containers readyContainers in waiting state
DeploymentreadyReplicas matches spec.replicasreadyReplicas: 0, unavailableReplicas set
Namespacephase: Activephase: Terminating
All othersReady condition TrueReady condition False
+

Testing with Events

+

The fake client automatically generates Kubernetes events for create, update, and delete operations. You can query them through the resource's events() method:

+
def test_resource_events(fake_client):
+    pod = Pod(
+        client=fake_client,
+        name="event-pod",
+        namespace="default",
+        containers=[{"name": "nginx", "image": "nginx:latest"}],
+    )
+    pod.deploy()
+
+    events = list(pod.events(timeout=1))
+    assert events
+
+ +

Testing with Deployments

+

Deployments get a full status template including replica counts, conditions, and observed generation:

+
from ocp_resources.deployment import Deployment
+
+def test_deployment(fake_client):
+    dep = Deployment(
+        client=fake_client,
+        name="my-deploy",
+        namespace="default",
+        replicas=3,
+        selector={"matchLabels": {"app": "web"}},
+        template={
+            "metadata": {"labels": {"app": "web"}},
+            "spec": {"containers": [{"name": "web", "image": "nginx:latest"}]},
+        },
+    )
+    dep.deploy()
+
+    assert dep.exists
+    assert dep.kind == "Deployment"
+
+    dep.clean_up(wait=False)
+    assert not dep.exists
+
+ +

Testing with ResourceList

+

Test bulk resource creation with ResourceList and NamespacedResourceList:

+
from ocp_resources.resource import ResourceList, NamespacedResourceList
+
+def test_resource_list(fake_client):
+    with ResourceList(
+        client=fake_client,
+        resource_class=Namespace,
+        name="batch-ns",
+        num_resources=3,
+    ) as namespaces:
+        assert len(namespaces) == 3
+    # All three namespaces are automatically cleaned up
+
+ +

See Managing Bulk Resources with ResourceList for more details.

+

Advanced Usage

+

Using the low-level API directly

+

You can bypass resource classes and work with the fake client's API directly — useful for testing custom logic:

+
def test_low_level_api(fake_client):
+    api = fake_client.resources.get(api_version="v1", kind="Pod")
+
+    # Create
+    pod = api.create(
+        body={
+            "apiVersion": "v1",
+            "kind": "Pod",
+            "metadata": {"name": "raw-pod", "namespace": "default"},
+            "spec": {"containers": [{"name": "app", "image": "nginx:latest"}]},
+        },
+        namespace="default",
+    )
+    assert pod.metadata.name == "raw-pod"
+    assert pod.status.phase == "Running"
+
+    # List
+    pods = api.get(namespace="default")
+    assert len(pods.items) >= 1
+
+    # Patch
+    updated = api.patch(
+        name="raw-pod",
+        namespace="default",
+        body={"metadata": {"labels": {"env": "test"}}},
+    )
+    assert updated.metadata.labels["env"] == "test"
+
+    # Delete
+    api.delete(name="raw-pod", namespace="default")
+
+ +

Label and field selectors

+

The fake client supports filtering resources by labels and fields:

+
def test_label_selector(fake_client):
+    api = fake_client.resources.get(api_version="v1", kind="Pod")
+
+    api.create(body={
+        "apiVersion": "v1", "kind": "Pod",
+        "metadata": {"name": "pod-a", "namespace": "default", "labels": {"app": "web"}},
+        "spec": {"containers": [{"name": "c", "image": "nginx"}]},
+    }, namespace="default")
+
+    api.create(body={
+        "apiVersion": "v1", "kind": "Pod",
+        "metadata": {"name": "pod-b", "namespace": "default", "labels": {"app": "api"}},
+        "spec": {"containers": [{"name": "c", "image": "nginx"}]},
+    }, namespace="default")
+
+    # Filter by label
+    web_pods = api.get(namespace="default", label_selector="app=web")
+    assert len(web_pods.items) == 1
+    assert web_pods.items[0].metadata.name == "pod-a"
+
+ +

Field selectors support metadata.name and metadata.namespace:

+
def test_field_selector(fake_client):
+    api = fake_client.resources.get(api_version="v1", kind="Pod")
+    result = api.get(namespace="default", field_selector="metadata.name=my-pod")
+
+ +

Watch for resource changes

+

The watch() method yields existing resources as ADDED events:

+
def test_watch(fake_client):
+    api = fake_client.resources.get(api_version="v1", kind="Pod")
+
+    api.create(body={
+        "apiVersion": "v1", "kind": "Pod",
+        "metadata": {"name": "watch-pod", "namespace": "default"},
+        "spec": {"containers": [{"name": "c", "image": "nginx"}]},
+    }, namespace="default")
+
+    for event in api.watch(namespace="default"):
+        assert event["type"] == "ADDED"
+        assert event["object"].metadata.name
+        break  # Stop after first event
+
+ +

Registering Custom Resource Definitions

+

To test with CRDs not bundled in the default schema, register them on the client:

+
def test_custom_resource(fake_client):
+    # Register the CRD
+    fake_client.register_resources({
+        "kind": "MyApp",
+        "api_version": "v1alpha1",
+        "group": "apps.example.com",
+        "namespaced": True,
+    })
+
+    # Use it through the low-level API
+    api = fake_client.resources.get(
+        api_version="apps.example.com/v1alpha1",
+        kind="MyApp",
+    )
+
+    created = api.create(
+        body={
+            "apiVersion": "apps.example.com/v1alpha1",
+            "kind": "MyApp",
+            "metadata": {"name": "my-app", "namespace": "default"},
+            "spec": {"replicas": 2},
+        },
+        namespace="default",
+    )
+    assert created.metadata.name == "my-app"
+
+ +

You can also register multiple CRDs at once:

+
fake_client.register_resources([
+    {"kind": "MyApp", "api_version": "v1", "group": "apps.example.com"},
+    {"kind": "MyConfig", "api_version": "v1beta1", "group": "config.example.com", "namespaced": False},
+])
+
+ +

Testing incremental workflows

+

Use @pytest.mark.incremental to test a resource through its full lifecycle. Each test depends on the previous one succeeding:

+
import pytest
+from ocp_resources.pod import Pod
+
+@pytest.mark.incremental
+class TestPodLifecycle:
+    @pytest.fixture(scope="class")
+    def pod(self, fake_client):
+        return Pod(
+            client=fake_client,
+            name="lifecycle-pod",
+            namespace="default",
+            containers=[{"name": "nginx", "image": "nginx:latest"}],
+        )
+
+    def test_01_create(self, pod):
+        deployed = pod.deploy()
+        assert deployed.name == "lifecycle-pod"
+        assert pod.exists
+
+    def test_02_read(self, pod):
+        assert pod.instance
+        assert pod.kind == "Pod"
+
+    def test_03_update(self, pod):
+        resource_dict = pod.instance.to_dict()
+        resource_dict["metadata"]["labels"] = {"updated": "true"}
+        pod.update(resource_dict=resource_dict)
+        assert pod.labels["updated"] == "true"
+
+    def test_04_delete(self, pod):
+        pod.clean_up(wait=False)
+        assert not pod.exists
+
+ +
+

Note: The @pytest.mark.incremental marker requires custom conftest.py hooks. Add the following to your conftest.py: +```python +def pytest_runtest_makereport(item, call): + if call.excinfo is not None and "incremental" in item.keywords: + parent = item.parent + parent._previousfailed = item +

+

def pytest_runtest_setup(item): + if "incremental" in item.keywords: + previousfailed = getattr(item.parent, "_previousfailed", None) + if previousfailed is not None: + pytest.xfail(f"previous test failed ({previousfailed.name})") +```

+
+

Conflict detection

+

The fake client raises errors for duplicate creates and missing resources, just like a real cluster:

+
from fake_kubernetes_client import NotFoundError, ConflictError
+
+def test_conflict_on_duplicate_create(fake_client):
+    api = fake_client.resources.get(api_version="v1", kind="Namespace")
+
+    api.create(body={"metadata": {"name": "unique-ns"}})
+
+    with pytest.raises(ConflictError):
+        api.create(body={"metadata": {"name": "unique-ns"}})
+
+def test_not_found_on_missing(fake_client):
+    api = fake_client.resources.get(api_version="v1", kind="Pod")
+
+    with pytest.raises(NotFoundError):
+        api.get(name="does-not-exist", namespace="default")
+
+ +

Accessing response data

+

The fake client returns response objects that support both attribute access and dictionary access:

+
def test_response_access(fake_client):
+    api = fake_client.resources.get(api_version="v1", kind="Pod")
+
+    created = api.create(
+        body={
+            "apiVersion": "v1", "kind": "Pod",
+            "metadata": {"name": "access-pod", "namespace": "default"},
+            "spec": {"containers": [{"name": "c", "image": "nginx"}]},
+        },
+        namespace="default",
+    )
+
+    # Attribute access
+    assert created.metadata.name == "access-pod"
+    assert created.status.phase == "Running"
+
+    # Dictionary-style access
+    assert created["metadata"]["name"] == "access-pod"
+
+    # Convert to a plain dict
+    data = created.to_dict()
+    assert isinstance(data, dict)
+    assert data["metadata"]["name"] == "access-pod"
+
+ +

Troubleshooting

+

ResourceNotFoundError when creating a resource

+

The fake client must recognize the resource's API group and kind. Standard Kubernetes and OpenShift resources are pre-registered. For CRDs, call fake_client.register_resources(...) before using them.

+

Status fields are missing or empty

+

Pods, Deployments, Services, and Namespaces have hardcoded status templates. Other resources get a generic status with a Ready condition. If the resource has a schema in the bundled OpenAPI definitions, a richer status is generated automatically.

+

Tests interfere with each other

+

Each call to get_client(fake=True) creates an independent in-memory store. Use separate fixtures or narrower fixture scopes to isolate tests that share a client.

+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/testing-without-cluster.md b/docs/testing-without-cluster.md new file mode 100644 index 0000000000..481e2c4f79 --- /dev/null +++ b/docs/testing-without-cluster.md @@ -0,0 +1,584 @@ +Let me start by exploring the repository structure and the knowledge graph. + +Now let me look at the test files for the fake client: + +Let me look at a few more test files to understand patterns used: + +Now let me look at how `get_client` works with `fake=True` and how resource classes use the client: + +Now let me look at a few more test examples to understand patterns: + +Now let me check how resources use the fake client and how `deploy`, `clean_up`, `update` work: + +Let me look at additional test files for more patterns - ConfigMap, events, and the configmap resource: + +Now let me check how watch and events are used, and how to use the lower-level API: + +Now let me check the `events` and `watch` implementation in the Resource class: + +Now let me look at a few more resource test files and the low-level API usage: + +Let me check for import paths and the `ocp_resources.resource` module to understand the public API: + +Let me check how the `wait_for_condition` and other resource methods work with the fake client: + +Now I have enough information to write the documentation page. Let me check a couple more things: + +Now I have all the information I need. Let me write the documentation page. + +# Testing Without a Cluster Using the Fake Client + +Write unit tests that create, read, update, and delete Kubernetes resources — including status checks, watch events, and custom CRDs — without connecting to a real cluster. + +## Prerequisites + +- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html)) +- `pytest` installed for running tests + +## Quick Example + +```python +import pytest +from ocp_resources.resource import get_client +from ocp_resources.namespace import Namespace + +@pytest.fixture(scope="class") +def fake_client(): + return get_client(fake=True) + +class TestMyFeature: + def test_create_namespace(self, fake_client): + ns = Namespace(client=fake_client, name="my-namespace") + ns.deploy() + + assert ns.exists + assert ns.kind == "Namespace" + assert ns.status == Namespace.Status.ACTIVE + + ns.clean_up(wait=False) + assert not ns.exists +``` + +Run it with `pytest` — no cluster, no kubeconfig, no network calls. + +## How It Works + +Calling `get_client(fake=True)` returns an in-memory client that simulates the Kubernetes API. It supports all standard resources (Pods, Deployments, Services, Namespaces, etc.) and OpenShift resources (Routes, Projects, etc.) out of the box. + +All resource classes (`Pod`, `Deployment`, `Namespace`, etc.) accept this fake client through their `client` parameter, and all CRUD methods work identically to a real cluster. + +## Step-by-Step: CRUD Operations + +### 1. Set up a shared fixture + +Create a `conftest.py` in your test directory: + +```python +import pytest +from ocp_resources.resource import get_client + +@pytest.fixture(scope="class") +def fake_client(): + """Provides a fake Kubernetes client for all tests.""" + return get_client(fake=True) +``` + +> **Tip:** Use `scope="class"` so resources created in one test method persist for subsequent methods in the same class. + +### 2. Create a resource + +```python +from ocp_resources.pod import Pod + +def test_create_pod(fake_client): + pod = Pod( + client=fake_client, + name="my-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + ) + result = pod.deploy() + + assert result.name == "my-pod" + assert pod.exists +``` + +The fake client automatically generates metadata (`uid`, `resourceVersion`, `creationTimestamp`) and a realistic `status` section for the resource. + +### 3. Read and query resources + +```python +from ocp_resources.namespace import Namespace + +def test_list_resources(fake_client): + # Create some namespaces first + Namespace(client=fake_client, name="ns-a").deploy() + Namespace(client=fake_client, name="ns-b").deploy() + + # Iterate over all Namespace resources + for ns in Namespace.get(client=fake_client): + assert ns.name +``` + +Access individual resource data via the `instance` property: + +```python +def test_get_instance(fake_client): + ns = Namespace(client=fake_client, name="test-ns") + ns.deploy() + + assert ns.instance + assert ns.kind == "Namespace" +``` + +### 4. Update a resource + +Use `update()` to patch a resource or `update_replace()` to replace it entirely: + +```python +def test_update_labels(fake_client): + ns = Namespace(client=fake_client, name="labeled-ns") + ns.deploy() + + # Patch: add a label + resource_dict = ns.instance.to_dict() + resource_dict["metadata"]["labels"]["env"] = "staging" + ns.update(resource_dict=resource_dict) + + assert ns.labels["env"] == "staging" +``` + +```python +def test_replace_labels(fake_client): + ns = Namespace(client=fake_client, name="replace-ns") + ns.deploy() + + resource_dict = ns.instance.to_dict() + resource_dict["metadata"]["labels"] = {"new-label": "value"} + ns.update_replace(resource_dict=resource_dict) + + assert "new-label" in ns.labels.keys() +``` + +### 5. Delete a resource + +```python +def test_delete_resource(fake_client): + ns = Namespace(client=fake_client, name="temp-ns") + ns.deploy() + assert ns.exists + + ns.clean_up(wait=False) + assert not ns.exists +``` + +### 6. Use context managers for automatic cleanup + +Resources support Python's `with` statement. The resource deploys on entry and is cleaned up on exit: + +```python +from ocp_resources.secret import Secret + +def test_context_manager(fake_client): + with Secret(name="my-secret", namespace="default", client=fake_client) as sec: + assert sec.exists + + # Automatically cleaned up + assert not sec.exists +``` + +## Testing Status and Conditions + +The fake client generates realistic status objects so you can test code that reads resource status. + +### Checking resource status + +```python +def test_namespace_status(fake_client): + ns = Namespace(client=fake_client, name="status-ns") + ns.deploy() + + assert ns.status == Namespace.Status.ACTIVE +``` + +### Waiting for conditions + +```python +from ocp_resources.pod import Pod + +def test_wait_for_condition(fake_client): + pod = Pod( + client=fake_client, + name="ready-pod", + namespace="default", + containers=[{"name": "app", "image": "nginx:latest"}], + ) + pod.deploy() + + pod.wait_for_status(status=Pod.Status.RUNNING, timeout=5) +``` + +### Simulating "not ready" resources + +Control a resource's readiness state with the `fake-client.io/ready` annotation: + +```python +def test_not_ready_pod(fake_client): + pod = Pod( + client=fake_client, + name="failing-pod", + namespace="default", + containers=[{"name": "app", "image": "nginx:latest"}], + annotations={"fake-client.io/ready": "false"}, + ) + pod.deploy() + + # The pod will have Ready condition set to False + pod.wait_for_condition( + condition=pod.Condition.READY, + status=pod.Condition.Status.FALSE, + timeout=5, + ) + + message = pod.get_condition_message( + condition_type=pod.Condition.READY, + condition_status=pod.Condition.Status.FALSE, + ) + assert message +``` + +Resource-specific status behavior when marked "not ready": + +| Resource | Ready behavior | Not-ready behavior | +|---|---|---| +| Pod | `phase: Running`, containers ready | Containers in `waiting` state | +| Deployment | `readyReplicas` matches `spec.replicas` | `readyReplicas: 0`, `unavailableReplicas` set | +| Namespace | `phase: Active` | `phase: Terminating` | +| All others | `Ready` condition `True` | `Ready` condition `False` | + +## Testing with Events + +The fake client automatically generates Kubernetes events for create, update, and delete operations. You can query them through the resource's `events()` method: + +```python +def test_resource_events(fake_client): + pod = Pod( + client=fake_client, + name="event-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + ) + pod.deploy() + + events = list(pod.events(timeout=1)) + assert events +``` + +## Testing with Deployments + +Deployments get a full status template including replica counts, conditions, and observed generation: + +```python +from ocp_resources.deployment import Deployment + +def test_deployment(fake_client): + dep = Deployment( + client=fake_client, + name="my-deploy", + namespace="default", + replicas=3, + selector={"matchLabels": {"app": "web"}}, + template={ + "metadata": {"labels": {"app": "web"}}, + "spec": {"containers": [{"name": "web", "image": "nginx:latest"}]}, + }, + ) + dep.deploy() + + assert dep.exists + assert dep.kind == "Deployment" + + dep.clean_up(wait=False) + assert not dep.exists +``` + +## Testing with ResourceList + +Test bulk resource creation with `ResourceList` and `NamespacedResourceList`: + +```python +from ocp_resources.resource import ResourceList, NamespacedResourceList + +def test_resource_list(fake_client): + with ResourceList( + client=fake_client, + resource_class=Namespace, + name="batch-ns", + num_resources=3, + ) as namespaces: + assert len(namespaces) == 3 + # All three namespaces are automatically cleaned up +``` + +See [Managing Bulk Resources with ResourceList](managing-resource-lists.html) for more details. + +## Advanced Usage + +### Using the low-level API directly + +You can bypass resource classes and work with the fake client's API directly — useful for testing custom logic: + +```python +def test_low_level_api(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + + # Create + pod = api.create( + body={ + "apiVersion": "v1", + "kind": "Pod", + "metadata": {"name": "raw-pod", "namespace": "default"}, + "spec": {"containers": [{"name": "app", "image": "nginx:latest"}]}, + }, + namespace="default", + ) + assert pod.metadata.name == "raw-pod" + assert pod.status.phase == "Running" + + # List + pods = api.get(namespace="default") + assert len(pods.items) >= 1 + + # Patch + updated = api.patch( + name="raw-pod", + namespace="default", + body={"metadata": {"labels": {"env": "test"}}}, + ) + assert updated.metadata.labels["env"] == "test" + + # Delete + api.delete(name="raw-pod", namespace="default") +``` + +### Label and field selectors + +The fake client supports filtering resources by labels and fields: + +```python +def test_label_selector(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + + api.create(body={ + "apiVersion": "v1", "kind": "Pod", + "metadata": {"name": "pod-a", "namespace": "default", "labels": {"app": "web"}}, + "spec": {"containers": [{"name": "c", "image": "nginx"}]}, + }, namespace="default") + + api.create(body={ + "apiVersion": "v1", "kind": "Pod", + "metadata": {"name": "pod-b", "namespace": "default", "labels": {"app": "api"}}, + "spec": {"containers": [{"name": "c", "image": "nginx"}]}, + }, namespace="default") + + # Filter by label + web_pods = api.get(namespace="default", label_selector="app=web") + assert len(web_pods.items) == 1 + assert web_pods.items[0].metadata.name == "pod-a" +``` + +Field selectors support `metadata.name` and `metadata.namespace`: + +```python +def test_field_selector(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + result = api.get(namespace="default", field_selector="metadata.name=my-pod") +``` + +### Watch for resource changes + +The `watch()` method yields existing resources as `ADDED` events: + +```python +def test_watch(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + + api.create(body={ + "apiVersion": "v1", "kind": "Pod", + "metadata": {"name": "watch-pod", "namespace": "default"}, + "spec": {"containers": [{"name": "c", "image": "nginx"}]}, + }, namespace="default") + + for event in api.watch(namespace="default"): + assert event["type"] == "ADDED" + assert event["object"].metadata.name + break # Stop after first event +``` + +### Registering Custom Resource Definitions + +To test with CRDs not bundled in the default schema, register them on the client: + +```python +def test_custom_resource(fake_client): + # Register the CRD + fake_client.register_resources({ + "kind": "MyApp", + "api_version": "v1alpha1", + "group": "apps.example.com", + "namespaced": True, + }) + + # Use it through the low-level API + api = fake_client.resources.get( + api_version="apps.example.com/v1alpha1", + kind="MyApp", + ) + + created = api.create( + body={ + "apiVersion": "apps.example.com/v1alpha1", + "kind": "MyApp", + "metadata": {"name": "my-app", "namespace": "default"}, + "spec": {"replicas": 2}, + }, + namespace="default", + ) + assert created.metadata.name == "my-app" +``` + +You can also register multiple CRDs at once: + +```python +fake_client.register_resources([ + {"kind": "MyApp", "api_version": "v1", "group": "apps.example.com"}, + {"kind": "MyConfig", "api_version": "v1beta1", "group": "config.example.com", "namespaced": False}, +]) +``` + +### Testing incremental workflows + +Use `@pytest.mark.incremental` to test a resource through its full lifecycle. Each test depends on the previous one succeeding: + +```python +import pytest +from ocp_resources.pod import Pod + +@pytest.mark.incremental +class TestPodLifecycle: + @pytest.fixture(scope="class") + def pod(self, fake_client): + return Pod( + client=fake_client, + name="lifecycle-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + ) + + def test_01_create(self, pod): + deployed = pod.deploy() + assert deployed.name == "lifecycle-pod" + assert pod.exists + + def test_02_read(self, pod): + assert pod.instance + assert pod.kind == "Pod" + + def test_03_update(self, pod): + resource_dict = pod.instance.to_dict() + resource_dict["metadata"]["labels"] = {"updated": "true"} + pod.update(resource_dict=resource_dict) + assert pod.labels["updated"] == "true" + + def test_04_delete(self, pod): + pod.clean_up(wait=False) + assert not pod.exists +``` + +> **Note:** The `@pytest.mark.incremental` marker requires custom `conftest.py` hooks. Add the following to your `conftest.py`: +> ```python +> def pytest_runtest_makereport(item, call): +> if call.excinfo is not None and "incremental" in item.keywords: +> parent = item.parent +> parent._previousfailed = item +> +> def pytest_runtest_setup(item): +> if "incremental" in item.keywords: +> previousfailed = getattr(item.parent, "_previousfailed", None) +> if previousfailed is not None: +> pytest.xfail(f"previous test failed ({previousfailed.name})") +> ``` + +### Conflict detection + +The fake client raises errors for duplicate creates and missing resources, just like a real cluster: + +```python +from fake_kubernetes_client import NotFoundError, ConflictError + +def test_conflict_on_duplicate_create(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Namespace") + + api.create(body={"metadata": {"name": "unique-ns"}}) + + with pytest.raises(ConflictError): + api.create(body={"metadata": {"name": "unique-ns"}}) + +def test_not_found_on_missing(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + + with pytest.raises(NotFoundError): + api.get(name="does-not-exist", namespace="default") +``` + +### Accessing response data + +The fake client returns response objects that support both attribute access and dictionary access: + +```python +def test_response_access(fake_client): + api = fake_client.resources.get(api_version="v1", kind="Pod") + + created = api.create( + body={ + "apiVersion": "v1", "kind": "Pod", + "metadata": {"name": "access-pod", "namespace": "default"}, + "spec": {"containers": [{"name": "c", "image": "nginx"}]}, + }, + namespace="default", + ) + + # Attribute access + assert created.metadata.name == "access-pod" + assert created.status.phase == "Running" + + # Dictionary-style access + assert created["metadata"]["name"] == "access-pod" + + # Convert to a plain dict + data = created.to_dict() + assert isinstance(data, dict) + assert data["metadata"]["name"] == "access-pod" +``` + +## Troubleshooting + +**`ResourceNotFoundError` when creating a resource** + +The fake client must recognize the resource's API group and kind. Standard Kubernetes and OpenShift resources are pre-registered. For CRDs, call `fake_client.register_resources(...)` before using them. + +**Status fields are missing or empty** + +Pods, Deployments, Services, and Namespaces have hardcoded status templates. Other resources get a generic status with a `Ready` condition. If the resource has a schema in the bundled OpenAPI definitions, a richer status is generated automatically. + +**Tests interfere with each other** + +Each call to `get_client(fake=True)` creates an independent in-memory store. Use separate fixtures or narrower fixture scopes to isolate tests that share a client. + +## Related Pages + +- [Connecting to Clusters](connecting-to-clusters.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Querying and Watching Resources](querying-resources.html) +- [Managing Bulk Resources with ResourceList](managing-resource-lists.html) +- [Common Resource Patterns](common-patterns.html) diff --git a/docs/validating-resources.html b/docs/validating-resources.html new file mode 100644 index 0000000000..1e0d38a3d8 --- /dev/null +++ b/docs/validating-resources.html @@ -0,0 +1,667 @@ + + + + + + Validating Resources Against OpenAPI Schemas - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by reading the exclusions file and pages manifest, then explore the codebase.

+

Now let me look at how the Resource base class integrates validation:

+

Now let me check the exceptions module:

+

Now let me check how deploy and context managers interact with validation:

+

Now I have a comprehensive understanding of the validation feature. Let me write the documentation.

+

Validating Resources Against OpenAPI Schemas

+

Catch configuration mistakes before they reach the Kubernetes API by validating resource definitions against OpenAPI schemas. This saves debugging time and prevents failed deployments caused by typos, missing fields, or incorrect types.

+

Prerequisites

+ +

Quick Example

+
from ocp_resources.pod import Pod
+
+pod = Pod(
+    name="nginx-pod",
+    namespace="default",
+    containers=[{"name": "nginx", "image": "nginx:latest"}],
+)
+
+# Validate against the OpenAPI schema — raises ValidationError if invalid
+pod.validate()
+
+ +

If the resource is valid, validate() returns silently. If something is wrong, it raises a ValidationError with a clear message pointing to the problematic field.

+

Manual Validation

+

Call .validate() on any resource instance to check it against the OpenAPI schema for its kind:

+
from ocp_resources.exceptions import ValidationError
+from ocp_resources.service import Service
+
+service = Service(
+    name="nginx-service",
+    namespace="default",
+    selector={"app": "nginx"},
+    ports=[{"port": 80, "targetPort": 80, "protocol": "TCP"}],
+)
+
+try:
+    service.validate()
+    print("Service configuration is valid")
+except ValidationError as e:
+    print(f"Validation failed: {e}")
+
+ +
+

Tip: Use manual validation in CI pipelines or pre-commit hooks to catch errors before applying resources to a cluster.

+
+

Validating Dictionaries Without Creating Objects

+

Use the validate_dict() class method to validate a raw resource dictionary without instantiating a resource object:

+
from ocp_resources.deployment import Deployment
+from ocp_resources.exceptions import ValidationError
+
+deployment_dict = {
+    "apiVersion": "apps/v1",
+    "kind": "Deployment",
+    "metadata": {"name": "nginx-deployment", "namespace": "default"},
+    "spec": {
+        "replicas": 3,
+        "selector": {"matchLabels": {"app": "nginx"}},
+        "template": {
+            "metadata": {"labels": {"app": "nginx"}},
+            "spec": {
+                "containers": [
+                    {"name": "nginx", "image": "nginx:1.21", "ports": [{"containerPort": 80}]}
+                ],
+            },
+        },
+    },
+}
+
+try:
+    Deployment.validate_dict(resource_dict=deployment_dict)
+    print("Deployment configuration is valid")
+except ValidationError as e:
+    print(f"Invalid: {e}")
+
+ +

This is useful when you have a dictionary from a YAML file or external source and want to validate it before creating the resource.

+

Enabling Auto-Validation on Create

+

Set schema_validation_enabled=True when creating a resource instance to automatically validate before every create(), deploy(), or update_replace() call:

+
from ocp_resources.pod import Pod
+from ocp_resources.exceptions import ValidationError
+
+pod = Pod(
+    client=client,
+    name="auto-validated-pod",
+    namespace="default",
+    containers=[{"name": "nginx", "image": "nginx:latest"}],
+    schema_validation_enabled=True,
+)
+
+# Validation runs automatically before the resource is sent to the API
+pod.deploy()
+
+ +

If validation fails, the resource is not created and a ValidationError is raised:

+
invalid_pod = Pod(
+    client=client,
+    name="bad-pod",
+    namespace="default",
+    containers=[{"name": "nginx"}],  # Missing required 'image' field
+    schema_validation_enabled=True,
+)
+
+try:
+    invalid_pod.deploy()
+except ValidationError as e:
+    print(f"Blocked: {e}")
+
+ +
+

Note: Auto-validation is disabled by default (schema_validation_enabled=False). You must opt in per instance.

+
+

When Auto-Validation Runs

+ + + + + + + + + + + + + + + + + + + + + + + + + +
OperationAuto-validates?Notes
create() / deploy()✅ YesValidates the full resource before sending
update_replace()✅ YesValidates the replacement dictionary
update()❌ NoSends a partial patch, not a full resource
+

Auto-validation also applies when using context managers, since they call deploy() internally. See Creating and Managing Resources for details on resource lifecycle methods.

+

Toggling Validation at Runtime

+

You can enable or disable validation on an existing instance:

+
pod = Pod(
+    client=client,
+    name="my-pod",
+    namespace="default",
+    containers=[{"name": "nginx", "image": "nginx:latest"}],
+)
+
+# Enable after creation
+pod.schema_validation_enabled = True
+pod.deploy()  # Will validate first
+
+# Disable for a subsequent operation
+pod.schema_validation_enabled = False
+
+ +

Advanced Usage

+

Validating Resources with Duplicate API Groups

+

Some resource kinds (like DNS) exist in multiple API groups. The validator uses the resource's api_group to select the correct schema automatically:

+
# These two resources share the kind "DNS" but use different API groups.
+# Each is validated against its own schema.
+dns_config.validate()       # Uses config.openshift.io schema
+dns_operator.validate()     # Uses operator.openshift.io schema
+
+ +

See Common Resource Patterns for more on working with resources that share a kind name.

+

Pre-Warming the Schema Cache

+

The first validation for a resource kind loads and resolves the schema (~25ms). Subsequent validations for the same kind use a cache and are much faster (~2ms). Pre-warm the cache at application startup for critical resource types:

+
from ocp_resources.pod import Pod
+from ocp_resources.deployment import Deployment
+from ocp_resources.service import Service
+from ocp_resources.exceptions import ValidationError
+
+for resource_cls in [Pod, Deployment, Service]:
+    try:
+        resource_cls.validate_dict({
+            "apiVersion": "v1",
+            "kind": resource_cls.kind,
+            "metadata": {"name": "warmup"}
+        })
+    except ValidationError:
+        pass  # Expected — we just want to load the schemas
+
+ +

Disabling Validation for Bulk Operations

+

When creating many resources in a loop, disable auto-validation to avoid repeated checks if you've already validated your templates:

+
# Validate the template once
+Pod.validate_dict(resource_dict=pod_template)
+
+# Then create many instances without per-instance validation
+for i in range(100):
+    pod = Pod(
+        client=client,
+        name=f"worker-{i}",
+        namespace="default",
+        containers=[{"name": "app", "image": "myapp:latest"}],
+        schema_validation_enabled=False,  # Skip — already validated
+    )
+    pod.deploy()
+
+ +

Troubleshooting

+

Reading Validation Error Messages

+

Validation errors include three key pieces of information:

+
Resource validation failed for Pod/my-pod
+  Field: spec.containers[0].image
+  Error: 123 is not of type 'string'
+  Expected type: string
+
+ +
    +
  • Resource identifier — which resource failed (kind and name)
  • +
  • Field path — the JSON path to the problematic field
  • +
  • Error details — what went wrong and what was expected
  • +
+

Common Validation Error Patterns

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Error message patternCauseFix
'X' is a required propertyMissing a required fieldAdd the missing field to your resource definition
'X' is not of type 'Y'Wrong data typeUse the correct type (e.g., 3 not "3")
'X' does not match patternInvalid formatFollow Kubernetes naming rules (lowercase, alphanumeric, hyphens)
'X' is not one of [...]Invalid enum valueUse one of the allowed values listed in the error
Additional properties are not allowedUnknown field nameCheck for typos in field names
+

Validation Passes But API Rejects the Resource

+

OpenAPI schema validation catches structural issues (wrong types, missing fields, invalid formats) but cannot check everything the API server validates. For example:

+
    +
  • Unique name constraints within a namespace
  • +
  • Resource quota limits
  • +
  • Admission webhook rules
  • +
  • Cross-resource references (e.g., a ConfigMap that doesn't exist)
  • +
+

If validation passes but the API still rejects your resource, the issue is likely a server-side constraint.

+

No Schema Available for a Resource

+

If no schema is bundled for a resource kind, validate() silently succeeds instead of raising an error. A debug-level log message is emitted:

+
No schema found for MyCustomResource, skipping validation
+
+ +

To update bundled schemas, see class-generator CLI Reference.

+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/validating-resources.md b/docs/validating-resources.md new file mode 100644 index 0000000000..e15a5d78e6 --- /dev/null +++ b/docs/validating-resources.md @@ -0,0 +1,275 @@ +Let me start by reading the exclusions file and pages manifest, then explore the codebase. + +Now let me look at how the Resource base class integrates validation: + +Now let me check the exceptions module: + +Now let me check how `deploy` and context managers interact with validation: + +Now I have a comprehensive understanding of the validation feature. Let me write the documentation. + +# Validating Resources Against OpenAPI Schemas + +Catch configuration mistakes before they reach the Kubernetes API by validating resource definitions against OpenAPI schemas. This saves debugging time and prevents failed deployments caused by typos, missing fields, or incorrect types. + +## Prerequisites + +- openshift-python-wrapper installed (see [Installing and Creating Your First Resource](quickstart.html)) +- A connection to a cluster, or use the fake client for offline validation (see [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html)) + +## Quick Example + +```python +from ocp_resources.pod import Pod + +pod = Pod( + name="nginx-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], +) + +# Validate against the OpenAPI schema — raises ValidationError if invalid +pod.validate() +``` + +If the resource is valid, `validate()` returns silently. If something is wrong, it raises a `ValidationError` with a clear message pointing to the problematic field. + +## Manual Validation + +Call `.validate()` on any resource instance to check it against the OpenAPI schema for its kind: + +```python +from ocp_resources.exceptions import ValidationError +from ocp_resources.service import Service + +service = Service( + name="nginx-service", + namespace="default", + selector={"app": "nginx"}, + ports=[{"port": 80, "targetPort": 80, "protocol": "TCP"}], +) + +try: + service.validate() + print("Service configuration is valid") +except ValidationError as e: + print(f"Validation failed: {e}") +``` + +> **Tip:** Use manual validation in CI pipelines or pre-commit hooks to catch errors before applying resources to a cluster. + +## Validating Dictionaries Without Creating Objects + +Use the `validate_dict()` class method to validate a raw resource dictionary without instantiating a resource object: + +```python +from ocp_resources.deployment import Deployment +from ocp_resources.exceptions import ValidationError + +deployment_dict = { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": {"name": "nginx-deployment", "namespace": "default"}, + "spec": { + "replicas": 3, + "selector": {"matchLabels": {"app": "nginx"}}, + "template": { + "metadata": {"labels": {"app": "nginx"}}, + "spec": { + "containers": [ + {"name": "nginx", "image": "nginx:1.21", "ports": [{"containerPort": 80}]} + ], + }, + }, + }, +} + +try: + Deployment.validate_dict(resource_dict=deployment_dict) + print("Deployment configuration is valid") +except ValidationError as e: + print(f"Invalid: {e}") +``` + +This is useful when you have a dictionary from a YAML file or external source and want to validate it before creating the resource. + +## Enabling Auto-Validation on Create + +Set `schema_validation_enabled=True` when creating a resource instance to automatically validate before every `create()`, `deploy()`, or `update_replace()` call: + +```python +from ocp_resources.pod import Pod +from ocp_resources.exceptions import ValidationError + +pod = Pod( + client=client, + name="auto-validated-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], + schema_validation_enabled=True, +) + +# Validation runs automatically before the resource is sent to the API +pod.deploy() +``` + +If validation fails, the resource is **not** created and a `ValidationError` is raised: + +```python +invalid_pod = Pod( + client=client, + name="bad-pod", + namespace="default", + containers=[{"name": "nginx"}], # Missing required 'image' field + schema_validation_enabled=True, +) + +try: + invalid_pod.deploy() +except ValidationError as e: + print(f"Blocked: {e}") +``` + +> **Note:** Auto-validation is **disabled by default** (`schema_validation_enabled=False`). You must opt in per instance. + +### When Auto-Validation Runs + +| Operation | Auto-validates? | Notes | +|--------------------|-----------------|---------------------------------------------| +| `create()` / `deploy()` | ✅ Yes | Validates the full resource before sending | +| `update_replace()` | ✅ Yes | Validates the replacement dictionary | +| `update()` | ❌ No | Sends a partial patch, not a full resource | + +Auto-validation also applies when using context managers, since they call `deploy()` internally. See [Creating and Managing Resources](creating-and-managing-resources.html) for details on resource lifecycle methods. + +### Toggling Validation at Runtime + +You can enable or disable validation on an existing instance: + +```python +pod = Pod( + client=client, + name="my-pod", + namespace="default", + containers=[{"name": "nginx", "image": "nginx:latest"}], +) + +# Enable after creation +pod.schema_validation_enabled = True +pod.deploy() # Will validate first + +# Disable for a subsequent operation +pod.schema_validation_enabled = False +``` + +## Advanced Usage + +### Validating Resources with Duplicate API Groups + +Some resource kinds (like `DNS`) exist in multiple API groups. The validator uses the resource's `api_group` to select the correct schema automatically: + +```python +# These two resources share the kind "DNS" but use different API groups. +# Each is validated against its own schema. +dns_config.validate() # Uses config.openshift.io schema +dns_operator.validate() # Uses operator.openshift.io schema +``` + +See [Common Resource Patterns](common-patterns.html) for more on working with resources that share a kind name. + +### Pre-Warming the Schema Cache + +The first validation for a resource kind loads and resolves the schema (~25ms). Subsequent validations for the same kind use a cache and are much faster (~2ms). Pre-warm the cache at application startup for critical resource types: + +```python +from ocp_resources.pod import Pod +from ocp_resources.deployment import Deployment +from ocp_resources.service import Service +from ocp_resources.exceptions import ValidationError + +for resource_cls in [Pod, Deployment, Service]: + try: + resource_cls.validate_dict({ + "apiVersion": "v1", + "kind": resource_cls.kind, + "metadata": {"name": "warmup"} + }) + except ValidationError: + pass # Expected — we just want to load the schemas +``` + +### Disabling Validation for Bulk Operations + +When creating many resources in a loop, disable auto-validation to avoid repeated checks if you've already validated your templates: + +```python +# Validate the template once +Pod.validate_dict(resource_dict=pod_template) + +# Then create many instances without per-instance validation +for i in range(100): + pod = Pod( + client=client, + name=f"worker-{i}", + namespace="default", + containers=[{"name": "app", "image": "myapp:latest"}], + schema_validation_enabled=False, # Skip — already validated + ) + pod.deploy() +``` + +## Troubleshooting + +### Reading Validation Error Messages + +Validation errors include three key pieces of information: + +``` +Resource validation failed for Pod/my-pod + Field: spec.containers[0].image + Error: 123 is not of type 'string' + Expected type: string +``` + +- **Resource identifier** — which resource failed (kind and name) +- **Field path** — the JSON path to the problematic field +- **Error details** — what went wrong and what was expected + +### Common Validation Error Patterns + +| Error message pattern | Cause | Fix | +|----------------------------------------------|-------------------------------------|--------------------------------------------------| +| `'X' is a required property` | Missing a required field | Add the missing field to your resource definition | +| `'X' is not of type 'Y'` | Wrong data type | Use the correct type (e.g., `3` not `"3"`) | +| `'X' does not match pattern` | Invalid format | Follow Kubernetes naming rules (lowercase, alphanumeric, hyphens) | +| `'X' is not one of [...]` | Invalid enum value | Use one of the allowed values listed in the error | +| `Additional properties are not allowed` | Unknown field name | Check for typos in field names | + +### Validation Passes But API Rejects the Resource + +OpenAPI schema validation catches structural issues (wrong types, missing fields, invalid formats) but cannot check everything the API server validates. For example: + +- Unique name constraints within a namespace +- Resource quota limits +- Admission webhook rules +- Cross-resource references (e.g., a ConfigMap that doesn't exist) + +If validation passes but the API still rejects your resource, the issue is likely a server-side constraint. + +### No Schema Available for a Resource + +If no schema is bundled for a resource kind, `validate()` silently succeeds instead of raising an error. A debug-level log message is emitted: + +``` +No schema found for MyCustomResource, skipping validation +``` + +To update bundled schemas, see [class-generator CLI Reference](class-generator-cli.html). + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) +- [Error Handling and Exception Reference](error-handling.html) +- [class-generator CLI Reference](class-generator-cli.html) +- [Resource and NamespacedResource API](resource-api.html) diff --git a/docs/waiting-for-conditions.html b/docs/waiting-for-conditions.html new file mode 100644 index 0000000000..e961b00728 --- /dev/null +++ b/docs/waiting-for-conditions.html @@ -0,0 +1,885 @@ + + + + + + Waiting for Resource Conditions and Status - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by reading the knowledge graph and page manifest, then explore the relevant source code.

+

Now let me examine the resource-specific wait methods, the wait_for_condition more closely, and the constants used:

+

Now let me look at the Status class, Condition class, and how create integrates with wait:

+

Now let me look at how Status/Condition are exposed on the base Resource class and how deploy / create uses wait:

+

Now let me see the deploy method more fully and how wait is passed through:

+

Now let me check the sriov_network_node_state.py and virtual_machine_restore.py for more wait patterns, and also look at the TimeoutExpiredError import:

+

Now let me check the cdi_config.py wait method and look at the wait_for_resource parameter in the constructor:

+

Now let me look at how create uses wait:

+

Now let me look at more of the status property:

+

Now I have all the information I need. Let me also check the TimeoutSampler import to understand the exception:

+

Let me check if there are any virtual machine import wait patterns with more stop_status details:

+

Now let me look at the node_network_state.py wait patterns:

+

I now have comprehensive understanding of all the wait methods. Let me compile the documentation.

+

Waiting for Resource Conditions and Status

+

Wait for Kubernetes and OpenShift resources to reach a desired status, condition, or lifecycle state using built-in polling methods with configurable timeouts and early exit on failure.

+

Prerequisites

+ +

Quick Example

+
from ocp_resources.pod import Pod
+
+pod = Pod(client=client, name="my-pod", namespace="default")
+pod.create(wait=True)  # Creates the pod and waits for it to exist
+
+# Wait for the pod to reach "Running" status
+pod.wait_for_status(status=Pod.Status.RUNNING)
+
+ +

Waiting for a Resource to Exist

+

The wait() method polls until a resource exists on the cluster. This is useful after creating a resource or when waiting for an external controller to produce one.

+
from ocp_resources.namespace import Namespace
+
+ns = Namespace(client=client, name="my-namespace")
+ns.create()
+ns.wait(timeout=120, sleep=2)
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
timeoutint240 (4 minutes)Maximum seconds to wait
sleepint1Seconds between polls
+

Raises: TimeoutExpiredError if the resource does not appear before the timeout.

+
+

Tip: Pass wait=True to create() or deploy() to combine creation and existence waiting in one call: +```python +pod.create(wait=True)

+

or

+

pod.deploy(wait=True) +```

+
+

Waiting for Deletion

+

The wait_deleted() method polls until a resource no longer exists:

+
pod.delete(wait=True, timeout=120)
+
+# Or call wait_deleted() separately:
+pod.delete()
+success = pod.wait_deleted(timeout=120)
+if not success:
+    print("Resource still exists after timeout")
+
+ + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
timeoutint240 (4 minutes)Maximum seconds to wait
+

Returns: True if the resource was deleted, False if the timeout was reached.

+
+

Note: Unlike most wait methods, wait_deleted() returns False on timeout rather than raising an exception.

+
+

Waiting for Status (Phase)

+

The wait_for_status() method polls status.phase until it matches the expected value:

+
from ocp_resources.datavolume import DataVolume
+
+dv = DataVolume(client=client, name="my-dv", namespace="default")
+dv.wait_for_status(status=DataVolume.Status.SUCCEEDED, timeout=600)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
statusstr(required)Expected status.phase value
timeoutint240 (4 minutes)Maximum seconds to wait
stop_statusstr \| NoneStatus.FAILEDStatus that aborts the wait immediately
sleepint1Seconds between polls
exceptions_dictdictProtocol + cluster retry errorsExceptions to catch and retry
+

Raises: TimeoutExpiredError if the resource does not reach the desired status, or if stop_status is encountered.

+

Using stop_status for early failure

+

By default, wait_for_status() aborts immediately if the resource enters a "Failed" phase. Override this to detect a different failure phase:

+
from ocp_resources.virtual_machine_instance import VirtualMachineInstance
+
+vmi = VirtualMachineInstance(client=client, name="my-vmi", namespace="default")
+vmi.wait_for_status(
+    status=VirtualMachineInstance.Status.RUNNING,
+    stop_status="ErrorUnschedulable",
+    timeout=300,
+)
+
+ +

Built-in status constants

+

Every resource inherits status constants from its base class. Resource-specific subclasses extend these with additional values:

+
# Base status constants (available on all resources)
+from ocp_resources.resource import Resource
+
+Resource.Status.SUCCEEDED    # "Succeeded"
+Resource.Status.FAILED       # "Failed"
+Resource.Status.RUNNING      # "Running"
+Resource.Status.PENDING      # "Pending"
+
+# Resource-specific statuses
+from ocp_resources.virtual_machine import VirtualMachine
+
+VirtualMachine.Status.MIGRATING     # "Migrating"
+VirtualMachine.Status.STOPPED       # "Stopped"
+VirtualMachine.Status.STARTING      # "Starting"
+VirtualMachine.Status.PAUSED        # "Paused"
+
+from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
+
+PersistentVolumeClaim.Status.BOUND  # "Bound"
+
+ +

Waiting for Conditions

+

The wait_for_condition() method waits for a specific entry in status.conditions[] to match a desired type, status, and optionally a reason or message:

+
from ocp_resources.user_defined_network import UserDefinedNetwork
+
+udn = UserDefinedNetwork(client=client, name="my-net", namespace="default")
+udn.wait_for_condition(
+    condition=UserDefinedNetwork.Condition.NETWORK_READY,
+    status=UserDefinedNetwork.Condition.Status.TRUE,
+    timeout=30,
+)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
conditionstr(required)Condition type to match (e.g. "Ready", "Available")
statusstr(required)Expected condition status ("True", "False", "Unknown")
timeoutint300 (5 minutes)Maximum seconds to wait
sleep_timeint1Seconds between polls
reasonstr \| NoneNoneIf set, the condition's reason field must also match
messagestr""Text that must appear in the condition's message field
stop_conditionstr \| NoneNoneA condition type that aborts the wait immediately
stop_statusstr"True"The status of the stop condition that triggers early abort
+

Raises:

+
    +
  • TimeoutExpiredError — if the condition is not met before the timeout
  • +
  • ConditionError — if the stop_condition is detected before the desired condition
  • +
+

Matching by reason and message

+
pod.wait_for_condition(
+    condition="Ready",
+    status="True",
+    reason="PodCompleted",
+    message="containers with",
+    timeout=120,
+)
+
+ +

The reason must be an exact match. The message is checked using substring inclusion — the condition's message field must contain the provided string.

+

Using stop_condition for early abort

+

Stop conditions let you fail fast when an unrecoverable condition appears:

+
from ocp_resources.exceptions import ConditionError
+
+try:
+    resource.wait_for_condition(
+        condition="Ready",
+        status="True",
+        stop_condition="Failed",
+        stop_status="True",
+        timeout=300,
+    )
+except ConditionError as e:
+    print(f"Resource hit a failure condition: {e}")
+
+ +
+

Note: The stop_condition matching only checks the condition type and status. The reason and message parameters are ignored when evaluating stop conditions.

+
+

Built-in condition constants

+
from ocp_resources.resource import Resource
+
+# Condition types
+Resource.Condition.READY         # "Ready"
+Resource.Condition.AVAILABLE     # "Available"
+Resource.Condition.DEGRADED      # "Degraded"
+Resource.Condition.PROGRESSING   # "Progressing"
+Resource.Condition.UPGRADEABLE   # "Upgradeable"
+Resource.Condition.NETWORK_READY # "NetworkReady"
+
+# Condition statuses
+Resource.Condition.Status.TRUE    # "True"
+Resource.Condition.Status.FALSE   # "False"
+Resource.Condition.Status.UNKNOWN # "Unknown"
+
+# Condition reasons
+Resource.Condition.Reason.ALL_REQUIREMENTS_MET  # "AllRequirementsMet"
+Resource.Condition.Reason.INSTALL_SUCCEEDED     # "InstallSucceeded"
+
+ +

Waiting with Context Managers

+

When using a resource as a context manager, set wait_for_resource=True to automatically wait for the resource to exist after creation:

+
from ocp_resources.pod import Pod
+
+with Pod(
+    client=client,
+    name="my-pod",
+    namespace="default",
+    wait_for_resource=True,
+) as pod:
+    # Pod exists when you reach this line
+    pod.wait_for_status(status=Pod.Status.RUNNING)
+
+ +

See Creating and Managing Resources for more on context manager usage.

+

Advanced Usage

+

Waiting for any conditions to appear

+

The wait_for_conditions() method waits up to 30 seconds for the resource's status.conditions list to be populated (non-empty). This is useful when you need to inspect conditions but aren't sure they've been set yet:

+
resource.wait_for_conditions()
+# Now resource.instance.status.conditions is available
+
+ +

Resource-specific wait methods

+

Many resource types provide specialized wait methods beyond the base wait_for_status() and wait_for_condition(). These methods encapsulate domain-specific logic.

+

Deployments

+
from ocp_resources.deployment import Deployment
+
+deploy = Deployment(client=client, name="my-app", namespace="default")
+deploy.wait_for_replicas(deployed=True, timeout=240)   # All replicas ready
+deploy.wait_for_replicas(deployed=False, timeout=120)   # Scale to zero complete
+
+ +

DaemonSets

+
from ocp_resources.daemonset import DaemonSet
+
+ds = DaemonSet(client=client, name="my-ds", namespace="default")
+ds.wait_until_deployed(timeout=240)  # All desired pods are ready
+
+ +

VirtualMachines (KubeVirt)

+
from ocp_resources.virtual_machine import VirtualMachine
+from ocp_resources.virtual_machine_instance import VirtualMachineInstance
+
+vm = VirtualMachine(client=client, name="my-vm", namespace="default")
+
+# Wait for VM ready status (True = running, None = stopped)
+vm.wait_for_ready_status(status=True, timeout=240)
+
+# Wait for a specific status field to become None
+vm.wait_for_status_none(status="snapshotInProgress", timeout=240)
+
+# Wait for VMI to be running
+vmi = VirtualMachineInstance(client=client, name="my-vm", namespace="default")
+vmi.wait_until_running(timeout=240, logs=True)
+
+# Wait for VMI pause state
+vmi.wait_for_pause_status(pause=True, timeout=240)
+
+ +

See Working with Virtual Machines (KubeVirt) for the full VM lifecycle.

+

DataVolumes

+
from ocp_resources.datavolume import DataVolume
+
+dv = DataVolume(client=client, name="my-dv", namespace="default")
+dv.wait_for_dv_success(
+    timeout=600,
+    failure_timeout=120,
+    pvc_wait_for_bound_timeout=60,
+)
+
+ +

The wait_for_dv_success() method handles the full DataVolume lifecycle: it waits for the status to leave Pending, then waits for Succeeded, and finally confirms the PVC is Bound.

+

You can pass a stop_status_func callback to abort early on custom conditions:

+
def dv_is_stuck(dv):
+    return dv.instance.status.conditions.restartCount > 3
+
+dv.wait_for_dv_success(
+    stop_status_func=dv_is_stuck,
+    dv=dv,  # passed as keyword argument to the function
+)
+
+ +

VirtualMachineSnapshot / VirtualMachineRestore

+
from ocp_resources.virtual_machine_snapshot import VirtualMachineSnapshot
+from ocp_resources.virtual_machine_restore import VirtualMachineRestore
+
+snapshot = VirtualMachineSnapshot(client=client, name="snap-1", namespace="default")
+snapshot.wait_snapshot_done(timeout=240)    # readyToUse + VM snapshotInProgress is None
+
+restore = VirtualMachineRestore(client=client, name="restore-1", namespace="default")
+restore.wait_restore_done(timeout=240)     # complete + VM restoreInProgress is None
+
+ +

MachineSet

+
from ocp_resources.machine_set import MachineSet
+
+ms = MachineSet(client=client, name="worker-set", namespace="openshift-machine-api")
+ms.wait_for_replicas(timeout=300, sleep=1)
+
+ +

Returns True when ready replicas match desired replicas, False on timeout.

+

NodeNetworkConfigurationPolicy

+
from ocp_resources.node_network_configuration_policy import NodeNetworkConfigurationPolicy
+
+nncp = NodeNetworkConfigurationPolicy(client=client, name="br1-policy")
+nncp.wait_for_status_success()  # Uses the resource's own success_timeout
+
+ +

Raises NNCPConfigurationFailed if the policy fails or finds no matching nodes.

+

Timeout constants

+

The library provides predefined timeout constants you can use with any wait method:

+
from ocp_resources.utils.constants import (
+    TIMEOUT_1SEC,       # 1
+    TIMEOUT_5SEC,       # 5
+    TIMEOUT_10SEC,      # 10
+    TIMEOUT_30SEC,      # 30
+    TIMEOUT_1MINUTE,    # 60
+    TIMEOUT_2MINUTES,   # 120
+    TIMEOUT_4MINUTES,   # 240
+    TIMEOUT_10MINUTES,  # 600
+)
+
+pod.wait_for_status(status=Pod.Status.RUNNING, timeout=TIMEOUT_10MINUTES)
+
+ +

Custom exception retries

+

Wait methods handle transient cluster errors automatically (connection resets, etcd leader changes, protocol errors). You can customize which exceptions are retried:

+
from ocp_resources.utils.constants import (
+    PROTOCOL_ERROR_EXCEPTION_DICT,
+    DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
+)
+
+# Combine default retry exceptions
+pod.wait_for_status(
+    status=Pod.Status.RUNNING,
+    exceptions_dict={
+        **PROTOCOL_ERROR_EXCEPTION_DICT,
+        **DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
+    },
+)
+
+ +

The default retry exceptions cover:

+
    +
  • ProtocolError — network-level failures
  • +
  • MaxRetryError, ConnectionAbortedError, ConnectionResetError — connection failures
  • +
  • InternalServerError — etcd leader changes, webhook failures, RPC errors
  • +
  • ServerTimeoutError — API server timeouts
  • +
  • ForbiddenError — context deadline exceeded errors
  • +
+

Troubleshooting

+

TimeoutExpiredError is raised unexpectedly

+
    +
  • Increase the timeout. The default is 240 seconds (4 minutes), which may not be enough for slow operations like DataVolume imports or VM migrations.
  • +
  • Check stop_status. wait_for_status() defaults stop_status to Status.FAILED. If your resource legitimately passes through a "Failed" phase, set stop_status to a value that won't match, or set it to None.
  • +
  • Inspect the current status. The error log includes the last observed status. Read it to understand where the resource got stuck.
  • +
+

ConditionError on wait_for_condition()

+

This means the stop_condition was matched before the desired condition. The error message includes the full condition details. Review your stop_condition and stop_status settings.

+

Resource exists but wait_for_status() times out

+

The wait_for_status() method reads from status.phase. Some resources use different status fields (e.g., status.ready, status.readyToUse, status.syncStatus). For those resources, use the resource-specific wait methods documented above, or use wait_for_condition() to match on status.conditions[] entries instead.

+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/waiting-for-conditions.md b/docs/waiting-for-conditions.md new file mode 100644 index 0000000000..ed05d65a6a --- /dev/null +++ b/docs/waiting-for-conditions.md @@ -0,0 +1,454 @@ +Let me start by reading the knowledge graph and page manifest, then explore the relevant source code. + +Now let me examine the resource-specific wait methods, the `wait_for_condition` more closely, and the constants used: + +Now let me look at the `Status` class, `Condition` class, and how `create` integrates with `wait`: + +Now let me look at how Status/Condition are exposed on the base Resource class and how `deploy` / `create` uses wait: + +Now let me see the `deploy` method more fully and how `wait` is passed through: + +Now let me check the `sriov_network_node_state.py` and `virtual_machine_restore.py` for more wait patterns, and also look at the `TimeoutExpiredError` import: + +Now let me check the `cdi_config.py` wait method and look at the `wait_for_resource` parameter in the constructor: + +Now let me look at how `create` uses `wait`: + +Now let me look at more of the `status` property: + +Now I have all the information I need. Let me also check the `TimeoutSampler` import to understand the exception: + +Let me check if there are any virtual machine import wait patterns with more stop_status details: + +Now let me look at the `node_network_state.py` wait patterns: + +I now have comprehensive understanding of all the wait methods. Let me compile the documentation. + +# Waiting for Resource Conditions and Status + +Wait for Kubernetes and OpenShift resources to reach a desired status, condition, or lifecycle state using built-in polling methods with configurable timeouts and early exit on failure. + +## Prerequisites + +- `openshift-python-wrapper` installed in your project +- A connected client (see [Connecting to Clusters](connecting-to-clusters.html)) +- A resource instance to wait on (see [Creating and Managing Resources](creating-and-managing-resources.html)) + +## Quick Example + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") +pod.create(wait=True) # Creates the pod and waits for it to exist + +# Wait for the pod to reach "Running" status +pod.wait_for_status(status=Pod.Status.RUNNING) +``` + +## Waiting for a Resource to Exist + +The `wait()` method polls until a resource exists on the cluster. This is useful after creating a resource or when waiting for an external controller to produce one. + +```python +from ocp_resources.namespace import Namespace + +ns = Namespace(client=client, name="my-namespace") +ns.create() +ns.wait(timeout=120, sleep=2) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `timeout` | `int` | `240` (4 minutes) | Maximum seconds to wait | +| `sleep` | `int` | `1` | Seconds between polls | + +**Raises:** `TimeoutExpiredError` if the resource does not appear before the timeout. + +> **Tip:** Pass `wait=True` to `create()` or `deploy()` to combine creation and existence waiting in one call: +> ```python +> pod.create(wait=True) +> # or +> pod.deploy(wait=True) +> ``` + +## Waiting for Deletion + +The `wait_deleted()` method polls until a resource no longer exists: + +```python +pod.delete(wait=True, timeout=120) + +# Or call wait_deleted() separately: +pod.delete() +success = pod.wait_deleted(timeout=120) +if not success: + print("Resource still exists after timeout") +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `timeout` | `int` | `240` (4 minutes) | Maximum seconds to wait | + +**Returns:** `True` if the resource was deleted, `False` if the timeout was reached. + +> **Note:** Unlike most wait methods, `wait_deleted()` returns `False` on timeout rather than raising an exception. + +## Waiting for Status (Phase) + +The `wait_for_status()` method polls `status.phase` until it matches the expected value: + +```python +from ocp_resources.datavolume import DataVolume + +dv = DataVolume(client=client, name="my-dv", namespace="default") +dv.wait_for_status(status=DataVolume.Status.SUCCEEDED, timeout=600) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `status` | `str` | *(required)* | Expected `status.phase` value | +| `timeout` | `int` | `240` (4 minutes) | Maximum seconds to wait | +| `stop_status` | `str \| None` | `Status.FAILED` | Status that aborts the wait immediately | +| `sleep` | `int` | `1` | Seconds between polls | +| `exceptions_dict` | `dict` | Protocol + cluster retry errors | Exceptions to catch and retry | + +**Raises:** `TimeoutExpiredError` if the resource does not reach the desired status, or if `stop_status` is encountered. + +### Using `stop_status` for early failure + +By default, `wait_for_status()` aborts immediately if the resource enters a `"Failed"` phase. Override this to detect a different failure phase: + +```python +from ocp_resources.virtual_machine_instance import VirtualMachineInstance + +vmi = VirtualMachineInstance(client=client, name="my-vmi", namespace="default") +vmi.wait_for_status( + status=VirtualMachineInstance.Status.RUNNING, + stop_status="ErrorUnschedulable", + timeout=300, +) +``` + +### Built-in status constants + +Every resource inherits status constants from its base class. Resource-specific subclasses extend these with additional values: + +```python +# Base status constants (available on all resources) +from ocp_resources.resource import Resource + +Resource.Status.SUCCEEDED # "Succeeded" +Resource.Status.FAILED # "Failed" +Resource.Status.RUNNING # "Running" +Resource.Status.PENDING # "Pending" + +# Resource-specific statuses +from ocp_resources.virtual_machine import VirtualMachine + +VirtualMachine.Status.MIGRATING # "Migrating" +VirtualMachine.Status.STOPPED # "Stopped" +VirtualMachine.Status.STARTING # "Starting" +VirtualMachine.Status.PAUSED # "Paused" + +from ocp_resources.persistent_volume_claim import PersistentVolumeClaim + +PersistentVolumeClaim.Status.BOUND # "Bound" +``` + +## Waiting for Conditions + +The `wait_for_condition()` method waits for a specific entry in `status.conditions[]` to match a desired type, status, and optionally a reason or message: + +```python +from ocp_resources.user_defined_network import UserDefinedNetwork + +udn = UserDefinedNetwork(client=client, name="my-net", namespace="default") +udn.wait_for_condition( + condition=UserDefinedNetwork.Condition.NETWORK_READY, + status=UserDefinedNetwork.Condition.Status.TRUE, + timeout=30, +) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `condition` | `str` | *(required)* | Condition type to match (e.g. `"Ready"`, `"Available"`) | +| `status` | `str` | *(required)* | Expected condition status (`"True"`, `"False"`, `"Unknown"`) | +| `timeout` | `int` | `300` (5 minutes) | Maximum seconds to wait | +| `sleep_time` | `int` | `1` | Seconds between polls | +| `reason` | `str \| None` | `None` | If set, the condition's `reason` field must also match | +| `message` | `str` | `""` | Text that must appear in the condition's `message` field | +| `stop_condition` | `str \| None` | `None` | A condition type that aborts the wait immediately | +| `stop_status` | `str` | `"True"` | The status of the stop condition that triggers early abort | + +**Raises:** +- `TimeoutExpiredError` — if the condition is not met before the timeout +- `ConditionError` — if the `stop_condition` is detected before the desired condition + +### Matching by reason and message + +```python +pod.wait_for_condition( + condition="Ready", + status="True", + reason="PodCompleted", + message="containers with", + timeout=120, +) +``` + +The `reason` must be an exact match. The `message` is checked using substring inclusion — the condition's message field must *contain* the provided string. + +### Using `stop_condition` for early abort + +Stop conditions let you fail fast when an unrecoverable condition appears: + +```python +from ocp_resources.exceptions import ConditionError + +try: + resource.wait_for_condition( + condition="Ready", + status="True", + stop_condition="Failed", + stop_status="True", + timeout=300, + ) +except ConditionError as e: + print(f"Resource hit a failure condition: {e}") +``` + +> **Note:** The `stop_condition` matching only checks the condition `type` and `status`. The `reason` and `message` parameters are ignored when evaluating stop conditions. + +### Built-in condition constants + +```python +from ocp_resources.resource import Resource + +# Condition types +Resource.Condition.READY # "Ready" +Resource.Condition.AVAILABLE # "Available" +Resource.Condition.DEGRADED # "Degraded" +Resource.Condition.PROGRESSING # "Progressing" +Resource.Condition.UPGRADEABLE # "Upgradeable" +Resource.Condition.NETWORK_READY # "NetworkReady" + +# Condition statuses +Resource.Condition.Status.TRUE # "True" +Resource.Condition.Status.FALSE # "False" +Resource.Condition.Status.UNKNOWN # "Unknown" + +# Condition reasons +Resource.Condition.Reason.ALL_REQUIREMENTS_MET # "AllRequirementsMet" +Resource.Condition.Reason.INSTALL_SUCCEEDED # "InstallSucceeded" +``` + +## Waiting with Context Managers + +When using a resource as a context manager, set `wait_for_resource=True` to automatically wait for the resource to exist after creation: + +```python +from ocp_resources.pod import Pod + +with Pod( + client=client, + name="my-pod", + namespace="default", + wait_for_resource=True, +) as pod: + # Pod exists when you reach this line + pod.wait_for_status(status=Pod.Status.RUNNING) +``` + +See [Creating and Managing Resources](creating-and-managing-resources.html) for more on context manager usage. + +## Advanced Usage + +### Waiting for any conditions to appear + +The `wait_for_conditions()` method waits up to 30 seconds for the resource's `status.conditions` list to be populated (non-empty). This is useful when you need to inspect conditions but aren't sure they've been set yet: + +```python +resource.wait_for_conditions() +# Now resource.instance.status.conditions is available +``` + +### Resource-specific wait methods + +Many resource types provide specialized wait methods beyond the base `wait_for_status()` and `wait_for_condition()`. These methods encapsulate domain-specific logic. + +#### Deployments + +```python +from ocp_resources.deployment import Deployment + +deploy = Deployment(client=client, name="my-app", namespace="default") +deploy.wait_for_replicas(deployed=True, timeout=240) # All replicas ready +deploy.wait_for_replicas(deployed=False, timeout=120) # Scale to zero complete +``` + +#### DaemonSets + +```python +from ocp_resources.daemonset import DaemonSet + +ds = DaemonSet(client=client, name="my-ds", namespace="default") +ds.wait_until_deployed(timeout=240) # All desired pods are ready +``` + +#### VirtualMachines (KubeVirt) + +```python +from ocp_resources.virtual_machine import VirtualMachine +from ocp_resources.virtual_machine_instance import VirtualMachineInstance + +vm = VirtualMachine(client=client, name="my-vm", namespace="default") + +# Wait for VM ready status (True = running, None = stopped) +vm.wait_for_ready_status(status=True, timeout=240) + +# Wait for a specific status field to become None +vm.wait_for_status_none(status="snapshotInProgress", timeout=240) + +# Wait for VMI to be running +vmi = VirtualMachineInstance(client=client, name="my-vm", namespace="default") +vmi.wait_until_running(timeout=240, logs=True) + +# Wait for VMI pause state +vmi.wait_for_pause_status(pause=True, timeout=240) +``` + +See [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) for the full VM lifecycle. + +#### DataVolumes + +```python +from ocp_resources.datavolume import DataVolume + +dv = DataVolume(client=client, name="my-dv", namespace="default") +dv.wait_for_dv_success( + timeout=600, + failure_timeout=120, + pvc_wait_for_bound_timeout=60, +) +``` + +The `wait_for_dv_success()` method handles the full DataVolume lifecycle: it waits for the status to leave `Pending`, then waits for `Succeeded`, and finally confirms the PVC is `Bound`. + +You can pass a `stop_status_func` callback to abort early on custom conditions: + +```python +def dv_is_stuck(dv): + return dv.instance.status.conditions.restartCount > 3 + +dv.wait_for_dv_success( + stop_status_func=dv_is_stuck, + dv=dv, # passed as keyword argument to the function +) +``` + +#### VirtualMachineSnapshot / VirtualMachineRestore + +```python +from ocp_resources.virtual_machine_snapshot import VirtualMachineSnapshot +from ocp_resources.virtual_machine_restore import VirtualMachineRestore + +snapshot = VirtualMachineSnapshot(client=client, name="snap-1", namespace="default") +snapshot.wait_snapshot_done(timeout=240) # readyToUse + VM snapshotInProgress is None + +restore = VirtualMachineRestore(client=client, name="restore-1", namespace="default") +restore.wait_restore_done(timeout=240) # complete + VM restoreInProgress is None +``` + +#### MachineSet + +```python +from ocp_resources.machine_set import MachineSet + +ms = MachineSet(client=client, name="worker-set", namespace="openshift-machine-api") +ms.wait_for_replicas(timeout=300, sleep=1) +``` + +Returns `True` when ready replicas match desired replicas, `False` on timeout. + +#### NodeNetworkConfigurationPolicy + +```python +from ocp_resources.node_network_configuration_policy import NodeNetworkConfigurationPolicy + +nncp = NodeNetworkConfigurationPolicy(client=client, name="br1-policy") +nncp.wait_for_status_success() # Uses the resource's own success_timeout +``` + +Raises `NNCPConfigurationFailed` if the policy fails or finds no matching nodes. + +### Timeout constants + +The library provides predefined timeout constants you can use with any wait method: + +```python +from ocp_resources.utils.constants import ( + TIMEOUT_1SEC, # 1 + TIMEOUT_5SEC, # 5 + TIMEOUT_10SEC, # 10 + TIMEOUT_30SEC, # 30 + TIMEOUT_1MINUTE, # 60 + TIMEOUT_2MINUTES, # 120 + TIMEOUT_4MINUTES, # 240 + TIMEOUT_10MINUTES, # 600 +) + +pod.wait_for_status(status=Pod.Status.RUNNING, timeout=TIMEOUT_10MINUTES) +``` + +### Custom exception retries + +Wait methods handle transient cluster errors automatically (connection resets, etcd leader changes, protocol errors). You can customize which exceptions are retried: + +```python +from ocp_resources.utils.constants import ( + PROTOCOL_ERROR_EXCEPTION_DICT, + DEFAULT_CLUSTER_RETRY_EXCEPTIONS, +) + +# Combine default retry exceptions +pod.wait_for_status( + status=Pod.Status.RUNNING, + exceptions_dict={ + **PROTOCOL_ERROR_EXCEPTION_DICT, + **DEFAULT_CLUSTER_RETRY_EXCEPTIONS, + }, +) +``` + +The default retry exceptions cover: +- `ProtocolError` — network-level failures +- `MaxRetryError`, `ConnectionAbortedError`, `ConnectionResetError` — connection failures +- `InternalServerError` — etcd leader changes, webhook failures, RPC errors +- `ServerTimeoutError` — API server timeouts +- `ForbiddenError` — context deadline exceeded errors + +## Troubleshooting + +### `TimeoutExpiredError` is raised unexpectedly + +- **Increase the timeout.** The default is 240 seconds (4 minutes), which may not be enough for slow operations like DataVolume imports or VM migrations. +- **Check `stop_status`.** `wait_for_status()` defaults `stop_status` to `Status.FAILED`. If your resource legitimately passes through a `"Failed"` phase, set `stop_status` to a value that won't match, or set it to `None`. +- **Inspect the current status.** The error log includes the last observed status. Read it to understand where the resource got stuck. + +### `ConditionError` on `wait_for_condition()` + +This means the `stop_condition` was matched before the desired condition. The error message includes the full condition details. Review your `stop_condition` and `stop_status` settings. + +### Resource exists but `wait_for_status()` times out + +The `wait_for_status()` method reads from `status.phase`. Some resources use different status fields (e.g., `status.ready`, `status.readyToUse`, `status.syncStatus`). For those resources, use the resource-specific wait methods documented above, or use `wait_for_condition()` to match on `status.conditions[]` entries instead. + +## Related Pages + +- [Querying and Watching Resources](querying-resources.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) +- [Error Handling and Exception Reference](error-handling.html) +- [Resource and NamespacedResource API](resource-api.html) diff --git a/docs/working-with-events.html b/docs/working-with-events.html new file mode 100644 index 0000000000..b3014753d1 --- /dev/null +++ b/docs/working-with-events.html @@ -0,0 +1,935 @@ + + + + + + Working with Kubernetes Events - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by exploring the repository structure and the knowledge graph.

+

Now let me explore the Event-related source code:

+

Now let me look at the _build_event_field_selector function and related event test files:

+

Now let me check the events test files in the tests directory:

+

Now let me check the resource.py import and the events method more carefully, and also look at how TIMEOUT_4MINUTES is defined:

+

Let me check where TIMEOUT_4MINUTES is defined in resource.py:

+

Now let me check the constants value for TIMEOUT_4MINUTES:

+

Now let me check how the Event class is used in the fake client:

+

Now let me look at the _extract_event_info and the _process_watch_events functions to understand the event data structure better:

+

Now let me look at the event object structure from watch events:

+

Now let me check how to properly import and use Event:

+

Let me also look at the test file for events in more detail:

+

Now let me check the watch method's return structure for events:

+

Now I have a thorough understanding. Let me write the documentation.

+

Working with Kubernetes Events

+

Monitor, retrieve, and clean up Kubernetes events to debug resource behavior and automate event-driven workflows using the Event class and the resource.events() method.

+

Prerequisites

+ +

Quick Example

+

Stream events from a namespace in real-time:

+
from ocp_resources.event import Event
+
+for event in Event.get(client=client, namespace="my-namespace", timeout=30):
+    print(f"{event['type']}: {event.object.reason}{event.object.message}")
+
+ +

Streaming Events in Real-Time with Event.get()

+

Event.get() opens a watch connection and yields events as they occur. It's a generator that blocks until the timeout expires or you break out of the loop.

+
from ocp_resources.event import Event
+
+for event in Event.get(
+    client=client,
+    namespace="my-namespace",
+    timeout=60,
+):
+    obj = event.object
+    print(f"[{obj.type}] {obj.reason}: {obj.message}")
+
+ +

Each yielded event is a watch event dictionary with these keys:

+ + + + + + + + + + + + + + + + + + + + + +
KeyDescription
typeWatch event type: ADDED, MODIFIED, or DELETED
objectThe Event resource object (access .type, .reason, .message, .involvedObject, etc.)
raw_objectThe raw dictionary representation of the event
+

Event.get() Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
clientDynamicClientNoneKubernetes dynamic client (required)
namespacestr \| NoneNoneFilter events to a specific namespace
namestr \| NoneNoneFilter by event name
label_selectorstr \| NoneNoneFilter by labels (e.g. "app=nginx")
field_selectorstr \| NoneNoneFilter by fields (e.g. "type==Warning")
resource_versionstr \| NoneNoneStart watching from a specific resource version
timeoutint \| NoneNoneTimeout in seconds; None watches indefinitely
+

Returns: Generator — yields watch event dictionaries.

+

Filtering with Field Selectors

+

Field selectors let you narrow down events to exactly what you care about. Combine multiple selectors with commas:

+
# Only Warning events for ClusterServiceVersion resources
+for event in Event.get(
+    client=client,
+    namespace="my-namespace",
+    field_selector="involvedObject.kind==ClusterServiceVersion,type==Warning,reason==AnEventReason",
+    timeout=10,
+):
+    print(event.object.message)
+
+ +

Common field selector fields:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldExample
typetype==Warning or type==Normal
reasonreason==FailedScheduling
involvedObject.kindinvolvedObject.kind==Pod
involvedObject.nameinvolvedObject.name==my-pod
involvedObject.namespaceinvolvedObject.namespace==default
+

Listing Existing Events with Event.list()

+

Unlike Event.get(), which streams events in real-time, Event.list() returns existing events immediately as a list. By default it only returns events from the last 5 minutes.

+
from ocp_resources.event import Event
+
+# List all Warning events from the last 5 minutes
+events = Event.list(
+    client=client,
+    namespace="my-namespace",
+    field_selector="type==Warning",
+)
+
+for event in events:
+    print(f"{event.reason}: {event.message}")
+
+ +

Results are sorted by lastTimestamp descending (most recent first).

+

Event.list() Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
clientDynamicClientKubernetes dynamic client (required)
namespacestr \| NoneNoneFilter events to a specific namespace
field_selectorstr \| NoneNoneFilter by fields (e.g. "type==Warning")
label_selectorstr \| NoneNoneFilter by labels
since_secondsint300Only return events from the last N seconds
+

Returns: list — event resource objects sorted by timestamp (most recent first).

+

Raises: ValueError — if since_seconds is negative.

+
# Events from the last 30 minutes
+events = Event.list(client=client, since_seconds=1800)
+
+# All events (no time filter — uses a very large window)
+events = Event.list(client=client, since_seconds=999999)
+
+ +

When to Use Event.get() vs Event.list()

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Use caseMethod
Watch for new events as they happenEvent.get()
Fetch events that already occurredEvent.list()
Collect events during a test runEvent.get()
Check recent events after a failureEvent.list()
Stream events with a timeoutEvent.get()
Get a snapshot sorted by timeEvent.list()
+

Getting Events for a Specific Resource

+

Every resource instance has an .events() method that automatically filters events to that specific resource by setting involvedObject.name in the field selector.

+
from ocp_resources.pod import Pod
+
+pod = Pod(client=client, name="my-pod", namespace="default")
+
+for event in pod.events(timeout=10):
+    print(f"{event.object.reason}: {event.object.message}")
+
+ +

You can add extra filters on top of the automatic resource filter:

+
# Only Warning events for this specific pod
+for event in pod.events(
+    field_selector="type==Warning",
+    timeout=10,
+):
+    print(event.object.message)
+
+ +

resource.events() Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
namestr""Filter by event name
label_selectorstr""Filter by labels
field_selectorstr""Additional field selectors (combined with involvedObject.name automatically)
resource_versionstr""Start watching from a specific resource version
timeoutint240Timeout in seconds (default: 4 minutes)
+

Returns: Generator — yields watch event dictionaries, same format as Event.get().

+
+

Note: The field_selector you provide is appended to the automatic involvedObject.name==<resource-name> filter. You don't need to specify the resource name yourself.

+
+

Deleting Events with Event.delete_events()

+

Clean up events before a test run to avoid false positives from stale events:

+
from ocp_resources.event import Event
+
+# Delete all events in a namespace
+Event.delete_events(client=client, namespace="my-namespace")
+
+# Delete events matching a specific reason
+Event.delete_events(
+    client=client,
+    namespace="my-namespace",
+    field_selector="reason==AnEventReason",
+)
+
+ +

Event.delete_events() Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
clientDynamicClientNoneKubernetes dynamic client (required)
namespacestr \| NoneNoneTarget namespace
namestr \| NoneNoneSpecific event name to delete
label_selectorstr \| NoneNoneFilter by labels
field_selectorstr \| NoneNoneFilter by fields
resource_versionstr \| NoneNoneFilter by resource version
timeoutint \| NoneNoneTimeout in seconds
+

Returns: None

+

Advanced Usage

+

Test Setup: Clean Events Before Each Test

+

A common pattern is deleting events before a test so that only events generated during the test are captured:

+
from ocp_resources.event import Event
+
+
+def test_pod_scheduling(client, namespace):
+    # Clean slate — remove old events
+    Event.delete_events(client=client, namespace=namespace)
+
+    # ... create resources and trigger the behavior under test ...
+
+    # Verify expected events occurred
+    events = Event.list(
+        client=client,
+        namespace=namespace,
+        field_selector="reason==Scheduled",
+        since_seconds=60,
+    )
+    assert len(events) > 0, "Pod was not scheduled"
+
+ +

Collecting Events During an Operation

+

Use Event.get() with a timeout to capture all events that occur during an operation:

+
from ocp_resources.event import Event
+
+events = []
+for event in Event.get(
+    client=client,
+    namespace="my-namespace",
+    field_selector="involvedObject.kind==Deployment",
+    timeout=30,
+):
+    events.append(event.object)
+    if event.object.reason == "ScalingReplicaSet":
+        break  # Found what we were looking for
+
+print(f"Captured {len(events)} events")
+
+ +

Watching Cluster-Wide Events

+

Omit the namespace parameter to watch events across all namespaces:

+
from ocp_resources.event import Event
+
+# Watch all Warning events cluster-wide
+for event in Event.get(
+    client=client,
+    field_selector="type==Warning",
+    timeout=60,
+):
+    ns = event.object.involvedObject.get("namespace", "cluster-scoped")
+    print(f"[{ns}] {event.object.reason}: {event.object.message}")
+
+ +

Accessing Event Object Properties

+

The event.object yielded by Event.get() and resource.events() is a Kubernetes ResourceInstance with these commonly used attributes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeDescription
event.object.type"Normal" or "Warning"
event.object.reasonShort reason string (e.g. "Scheduled")
event.object.messageHuman-readable event message
event.object.countNumber of times this event occurred
event.object.firstTimestampWhen the event first occurred
event.object.lastTimestampWhen the event most recently occurred
event.object.sourceDict with component and host
event.object.involvedObjectDict with kind, name, namespace, etc.
+

Troubleshooting

+

Event.get() hangs indefinitely +You likely omitted the timeout parameter. Always set a timeout value to avoid blocking forever:

+
# Bad — hangs if no events arrive
+for event in Event.get(client=client, namespace="ns"):
+    ...
+
+# Good — stops after 30 seconds
+for event in Event.get(client=client, namespace="ns", timeout=30):
+    ...
+
+ +

Event.list() returns an empty list

+
    +
  • Check the since_seconds value. The default is 300 (5 minutes). If the events are older, increase the value.
  • +
  • Verify the namespace is correct.
  • +
  • Ensure the field_selector syntax uses == (double equals), not =.
  • +
+

resource.events() returns events for other resources too +This method filters only by involvedObject.name. If multiple resources share the same name across different kinds, add a field_selector to narrow it down:

+
for event in pod.events(
+    field_selector="involvedObject.kind==Pod",
+    timeout=10,
+):
+    print(event.object.message)
+
+ +
+

Warning: The dyn_client parameter on Event.get() and Event.delete_events() is deprecated and will be removed in the next major release. Use client instead.

+
+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/working-with-events.md b/docs/working-with-events.md new file mode 100644 index 0000000000..828044c0dd --- /dev/null +++ b/docs/working-with-events.md @@ -0,0 +1,353 @@ +Let me start by exploring the repository structure and the knowledge graph. + +Now let me explore the Event-related source code: + +Now let me look at the `_build_event_field_selector` function and related event test files: + +Now let me check the `events` test files in the tests directory: + +Now let me check the resource.py import and the `events` method more carefully, and also look at how TIMEOUT_4MINUTES is defined: + +Let me check where TIMEOUT_4MINUTES is defined in resource.py: + +Now let me check the constants value for TIMEOUT_4MINUTES: + +Now let me check how the Event class is used in the fake client: + +Now let me look at the `_extract_event_info` and the `_process_watch_events` functions to understand the event data structure better: + +Now let me look at the event object structure from watch events: + +Now let me check how to properly import and use Event: + +Let me also look at the test file for events in more detail: + +Now let me check the watch method's return structure for events: + +Now I have a thorough understanding. Let me write the documentation. + +# Working with Kubernetes Events + +Monitor, retrieve, and clean up Kubernetes events to debug resource behavior and automate event-driven workflows using the `Event` class and the `resource.events()` method. + +## Prerequisites + +- A connected Kubernetes/OpenShift client — see [Connecting to Clusters](connecting-to-clusters.html) +- The `ocp_resources` package installed — see [Installing and Creating Your First Resource](quickstart.html) + +## Quick Example + +Stream events from a namespace in real-time: + +```python +from ocp_resources.event import Event + +for event in Event.get(client=client, namespace="my-namespace", timeout=30): + print(f"{event['type']}: {event.object.reason} — {event.object.message}") +``` + +## Streaming Events in Real-Time with `Event.get()` + +`Event.get()` opens a watch connection and yields events as they occur. It's a generator that blocks until the timeout expires or you break out of the loop. + +```python +from ocp_resources.event import Event + +for event in Event.get( + client=client, + namespace="my-namespace", + timeout=60, +): + obj = event.object + print(f"[{obj.type}] {obj.reason}: {obj.message}") +``` + +Each yielded event is a watch event dictionary with these keys: + +| Key | Description | +|----------------|--------------------------------------------------------------------| +| `type` | Watch event type: `ADDED`, `MODIFIED`, or `DELETED` | +| `object` | The Event resource object (access `.type`, `.reason`, `.message`, `.involvedObject`, etc.) | +| `raw_object` | The raw dictionary representation of the event | + +### `Event.get()` Parameters + +| Parameter | Type | Default | Description | +|--------------------|-------------------------|----------|-----------------------------------------------------------| +| `client` | `DynamicClient` | `None` | Kubernetes dynamic client (required) | +| `namespace` | `str \| None` | `None` | Filter events to a specific namespace | +| `name` | `str \| None` | `None` | Filter by event name | +| `label_selector` | `str \| None` | `None` | Filter by labels (e.g. `"app=nginx"`) | +| `field_selector` | `str \| None` | `None` | Filter by fields (e.g. `"type==Warning"`) | +| `resource_version` | `str \| None` | `None` | Start watching from a specific resource version | +| `timeout` | `int \| None` | `None` | Timeout in seconds; `None` watches indefinitely | + +**Returns:** `Generator` — yields watch event dictionaries. + +### Filtering with Field Selectors + +Field selectors let you narrow down events to exactly what you care about. Combine multiple selectors with commas: + +```python +# Only Warning events for ClusterServiceVersion resources +for event in Event.get( + client=client, + namespace="my-namespace", + field_selector="involvedObject.kind==ClusterServiceVersion,type==Warning,reason==AnEventReason", + timeout=10, +): + print(event.object.message) +``` + +Common field selector fields: + +| Field | Example | +|------------------------------|-------------------------------------------------| +| `type` | `type==Warning` or `type==Normal` | +| `reason` | `reason==FailedScheduling` | +| `involvedObject.kind` | `involvedObject.kind==Pod` | +| `involvedObject.name` | `involvedObject.name==my-pod` | +| `involvedObject.namespace` | `involvedObject.namespace==default` | + +## Listing Existing Events with `Event.list()` + +Unlike `Event.get()`, which streams events in real-time, `Event.list()` returns existing events immediately as a list. By default it only returns events from the last 5 minutes. + +```python +from ocp_resources.event import Event + +# List all Warning events from the last 5 minutes +events = Event.list( + client=client, + namespace="my-namespace", + field_selector="type==Warning", +) + +for event in events: + print(f"{event.reason}: {event.message}") +``` + +Results are sorted by `lastTimestamp` descending (most recent first). + +### `Event.list()` Parameters + +| Parameter | Type | Default | Description | +|------------------|---------------------|---------|-----------------------------------------------------------| +| `client` | `DynamicClient` | — | Kubernetes dynamic client (required) | +| `namespace` | `str \| None` | `None` | Filter events to a specific namespace | +| `field_selector` | `str \| None` | `None` | Filter by fields (e.g. `"type==Warning"`) | +| `label_selector` | `str \| None` | `None` | Filter by labels | +| `since_seconds` | `int` | `300` | Only return events from the last N seconds | + +**Returns:** `list` — event resource objects sorted by timestamp (most recent first). + +**Raises:** `ValueError` — if `since_seconds` is negative. + +```python +# Events from the last 30 minutes +events = Event.list(client=client, since_seconds=1800) + +# All events (no time filter — uses a very large window) +events = Event.list(client=client, since_seconds=999999) +``` + +### When to Use `Event.get()` vs `Event.list()` + +| Use case | Method | +|--------------------------------------------------|----------------| +| Watch for new events as they happen | `Event.get()` | +| Fetch events that already occurred | `Event.list()` | +| Collect events during a test run | `Event.get()` | +| Check recent events after a failure | `Event.list()` | +| Stream events with a timeout | `Event.get()` | +| Get a snapshot sorted by time | `Event.list()` | + +## Getting Events for a Specific Resource + +Every resource instance has an `.events()` method that automatically filters events to that specific resource by setting `involvedObject.name` in the field selector. + +```python +from ocp_resources.pod import Pod + +pod = Pod(client=client, name="my-pod", namespace="default") + +for event in pod.events(timeout=10): + print(f"{event.object.reason}: {event.object.message}") +``` + +You can add extra filters on top of the automatic resource filter: + +```python +# Only Warning events for this specific pod +for event in pod.events( + field_selector="type==Warning", + timeout=10, +): + print(event.object.message) +``` + +### `resource.events()` Parameters + +| Parameter | Type | Default | Description | +|--------------------|---------|---------|-----------------------------------------------------------| +| `name` | `str` | `""` | Filter by event name | +| `label_selector` | `str` | `""` | Filter by labels | +| `field_selector` | `str` | `""` | Additional field selectors (combined with `involvedObject.name` automatically) | +| `resource_version` | `str` | `""` | Start watching from a specific resource version | +| `timeout` | `int` | `240` | Timeout in seconds (default: 4 minutes) | + +**Returns:** `Generator` — yields watch event dictionaries, same format as `Event.get()`. + +> **Note:** The `field_selector` you provide is appended to the automatic `involvedObject.name==` filter. You don't need to specify the resource name yourself. + +## Deleting Events with `Event.delete_events()` + +Clean up events before a test run to avoid false positives from stale events: + +```python +from ocp_resources.event import Event + +# Delete all events in a namespace +Event.delete_events(client=client, namespace="my-namespace") + +# Delete events matching a specific reason +Event.delete_events( + client=client, + namespace="my-namespace", + field_selector="reason==AnEventReason", +) +``` + +### `Event.delete_events()` Parameters + +| Parameter | Type | Default | Description | +|--------------------|-------------------------|----------|-----------------------------------------------------------| +| `client` | `DynamicClient` | `None` | Kubernetes dynamic client (required) | +| `namespace` | `str \| None` | `None` | Target namespace | +| `name` | `str \| None` | `None` | Specific event name to delete | +| `label_selector` | `str \| None` | `None` | Filter by labels | +| `field_selector` | `str \| None` | `None` | Filter by fields | +| `resource_version` | `str \| None` | `None` | Filter by resource version | +| `timeout` | `int \| None` | `None` | Timeout in seconds | + +**Returns:** `None` + +## Advanced Usage + +### Test Setup: Clean Events Before Each Test + +A common pattern is deleting events before a test so that only events generated during the test are captured: + +```python +from ocp_resources.event import Event + + +def test_pod_scheduling(client, namespace): + # Clean slate — remove old events + Event.delete_events(client=client, namespace=namespace) + + # ... create resources and trigger the behavior under test ... + + # Verify expected events occurred + events = Event.list( + client=client, + namespace=namespace, + field_selector="reason==Scheduled", + since_seconds=60, + ) + assert len(events) > 0, "Pod was not scheduled" +``` + +### Collecting Events During an Operation + +Use `Event.get()` with a timeout to capture all events that occur during an operation: + +```python +from ocp_resources.event import Event + +events = [] +for event in Event.get( + client=client, + namespace="my-namespace", + field_selector="involvedObject.kind==Deployment", + timeout=30, +): + events.append(event.object) + if event.object.reason == "ScalingReplicaSet": + break # Found what we were looking for + +print(f"Captured {len(events)} events") +``` + +### Watching Cluster-Wide Events + +Omit the `namespace` parameter to watch events across all namespaces: + +```python +from ocp_resources.event import Event + +# Watch all Warning events cluster-wide +for event in Event.get( + client=client, + field_selector="type==Warning", + timeout=60, +): + ns = event.object.involvedObject.get("namespace", "cluster-scoped") + print(f"[{ns}] {event.object.reason}: {event.object.message}") +``` + +### Accessing Event Object Properties + +The `event.object` yielded by `Event.get()` and `resource.events()` is a Kubernetes `ResourceInstance` with these commonly used attributes: + +| Attribute | Description | +|-------------------------------|----------------------------------------------| +| `event.object.type` | `"Normal"` or `"Warning"` | +| `event.object.reason` | Short reason string (e.g. `"Scheduled"`) | +| `event.object.message` | Human-readable event message | +| `event.object.count` | Number of times this event occurred | +| `event.object.firstTimestamp` | When the event first occurred | +| `event.object.lastTimestamp` | When the event most recently occurred | +| `event.object.source` | Dict with `component` and `host` | +| `event.object.involvedObject` | Dict with `kind`, `name`, `namespace`, etc. | + +## Troubleshooting + +**`Event.get()` hangs indefinitely** +You likely omitted the `timeout` parameter. Always set a `timeout` value to avoid blocking forever: + +```python +# Bad — hangs if no events arrive +for event in Event.get(client=client, namespace="ns"): + ... + +# Good — stops after 30 seconds +for event in Event.get(client=client, namespace="ns", timeout=30): + ... +``` + +**`Event.list()` returns an empty list** +- Check the `since_seconds` value. The default is 300 (5 minutes). If the events are older, increase the value. +- Verify the `namespace` is correct. +- Ensure the `field_selector` syntax uses `==` (double equals), not `=`. + +**`resource.events()` returns events for other resources too** +This method filters only by `involvedObject.name`. If multiple resources share the same name across different kinds, add a `field_selector` to narrow it down: + +```python +for event in pod.events( + field_selector="involvedObject.kind==Pod", + timeout=10, +): + print(event.object.message) +``` + +> **Warning:** The `dyn_client` parameter on `Event.get()` and `Event.delete_events()` is deprecated and will be removed in the next major release. Use `client` instead. + +## Related Pages + +- [Querying and Watching Resources](querying-resources.html) +- [Common Resource Patterns](common-patterns.html) +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Creating and Managing Resources](creating-and-managing-resources.html) diff --git a/docs/working-with-virtual-machines.html b/docs/working-with-virtual-machines.html new file mode 100644 index 0000000000..c5e111c5d7 --- /dev/null +++ b/docs/working-with-virtual-machines.html @@ -0,0 +1,1231 @@ + + + + + + Working with Virtual Machines (KubeVirt) - openshift-python-wrapper + + + + + + + + + +
+
+
+ + openshift-python-wrapper +
+
+
+ + + ⚡ docsfy + + + + + + + + +
+
+ +
+ +
+
+ +

Let me start by exploring the knowledge graph and understanding the repository structure.

+

Now let me look at the remaining VM-related resources and check the RunStrategy and Status constants, as well as the data_volume.py:

+

Now let me check how the api_request method works in the base Resource class:

+

Now let me look at the data_volume.py to check any VM-related interactions:

+

Now let me check the RunStrategy constants in VirtualMachineInstance:

+

Now let me look at the Status class in the base Resource for context and the VMI's RunStrategy:

+

Let me find the Status class definition:

+

Now let me check the ApiGroup constants for KubeVirt:

+

Now let me check for any tests related to VMs:

+

Now let me check the deploy and clean_up method signatures:

+

Now I have a thorough understanding of the VM-related API. Let me write the documentation:

+

Working with Virtual Machines (KubeVirt)

+

Manage the full lifecycle of KubeVirt virtual machines on OpenShift — create VMs, start and stop them, live-migrate between nodes, take snapshots, and interact with their running instances — all from Python.

+

Prerequisites

+ +

Quick Example

+
from ocp_resources.virtual_machine import VirtualMachine
+
+vm = VirtualMachine(
+    name="my-vm",
+    namespace="my-namespace",
+    body={
+        "spec": {
+            "domain": {
+                "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]},
+                "resources": {"requests": {"memory": "1Gi"}},
+            },
+            "volumes": [{"name": "rootdisk", "containerDisk": {"image": "quay.io/containerdisks/fedora"}}],
+        }
+    },
+)
+
+with vm:
+    vm.start(wait=True)
+    print(f"VM ready: {vm.ready}")       # True
+    print(f"Status: {vm.printable_status}")  # "Running"
+    vm.stop(wait=True)
+# VM is automatically cleaned up when the context manager exits
+
+ +

Creating a VirtualMachine

+
from ocp_resources.virtual_machine import VirtualMachine
+
+vm = VirtualMachine(
+    name="my-vm",
+    namespace="my-namespace",
+    body={
+        "spec": {
+            "domain": {
+                "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]},
+                "resources": {"requests": {"memory": "2Gi"}},
+            },
+            "volumes": [
+                {
+                    "name": "rootdisk",
+                    "containerDisk": {"image": "quay.io/containerdisks/fedora"},
+                }
+            ],
+        }
+    },
+)
+vm.deploy()
+
+ +

The body parameter accepts a dictionary that maps to the KubeVirt VM spec.template structure. If you omit body, a minimal skeleton ({"template": {"spec": {}}}) is created automatically.

+

You can also create a VM from a YAML file:

+
vm = VirtualMachine(
+    name="my-vm",
+    namespace="my-namespace",
+    yaml_file="vm-definition.yaml",
+)
+vm.deploy()
+
+ +

Constructor Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
namestrNoneName of the VirtualMachine resource
namespacestrNoneTarget namespace
clientDynamicClientNoneKubernetes client; auto-resolved if omitted
bodydictNoneVM spec body dictionary
teardownboolTrueWhether to delete the resource on cleanup
yaml_filestrNonePath to a YAML file defining the VM
delete_timeoutint240 (seconds)Timeout for delete operations
+
+

Tip: Use the context manager (with vm:) to ensure automatic cleanup. See Creating and Managing Resources for details on deploy(), clean_up(), and context managers.

+
+

Starting, Stopping, and Restarting

+

Start a VM

+
# Fire-and-forget
+vm.start()
+
+# Wait until the VM is ready
+vm.start(wait=True)
+
+# Custom timeout (seconds)
+vm.start(timeout=600, wait=True)
+
+ +

Stop a VM

+
# Fire-and-forget
+vm.stop()
+
+# Wait for the VM to stop and the VMI to be deleted
+vm.stop(wait=True)
+
+# Custom timeouts
+vm.stop(timeout=300, vmi_delete_timeout=300, wait=True)
+
+ +

Restart a VM

+
vm.restart(wait=True)
+
+ +

When wait=True, restart() waits for the old virt-launcher pod to be deleted and the new VMI to reach the Running state.

+

Checking VM Status

+
# Boolean ready status: True if running, None if stopped
+vm.ready  # True
+
+# Human-readable status string
+vm.printable_status  # "Running", "Stopped", "Starting", "Migrating", etc.
+
+# Wait for ready status
+vm.wait_for_ready_status(status=True, timeout=300)   # Wait for running
+vm.wait_for_ready_status(status=None, timeout=300)    # Wait for stopped
+
+# Wait for a specific status field to clear
+vm.wait_for_status_none(status="snapshotInProgress")
+
+ +

Run Strategy Constants

+

Use built-in constants for the runStrategy field:

+
VirtualMachine.RunStrategy.ALWAYS            # "Always"
+VirtualMachine.RunStrategy.HALTED            # "Halted"
+VirtualMachine.RunStrategy.MANUAL            # "Manual"
+VirtualMachine.RunStrategy.RERUNONFAILURE    # "RerunOnFailure"
+
+ +

Status Constants

+
VirtualMachine.Status.STARTING       # "Starting"
+VirtualMachine.Status.RUNNING        # "Running"  (inherited)
+VirtualMachine.Status.STOPPED        # "Stopped"
+VirtualMachine.Status.STOPPING       # "Stopping"
+VirtualMachine.Status.MIGRATING      # "Migrating"
+VirtualMachine.Status.PAUSED         # "Paused"
+VirtualMachine.Status.PROVISIONING   # "Provisioning"
+VirtualMachine.Status.ERROR_UNSCHEDULABLE  # "ErrorUnschedulable"
+VirtualMachine.Status.DATAVOLUME_ERROR     # "DataVolumeError"
+
+ +

Working with VirtualMachineInstances

+

Every running VM has an associated VirtualMachineInstance (VMI). Access it through the VM or create one directly.

+

Accessing the VMI from a VM

+
vmi = vm.vmi  # Returns a VirtualMachineInstance object
+
+ +

Creating a Standalone VMI

+
from ocp_resources.virtual_machine_instance import VirtualMachineInstance
+
+vmi = VirtualMachineInstance(
+    name="my-vmi",
+    namespace="my-namespace",
+    domain={
+        "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]},
+        "resources": {"requests": {"memory": "1Gi"}},
+    },
+    volumes=[
+        {"name": "rootdisk", "containerDisk": {"image": "quay.io/containerdisks/fedora"}}
+    ],
+)
+vmi.deploy()
+
+ +
+

Note: The domain parameter is required when creating a VMI programmatically. Omitting it raises MissingRequiredArgumentError.

+
+

VMI Constructor Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeRequiredDescription
domaindictYesDomain spec (CPU, memory, devices)
volumeslistNoVolumes to mount
networkslistNoNetwork interfaces
affinitydictNoScheduling affinity rules
node_selectordictNoNode label selector
tolerationslistNoToleration rules
eviction_strategystrNo"LiveMigrate", "LiveMigrateIfPossible", "None", or "External"
termination_grace_period_secondsintNoSeconds to wait before force-killing
hostnamestrNoVM hostname
priority_class_namestrNoPod priority class
scheduler_namestrNoCustom scheduler name
start_strategystrNoSet to "Paused" to start paused
access_credentialslistNoPublic keys to inject
topology_spread_constraintslistNoTopology spread rules
+

Waiting for a VMI to Run

+
vmi.wait_until_running(timeout=300)
+
+ +

If the VMI fails to start, wait_until_running raises TimeoutExpiredError and logs the virt-launcher pod status and logs for debugging. Set logs=False to suppress this:

+
vmi.wait_until_running(timeout=300, logs=False)
+
+ +

You can also specify a stop_status to abort early if the VMI enters a failure state:

+
vmi.wait_until_running(timeout=300, stop_status="Failed")
+
+ +

Pausing and Unpausing

+
vmi.pause(wait=True)    # Pause the VMI
+vmi.unpause(wait=True)  # Resume the VMI
+
+ +

Resetting a VMI

+
vmi.reset()  # Hard-reset the VMI (like pressing the reset button)
+
+ +

Getting the VMI Node

+
node = vmi.get_node()
+print(node.name)
+
+ +

Network Interfaces

+
# All interfaces from the VMI status
+interfaces = vmi.interfaces
+
+# Get IP address for a specific interface
+ip = vmi.interface_ip("eth0")
+
+ +

Guest Agent Information

+

When the QEMU guest agent is installed in the VM:

+
vmi.guest_os_info       # OS information
+vmi.guest_fs_info       # Filesystem information
+vmi.guest_user_info     # Logged-in user information
+vmi.os_version          # OS version string
+
+ +
+

Note: If no guest agent is installed, os_version returns an empty dict and logs a warning.

+
+

Accessing the Virt-Launcher Pod

+
pod = vmi.get_virt_launcher_pod()
+print(pod.name)
+print(pod.status)
+
+# Access logs from the compute container
+logs = pod.log(container="compute")
+
+ +

For clusters where a privileged client is required:

+
pod = vmi.get_virt_launcher_pod(privileged_client=admin_client)
+
+ +

Accessing the Virt-Handler Pod

+
handler_pod = vmi.get_virt_handler_pod(privileged_client=admin_client)
+
+ +

Executing Virsh Commands

+

Run virsh commands inside the virt-launcher pod:

+
# Get the XML definition
+xml_output = vmi.get_xml()
+
+# Get the XML as a parsed Python dict
+xml_dict = vmi.get_xml_dict()
+
+# Get domain memory stats
+memstats = vmi.get_dommemstat()
+
+# Run any arbitrary virsh command
+output = vmi.execute_virsh_command(command="dominfo")
+
+ +

Checking Root Status

+
pod = vmi.get_virt_launcher_pod()
+
+# Check if the virt-launcher pod runs as root
+VirtualMachineInstance.is_pod_root(pod=pod)  # True or False
+
+# Get the pod's runAsUser UID
+VirtualMachineInstance.get_pod_user_uid(pod=pod)  # int or None
+
+ +

Live Migration

+

Trigger a live migration by creating a VirtualMachineInstanceMigration:

+
from ocp_resources.virtual_machine_instance_migration import VirtualMachineInstanceMigration
+
+migration = VirtualMachineInstanceMigration(
+    name="migrate-my-vm",
+    namespace="my-namespace",
+    vmi_name="my-vm",
+)
+migration.deploy()
+
+ +

Monitor migration by checking the VMI's status. Once migration completes, the VMI runs on the target node. See Waiting for Resource Conditions and Status for condition-based waiting.

+

Migration Resource Quotas

+

Control resource allocation for migrations:

+
from ocp_resources.virtual_machine_migration_resource_quota import VirtualMachineMigrationResourceQuota
+
+quota = VirtualMachineMigrationResourceQuota(
+    name="migration-quota",
+    namespace="my-namespace",
+    requests_cpu="2",
+    requests_memory="4Gi",
+    limits_cpu="4",
+    limits_memory="8Gi",
+)
+quota.deploy()
+
+ +

Snapshots and Restores

+

Taking a Snapshot

+
from ocp_resources.virtual_machine_snapshot import VirtualMachineSnapshot
+
+snapshot = VirtualMachineSnapshot(
+    name="my-vm-snapshot",
+    namespace="my-namespace",
+    vm_name="my-vm",
+)
+snapshot.deploy()
+
+# Wait for the snapshot to be ready
+snapshot.wait_ready_to_use(timeout=300)
+
+# Wait for both snapshot readiness AND the VM's snapshotInProgress to clear
+snapshot.wait_snapshot_done(timeout=300)
+
+ +

Restoring from a Snapshot

+
from ocp_resources.virtual_machine_restore import VirtualMachineRestore
+
+restore = VirtualMachineRestore(
+    name="my-vm-restore",
+    namespace="my-namespace",
+    vm_name="my-vm",
+    snapshot_name="my-vm-snapshot",
+)
+restore.deploy()
+
+# Wait for restore to complete
+restore.wait_restore_done(timeout=300)
+
+ +

The wait_restore_done() method checks both the restore's complete status and that the VM's restoreInProgress field is cleared.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDescription
vm_namestrTarget VirtualMachine name
snapshot_namestrSource VirtualMachineSnapshot name
volume_restore_policystrOptional policy for volume restoration
+

Cloning VMs

+
from ocp_resources.virtual_machine_clone import VirtualMachineClone
+
+clone = VirtualMachineClone(
+    name="clone-my-vm",
+    namespace="my-namespace",
+    source_name="my-vm",
+    target_name="my-vm-clone",
+)
+clone.deploy()
+
+ +
+

Note: source_name is required. If omitted, MissingRequiredArgumentError is raised.

+
+

Clone Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeDefaultDescription
source_namestrRequiredName of the source VM
source_kindstr"VirtualMachine"Kind of the source resource
target_namestrAuto-generatedName for the cloned VM
label_filterslistNoneLabels to copy, e.g. ["*", "!someKey/*"]
annotation_filterslistNoneAnnotations to copy
new_mac_addressesdictNoneOverride MACs: {"nic0": "00:11:22:33:44:55"}
new_smbios_serialstrNoneOverride SMBIOS serial number
volume_name_policystrNoneVolume naming policy for the clone
+

Exporting VMs

+
from ocp_resources.virtual_machine_export import VirtualMachineExport
+
+export = VirtualMachineExport(
+    name="export-my-vm",
+    namespace="my-namespace",
+    source={"apiGroup": "kubevirt.io", "kind": "VirtualMachine", "name": "my-vm"},
+    token_secret_ref="my-export-token",
+    ttl_duration="1h",
+)
+export.deploy()
+
+ +
+

Note: The source parameter is required. If omitted, MissingRequiredArgumentError is raised.

+
+

Advanced Usage

+

Instance Types and Preferences

+

KubeVirt instance types define reusable VM sizing profiles. Preferences define non-sizing defaults (clock, firmware, devices).

+

Namespaced Instance Type

+
from ocp_resources.virtual_machine_instancetype import VirtualMachineInstancetype
+
+instancetype = VirtualMachineInstancetype(
+    name="small",
+    namespace="my-namespace",
+    cpu={"guest": 2},
+    memory={"guest": "4Gi"},
+)
+instancetype.deploy()
+
+ +

Both cpu and memory are required. Omitting either raises MissingRequiredArgumentError.

+

Cluster-Scoped Instance Type

+
from ocp_resources.virtual_machine_cluster_instancetype import VirtualMachineClusterInstancetype
+
+cluster_instancetype = VirtualMachineClusterInstancetype(
+    name="large",
+    cpu={"guest": 8},
+    memory={"guest": "16Gi"},
+    gpus=[{"deviceName": "nvidia.com/A100", "name": "gpu0"}],
+)
+cluster_instancetype.deploy()
+
+ +

Namespaced Preference

+
from ocp_resources.virtual_machine_preference import VirtualMachinePreference
+
+preference = VirtualMachinePreference(
+    name="linux-pref",
+    namespace="my-namespace",
+    cpu={"preferredCPUTopology": "preferSockets"},
+    firmware={"preferredUseEfi": True},
+)
+preference.deploy()
+
+ +

Cluster-Scoped Preference

+
from ocp_resources.virtual_machine_cluster_preference import VirtualMachineClusterPreference
+
+cluster_preference = VirtualMachineClusterPreference(
+    name="windows-pref",
+    clock={"preferredTimer": {"hpet": {"present": False}}},
+    features={"preferredHyperv": {"spinlocks": {"spinlocks": 8191}}},
+)
+cluster_preference.deploy()
+
+ +

VMI Replica Sets

+

Scale out VMIs horizontally:

+
from ocp_resources.virtual_machine_instance_replica_set import VirtualMachineInstanceReplicaSet
+
+replica_set = VirtualMachineInstanceReplicaSet(
+    name="my-vmi-rs",
+    namespace="my-namespace",
+    replicas=3,
+    selector={"matchLabels": {"app": "my-vmi"}},
+    template={
+        "metadata": {"labels": {"app": "my-vmi"}},
+        "spec": {
+            "domain": {
+                "devices": {},
+                "resources": {"requests": {"memory": "1Gi"}},
+            }
+        },
+    },
+)
+replica_set.deploy()
+
+ +

Both selector and template are required.

+

Storage Migration

+

Migrate VM storage between storage classes:

+
from ocp_resources.virtual_machine_storage_migration_plan import VirtualMachineStorageMigrationPlan
+from ocp_resources.virtual_machine_storage_migration import VirtualMachineStorageMigration
+
+# Create a migration plan
+plan = VirtualMachineStorageMigrationPlan(
+    name="migrate-storage",
+    namespace="my-namespace",
+    virtual_machines=[
+        {"name": "my-vm", "volumes": [{"name": "rootdisk", "target": {"storageClassName": "fast-ssd"}}]}
+    ],
+    retention_policy="deleteSource",
+)
+plan.deploy()
+
+# Execute the migration
+migration = VirtualMachineStorageMigration(
+    name="migrate-storage-exec",
+    namespace="my-namespace",
+    virtual_machine_storage_migration_plan_ref={"name": "migrate-storage", "namespace": "my-namespace"},
+)
+migration.deploy()
+
+ +

VM Templates

+
from ocp_resources.virtual_machine_template import VirtualMachineTemplate
+
+template = VirtualMachineTemplate(
+    name="fedora-template",
+    namespace="my-namespace",
+    virtual_machine={
+        "spec": {
+            "template": {
+                "spec": {
+                    "domain": {
+                        "devices": {},
+                        "resources": {"requests": {"memory": "2Gi"}},
+                    }
+                }
+            }
+        }
+    },
+    parameters=[
+        {"name": "VM_NAME", "description": "Name of the VM", "required": True}
+    ],
+    message="Use this template to create Fedora VMs.",
+)
+template.deploy()
+
+ +

Listing VMs Across Namespaces

+
# List all VMs in a namespace
+for vm in VirtualMachine.get(namespace="my-namespace"):
+    print(f"{vm.name}: {vm.instance.get('status', {}).get('printableStatus')}")
+
+# List with label selectors
+for vm in VirtualMachine.get(
+    namespace="my-namespace",
+    label_selector="app=webserver",
+):
+    print(vm.name)
+
+ +

See Querying and Watching Resources for more query options.

+

Getting VM Network Interfaces

+
interfaces = vm.get_interfaces()
+for iface in interfaces:
+    print(f"Name: {iface['name']}, Model: {iface.get('model', 'default')}")
+
+ +

Troubleshooting

+

VM Stuck in "Starting" Status

+

Check the VMI's virt-launcher pod for scheduling or image-pull issues:

+
vmi = vm.vmi
+try:
+    pod = vmi.get_virt_launcher_pod()
+    print(f"Pod status: {pod.status}")
+    print(pod.log(container="compute"))
+except ResourceNotFoundError:
+    print("Virt-launcher pod not found — VM may not have been scheduled yet")
+
+ +

TimeoutExpiredError on start/stop

+

Increase the timeout parameter:

+
vm.start(timeout=600, wait=True)  # 10 minutes instead of default 4
+
+ +

MissingRequiredArgumentError

+

This is raised when a required constructor parameter is missing. Common cases:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ResourceRequired Parameters
VirtualMachineInstancedomain
VirtualMachineInstancetypecpu, memory
VirtualMachineClonesource_name
VirtualMachineExportsource
VirtualMachineInstanceReplicaSetselector, template
VirtualMachineStorageMigrationvirtual_machine_storage_migration_plan_ref
VirtualMachineTemplatevirtual_machine
+

Guest Agent Not Reporting

+

If vmi.os_version returns an empty dict, the QEMU guest agent is not installed or not running inside the VM. Install and start qemu-guest-agent in the guest OS.

+ + +
+
+ + + + + +
+ + +
+ + + + + + + + + + + + + + diff --git a/docs/working-with-virtual-machines.md b/docs/working-with-virtual-machines.md new file mode 100644 index 0000000000..66aa4d27b6 --- /dev/null +++ b/docs/working-with-virtual-machines.md @@ -0,0 +1,701 @@ +Let me start by exploring the knowledge graph and understanding the repository structure. + +Now let me look at the remaining VM-related resources and check the `RunStrategy` and `Status` constants, as well as the `data_volume.py`: + +Now let me check how the `api_request` method works in the base `Resource` class: + +Now let me look at the `data_volume.py` to check any VM-related interactions: + +Now let me check the `RunStrategy` constants in VirtualMachineInstance: + +Now let me look at the `Status` class in the base `Resource` for context and the VMI's `RunStrategy`: + +Let me find the Status class definition: + +Now let me check the `ApiGroup` constants for KubeVirt: + +Now let me check for any tests related to VMs: + +Now let me check the `deploy` and `clean_up` method signatures: + +Now I have a thorough understanding of the VM-related API. Let me write the documentation: + +# Working with Virtual Machines (KubeVirt) + +Manage the full lifecycle of KubeVirt virtual machines on OpenShift — create VMs, start and stop them, live-migrate between nodes, take snapshots, and interact with their running instances — all from Python. + +## Prerequisites + +- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html)) +- A cluster connection configured (see [Connecting to Clusters](connecting-to-clusters.html)) +- KubeVirt / OpenShift Virtualization installed on your cluster + +## Quick Example + +```python +from ocp_resources.virtual_machine import VirtualMachine + +vm = VirtualMachine( + name="my-vm", + namespace="my-namespace", + body={ + "spec": { + "domain": { + "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]}, + "resources": {"requests": {"memory": "1Gi"}}, + }, + "volumes": [{"name": "rootdisk", "containerDisk": {"image": "quay.io/containerdisks/fedora"}}], + } + }, +) + +with vm: + vm.start(wait=True) + print(f"VM ready: {vm.ready}") # True + print(f"Status: {vm.printable_status}") # "Running" + vm.stop(wait=True) +# VM is automatically cleaned up when the context manager exits +``` + +## Creating a VirtualMachine + +```python +from ocp_resources.virtual_machine import VirtualMachine + +vm = VirtualMachine( + name="my-vm", + namespace="my-namespace", + body={ + "spec": { + "domain": { + "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]}, + "resources": {"requests": {"memory": "2Gi"}}, + }, + "volumes": [ + { + "name": "rootdisk", + "containerDisk": {"image": "quay.io/containerdisks/fedora"}, + } + ], + } + }, +) +vm.deploy() +``` + +The `body` parameter accepts a dictionary that maps to the KubeVirt VM `spec.template` structure. If you omit `body`, a minimal skeleton (`{"template": {"spec": {}}}`) is created automatically. + +You can also create a VM from a YAML file: + +```python +vm = VirtualMachine( + name="my-vm", + namespace="my-namespace", + yaml_file="vm-definition.yaml", +) +vm.deploy() +``` + +### Constructor Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `name` | `str` | `None` | Name of the VirtualMachine resource | +| `namespace` | `str` | `None` | Target namespace | +| `client` | `DynamicClient` | `None` | Kubernetes client; auto-resolved if omitted | +| `body` | `dict` | `None` | VM spec body dictionary | +| `teardown` | `bool` | `True` | Whether to delete the resource on cleanup | +| `yaml_file` | `str` | `None` | Path to a YAML file defining the VM | +| `delete_timeout` | `int` | `240` (seconds) | Timeout for delete operations | + +> **Tip:** Use the context manager (`with vm:`) to ensure automatic cleanup. See [Creating and Managing Resources](creating-and-managing-resources.html) for details on `deploy()`, `clean_up()`, and context managers. + +## Starting, Stopping, and Restarting + +### Start a VM + +```python +# Fire-and-forget +vm.start() + +# Wait until the VM is ready +vm.start(wait=True) + +# Custom timeout (seconds) +vm.start(timeout=600, wait=True) +``` + +### Stop a VM + +```python +# Fire-and-forget +vm.stop() + +# Wait for the VM to stop and the VMI to be deleted +vm.stop(wait=True) + +# Custom timeouts +vm.stop(timeout=300, vmi_delete_timeout=300, wait=True) +``` + +### Restart a VM + +```python +vm.restart(wait=True) +``` + +When `wait=True`, `restart()` waits for the old virt-launcher pod to be deleted and the new VMI to reach the `Running` state. + +## Checking VM Status + +```python +# Boolean ready status: True if running, None if stopped +vm.ready # True + +# Human-readable status string +vm.printable_status # "Running", "Stopped", "Starting", "Migrating", etc. + +# Wait for ready status +vm.wait_for_ready_status(status=True, timeout=300) # Wait for running +vm.wait_for_ready_status(status=None, timeout=300) # Wait for stopped + +# Wait for a specific status field to clear +vm.wait_for_status_none(status="snapshotInProgress") +``` + +### Run Strategy Constants + +Use built-in constants for the `runStrategy` field: + +```python +VirtualMachine.RunStrategy.ALWAYS # "Always" +VirtualMachine.RunStrategy.HALTED # "Halted" +VirtualMachine.RunStrategy.MANUAL # "Manual" +VirtualMachine.RunStrategy.RERUNONFAILURE # "RerunOnFailure" +``` + +### Status Constants + +```python +VirtualMachine.Status.STARTING # "Starting" +VirtualMachine.Status.RUNNING # "Running" (inherited) +VirtualMachine.Status.STOPPED # "Stopped" +VirtualMachine.Status.STOPPING # "Stopping" +VirtualMachine.Status.MIGRATING # "Migrating" +VirtualMachine.Status.PAUSED # "Paused" +VirtualMachine.Status.PROVISIONING # "Provisioning" +VirtualMachine.Status.ERROR_UNSCHEDULABLE # "ErrorUnschedulable" +VirtualMachine.Status.DATAVOLUME_ERROR # "DataVolumeError" +``` + +## Working with VirtualMachineInstances + +Every running VM has an associated `VirtualMachineInstance` (VMI). Access it through the VM or create one directly. + +### Accessing the VMI from a VM + +```python +vmi = vm.vmi # Returns a VirtualMachineInstance object +``` + +### Creating a Standalone VMI + +```python +from ocp_resources.virtual_machine_instance import VirtualMachineInstance + +vmi = VirtualMachineInstance( + name="my-vmi", + namespace="my-namespace", + domain={ + "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]}, + "resources": {"requests": {"memory": "1Gi"}}, + }, + volumes=[ + {"name": "rootdisk", "containerDisk": {"image": "quay.io/containerdisks/fedora"}} + ], +) +vmi.deploy() +``` + +> **Note:** The `domain` parameter is **required** when creating a VMI programmatically. Omitting it raises `MissingRequiredArgumentError`. + +### VMI Constructor Parameters + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `domain` | `dict` | **Yes** | Domain spec (CPU, memory, devices) | +| `volumes` | `list` | No | Volumes to mount | +| `networks` | `list` | No | Network interfaces | +| `affinity` | `dict` | No | Scheduling affinity rules | +| `node_selector` | `dict` | No | Node label selector | +| `tolerations` | `list` | No | Toleration rules | +| `eviction_strategy` | `str` | No | `"LiveMigrate"`, `"LiveMigrateIfPossible"`, `"None"`, or `"External"` | +| `termination_grace_period_seconds` | `int` | No | Seconds to wait before force-killing | +| `hostname` | `str` | No | VM hostname | +| `priority_class_name` | `str` | No | Pod priority class | +| `scheduler_name` | `str` | No | Custom scheduler name | +| `start_strategy` | `str` | No | Set to `"Paused"` to start paused | +| `access_credentials` | `list` | No | Public keys to inject | +| `topology_spread_constraints` | `list` | No | Topology spread rules | + +### Waiting for a VMI to Run + +```python +vmi.wait_until_running(timeout=300) +``` + +If the VMI fails to start, `wait_until_running` raises `TimeoutExpiredError` and logs the virt-launcher pod status and logs for debugging. Set `logs=False` to suppress this: + +```python +vmi.wait_until_running(timeout=300, logs=False) +``` + +You can also specify a `stop_status` to abort early if the VMI enters a failure state: + +```python +vmi.wait_until_running(timeout=300, stop_status="Failed") +``` + +### Pausing and Unpausing + +```python +vmi.pause(wait=True) # Pause the VMI +vmi.unpause(wait=True) # Resume the VMI +``` + +### Resetting a VMI + +```python +vmi.reset() # Hard-reset the VMI (like pressing the reset button) +``` + +### Getting the VMI Node + +```python +node = vmi.get_node() +print(node.name) +``` + +### Network Interfaces + +```python +# All interfaces from the VMI status +interfaces = vmi.interfaces + +# Get IP address for a specific interface +ip = vmi.interface_ip("eth0") +``` + +### Guest Agent Information + +When the QEMU guest agent is installed in the VM: + +```python +vmi.guest_os_info # OS information +vmi.guest_fs_info # Filesystem information +vmi.guest_user_info # Logged-in user information +vmi.os_version # OS version string +``` + +> **Note:** If no guest agent is installed, `os_version` returns an empty dict and logs a warning. + +### Accessing the Virt-Launcher Pod + +```python +pod = vmi.get_virt_launcher_pod() +print(pod.name) +print(pod.status) + +# Access logs from the compute container +logs = pod.log(container="compute") +``` + +For clusters where a privileged client is required: + +```python +pod = vmi.get_virt_launcher_pod(privileged_client=admin_client) +``` + +### Accessing the Virt-Handler Pod + +```python +handler_pod = vmi.get_virt_handler_pod(privileged_client=admin_client) +``` + +### Executing Virsh Commands + +Run virsh commands inside the virt-launcher pod: + +```python +# Get the XML definition +xml_output = vmi.get_xml() + +# Get the XML as a parsed Python dict +xml_dict = vmi.get_xml_dict() + +# Get domain memory stats +memstats = vmi.get_dommemstat() + +# Run any arbitrary virsh command +output = vmi.execute_virsh_command(command="dominfo") +``` + +### Checking Root Status + +```python +pod = vmi.get_virt_launcher_pod() + +# Check if the virt-launcher pod runs as root +VirtualMachineInstance.is_pod_root(pod=pod) # True or False + +# Get the pod's runAsUser UID +VirtualMachineInstance.get_pod_user_uid(pod=pod) # int or None +``` + +## Live Migration + +Trigger a live migration by creating a `VirtualMachineInstanceMigration`: + +```python +from ocp_resources.virtual_machine_instance_migration import VirtualMachineInstanceMigration + +migration = VirtualMachineInstanceMigration( + name="migrate-my-vm", + namespace="my-namespace", + vmi_name="my-vm", +) +migration.deploy() +``` + +Monitor migration by checking the VMI's status. Once migration completes, the VMI runs on the target node. See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for condition-based waiting. + +### Migration Resource Quotas + +Control resource allocation for migrations: + +```python +from ocp_resources.virtual_machine_migration_resource_quota import VirtualMachineMigrationResourceQuota + +quota = VirtualMachineMigrationResourceQuota( + name="migration-quota", + namespace="my-namespace", + requests_cpu="2", + requests_memory="4Gi", + limits_cpu="4", + limits_memory="8Gi", +) +quota.deploy() +``` + +## Snapshots and Restores + +### Taking a Snapshot + +```python +from ocp_resources.virtual_machine_snapshot import VirtualMachineSnapshot + +snapshot = VirtualMachineSnapshot( + name="my-vm-snapshot", + namespace="my-namespace", + vm_name="my-vm", +) +snapshot.deploy() + +# Wait for the snapshot to be ready +snapshot.wait_ready_to_use(timeout=300) + +# Wait for both snapshot readiness AND the VM's snapshotInProgress to clear +snapshot.wait_snapshot_done(timeout=300) +``` + +### Restoring from a Snapshot + +```python +from ocp_resources.virtual_machine_restore import VirtualMachineRestore + +restore = VirtualMachineRestore( + name="my-vm-restore", + namespace="my-namespace", + vm_name="my-vm", + snapshot_name="my-vm-snapshot", +) +restore.deploy() + +# Wait for restore to complete +restore.wait_restore_done(timeout=300) +``` + +The `wait_restore_done()` method checks both the restore's `complete` status and that the VM's `restoreInProgress` field is cleared. + +| Parameter | Type | Description | +|---|---|---| +| `vm_name` | `str` | Target VirtualMachine name | +| `snapshot_name` | `str` | Source VirtualMachineSnapshot name | +| `volume_restore_policy` | `str` | Optional policy for volume restoration | + +## Cloning VMs + +```python +from ocp_resources.virtual_machine_clone import VirtualMachineClone + +clone = VirtualMachineClone( + name="clone-my-vm", + namespace="my-namespace", + source_name="my-vm", + target_name="my-vm-clone", +) +clone.deploy() +``` + +> **Note:** `source_name` is **required**. If omitted, `MissingRequiredArgumentError` is raised. + +### Clone Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `source_name` | `str` | **Required** | Name of the source VM | +| `source_kind` | `str` | `"VirtualMachine"` | Kind of the source resource | +| `target_name` | `str` | Auto-generated | Name for the cloned VM | +| `label_filters` | `list` | `None` | Labels to copy, e.g. `["*", "!someKey/*"]` | +| `annotation_filters` | `list` | `None` | Annotations to copy | +| `new_mac_addresses` | `dict` | `None` | Override MACs: `{"nic0": "00:11:22:33:44:55"}` | +| `new_smbios_serial` | `str` | `None` | Override SMBIOS serial number | +| `volume_name_policy` | `str` | `None` | Volume naming policy for the clone | + +## Exporting VMs + +```python +from ocp_resources.virtual_machine_export import VirtualMachineExport + +export = VirtualMachineExport( + name="export-my-vm", + namespace="my-namespace", + source={"apiGroup": "kubevirt.io", "kind": "VirtualMachine", "name": "my-vm"}, + token_secret_ref="my-export-token", + ttl_duration="1h", +) +export.deploy() +``` + +> **Note:** The `source` parameter is **required**. If omitted, `MissingRequiredArgumentError` is raised. + +## Advanced Usage + +### Instance Types and Preferences + +KubeVirt instance types define reusable VM sizing profiles. Preferences define non-sizing defaults (clock, firmware, devices). + +#### Namespaced Instance Type + +```python +from ocp_resources.virtual_machine_instancetype import VirtualMachineInstancetype + +instancetype = VirtualMachineInstancetype( + name="small", + namespace="my-namespace", + cpu={"guest": 2}, + memory={"guest": "4Gi"}, +) +instancetype.deploy() +``` + +Both `cpu` and `memory` are **required**. Omitting either raises `MissingRequiredArgumentError`. + +#### Cluster-Scoped Instance Type + +```python +from ocp_resources.virtual_machine_cluster_instancetype import VirtualMachineClusterInstancetype + +cluster_instancetype = VirtualMachineClusterInstancetype( + name="large", + cpu={"guest": 8}, + memory={"guest": "16Gi"}, + gpus=[{"deviceName": "nvidia.com/A100", "name": "gpu0"}], +) +cluster_instancetype.deploy() +``` + +#### Namespaced Preference + +```python +from ocp_resources.virtual_machine_preference import VirtualMachinePreference + +preference = VirtualMachinePreference( + name="linux-pref", + namespace="my-namespace", + cpu={"preferredCPUTopology": "preferSockets"}, + firmware={"preferredUseEfi": True}, +) +preference.deploy() +``` + +#### Cluster-Scoped Preference + +```python +from ocp_resources.virtual_machine_cluster_preference import VirtualMachineClusterPreference + +cluster_preference = VirtualMachineClusterPreference( + name="windows-pref", + clock={"preferredTimer": {"hpet": {"present": False}}}, + features={"preferredHyperv": {"spinlocks": {"spinlocks": 8191}}}, +) +cluster_preference.deploy() +``` + +### VMI Replica Sets + +Scale out VMIs horizontally: + +```python +from ocp_resources.virtual_machine_instance_replica_set import VirtualMachineInstanceReplicaSet + +replica_set = VirtualMachineInstanceReplicaSet( + name="my-vmi-rs", + namespace="my-namespace", + replicas=3, + selector={"matchLabels": {"app": "my-vmi"}}, + template={ + "metadata": {"labels": {"app": "my-vmi"}}, + "spec": { + "domain": { + "devices": {}, + "resources": {"requests": {"memory": "1Gi"}}, + } + }, + }, +) +replica_set.deploy() +``` + +Both `selector` and `template` are **required**. + +### Storage Migration + +Migrate VM storage between storage classes: + +```python +from ocp_resources.virtual_machine_storage_migration_plan import VirtualMachineStorageMigrationPlan +from ocp_resources.virtual_machine_storage_migration import VirtualMachineStorageMigration + +# Create a migration plan +plan = VirtualMachineStorageMigrationPlan( + name="migrate-storage", + namespace="my-namespace", + virtual_machines=[ + {"name": "my-vm", "volumes": [{"name": "rootdisk", "target": {"storageClassName": "fast-ssd"}}]} + ], + retention_policy="deleteSource", +) +plan.deploy() + +# Execute the migration +migration = VirtualMachineStorageMigration( + name="migrate-storage-exec", + namespace="my-namespace", + virtual_machine_storage_migration_plan_ref={"name": "migrate-storage", "namespace": "my-namespace"}, +) +migration.deploy() +``` + +### VM Templates + +```python +from ocp_resources.virtual_machine_template import VirtualMachineTemplate + +template = VirtualMachineTemplate( + name="fedora-template", + namespace="my-namespace", + virtual_machine={ + "spec": { + "template": { + "spec": { + "domain": { + "devices": {}, + "resources": {"requests": {"memory": "2Gi"}}, + } + } + } + } + }, + parameters=[ + {"name": "VM_NAME", "description": "Name of the VM", "required": True} + ], + message="Use this template to create Fedora VMs.", +) +template.deploy() +``` + +### Listing VMs Across Namespaces + +```python +# List all VMs in a namespace +for vm in VirtualMachine.get(namespace="my-namespace"): + print(f"{vm.name}: {vm.instance.get('status', {}).get('printableStatus')}") + +# List with label selectors +for vm in VirtualMachine.get( + namespace="my-namespace", + label_selector="app=webserver", +): + print(vm.name) +``` + +See [Querying and Watching Resources](querying-resources.html) for more query options. + +### Getting VM Network Interfaces + +```python +interfaces = vm.get_interfaces() +for iface in interfaces: + print(f"Name: {iface['name']}, Model: {iface.get('model', 'default')}") +``` + +## Troubleshooting + +### VM Stuck in "Starting" Status + +Check the VMI's virt-launcher pod for scheduling or image-pull issues: + +```python +vmi = vm.vmi +try: + pod = vmi.get_virt_launcher_pod() + print(f"Pod status: {pod.status}") + print(pod.log(container="compute")) +except ResourceNotFoundError: + print("Virt-launcher pod not found — VM may not have been scheduled yet") +``` + +### TimeoutExpiredError on start/stop + +Increase the timeout parameter: + +```python +vm.start(timeout=600, wait=True) # 10 minutes instead of default 4 +``` + +### MissingRequiredArgumentError + +This is raised when a required constructor parameter is missing. Common cases: + +| Resource | Required Parameters | +|---|---| +| `VirtualMachineInstance` | `domain` | +| `VirtualMachineInstancetype` | `cpu`, `memory` | +| `VirtualMachineClone` | `source_name` | +| `VirtualMachineExport` | `source` | +| `VirtualMachineInstanceReplicaSet` | `selector`, `template` | +| `VirtualMachineStorageMigration` | `virtual_machine_storage_migration_plan_ref` | +| `VirtualMachineTemplate` | `virtual_machine` | + +### Guest Agent Not Reporting + +If `vmi.os_version` returns an empty dict, the QEMU guest agent is not installed or not running inside the VM. Install and start `qemu-guest-agent` in the guest OS. + +## Related Pages + +- [Creating and Managing Resources](creating-and-managing-resources.html) +- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) +- [Querying and Watching Resources](querying-resources.html) +- [Common Resource Patterns](common-patterns.html) +- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html)