Repository: cyberdesk-hq/cyberdesk
Branch: main
Commit: 6536bfbb543b
Files: 568
Total size: 3.4 MB
Directory structure:
gitextract_b1wrjbeg/
├── .gitignore
├── LICENSE
├── README.md
├── apps/
│ ├── api/
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── drizzle/
│ │ │ └── migrations/
│ │ │ ├── 0000_oval_outlaw_kid.sql
│ │ │ ├── 0001_busy_warstar.sql
│ │ │ ├── 0002_regular_doctor_faustus.sql
│ │ │ ├── 0003_superb_betty_brant.sql
│ │ │ ├── 0004_simple_komodo.sql
│ │ │ ├── 0005_mighty_hiroim.sql
│ │ │ ├── 0006_icy_black_bird.sql
│ │ │ ├── 0007_panoramic_tomorrow_man.sql
│ │ │ └── meta/
│ │ │ ├── 0000_snapshot.json
│ │ │ ├── 0001_snapshot.json
│ │ │ ├── 0002_snapshot.json
│ │ │ ├── 0003_snapshot.json
│ │ │ ├── 0004_snapshot.json
│ │ │ ├── 0005_snapshot.json
│ │ │ ├── 0006_snapshot.json
│ │ │ ├── 0007_snapshot.json
│ │ │ └── _journal.json
│ │ ├── drizzle.config.ts
│ │ ├── fly.toml
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── db/
│ │ │ │ ├── dbActions.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── schema.ts
│ │ │ ├── index.ts
│ │ │ ├── lib/
│ │ │ │ ├── cache.ts
│ │ │ │ ├── errors.ts
│ │ │ │ ├── hono.ts
│ │ │ │ ├── posthog.ts
│ │ │ │ └── ratelimit.ts
│ │ │ ├── routes/
│ │ │ │ └── desktop.ts
│ │ │ └── schema/
│ │ │ ├── desktop.ts
│ │ │ ├── errors.ts
│ │ │ └── gateway.ts
│ │ └── tsconfig.json
│ ├── docs/
│ │ ├── .gitignore
│ │ ├── .map.ts
│ │ ├── README.md
│ │ ├── app/
│ │ │ ├── api/
│ │ │ │ └── search/
│ │ │ │ └── route.ts
│ │ │ ├── docs/
│ │ │ │ ├── [[...slug]]/
│ │ │ │ │ └── page.tsx
│ │ │ │ └── layout.tsx
│ │ │ ├── global.css
│ │ │ ├── layout.config.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── source.ts
│ │ ├── content/
│ │ │ └── docs/
│ │ │ ├── api-reference.mdx
│ │ │ ├── conceptual-guide.mdx
│ │ │ ├── index.mdx
│ │ │ ├── introduction.mdx
│ │ │ ├── meta.json
│ │ │ ├── quickstart.mdx
│ │ │ └── tutorials.mdx
│ │ ├── mdx-components.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.mjs
│ │ ├── package.json
│ │ ├── postcss.config.js
│ │ ├── scripts/
│ │ │ └── generate-docs.mjs
│ │ ├── tailwind.config.js
│ │ └── tsconfig.json
│ └── web/
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── LICENSE.md
│ ├── README.md
│ ├── components.json
│ ├── config.ts
│ ├── middleware.ts
│ ├── next.config.mjs
│ ├── package.json
│ ├── postcss.config.js
│ ├── prettier.config.js
│ ├── radiant/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── eslint.config.mjs
│ │ ├── package.json
│ │ ├── sanity.cli.ts
│ │ ├── sanity.config.ts
│ │ ├── schemaTypes/
│ │ │ └── index.ts
│ │ ├── static/
│ │ │ └── .gitkeep
│ │ └── tsconfig.json
│ ├── sanity-typegen.json
│ ├── sanity.cli.ts
│ ├── sanity.config.ts
│ ├── src/
│ │ ├── app/
│ │ │ ├── api/
│ │ │ │ ├── playground/
│ │ │ │ │ ├── chat/
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ └── kill-desktop/
│ │ │ │ │ └── route.ts
│ │ │ │ ├── stripe/
│ │ │ │ │ ├── checkout/
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── portal/
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ └── webhook/
│ │ │ │ │ └── route.ts
│ │ │ │ └── unkey/
│ │ │ │ └── route.ts
│ │ │ ├── auth/
│ │ │ │ └── callback/
│ │ │ │ └── route.ts
│ │ │ ├── blog/
│ │ │ │ ├── [slug]/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── feed.xml/
│ │ │ │ │ └── route.ts
│ │ │ │ └── page.tsx
│ │ │ ├── company/
│ │ │ │ └── page.tsx
│ │ │ ├── dashboard/
│ │ │ │ ├── dashboard-content.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── demo/
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── login/
│ │ │ │ ├── login-form.d.ts
│ │ │ │ ├── login-form.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── page.tsx
│ │ │ ├── playground/
│ │ │ │ └── page.tsx
│ │ │ ├── pricing/
│ │ │ │ └── page.tsx
│ │ │ ├── privacy/
│ │ │ │ └── page.tsx
│ │ │ ├── studio/
│ │ │ │ └── [[...tool]]/
│ │ │ │ └── page.tsx
│ │ │ └── terms/
│ │ │ └── page.tsx
│ │ ├── components/
│ │ │ ├── LogoText.tsx
│ │ │ ├── PostHogProvider.tsx
│ │ │ ├── animated-number.tsx
│ │ │ ├── bento-card.tsx
│ │ │ ├── bento-section.tsx
│ │ │ ├── button.tsx
│ │ │ ├── container.tsx
│ │ │ ├── dark-bento-section.tsx
│ │ │ ├── dashboard/
│ │ │ │ ├── api-key-manager.tsx
│ │ │ │ ├── api-key-section.tsx
│ │ │ │ ├── dashboard-layout.tsx
│ │ │ │ ├── desktop-sidebar.tsx
│ │ │ │ ├── faq-section.tsx
│ │ │ │ ├── mobile-header.tsx
│ │ │ │ ├── mobile-sidebar.tsx
│ │ │ │ ├── sidebar-navigation.tsx
│ │ │ │ ├── subscription-section.tsx
│ │ │ │ └── vm-instances-manager.tsx
│ │ │ ├── demo-section.tsx
│ │ │ ├── feature-section.tsx
│ │ │ ├── footer.tsx
│ │ │ ├── gradient.tsx
│ │ │ ├── hero.tsx
│ │ │ ├── keyboard.tsx
│ │ │ ├── link.tsx
│ │ │ ├── linked-avatars.tsx
│ │ │ ├── logo-cloud.tsx
│ │ │ ├── logo-cluster.tsx
│ │ │ ├── logo-timeline.tsx
│ │ │ ├── logo.tsx
│ │ │ ├── map.tsx
│ │ │ ├── markdown-text.tsx
│ │ │ ├── navbar.tsx
│ │ │ ├── playground/
│ │ │ │ ├── chat-error.tsx
│ │ │ │ ├── icons.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── markdown.tsx
│ │ │ │ ├── message.tsx
│ │ │ │ ├── project-info.tsx
│ │ │ │ └── prompt-suggestions.tsx
│ │ │ ├── plus-grid.tsx
│ │ │ ├── screenshot.tsx
│ │ │ ├── shared/
│ │ │ │ └── app-logo.tsx
│ │ │ ├── stripe/
│ │ │ │ ├── checkout-button.tsx
│ │ │ │ ├── client-pricing-card.tsx
│ │ │ │ ├── client-pricing-cards.tsx
│ │ │ │ ├── payment-success.tsx
│ │ │ │ └── subscription-management.tsx
│ │ │ ├── testimonials.tsx
│ │ │ ├── text.tsx
│ │ │ ├── thread-list.tsx
│ │ │ ├── thread.tsx
│ │ │ ├── tooltip-icon-button.tsx
│ │ │ ├── ui/
│ │ │ │ ├── button.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── resizable.tsx
│ │ │ │ ├── sonner.tsx
│ │ │ │ └── tooltip.tsx
│ │ │ └── yc-banner.tsx
│ │ ├── sanity/
│ │ │ ├── client.ts
│ │ │ ├── env.ts
│ │ │ ├── image.ts
│ │ │ ├── queries.ts
│ │ │ ├── schema.ts
│ │ │ ├── types/
│ │ │ │ ├── author.ts
│ │ │ │ ├── block-content.ts
│ │ │ │ ├── category.ts
│ │ │ │ └── post.ts
│ │ │ └── types.ts
│ │ ├── styles/
│ │ │ └── tailwind.css
│ │ ├── types/
│ │ │ └── database.ts
│ │ └── utils/
│ │ ├── misc-utils.ts
│ │ ├── playground/
│ │ │ ├── cyberdesk-client.ts
│ │ │ ├── misc-demo-utils.ts
│ │ │ ├── server-actions.ts
│ │ │ ├── tools.ts
│ │ │ └── use-scroll-to-bottom.ts
│ │ ├── posthog/
│ │ │ └── posthog.ts
│ │ ├── stripe/
│ │ │ ├── stripe-server.ts
│ │ │ ├── stripe.ts
│ │ │ └── tiers.ts
│ │ └── supabase/
│ │ ├── client.ts
│ │ ├── middleware.ts
│ │ ├── server.ts
│ │ ├── supabaseClient.js
│ │ └── supabaseServerClient.ts
│ └── tsconfig.json
├── cyberdesk-architecture.md
├── infra/
│ ├── README.md
│ ├── kubernetes/
│ │ ├── azure-snapshot-class.yaml
│ │ ├── cdi-cr.yaml
│ │ ├── cdi-operator.yaml
│ │ ├── cluster-issuer.yaml
│ │ ├── cyberdesk-cr-v2.yaml
│ │ ├── cyberdesk-cr.yaml
│ │ ├── cyberdesk-operator.yaml
│ │ ├── default-backend.yaml
│ │ ├── gateway-deploy.yaml
│ │ ├── gateway-ingress-dev.yaml
│ │ ├── gateway-ingress-prod.yaml
│ │ ├── golden-vm-snapshot-request.yaml
│ │ ├── kubevirt-cr.yaml
│ │ ├── kubevirt-operator.yaml
│ │ ├── readme-todo.txt
│ │ ├── start-cyberdesk-operator-cr.yaml
│ │ └── warm-pool.yaml
│ └── terraform/
│ ├── .terraform.lock.hcl
│ ├── main.tf
│ └── variables.tf
├── sdks/
│ ├── openapi.json
│ ├── py-sdk/
│ │ ├── .gitignore
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── cyberdesk/
│ │ │ ├── __init__.py
│ │ │ ├── actions.py
│ │ │ ├── client.py
│ │ │ └── types.py
│ │ ├── openapi_client/
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── api_reference_client/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── api/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ └── desktop/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── get_v1_desktop_id.py
│ │ │ │ │ ├── post_v1_desktop.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action.py
│ │ │ │ │ └── post_v1_desktop_id_stop.py
│ │ │ │ ├── client.py
│ │ │ │ ├── errors.py
│ │ │ │ ├── models/
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── get_v1_desktop_id_response_200.py
│ │ │ │ │ ├── get_v1_desktop_id_response_200_status.py
│ │ │ │ │ ├── get_v1_desktop_id_response_400.py
│ │ │ │ │ ├── get_v1_desktop_id_response_400_status.py
│ │ │ │ │ ├── get_v1_desktop_id_response_401.py
│ │ │ │ │ ├── get_v1_desktop_id_response_401_status.py
│ │ │ │ │ ├── get_v1_desktop_id_response_403.py
│ │ │ │ │ ├── get_v1_desktop_id_response_403_status.py
│ │ │ │ │ ├── get_v1_desktop_id_response_404.py
│ │ │ │ │ ├── get_v1_desktop_id_response_404_status.py
│ │ │ │ │ ├── get_v1_desktop_id_response_409.py
│ │ │ │ │ ├── get_v1_desktop_id_response_409_status.py
│ │ │ │ │ ├── get_v1_desktop_id_response_429.py
│ │ │ │ │ ├── get_v1_desktop_id_response_429_status.py
│ │ │ │ │ ├── get_v1_desktop_id_response_500.py
│ │ │ │ │ ├── get_v1_desktop_id_response_500_status.py
│ │ │ │ │ ├── get_v1_desktop_id_response_502.py
│ │ │ │ │ ├── get_v1_desktop_id_response_502_status.py
│ │ │ │ │ ├── post_v1_desktop_body.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_body.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_200.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_400.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_400_status.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_401.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_401_status.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_403.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_403_status.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_404.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_404_status.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_409.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_409_status.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_429.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_429_status.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_500.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_500_status.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_502.py
│ │ │ │ │ ├── post_v1_desktop_id_bash_action_response_502_status.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_click_mouse_action.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_click_mouse_action_button.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_click_mouse_action_click_type.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_click_mouse_action_type.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_drag_mouse_action.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_drag_mouse_action_end.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_drag_mouse_action_start.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_drag_mouse_action_type.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_get_cursor_position_action.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_get_cursor_position_action_type.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_move_mouse_action.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_move_mouse_action_type.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_press_keys_action.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_press_keys_action_key_action_type.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_press_keys_action_type.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_200.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_400.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_400_status.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_401.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_401_status.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_403.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_403_status.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_404.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_404_status.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_409.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_409_status.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_429.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_429_status.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_500.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_500_status.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_502.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_response_502_status.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_screenshot_action.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_screenshot_action_type.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_scroll_action.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_scroll_action_direction.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_scroll_action_type.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_type_text_action.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_type_text_action_type.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_wait_action.py
│ │ │ │ │ ├── post_v1_desktop_id_computer_action_wait_action_type.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_200.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_200_status.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_400.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_400_status.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_401.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_401_status.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_403.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_403_status.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_404.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_404_status.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_409.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_409_status.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_429.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_429_status.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_500.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_500_status.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_502.py
│ │ │ │ │ ├── post_v1_desktop_id_stop_response_502_status.py
│ │ │ │ │ ├── post_v1_desktop_response_200.py
│ │ │ │ │ ├── post_v1_desktop_response_200_status.py
│ │ │ │ │ ├── post_v1_desktop_response_400.py
│ │ │ │ │ ├── post_v1_desktop_response_400_status.py
│ │ │ │ │ ├── post_v1_desktop_response_401.py
│ │ │ │ │ ├── post_v1_desktop_response_401_status.py
│ │ │ │ │ ├── post_v1_desktop_response_403.py
│ │ │ │ │ ├── post_v1_desktop_response_403_status.py
│ │ │ │ │ ├── post_v1_desktop_response_404.py
│ │ │ │ │ ├── post_v1_desktop_response_404_status.py
│ │ │ │ │ ├── post_v1_desktop_response_409.py
│ │ │ │ │ ├── post_v1_desktop_response_409_status.py
│ │ │ │ │ ├── post_v1_desktop_response_429.py
│ │ │ │ │ ├── post_v1_desktop_response_429_status.py
│ │ │ │ │ ├── post_v1_desktop_response_500.py
│ │ │ │ │ ├── post_v1_desktop_response_500_status.py
│ │ │ │ │ ├── post_v1_desktop_response_502.py
│ │ │ │ │ └── post_v1_desktop_response_502_status.py
│ │ │ │ ├── py.typed
│ │ │ │ └── types.py
│ │ │ └── pyproject.toml
│ │ ├── pyproject.toml
│ │ └── scripts/
│ │ └── generate.py
│ ├── sandbox/
│ │ └── py-sdk/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ └── test_sdk.py
│ └── ts-sdk/
│ ├── .gitignore
│ ├── LICENSE
│ ├── README.md
│ ├── openapi-ts.config.ts
│ ├── package.json
│ ├── src/
│ │ ├── client/
│ │ │ ├── client.gen.ts
│ │ │ ├── index.ts
│ │ │ ├── sdk.gen.ts
│ │ │ └── types.gen.ts
│ │ └── index.ts
│ └── tsconfig.json
├── self-host.md
└── services/
├── cyberdesk-operator/
│ ├── .gitignore
│ ├── Dockerfile
│ ├── README.md
│ ├── checklist.md
│ ├── docs/
│ │ └── troubleshooting.md
│ ├── handlers/
│ │ └── controller.py
│ ├── requirements.txt
│ └── tests/
│ ├── README.md
│ ├── test-cyberdesk-cr.yaml
│ ├── test-start-operator-cr.yaml
│ ├── test-start-operator-crd.yaml
│ └── test.py
└── gateway/
├── Dockerfile
├── README.md
├── main.py
├── noVNC/
│ ├── .github/
│ │ ├── ISSUE_TEMPLATE/
│ │ │ ├── bug_report.md
│ │ │ ├── config.yml
│ │ │ └── feature_request.md
│ │ └── workflows/
│ │ ├── deploy.yml
│ │ ├── lint.yml
│ │ ├── test.yml
│ │ └── translate.yml
│ ├── .gitignore
│ ├── .gitmodules
│ ├── AUTHORS
│ ├── LICENSE.txt
│ ├── README.md
│ ├── app/
│ │ ├── error-handler.js
│ │ ├── images/
│ │ │ └── icons/
│ │ │ └── Makefile
│ │ ├── locale/
│ │ │ ├── README
│ │ │ ├── cs.json
│ │ │ ├── de.json
│ │ │ ├── el.json
│ │ │ ├── es.json
│ │ │ ├── fr.json
│ │ │ ├── it.json
│ │ │ ├── ja.json
│ │ │ ├── ko.json
│ │ │ ├── nl.json
│ │ │ ├── pl.json
│ │ │ ├── pt_BR.json
│ │ │ ├── ru.json
│ │ │ ├── sv.json
│ │ │ ├── tr.json
│ │ │ ├── zh_CN.json
│ │ │ └── zh_TW.json
│ │ ├── localization.js
│ │ ├── sounds/
│ │ │ ├── CREDITS
│ │ │ └── bell.oga
│ │ ├── styles/
│ │ │ ├── base.css
│ │ │ ├── constants.css
│ │ │ └── input.css
│ │ ├── ui.js
│ │ └── webutil.js
│ ├── core/
│ │ ├── base64.js
│ │ ├── crypto/
│ │ │ ├── aes.js
│ │ │ ├── bigint.js
│ │ │ ├── crypto.js
│ │ │ ├── des.js
│ │ │ ├── dh.js
│ │ │ ├── md5.js
│ │ │ └── rsa.js
│ │ ├── decoders/
│ │ │ ├── copyrect.js
│ │ │ ├── h264.js
│ │ │ ├── hextile.js
│ │ │ ├── jpeg.js
│ │ │ ├── raw.js
│ │ │ ├── rre.js
│ │ │ ├── tight.js
│ │ │ ├── tightpng.js
│ │ │ ├── zlib.js
│ │ │ └── zrle.js
│ │ ├── deflator.js
│ │ ├── display.js
│ │ ├── encodings.js
│ │ ├── inflator.js
│ │ ├── input/
│ │ │ ├── domkeytable.js
│ │ │ ├── fixedkeys.js
│ │ │ ├── gesturehandler.js
│ │ │ ├── keyboard.js
│ │ │ ├── keysym.js
│ │ │ ├── keysymdef.js
│ │ │ ├── util.js
│ │ │ ├── vkeys.js
│ │ │ └── xtscancodes.js
│ │ ├── ra2.js
│ │ ├── rfb.js
│ │ ├── util/
│ │ │ ├── browser.js
│ │ │ ├── cursor.js
│ │ │ ├── element.js
│ │ │ ├── events.js
│ │ │ ├── eventtarget.js
│ │ │ ├── int.js
│ │ │ ├── logging.js
│ │ │ └── strings.js
│ │ └── websock.js
│ ├── defaults.json
│ ├── docs/
│ │ ├── API-internal.md
│ │ ├── API.md
│ │ ├── EMBEDDING.md
│ │ ├── LIBRARY.md
│ │ ├── LICENSE.BSD-2-Clause
│ │ ├── LICENSE.BSD-3-Clause
│ │ ├── LICENSE.MPL-2.0
│ │ ├── LICENSE.OFL-1.1
│ │ ├── flash_policy.txt
│ │ ├── links
│ │ ├── notes
│ │ ├── novnc_proxy.1
│ │ └── rfb_notes
│ ├── eslint.config.mjs
│ ├── karma.conf.js
│ ├── mandatory.json
│ ├── package.json
│ ├── po/
│ │ ├── Makefile
│ │ ├── cs.po
│ │ ├── de.po
│ │ ├── el.po
│ │ ├── es.po
│ │ ├── fr.po
│ │ ├── it.po
│ │ ├── ja.po
│ │ ├── ko.po
│ │ ├── nl.po
│ │ ├── noVNC.pot
│ │ ├── pl.po
│ │ ├── po2js
│ │ ├── pt_BR.po
│ │ ├── ru.po
│ │ ├── sv.po
│ │ ├── tr.po
│ │ ├── xgettext-html
│ │ ├── zh_CN.po
│ │ └── zh_TW.po
│ ├── snap/
│ │ ├── hooks/
│ │ │ └── configure
│ │ ├── local/
│ │ │ └── svc_wrapper.sh
│ │ └── snapcraft.yaml
│ ├── tests/
│ │ ├── assertions.js
│ │ ├── fake.websocket.js
│ │ ├── playback-ui.js
│ │ ├── playback.js
│ │ ├── test.base64.js
│ │ ├── test.browser.js
│ │ ├── test.copyrect.js
│ │ ├── test.deflator.js
│ │ ├── test.display.js
│ │ ├── test.gesturehandler.js
│ │ ├── test.h264.js
│ │ ├── test.helper.js
│ │ ├── test.hextile.js
│ │ ├── test.inflator.js
│ │ ├── test.int.js
│ │ ├── test.jpeg.js
│ │ ├── test.keyboard.js
│ │ ├── test.localization.js
│ │ ├── test.raw.js
│ │ ├── test.rfb.js
│ │ ├── test.rre.js
│ │ ├── test.tight.js
│ │ ├── test.tightpng.js
│ │ ├── test.util.js
│ │ ├── test.websock.js
│ │ ├── test.webutil.js
│ │ ├── test.zlib.js
│ │ ├── test.zrle.js
│ │ └── vnc_playback.html
│ ├── utils/
│ │ ├── README.md
│ │ ├── b64-to-binary.pl
│ │ ├── convert.js
│ │ ├── genkeysymdef.js
│ │ ├── novnc_proxy
│ │ ├── u2x11
│ │ └── validate
│ ├── vendor/
│ │ └── pako/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── lib/
│ │ ├── utils/
│ │ │ └── common.js
│ │ └── zlib/
│ │ ├── adler32.js
│ │ ├── constants.js
│ │ ├── crc32.js
│ │ ├── deflate.js
│ │ ├── gzheader.js
│ │ ├── inffast.js
│ │ ├── inflate.js
│ │ ├── inftrees.js
│ │ ├── messages.js
│ │ ├── trees.js
│ │ └── zstream.js
│ ├── vnc.html
│ └── vnc_lite.html
└── requirements.txt
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files, which are likely to contain sensitive data
*.tfvars
*.tfvars.json
# Ignore override files as they are usually used for local development
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Ignore CLI configuration files
.terraformrc
terraform.rc
# Ignore kubeconfig
kubeconfig.yaml
# Ignore testing-do-not-push
testing-do-not-push/*
user-data.yaml
user-data-base64.txt
# Kubernetes secrets - DO NOT COMMIT REAL CREDENTIALS
infra/kubernetes/cyberdesk-secret.yaml
infra/kubernetes/checkpoint-cyberdesk-secret.yaml
infra/kubernetes/golden-vm-deploy.yaml
# Local development environment variables
.env
infra/kubernetes/test-secret.yaml
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[ ]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright 2024 Cyberdesk Team
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
The open source infra for virtual desktop orchestration, tailored for computer agents
A computer agent operating a Cyberdesk virtual desktop from a user prompt
---
## 🚀 Quick Start
### TypeScript
```bash
npm install cyberdesk@0.2.1
```
```typescript
import { createCyberdeskClient } from 'cyberdesk';
const cyberdesk = createCyberdeskClient({ apiKey: 'YOUR_API_KEY' });
const launchResult = await cyberdesk.launchDesktop({ body: { timeout_ms: 10000 } });
const desktopId = launchResult.id;
// Take a screenshot
const screenshot = await cyberdesk.executeComputerAction({
path: { id: desktopId },
body: { type: 'screenshot' }
});
// Left click at (100, 150)
await cyberdesk.executeComputerAction({
path: { id: desktopId },
body: { type: 'click_mouse', x: 100, y: 150, button: 'left' }
});
```
### Python
```bash
pip install cyberdesk==0.2.7
```
```python
from cyberdesk import CyberdeskClient
from cyberdesk.actions import click_mouse, screenshot, ClickMouseButton
client = CyberdeskClient(api_key="YOUR_API_KEY")
result = client.launch_desktop(timeout_ms=10000)
desktop_id = result.id
# Take a screenshot
screenshot_action = screenshot()
screenshot_result = client.execute_computer_action(desktop_id, screenshot_action)
# Left click at (100, 150)
click_action = click_mouse(x=100, y=150, button=ClickMouseButton.LEFT)
client.execute_computer_action(desktop_id, click_action)
```
👉 For more details and advanced usage, see the [Quickstart Guide](https://docs.cyberdesk.io/docs/quickstart) and [Official Documentation](#-official-documentation).
---
## ✨ Features
🚀 Fast Launch Spin up virtual desktops in seconds, ready for automation or remote use.
🖱️ Full Automation Control mouse, keyboard, and more—perfect for computer agents.
🖥️ Cloud Native Runs on AKS, or self-hosted on your own infrastructure.
🔒 Secure & Auditable Session logs, API keys, and enterprise-grade security.
🧩 Type-Safe SDKs Official Python & TypeScript SDKs with full type hints.
🤖 AI-Ready Tailor built for the next generation of computer use agents
---
## 📚 Official Documentation
- [Quickstart Guide](https://docs.cyberdesk.io/docs/quickstart)
- [API Reference](https://docs.cyberdesk.io/docs/api-reference)
- [TypeScript SDK](sdks/ts-sdk/README.md)
- [Python SDK](sdks/py-sdk/README.md)
---
## 🛠️ Project Structure
### /apps
- **web**: Landing page and dashboard ([README](apps/web/README.md))
- **api**: Developer-facing API ([README](apps/api/README.md))
- **docs**: Documentation site ([README](apps/docs/README.md))
### /services
- **cyberdesk-operator**: Kubernetes operator for managing Cyberdesk Custom Resources, and starting/stopping Kubevirt virtual machines ([README](services/cyberdesk-operator/README.md))
- **gateway**: HTTP service that proxies requests to the Kubevirt API, and routes them to the correct virtual machine ([README](services/gateway/README.md))
### /sdks
- **ts-sdk**: TypeScript SDK ([README](sdks/ts-sdk/README.md))
- **py-sdk**: Python SDK ([README](sdks/py-sdk/README.md))
### /infrastructure
- **terraform**: AKS Cluster Setup (Terraform) ([README](infrastructure/README.md))
- **kubernetes**: Kubernetes resources for the Cyberdesk operator
---
## 🤝 Contributing
We welcome contributions!
- Join the [Discord](https://discord.gg/ws5ddx5yZ8) for discussion and support
- Get a personal 1-1 walkthrough of how to self host the project by contacting us on [Discord](https://discord.gg/ws5ddx5yZ8)
---
## 📣 Community & Support
- [Discord](https://discord.gg/ws5ddx5yZ8) for help and chat
- [Good First Issues](https://github.com/cyberdesk-hq/cyberdesk/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
- [Open an Issue](https://github.com/cyberdesk-hq/cyberdesk/issues)
---
## 💡 Philosophy
> At **Cyberdesk** our mission is to make building computer agents as easy as playing with legos. We believe in open, simple, and extensible tools for the new generation of developers: *computer agent developers*.
---
## 📄 License
Apache License 2.0. See [LICENSE](LICENSE).
---
Made with ❤️ by the Cyberdesk Team
================================================
FILE: apps/api/.dockerignore
================================================
/.git
/node_modules
.dockerignore
.env
Dockerfile
fly.toml
================================================
FILE: apps/api/.gitignore
================================================
# prod
dist/
# dev
.yarn/
!.yarn/releases
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf
# deps
node_modules/
.wrangler
# env
.env
.env.production
.dev.vars
# logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# misc
.DS_Store
# turbo
.turbo
================================================
FILE: apps/api/Dockerfile
================================================
# syntax = docker/dockerfile:1
# Adjust NODE_VERSION as desired
ARG NODE_VERSION=20.18.0
FROM node:${NODE_VERSION}-slim AS base
LABEL fly_launch_runtime="Node.js"
# Node.js app lives here
WORKDIR /app
# Set production environment
ENV NODE_ENV="production"
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build node modules
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3
# Install node modules
COPY package.json ./
RUN npm install --include=dev
# Copy application code
COPY . .
# Build application
RUN npm run build
# Remove development dependencies
RUN npm prune --omit=dev
# Final stage for app image
FROM base
# Copy built application
COPY --from=build /app /app
# Start the server by default, this can be overwritten at runtime
EXPOSE 3000
CMD [ "npm", "run", "start" ]
================================================
FILE: apps/api/README.md
================================================
# Created by Unkey's toolbox
This API is built with speed, and security in mind. The API is built with [Hono](https://hono.dev), [Unkey](https://unkey.com) and [Supabase](https://supabase.com) with hosting on [Fly.io](https://fly.io).
## Getting Started
You will need a free account for both Unkey and Supabase to run this project.
### Unkey
For Unkey you will need your API ID and a root key scoped to:
- Create Key
- Create Namespace
- Limit
You can of course add more scopes as required.
### Supabase
For Supabase, you'll need to create a project and get your:
- Supabase URL
- Supabase Anon Key
- Supabase Connection String (found in the Database settings under Connection Pooling)
## Environment Variables
To run this project, you will need to add the following environment variables to your .dev.vars file
```env
SUPABASE_URL=https://your-project-id.supabase.co
SUPABASE_ANON_KEY=your-supabase-anon-key
SUPABASE_CONNECTION_STRING=postgres://postgres:your-password@your-project-id.supabase.co:5432/postgres?pgbouncer=true
UNKEY_API_ID=UNKEY_API_ID
UNKEY_ROOT_KEY=UNKEY_ROOT_KEY
```
## Usage
Make sure that you have run:
```bash
npm run db:generate
npm run db:push
```
You can then run `npm run dev`
Then you will have access to the following routes:
`/keys/create` - To create an API key to use with the other endpoints.
Then the desktop routes (all under the `/v1` prefix):
```bash
/v1/desktop # Create a new desktop instance
/v1/desktop/{id}/stop # Stop a desktop instance
/v1/desktop/{id}/computer-action # Perform a computer action
/v1/desktop/{id}/bash-action # Execute a bash command
```
You also have access to the open-api spec found at [http://localhost:8787/open-api](http://localhost:8787/open-api)
================================================
FILE: apps/api/drizzle/migrations/0000_oval_outlaw_kid.sql
================================================
CREATE TABLE IF NOT EXISTS "desktop_instances" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"created_at" timestamp DEFAULT now(),
"ended_at" timestamp
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "desktop_instances" ADD CONSTRAINT "desktop_instances_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
================================================
FILE: apps/api/drizzle/migrations/0001_busy_warstar.sql
================================================
ALTER TABLE "desktop_instances" ADD COLUMN "remote_id" varchar(255);
================================================
FILE: apps/api/drizzle/migrations/0002_regular_doctor_faustus.sql
================================================
CREATE TABLE IF NOT EXISTS "profiles" (
"id" uuid PRIMARY KEY NOT NULL,
"unkey_key_id" varchar(255),
"stripe_customer_id" varchar(255),
"stripe_subscription_id" varchar(255),
"current_period_end" timestamp,
"subscription_status" varchar(50),
"plan_id" varchar(100),
"cancel_at_period_end" boolean DEFAULT false,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now()
);
--> statement-breakpoint
ALTER TABLE "desktop_instances" ALTER COLUMN "remote_id" SET NOT NULL;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_id_users_id_fk" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
================================================
FILE: apps/api/drizzle/migrations/0003_superb_betty_brant.sql
================================================
ALTER TABLE "desktop_instances" ADD COLUMN "stream_url" varchar(1024) NOT NULL;
================================================
FILE: apps/api/drizzle/migrations/0004_simple_komodo.sql
================================================
ALTER TABLE "desktop_instances" ALTER COLUMN "stream_url" SET DEFAULT 'https://placeholder-stream-url.cyberdesk.dev';
================================================
FILE: apps/api/drizzle/migrations/0005_mighty_hiroim.sql
================================================
ALTER TABLE "desktop_instances" DROP CONSTRAINT "desktop_instances_user_id_users_id_fk";
--> statement-breakpoint
ALTER TABLE "profiles" DROP CONSTRAINT "profiles_id_users_id_fk";
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "desktop_instances" ADD CONSTRAINT "desktop_instances_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_id_users_id_fk" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
================================================
FILE: apps/api/drizzle/migrations/0006_icy_black_bird.sql
================================================
DO $$ BEGIN
CREATE TYPE "public"."instance_status" AS ENUM('pending', 'running', 'completed', 'error');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "cyberdesk_instances" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp,
"status" "instance_status" DEFAULT 'pending' NOT NULL,
"timeout_at" timestamp DEFAULT NOW() + interval '24 hours' NOT NULL,
"stream_url" varchar(1024)
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "cyberdesk_instances" ADD CONSTRAINT "cyberdesk_instances_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
================================================
FILE: apps/api/drizzle/migrations/0007_panoramic_tomorrow_man.sql
================================================
ALTER TYPE "instance_status" ADD VALUE 'terminated';
================================================
FILE: apps/api/drizzle/migrations/meta/0000_snapshot.json
================================================
{
"id": "1816b590-90c5-4f05-9726-cc8f33d18251",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "6",
"dialect": "postgresql",
"tables": {
"public.desktop_instances": {
"name": "desktop_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"desktop_instances_user_id_users_id_fk": {
"name": "desktop_instances_user_id_users_id_fk",
"tableFrom": "desktop_instances",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
================================================
FILE: apps/api/drizzle/migrations/meta/0001_snapshot.json
================================================
{
"id": "61aff1d0-bd56-4301-852c-bc67c46a1cd9",
"prevId": "1816b590-90c5-4f05-9726-cc8f33d18251",
"version": "6",
"dialect": "postgresql",
"tables": {
"public.desktop_instances": {
"name": "desktop_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"remote_id": {
"name": "remote_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"desktop_instances_user_id_users_id_fk": {
"name": "desktop_instances_user_id_users_id_fk",
"tableFrom": "desktop_instances",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
================================================
FILE: apps/api/drizzle/migrations/meta/0002_snapshot.json
================================================
{
"id": "4d86e969-3e44-4e26-bb6d-b76b9daa733e",
"prevId": "61aff1d0-bd56-4301-852c-bc67c46a1cd9",
"version": "6",
"dialect": "postgresql",
"tables": {
"public.desktop_instances": {
"name": "desktop_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"remote_id": {
"name": "remote_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"desktop_instances_user_id_users_id_fk": {
"name": "desktop_instances_user_id_users_id_fk",
"tableFrom": "desktop_instances",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.profiles": {
"name": "profiles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"unkey_key_id": {
"name": "unkey_key_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_customer_id": {
"name": "stripe_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_subscription_id": {
"name": "stripe_subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"current_period_end": {
"name": "current_period_end",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"subscription_status": {
"name": "subscription_status",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"plan_id": {
"name": "plan_id",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"cancel_at_period_end": {
"name": "cancel_at_period_end",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"profiles_id_users_id_fk": {
"name": "profiles_id_users_id_fk",
"tableFrom": "profiles",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
================================================
FILE: apps/api/drizzle/migrations/meta/0003_snapshot.json
================================================
{
"id": "9b509c40-ab3f-410b-ac01-9401df6598c2",
"prevId": "4d86e969-3e44-4e26-bb6d-b76b9daa733e",
"version": "6",
"dialect": "postgresql",
"tables": {
"public.desktop_instances": {
"name": "desktop_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"remote_id": {
"name": "remote_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"stream_url": {
"name": "stream_url",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"desktop_instances_user_id_users_id_fk": {
"name": "desktop_instances_user_id_users_id_fk",
"tableFrom": "desktop_instances",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.profiles": {
"name": "profiles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"unkey_key_id": {
"name": "unkey_key_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_customer_id": {
"name": "stripe_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_subscription_id": {
"name": "stripe_subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"current_period_end": {
"name": "current_period_end",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"subscription_status": {
"name": "subscription_status",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"plan_id": {
"name": "plan_id",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"cancel_at_period_end": {
"name": "cancel_at_period_end",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"profiles_id_users_id_fk": {
"name": "profiles_id_users_id_fk",
"tableFrom": "profiles",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
================================================
FILE: apps/api/drizzle/migrations/meta/0004_snapshot.json
================================================
{
"id": "36e12ff1-cb7e-46bd-bdc4-796464df0849",
"prevId": "9b509c40-ab3f-410b-ac01-9401df6598c2",
"version": "6",
"dialect": "postgresql",
"tables": {
"public.desktop_instances": {
"name": "desktop_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"remote_id": {
"name": "remote_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"stream_url": {
"name": "stream_url",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": true,
"default": "'https://placeholder-stream-url.cyberdesk.io'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"desktop_instances_user_id_users_id_fk": {
"name": "desktop_instances_user_id_users_id_fk",
"tableFrom": "desktop_instances",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.profiles": {
"name": "profiles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"unkey_key_id": {
"name": "unkey_key_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_customer_id": {
"name": "stripe_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_subscription_id": {
"name": "stripe_subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"current_period_end": {
"name": "current_period_end",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"subscription_status": {
"name": "subscription_status",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"plan_id": {
"name": "plan_id",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"cancel_at_period_end": {
"name": "cancel_at_period_end",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"profiles_id_users_id_fk": {
"name": "profiles_id_users_id_fk",
"tableFrom": "profiles",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
================================================
FILE: apps/api/drizzle/migrations/meta/0005_snapshot.json
================================================
{
"id": "0efe305c-c062-4173-bc2d-8d92af93250f",
"prevId": "36e12ff1-cb7e-46bd-bdc4-796464df0849",
"version": "6",
"dialect": "postgresql",
"tables": {
"public.desktop_instances": {
"name": "desktop_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"remote_id": {
"name": "remote_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"stream_url": {
"name": "stream_url",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": true,
"default": "'https://placeholder-stream-url.cyberdesk.io'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"desktop_instances_user_id_users_id_fk": {
"name": "desktop_instances_user_id_users_id_fk",
"tableFrom": "desktop_instances",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.profiles": {
"name": "profiles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"unkey_key_id": {
"name": "unkey_key_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_customer_id": {
"name": "stripe_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_subscription_id": {
"name": "stripe_subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"current_period_end": {
"name": "current_period_end",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"subscription_status": {
"name": "subscription_status",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"plan_id": {
"name": "plan_id",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"cancel_at_period_end": {
"name": "cancel_at_period_end",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"profiles_id_users_id_fk": {
"name": "profiles_id_users_id_fk",
"tableFrom": "profiles",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
================================================
FILE: apps/api/drizzle/migrations/meta/0006_snapshot.json
================================================
{
"id": "1f1576f8-a386-4198-a4b4-dbe02dbc8468",
"prevId": "0efe305c-c062-4173-bc2d-8d92af93250f",
"version": "6",
"dialect": "postgresql",
"tables": {
"public.cyberdesk_instances": {
"name": "cyberdesk_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "instance_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"timeout_at": {
"name": "timeout_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "NOW() + interval '24 hours'"
},
"stream_url": {
"name": "stream_url",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"cyberdesk_instances_user_id_users_id_fk": {
"name": "cyberdesk_instances_user_id_users_id_fk",
"tableFrom": "cyberdesk_instances",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.desktop_instances": {
"name": "desktop_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"remote_id": {
"name": "remote_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"stream_url": {
"name": "stream_url",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": true,
"default": "'https://placeholder-stream-url.cyberdesk.io'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"desktop_instances_user_id_users_id_fk": {
"name": "desktop_instances_user_id_users_id_fk",
"tableFrom": "desktop_instances",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.profiles": {
"name": "profiles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"unkey_key_id": {
"name": "unkey_key_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_customer_id": {
"name": "stripe_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_subscription_id": {
"name": "stripe_subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"current_period_end": {
"name": "current_period_end",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"subscription_status": {
"name": "subscription_status",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"plan_id": {
"name": "plan_id",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"cancel_at_period_end": {
"name": "cancel_at_period_end",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"profiles_id_users_id_fk": {
"name": "profiles_id_users_id_fk",
"tableFrom": "profiles",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"public.instance_status": {
"name": "instance_status",
"schema": "public",
"values": [
"pending",
"running",
"completed",
"error"
]
}
},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
================================================
FILE: apps/api/drizzle/migrations/meta/0007_snapshot.json
================================================
{
"id": "39ccedc0-27c0-448a-971b-3217e72e8dfe",
"prevId": "1f1576f8-a386-4198-a4b4-dbe02dbc8468",
"version": "6",
"dialect": "postgresql",
"tables": {
"public.cyberdesk_instances": {
"name": "cyberdesk_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "instance_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"timeout_at": {
"name": "timeout_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "NOW() + interval '24 hours'"
},
"stream_url": {
"name": "stream_url",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"cyberdesk_instances_user_id_users_id_fk": {
"name": "cyberdesk_instances_user_id_users_id_fk",
"tableFrom": "cyberdesk_instances",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.desktop_instances": {
"name": "desktop_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"remote_id": {
"name": "remote_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"stream_url": {
"name": "stream_url",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": true,
"default": "'https://placeholder-stream-url.cyberdesk.io'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"desktop_instances_user_id_users_id_fk": {
"name": "desktop_instances_user_id_users_id_fk",
"tableFrom": "desktop_instances",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.profiles": {
"name": "profiles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"unkey_key_id": {
"name": "unkey_key_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_customer_id": {
"name": "stripe_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"stripe_subscription_id": {
"name": "stripe_subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"current_period_end": {
"name": "current_period_end",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"subscription_status": {
"name": "subscription_status",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"plan_id": {
"name": "plan_id",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"cancel_at_period_end": {
"name": "cancel_at_period_end",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"profiles_id_users_id_fk": {
"name": "profiles_id_users_id_fk",
"tableFrom": "profiles",
"tableTo": "users",
"schemaTo": "auth",
"columnsFrom": [
"id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"public.instance_status": {
"name": "instance_status",
"schema": "public",
"values": [
"pending",
"running",
"terminated",
"error"
]
}
},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
================================================
FILE: apps/api/drizzle/migrations/meta/_journal.json
================================================
{
"version": "5",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1743009299784,
"tag": "0000_oval_outlaw_kid",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1743018990086,
"tag": "0001_busy_warstar",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1743040137195,
"tag": "0002_regular_doctor_faustus",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1743119400161,
"tag": "0003_superb_betty_brant",
"breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1743119816127,
"tag": "0004_simple_komodo",
"breakpoints": true
},
{
"idx": 5,
"version": "6",
"when": 1743122830319,
"tag": "0005_mighty_hiroim",
"breakpoints": true
},
{
"idx": 6,
"version": "6",
"when": 1744919270291,
"tag": "0006_icy_black_bird",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1745534258042,
"tag": "0007_panoramic_tomorrow_man",
"breakpoints": true
}
]
}
================================================
FILE: apps/api/drizzle.config.ts
================================================
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/supabase.ts",
schemaFilter: ["public"],
out: "./drizzle/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.SUPABASE_CONNECTION_STRING!,
},
});
================================================
FILE: apps/api/fly.toml
================================================
# fly.toml app configuration file generated for cyberdesk-mvp-backend on 2025-03-27T05:00:43Z
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = 'cyberdesk-mvp-backend'
primary_region = 'dfw'
[build]
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = 'stop'
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[[vm]]
memory = '1gb'
cpu_kind = 'shared'
cpus = 1
memory_mb = 1024
================================================
FILE: apps/api/package.json
================================================
{
"name": "api",
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js",
"env": "dotenv",
"db:push": "npm run env -- drizzle-kit push",
"db:studio": "npm run env -- drizzle-kit studio",
"db:generate": "npm run env -- drizzle-kit generate",
"db:migrate": "npm run env -- drizzle-kit migrate"
},
"dependencies": {
"@hono/node-server": "^1.14.0",
"@hono/zod-openapi": "^0.14.5",
"@libsql/client": "^0.6.2",
"@supabase/supabase-js": "^2.49.3",
"@unkey/api": "^0.20.7",
"@unkey/cache": "^1.0.2",
"@unkey/hono": "^1.2.0",
"@unkey/ratelimit": "^0.1.12",
"axios": "^1.9.0",
"dotenv": "^16.4.7",
"drizzle-orm": "^0.30.10",
"hono": "^4.4.7",
"postgres": "^3.4.5",
"posthog-node": "^4.17.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240529.0",
"@types/node": "^22.15.0",
"dotenv-cli": "^7.4.2",
"drizzle-kit": "^0.21.4",
"eslint-plugin-drizzle": "^0.2.3",
"tsx": "^3.10.6",
"typescript": "^5.8.2",
"wrangler": "^3.57.2"
}
}
================================================
FILE: apps/api/src/db/dbActions.ts
================================================
import { eq, and, sql } from "drizzle-orm";
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import { schema } from "../index.js";
import { InstanceStatus, instanceStatusEnum } from "./schema.js";
import { NotFoundError } from "../lib/errors.js";
/**
* Creates a new Cyberdesk instance for a user.
* @param db Drizzle database instance
* @param userId The user ID to create the instance for
* @param timeoutMs Optional timeout in milliseconds. Defaults to 24 hours.
* @returns The newly created Cyberdesk instance details (id, status)
*/
export async function addDbInstance(
db: PostgresJsDatabase,
userId: string,
timeoutMs?: number
) {
const timeoutInterval = timeoutMs ? `${timeoutMs} milliseconds` : '24 hours';
const [newInstance] = await db
.insert(schema.cyberdeskInstances)
.values({
userId,
status: InstanceStatus.Pending,
timeoutAt: sql`NOW() + interval '${sql.raw(timeoutInterval)}'`,
})
.returning({
id: schema.cyberdeskInstances.id,
status: schema.cyberdeskInstances.status,
});
return newInstance;
}
/**
* Updates the status of a specific Cyberdesk instance, verifying ownership.
* @param db Drizzle database instance
* @param id The UUID of the Cyberdesk instance to update
* @param userId The user ID making the request (for authorization)
* @param status The new status to set
* @returns The updated instance details
* @throws NotFoundError if the instance is not found or the user is not authorized
*/
export async function updateDbInstanceStatus(
db: PostgresJsDatabase,
id: string,
userId: string,
status: InstanceStatus
) {
const [updatedInstance] = await db
.update(schema.cyberdeskInstances)
.set({
status: status,
updatedAt: new Date(),
})
.where(
and(
eq(schema.cyberdeskInstances.id, id),
eq(schema.cyberdeskInstances.userId, userId)
)
)
.returning({
id: schema.cyberdeskInstances.id,
status: schema.cyberdeskInstances.status,
});
if (!updatedInstance) {
throw new NotFoundError("Desktop instance not found or user not authorized.");
}
return updatedInstance;
}
/**
* Gets specific details (id, status, createdAt, timeoutAt) for a Cyberdesk instance by ID, verifying ownership.
* @param db Drizzle database instance
* @param id The UUID of the Cyberdesk instance to get details for
* @param userId The user ID making the request (for authorization)
* @returns The instance details
* @throws NotFoundError if the instance is not found or the user is not authorized
*/
export async function getDbInstanceDetails(
db: PostgresJsDatabase,
id: string,
userId: string
) {
const [result] = await db
.select({
id: schema.cyberdeskInstances.id,
status: schema.cyberdeskInstances.status,
createdAt: schema.cyberdeskInstances.createdAt,
timeoutAt: schema.cyberdeskInstances.timeoutAt,
streamUrl: schema.cyberdeskInstances.streamUrl,
})
.from(schema.cyberdeskInstances)
.where(
and(
eq(schema.cyberdeskInstances.id, id),
eq(schema.cyberdeskInstances.userId, userId)
)
)
.limit(1);
if (!result) {
throw new NotFoundError("Desktop instance not found or user not authorized.");
}
return result;
}
================================================
FILE: apps/api/src/db/index.ts
================================================
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import * as dotenv from 'dotenv';
// Ensure environment variables are loaded
dotenv.config();
import { schema } from "../index.js";
function connectDatabase(env: { SUPABASE_CONNECTION_STRING: string }): PostgresJsDatabase {
// Create a Postgres client for Drizzle
const connectionString = env.SUPABASE_CONNECTION_STRING;
const client = postgres(connectionString);
// Return a Drizzle instance with the schema
return drizzle(client, { schema });
}
// Use the connection string directly from process.env
const connectionString = process.env.SUPABASE_CONNECTION_STRING || "";
let db: PostgresJsDatabase;
try {
db = connectDatabase({
SUPABASE_CONNECTION_STRING: connectionString,
});
} catch (error) {
console.error("Failed to connect to database:", error);
throw error;
}
export { db };
================================================
FILE: apps/api/src/db/schema.ts
================================================
import { pgTable, serial, text, index, varchar, uuid, timestamp, pgSchema, jsonb, boolean, pgEnum } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
// Define the auth schema and users table
const authSchema = pgSchema('auth');
const users = authSchema.table('users', {
id: uuid('id').primaryKey(),
// You can add other fields from auth.users if needed
});
// Define the profiles table
export const profiles = pgTable("profiles", {
id: uuid("id").primaryKey().references(() => users.id, { onDelete: 'cascade' }),
unkeyKeyId: varchar("unkey_key_id", { length: 255 }),
stripeCustomerId: varchar("stripe_customer_id", { length: 255 }),
stripeSubscriptionId: varchar("stripe_subscription_id", { length: 255 }),
currentPeriodEnd: timestamp("current_period_end"),
subscriptionStatus: varchar("subscription_status", { length: 50 }),
planId: varchar("plan_id", { length: 100 }),
cancelAtPeriodEnd: boolean("cancel_at_period_end").default(false),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Define the status enum type
export enum InstanceStatus {
Pending = 'pending',
Running = 'running',
Terminated = 'terminated',
Error = 'error',
}
// Define the status enum type for Drizzle using the TS enum values
// Assert type as [string, ...string[]] to satisfy pgEnum's expectation
export const instanceStatusEnum = pgEnum('instance_status', Object.values(InstanceStatus) as [string, ...string[]]);
// Define the desktop_instances table
export const desktopInstances = pgTable("desktop_instances", {
id: uuid("id").primaryKey().defaultRandom(),
remoteId: varchar("remote_id", { length: 255 }).notNull(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: 'cascade' }), // Required field
streamUrl: varchar("stream_url", { length: 1024 }).notNull().default("https://placeholder-stream-url.cyberdesk.io"), // Default value for existing records
createdAt: timestamp("created_at").defaultNow(),
endedAt: timestamp("ended_at"), // Optional field (nullable by default)
});
// Define the cyberdesk_instances table (MVP 3)
export const cyberdeskInstances = pgTable("cyberdesk_instances", {
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: 'cascade' }), // Required field
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at"),
status: instanceStatusEnum("status").notNull().default(InstanceStatus.Pending),
timeoutAt: timestamp("timeout_at").notNull().default(sql`NOW() + interval '24 hours'`), // Set default to 24 hours from now
streamUrl: varchar("stream_url", { length: 1024 })
});
================================================
FILE: apps/api/src/index.ts
================================================
import { newApp } from "./lib/hono.js";
import desktop from "./routes/desktop.js";
import { serve } from "@hono/node-server";
import * as dotenv from 'dotenv'
export * as schema from "./db/schema.js"
// Load environment variables from .env.local file
dotenv.config()
const app = newApp();
// app.use(initCache());
// app.use(initRatelimiter());
app.route("/v1/", desktop);
// Use PORT environment variable with fallback to 3000 for local development
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
serve(
{
fetch: app.fetch,
port: port
},
(info) => {
console.log(`Server is running on port ${info.port}`);
}
);
================================================
FILE: apps/api/src/lib/cache.ts
================================================
import { createCache, type Cache as C} from "@unkey/cache";
import { MemoryStore } from "@unkey/cache/stores";
import type { Context, Middleware } from "./hono.js";
import type { Next } from "hono";
export type CacheNamespaces = {
// Define new namespaces here as needed
}
export type Cache = C
const persistentMap = new Map();
export function initCache(): Middleware {
return async (c: Context, next: Next) => {
const memory = new MemoryStore({ persistentMap: new Map() });
const cache = createCache({
// Add new namespaces as needed
});
c.set("cache", cache);
return next();
};
}
================================================
FILE: apps/api/src/lib/errors.ts
================================================
import { type Context, type Next } from 'hono';
import { HTTPException } from 'hono/http-exception';
import { ZodError } from 'zod';
import type { ContentfulStatusCode } from 'hono/utils/http-status';
// Base API Error class
export class ApiError extends Error {
public readonly statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.name = this.constructor.name; // Set the error name to the class name
Error.captureStackTrace(this, this.constructor); // Capture stack trace
}
}
// Specific Error Types
export class BadRequestError extends ApiError {
constructor(message = 'Bad Request') {
super(message, 400);
}
}
export class UnauthorizedError extends ApiError {
constructor(message = 'Unauthorized') {
super(message, 401);
}
}
export class NotFoundError extends ApiError {
constructor(message = 'Not Found') {
super(message, 404);
}
}
export class ConflictError extends ApiError {
constructor(message = 'Conflict') {
super(message, 409);
}
}
export class GatewayError extends ApiError {
constructor(message = 'Bad Gateway') {
super(message, 502);
}
}
export class InternalServerError extends ApiError {
constructor(message = 'Internal Server Error') {
super(message, 500);
}
}
// Specific error for when a command fails *inside* the CyberDesk instance
// We might still want to return a 200 OK with an error status for this scenario.
export class ActionExecutionError extends Error {
public readonly details?: string; // e.g., stderr output
constructor(message: string, details?: string) {
super(message);
this.details = details;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
// Hono Error Handler Middleware
export const honoErrorHandler = (err: Error, c: Context) => {
console.error("Error caught by middleware:", err); // Log the full error
// Handle Zod validation errors specifically
if (err instanceof ZodError) {
const validationErrors = err.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
return c.json(
{
status: "error",
error: `Validation failed: ${validationErrors}`,
docs: "https://docs.cyberdesk.io/docs/api-reference/",
},
400 // Bad Request for validation errors
);
}
// Handle our custom ApiError instances
if (err instanceof ApiError) {
return c.json(
{
status: "error",
error: err.message,
docs: "https://docs.cyberdesk.io/docs/api-reference/",
},
err.statusCode as ContentfulStatusCode
);
}
// Handle Hono's built-in HTTPException
if (err instanceof HTTPException) {
return c.json(
{
status: "error",
error: err.message,
docs: "https://docs.cyberdesk.io/docs/api-reference/",
},
err.status as ContentfulStatusCode
);
}
// Handle ActionExecutionError specifically (return 200 OK with error status)
if (err instanceof ActionExecutionError) {
return c.json(
{
status: "error",
error: err.message,
details: err.details, // Optionally include details like stderr
},
200 // Special case: Action failed but API communication was successful
);
}
// Fallback for unexpected errors
return c.json(
{
status: "error",
error: "An unexpected internal server error occurred.",
docs: "https://docs.cyberdesk.io/docs/api-reference/",
},
500 as const
);
};
================================================
FILE: apps/api/src/lib/hono.ts
================================================
import { OpenAPIHono, z } from "@hono/zod-openapi";
import type { UnkeyContext } from "@unkey/hono";
import type { Ratelimit } from "@unkey/ratelimit";
import type { Context as GenericContext, MiddlewareHandler } from "hono";
import type { ZodError } from "zod";
import type { Cache } from "./cache.js";
export type HonoEnv = {
Bindings: {
SUPABASE_CONNECTION_STRING: string;
// Unkey credentials
UNKEY_ROOT_KEY: string;
UNKEY_API_ID: string;
GATEWAY_URL: string;
};
Variables: {
cache: Cache
unkey: UnkeyContext;
ratelimit: Ratelimit;
};
};
export function parseZodErrorMessage(err: z.ZodError): string {
try {
const arr = JSON.parse(err.message) as Array<{
message: string;
path: Array;
}>;
const { path, message } = arr[0];
return `${path.join(".")}: ${message}`;
} catch {
return err.message;
}
}
export function handleZodError(
result:
| {
success: true;
data: any;
}
| {
success: false;
error: ZodError;
},
c: Context
) {
if (!result.success) {
return c.json(
{
error: parseZodErrorMessage(result.error),
},
{ status: 400 }
);
}
}
export function newApp() {
const app = new OpenAPIHono({
defaultHook: handleZodError,
});
app.onError((err: Error, c: Context) => {
console.error(err);
return c.json(
{
error: err.message,
},
{ status: 500 }
);
});
app.doc("/openapi.json", {
openapi: "3.1.0",
info: {
title: "API Reference",
version: "1.2.1",
description: "API for Cyberdesk, to create, control, and manage virtual desktop instances.",
},
servers: [
{
url: "https://api.cyberdesk.io",
description: "Production server"
}
],
});
app.openAPIRegistry.registerComponent("securitySchemes", "apiKeyAuth", {
type: "apiKey",
in: "header",
name: "x-api-key"
});
return app;
}
export type App = ReturnType;
export type Context = GenericContext;
export type Middleware = MiddlewareHandler;
================================================
FILE: apps/api/src/lib/posthog.ts
================================================
import { PostHog } from 'posthog-node'
import * as dotenv from 'dotenv';
// Ensure environment variables are loaded
dotenv.config();
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY;
if (!POSTHOG_API_KEY) {
throw new Error('POSTHOG_API_KEY is not set');
}
const POSTHOG_HOST = process.env.POSTHOG_HOST || 'https://us.i.posthog.com';
const client = new PostHog(
POSTHOG_API_KEY,
{ host: POSTHOG_HOST, enableExceptionAutocapture: true },
)
export default client;
================================================
FILE: apps/api/src/lib/ratelimit.ts
================================================
import { Ratelimit } from "@unkey/ratelimit";
import type { Context, Middleware } from "./hono.js";
import type { Next } from "hono";
export function initRatelimiter(): Middleware {
return async (c: Context, next: Next) => {
const ratelimit = new Ratelimit({
rootKey: c.env.UNKEY_ROOT_KEY,
namespace: "api-toolbox",
limit: 10,
duration: "30s",
async: true,
});
c.set("ratelimit", ratelimit);
return next();
};
}
================================================
FILE: apps/api/src/routes/desktop.ts
================================================
import { OpenAPIHono } from "@hono/zod-openapi";
import { env } from 'hono/adapter';
import { unkey, type UnkeyContext } from "@unkey/hono";
import { z } from "@hono/zod-openapi";
import axios from "axios";
import { profiles, cyberdeskInstances, InstanceStatus } from "../db/schema.js";
import {
bashAction,
computerAction,
createDesktop,
stopDesktop,
ComputerActionSchema,
CreateDesktopParamsSchema,
getDesktop,
BashActionSchema,
} from "../schema/desktop.js";
import { GatewayExecuteCommandRequestSchema, GatewayExecuteCommandResponseSchema } from "../schema/gateway.js";
import { db } from "../db/index.js";
import {
addDbInstance,
getDbInstanceDetails,
updateDbInstanceStatus,
} from "../db/dbActions.js";
import {
ApiError,
NotFoundError,
ConflictError,
BadRequestError,
GatewayError,
ActionExecutionError,
honoErrorHandler,
UnauthorizedError,
InternalServerError,
} from "../lib/errors.js";
import posthogClient from '../lib/posthog.js';
import type { Context } from "hono";
// Type definitions
type EnvVars = {
UNKEY_API_ID: string;
SUPABASE_URL: string;
SUPABASE_ANON_KEY: string;
SUPABASE_CONNECTION_STRING?: string;
WEB_URL: string;
GATEWAY_URL: string;
};
// Use the schema type for computer actions
type ComputerAction = z.infer;
// Use the schema type for desktop creation parameters
type CreateDesktopParams = z.infer;
type BashAction = z.infer;
// Create Hono instance
const desktop = new OpenAPIHono<{
Variables: {
unkey: UnkeyContext;
userId: string;
};
}>();
// Register the global error handler
desktop.onError(honoErrorHandler);
// API key verification middleware
desktop.use("*", async (c, next) => {
const { UNKEY_API_ID } = env(c);
const handler = unkey({
apiId: UNKEY_API_ID,
getKey: (c) => c.req.header("x-api-key"),
});
await handler(c, next);
});
// Helper function to capture API events in PostHog
async function captureApiEvent(
c: Context,
userId: string,
eventName: string,
additionalProperties: Record = {}
) {
const properties = {
path: c.req.path,
method: c.req.method,
userAgent: c.req.header('User-Agent'),
...additionalProperties,
};
posthogClient.capture({
distinctId: userId,
event: eventName,
properties: properties,
});
}
// Authentication and database connection middleware
desktop.use("*", async (c, next) => {
const result = c.get("unkey");
if (!result?.valid) {
throw new UnauthorizedError("Invalid API key");
}
const userId = result.ownerId;
if (!userId) {
throw new UnauthorizedError("No user associated with this key");
}
c.set("userId", userId);
await next();
});
// Route for getting a desktop instance's details
desktop.openapi(getDesktop, async (c) => {
const userId = c.get("userId");
const id = c.req.param("id");
const { GATEWAY_URL } = env(c);
if (!id) {
throw new BadRequestError("Instance ID is required");
}
await captureApiEvent(c, userId, 'Viewed Desktop Details');
const instanceDetails = await getDbInstanceDetails(db, id, userId);
// Adjust stream_url for dev environment if needed
let streamUrl: string | null = instanceDetails.streamUrl ?? null;
if (streamUrl && GATEWAY_URL.includes('dev-gateway')) {
// Replace any 'gateway.' (with or without subdomain) with 'dev-gateway.'
// This is to support the dev environment, where the gateway is accessible via dev-gateway.cyberdesk.io (or whatever subdomain you've set up)
streamUrl = streamUrl.replace(/\bgateway\./g, 'dev-gateway.');
}
return c.json({
id: instanceDetails.id,
status: instanceDetails.status,
created_at: (instanceDetails.createdAt || new Date(0)).toISOString(),
timeout_at: instanceDetails.timeoutAt.toISOString(),
stream_url: streamUrl
}, 200);
});
// Route for creating a new desktop instance
desktop.openapi(createDesktop, async (c) => {
const { GATEWAY_URL } = env(c);
const userId = c.get("userId");
let createDesktopParams: CreateDesktopParams;
try {
const body = await c.req.json().catch(() => ({}));
createDesktopParams = CreateDesktopParamsSchema.parse(body);
await captureApiEvent(c, userId, 'Created Desktop');
const newInstance = await addDbInstance(db, userId, createDesktopParams.timeout_ms);
try {
const provisioningUrl = `${GATEWAY_URL}/cyberdesk/${newInstance.id}`;
const response = await axios.post(provisioningUrl, {
timeoutMs: createDesktopParams.timeout_ms
});
console.log('Provisioning request successful:', response.data);
return c.json(
{
id: newInstance.id,
status: newInstance.status,
},
200
);
} catch (provisioningError) {
console.error('Error calling provisioning service during creation:', provisioningError);
await updateDbInstanceStatus(db, newInstance.id, userId, InstanceStatus.Error).catch(console.error);
if (axios.isAxiosError(provisioningError)) {
throw new GatewayError(`Failed to provision via Gateway: ${provisioningError.response?.statusText || provisioningError.message}`);
} else {
throw new GatewayError('Failed to initiate provisioning of Cyberdesk resource via Gateway for instance ' + newInstance.id);
}
}
} catch (error) {
if (error instanceof z.ZodError) {
throw error;
} else if (error instanceof ApiError) {
throw error;
} else {
console.error('Unexpected error during desktop creation:', error);
throw new InternalServerError("Failed to create desktop instance due to an unexpected error.");
}
}
});
// Route for stopping a desktop instance
desktop.openapi(stopDesktop, async (c) => {
const userId = c.get("userId");
const id = c.req.param("id");
const { GATEWAY_URL } = env(c);
await captureApiEvent(c, userId, 'Stopped Desktop');
const updatedInstance = await updateDbInstanceStatus(db, id, userId, InstanceStatus.Terminated);
try {
const provisioningUrl = `${GATEWAY_URL}/cyberdesk/${id}/stop`;
await axios.post(provisioningUrl);
} catch (provisioningError) {
console.error('Error calling provisioning service during stop:', provisioningError);
}
const responsePayload: { status: InstanceStatus } = {
status: updatedInstance.status as InstanceStatus,
};
return c.json(
responsePayload,
200
);
});
async function executeComputerAction(
id: string,
userId: string,
action: ComputerAction,
GATEWAY_URL: string
): Promise {
console.log("Executing computer action:", action);
const instance = await getDbInstanceDetails(db, id, userId);
if (!instance) {
throw new NotFoundError("Instance not found or unauthorized");
}
if (instance.status !== 'running') {
throw new ConflictError(`Instance is not running (status: ${instance.status}). Cannot perform action.`);
}
let command: string;
const displayPrefix = "export DISPLAY=:99;";
switch (action.type) {
case "click_mouse": {
const { x, y, button = "left", num_of_clicks = 1, click_type = "click" } = action;
const buttonMap: { [key: string]: number } = { left: 1, middle: 2, right: 3 };
const btn = buttonMap[button] || 1;
let moveCmd = "";
if (x !== undefined && y !== undefined) {
moveCmd = `xdotool mousemove ${x} ${y} && `;
}
let clickCmd: string;
if (click_type === "click") {
clickCmd = `xdotool click --repeat ${num_of_clicks} ${btn}`;
} else if (click_type === "down") {
clickCmd = `xdotool mousedown ${btn}`;
} else { // up
clickCmd = `xdotool mouseup ${btn}`;
}
command = `${displayPrefix} ${moveCmd}${clickCmd}`;
break;
}
case "scroll": {
const { direction, amount } = action;
const directionMap: { [key: string]: number } = { up: 4, down: 5, left: 6, right: 7 };
const btn = directionMap[direction];
// Ensure amount is a reasonable positive integer
const repeatCount = Math.max(1, Math.min(Math.floor(amount), 500)); // Cap repeat at 500 for sanity
const delayMs = 25; // Reduce delay for faster scrolling
command = `${displayPrefix} xdotool click --repeat ${repeatCount} --delay ${delayMs} ${btn}`;
break;
}
case "move_mouse": {
command = `${displayPrefix} xdotool mousemove ${action.x} ${action.y}`;
break;
}
case "drag_mouse": {
const { start, end } = action;
command = `${displayPrefix} xdotool mousemove ${start.x} ${start.y} mousedown 1 mousemove ${end.x} ${end.y} mouseup 1`;
break;
}
case "type": {
const escapedText = action.text.replace(/'/g, "'\''");
command = `${displayPrefix} xdotool type --clearmodifiers --delay 50 '${escapedText}'`;
break;
}
case "press_keys": {
const { keys, key_action_type = "press" } = action;
const keyString = Array.isArray(keys) ? keys.join('+') : keys;
let keyCmd: string;
if (key_action_type === "down") {
keyCmd = `keydown`;
} else if (key_action_type === "up") {
keyCmd = `keyup`;
} else { // press
keyCmd = `key`;
}
command = `${displayPrefix} xdotool ${keyCmd} --clearmodifiers ${keyString}`;
break;
}
case "wait": {
const seconds = Math.max(0, action.ms / 1000);
command = `sleep ${seconds}`;
break;
}
case "screenshot": {
command = `${displayPrefix} scrot -q 100 /tmp/screen.jpg && base64 /tmp/screen.jpg && rm /tmp/screen.jpg`;
break;
}
case "get_cursor_position": {
command = `${displayPrefix} xdotool getmouselocation --shell`;
break;
}
default:
throw new BadRequestError(`Unsupported action type: ${(action as any).type}`);
}
console.log(`Executing command for instance ${id}: ${command}`);
const provisioningUrl = `${GATEWAY_URL}/cyberdesk/${id}/execute-command`;
const requestBody = GatewayExecuteCommandRequestSchema.parse({ command });
try {
const response = await axios.post>(
provisioningUrl,
requestBody
);
const parsedResponse = GatewayExecuteCommandResponseSchema.parse(response.data);
if (parsedResponse.vm_response.return_code !== 0) {
throw new ActionExecutionError(
`Command failed with code ${parsedResponse.vm_response.return_code}`,
parsedResponse.vm_response.stderr || 'No stderr output'
);
}
return parsedResponse.vm_response.stdout.trim();
} catch (error: any) {
console.error(`Error executing command for instance ${id}:`, error);
if (error instanceof z.ZodError) {
throw new GatewayError(`Invalid response structure from gateway: ${error.errors.map(e => e.message).join(', ')}`);
} else if (axios.isAxiosError(error)) {
const gatewayMessage = error.response?.data?.message || error.message || "Failed to execute command via gateway";
throw new GatewayError(`Action execution failed via gateway: ${gatewayMessage}`);
} else if (error instanceof ApiError) {
throw error;
} else {
throw new InternalServerError(`Unexpected error during action execution: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
/**
* Executes a raw bash command on the target instance via the gateway.
* @param id Instance ID
* @param userId User ID for authorization
* @param command The bash command string to execute
* @param GATEWAY_URL Gateway URL
* @returns Promise Resolves with stdout on success
* @throws Error on command failure or communication issues
*/
async function executeBashCommand(
id: string,
userId: string,
command: string,
GATEWAY_URL: string
): Promise {
const instance = await getDbInstanceDetails(db, id, userId);
if (!instance) {
throw new NotFoundError("Instance not found or unauthorized");
}
if (instance.status !== 'running') {
throw new ConflictError(`Instance is not running (status: ${instance.status}). Cannot execute command.`);
}
console.log(`Executing bash command for instance ${id}: ${command}`);
const provisioningUrl = `${GATEWAY_URL}/cyberdesk/${id}/execute-command`;
const requestBody = GatewayExecuteCommandRequestSchema.parse({ command });
try {
const response = await axios.post>(
provisioningUrl,
requestBody
);
const parsedResponse = GatewayExecuteCommandResponseSchema.parse(response.data);
console.log(`Bash command execution response for instance ${id}:`, parsedResponse);
return parsedResponse.vm_response.stdout.trim() || parsedResponse.vm_response.stderr.trim();
} catch (error: any) {
console.error(`Error executing bash command for instance ${id}:`, error);
if (error instanceof z.ZodError) {
throw new GatewayError(`Invalid response structure from gateway: ${error.errors.map(e => e.message).join(', ')}`);
} else if (axios.isAxiosError(error)) {
const gatewayMessage = error.response?.data?.message || error.message || "Failed to execute command via gateway";
throw new GatewayError(`Bash command execution failed via gateway: ${gatewayMessage}`);
} else if (error instanceof ApiError) {
throw error;
} else {
throw new InternalServerError(`Unexpected error during bash command execution: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
// Route for performing a computer action on a desktop
desktop.openapi(computerAction, async (c) => {
const userId = c.get("userId");
const id = c.req.param("id");
const { GATEWAY_URL } = env(c);
let action: ComputerAction;
try {
const body = await c.req.json().catch(() => ({}));
action = ComputerActionSchema.parse(body);
// Capture event with actionType
await captureApiEvent(c, userId, 'Performed Computer Action', { actionType: action.type });
} catch (parseError: any) {
if (parseError instanceof z.ZodError) {
throw parseError;
}
throw new BadRequestError(`Invalid request body: ${parseError?.message || 'Unknown parsing error'}`);
}
const resultString = await executeComputerAction(id, userId, action, GATEWAY_URL);
if (action.type === "screenshot") {
// Remove all whitespace (including newlines) from base64 string
const cleanedBase64 = resultString.replace(/\s+/g, '');
return c.json({ base64_image: cleanedBase64 }, 200);
} else if (action.type === "get_cursor_position") {
return c.json({ output: resultString, }, 200);
} else {
return c.json({ output: resultString, }, 200);
}
});
// Route for executing a bash command on a desktop
desktop.openapi(bashAction, async (c) => {
const userId = c.get("userId");
const id = c.req.param("id");
const { GATEWAY_URL } = env(c);
let bashAction: BashAction;
try {
const body = await c.req.json().catch(() => ({}));
bashAction = BashActionSchema.parse(body);
await captureApiEvent(c, userId, 'Executed Bash Command');
} catch (parseError: any) {
if (parseError instanceof z.ZodError) {
throw parseError;
}
throw new BadRequestError(`Invalid request body: ${parseError?.message || 'Unknown parsing error'}`);
}
const resultString = await executeBashCommand(id, userId, bashAction.command, GATEWAY_URL);
return c.json({ status: "success", output: resultString, }, 200);
});
export default desktop;
================================================
FILE: apps/api/src/schema/desktop.ts
================================================
import { createRoute, z } from "@hono/zod-openapi";
import { openApiErrorResponses } from "./errors.js";
import { InstanceStatus, instanceStatusEnum } from "../db/schema.js";
// Header schema for API key authentication
const HeadersSchema = z.object({
"x-api-key": z.string().openapi({
description: "API key for authentication",
example: "api_12345",
}),
});
// Common schema for action oriented API responses
const ActionResponseSchema = z.object({
output: z.string().optional().openapi({
description: "Raw string output from the executed command (if any)",
example: "X=500 Y=300",
}),
error: z.string().optional().openapi({
description: "Error message if the operation failed (also indicated by non-2xx HTTP status)",
example: "Command failed with code 1: xdotool: command not found",
}),
base64_image: z.string().optional().openapi({
description: "Base64 encoded JPEG image data (only returned for screenshot actions)",
example: "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQ...",
})
});
// Point schema for coordinates
const PointSchema = z.object({
x: z.number().int().openapi({
description: "X coordinate on the screen",
example: 500,
}),
y: z.number().int().openapi({
description: "Y coordinate on the screen",
example: 300,
}),
});
// Schema for desktop creation parameters
export const CreateDesktopParamsSchema = z.object({
timeout_ms: z.number().int().optional().openapi({
description: "Timeout in milliseconds for the desktop session",
example: 3600000,
}),
});
// Schema for the response of the create desktop endpoint
export const CreateDesktopResponseSchema = z.object({
id: z.string().openapi({
description: "Unique identifier for the desktop instance",
example: "desktop_12345",
}),
status: z.enum(instanceStatusEnum.enumValues).openapi({
description: "Initial status of the desktop instance after creation request",
example: InstanceStatus.Pending,
}),
});
// Schema for the response of the stop desktop endpoint
export const StopDesktopResponseSchema = z.object({
status: z.enum(instanceStatusEnum.enumValues).openapi({
description: "Status of the desktop instance after stopping",
example: InstanceStatus.Terminated,
}),
});
// Computer action schema with discriminated union
export const ComputerActionSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("click_mouse").openapi({
description: "Perform a mouse action: click, press (down), or release (up). Defaults to a single left click at the current position.",
example: "click_mouse",
}),
x: z.number().int().optional().openapi({
description: "X coordinate for the action (optional, uses current position if omitted)",
example: 500,
}),
y: z.number().int().optional().openapi({
description: "Y coordinate for the action (optional, uses current position if omitted)",
example: 300,
}),
button: z.enum(["left", "right", "middle"]).optional().openapi({
description: "Mouse button to use (optional, defaults to 'left')",
example: "left",
}),
num_of_clicks: z.number().int().min(0).optional().openapi({
description: "Number of clicks to perform (optional, defaults to 1, only applicable for 'click' type)",
example: 1,
}),
click_type: z.enum(["click", "down", "up"]).optional().openapi({
description: "Type of mouse action (optional, defaults to 'click')",
example: "click",
}),
}).openapi({ title: "Click Mouse Action" }),
z.object({
type: z.literal("scroll").openapi({
description: "Scroll the mouse wheel in the specified direction",
example: "scroll",
}),
direction: z.enum(["up", "down", "left", "right"]).openapi({
description: "Direction to scroll",
example: "down",
}),
amount: z.number().int().openapi({
description: "Amount to scroll in pixels",
example: 100,
}),
}).openapi({ title: "Scroll Action" }),
z.object({
type: z.literal("move_mouse").openapi({
description: "Move the mouse cursor to the specified coordinates",
example: "move_mouse",
}),
x: z.number().int().openapi({
description: "X coordinate to move to",
example: 500,
}),
y: z.number().int().openapi({
description: "Y coordinate to move to",
example: 300,
}),
}).openapi({ title: "Move Mouse Action" }),
z.object({
type: z.literal("drag_mouse").openapi({
description: "Drag the mouse from start to end coordinates",
example: "drag_mouse",
}),
start: PointSchema.openapi({
description: "Starting coordinates for the drag operation",
example: { x: 100, y: 100 },
}),
end: PointSchema.openapi({
description: "Ending coordinates for the drag operation",
example: { x: 300, y: 300 },
}),
}).openapi({ title: "Drag Mouse Action" }),
z.object({
type: z.literal("type").openapi({
description: "Type text at the current cursor position",
example: "type",
}),
text: z.string().openapi({
description: "Text to type",
example: "Hello, World!",
}),
}).openapi({ title: "Type Text Action" }),
z.object({
type: z.literal("press_keys").openapi({
description: "Press, hold down, or release one or more keyboard keys. Defaults to a single press and release.",
example: "press_keys",
}),
keys: z.union([
z.string().openapi({
description: "Single key to press",
example: "Enter",
}),
z.array(z.string()).openapi({
description: "Multiple keys to press simultaneously",
example: ["Control", "c"],
})
]),
key_action_type: z.enum(["press", "down", "up"]).optional().openapi({
description: "Type of key action (optional, defaults to 'press' which is a down and up action)",
example: "press",
}),
}).openapi({ title: "Press Keys Action" }),
z.object({
type: z.literal("wait").openapi({
description: "Wait for the specified number of milliseconds",
example: "wait",
}),
ms: z.number().int().openapi({
description: "Time to wait in milliseconds",
example: 1000,
}),
}).openapi({ title: "Wait Action" }),
z.object({
type: z.literal("screenshot").openapi({
description: "Take a screenshot of the desktop",
example: "screenshot",
}),
}).openapi({ title: "Screenshot Action" }),
z.object({
type: z.literal("get_cursor_position").openapi({
description: "Get the current mouse cursor position",
example: "get_cursor_position",
}),
}).openapi({ title: "Get Cursor Position Action" }),
]);
// Schema for Bash Action parameters
export const BashActionSchema = z.object({
command: z.string().openapi({
description: "Bash command to execute",
example: "echo 'Hello, World!'",
}),
});
// Create Desktop Route
export const createDesktop = createRoute({
method: "post",
path: "/desktop",
tags: ["Desktop"],
summary: "Create a new virtual desktop instance",
description: "Creates a new virtual desktop instance and returns its ID and stream URL",
request: {
headers: HeadersSchema,
body: {
content: {
"application/json": {
schema: CreateDesktopParamsSchema,
},
},
},
},
responses: {
200: {
content: {
"application/json": {
schema: CreateDesktopResponseSchema,
},
},
description: "Desktop creation initiated successfully",
},
...openApiErrorResponses,
},
});
// Stop Desktop Route
export const stopDesktop = createRoute({
method: "post",
path: "/desktop/{id}/stop",
tags: ["Desktop"],
summary: "Stop a running desktop instance",
description: "Stops a running desktop instance and cleans up resources",
request: {
headers: HeadersSchema,
params: z.object({
id: z.string().openapi({
description: "Desktop instance ID to stop",
example: "desktop_12345",
}),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: StopDesktopResponseSchema,
},
},
description: "Desktop stopped successfully",
},
...openApiErrorResponses,
},
});
// Get Desktop Route Response Schema
const GetDesktopResponseSchema = z.object({
id: z.string().uuid().openapi({
description: "Unique identifier for the desktop instance",
example: "a1b2c3d4-e5f6-7890-1234-567890abcdef",
}),
status: z.enum(instanceStatusEnum.enumValues).openapi({
description: "Current status of the desktop instance",
example: "running",
}),
stream_url: z.string().nullable().openapi({
description: "URL for the desktop stream (null if the desktop is not running)",
example: "https://cyberdesk.com/vnc/a1b2c3d4-e5f6-7890-1234-567890abcdef",
}),
created_at: z.string().datetime().openapi({
description: "Timestamp when the instance was created",
example: "2023-10-27T10:00:00Z",
}),
timeout_at: z.string().datetime().openapi({
description: "Timestamp when the instance will automatically time out",
example: "2023-10-28T10:00:00Z",
}),
});
// Get Desktop Route
export const getDesktop = createRoute({
method: "get",
path: "/desktop/{id}",
tags: ["Desktop"],
summary: "Get details of a specific desktop instance",
description: "Returns the ID, status, creation timestamp, and timeout timestamp for a given desktop instance.",
request: {
headers: HeadersSchema,
params: z.object({
id: z.string().uuid().openapi({
description: "The UUID of the desktop instance to retrieve",
example: "a1b2c3d4-e5f6-7890-1234-567890abcdef",
}),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: GetDesktopResponseSchema,
},
},
description: "Desktop instance details retrieved successfully",
},
...openApiErrorResponses,
},
});
// Computer Action Route
export const computerAction = createRoute({
method: "post",
path: "/desktop/{id}/computer-action",
tags: ["Desktop"],
summary: "Perform an action on the desktop",
description: "Executes a computer action such as mouse clicks, keyboard input, or screenshots on the desktop",
request: {
headers: HeadersSchema,
params: z.object({
id: z.string().openapi({
description: "Desktop instance ID to perform the action on",
example: "desktop_12345",
}),
}),
body: {
content: {
"application/json": {
schema: ComputerActionSchema,
},
},
},
},
responses: {
200: {
content: {
"application/json": {
schema: ActionResponseSchema,
},
},
description: "Action executed successfully. Response may contain output or image data depending on the action.",
},
...openApiErrorResponses,
},
});
// Bash Action Route
export const bashAction = createRoute({
method: "post",
path: "/desktop/{id}/bash-action",
tags: ["Desktop"],
summary: "Execute a bash command on the desktop",
description: "Runs a bash command on the desktop and returns the command output",
request: {
headers: HeadersSchema,
params: z.object({
id: z.string().openapi({
description: "Desktop instance ID to run the command on",
example: "desktop_12345",
}),
}),
body: {
content: {
"application/json": {
schema: BashActionSchema,
},
},
},
},
responses: {
200: {
content: {
"application/json": {
schema: ActionResponseSchema,
},
},
description: "Command executed successfully. Response contains command output.",
},
...openApiErrorResponses,
},
});
================================================
FILE: apps/api/src/schema/errors.ts
================================================
import { z } from "@hono/zod-openapi";
const errorSchema = z.object({
status: z.literal("error").openapi({ example: "error" }),
error: z.string().openapi({
description: "Error message detailing what went wrong",
example: "Instance not found or unauthorized"
})
});
export const openApiErrorResponses = {
400: {
description:
"The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).",
content: {
"application/json": {
schema: errorSchema,
},
},
},
401: {
description: `Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.`,
content: {
"application/json": {
schema: errorSchema,
},
},
},
403: {
description:
"The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.",
content: {
"application/json": {
schema: errorSchema,
},
},
},
404: {
description:
"The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.",
content: {
"application/json": {
schema: errorSchema,
},
},
},
409: {
description:
"This response is sent when a request conflicts with the current state of the server.",
content: {
"application/json": {
schema: errorSchema,
},
},
},
429: {
description: `The user has sent too many requests in a given amount of time ("rate limiting")`,
content: {
"application/json": {
schema: errorSchema,
},
},
},
500: {
description:
"The server has encountered a situation it does not know how to handle.",
content: {
"application/json": {
schema: errorSchema,
},
},
},
502: {
description:
"The server, while acting as a gateway or proxy, received an invalid response from the upstream server.",
content: {
"application/json": {
schema: errorSchema,
},
},
},
};
================================================
FILE: apps/api/src/schema/gateway.ts
================================================
import { z } from 'zod';
// Schema for Gateway Execute Command Request
export const GatewayExecuteCommandRequestSchema = z.object({
command: z.string()
});
// Schema for Gateway Execute Command Response
export const GatewayExecuteCommandResponseSchema = z.object({
status: z.string(),
vm_status_code: z.number().int(),
vm_response: z.object({
args: z.array(z.string()).optional(),
return_code: z.number().int(),
stdout: z.string(),
stderr: z.string(),
duration_s: z.number().optional(),
})
});
================================================
FILE: apps/api/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"verbatimModuleSyntax": false,
"esModuleInterop": true,
"skipLibCheck": true,
"types": [
"node"
],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"outDir": "./dist",
"incremental": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
================================================
FILE: apps/docs/.gitignore
================================================
# Dependencies
node_modules/
.pnp
.pnp.js
# Build outputs
.next/
out/
build/
dist/
.cache/
# Generated files
.docusaurus/
.cache-loader/
.vercel/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Debug logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Editor directories and files
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Temporary files
*.log
*.tmp
tmp/
temp/
# Coverage directory
coverage/
================================================
FILE: apps/docs/.map.ts
================================================
/** Auto-generated **/
declare const map: Record
export { map }
================================================
FILE: apps/docs/README.md
================================================
# docs
This is a Next.js application generated with
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
Run development server:
```bash
npm run dev
# or
pnpm dev
# or
yarn dev
```
Open http://localhost:3000/docs to view the documentation.
You can also view the reference documentation for the API at https://docs.cyberdesk.io/docs/api-reference which is generated from the /scripts/ directory.
## Learn More
To learn more about Fumadocs features , take a look at the following
resources:
- [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs
================================================
FILE: apps/docs/app/api/search/route.ts
================================================
import { getPages } from "@/app/source";
import { createSearchAPI } from "fumadocs-core/search/server";
export const { GET } = createSearchAPI("advanced", {
indexes: getPages().map((page) => ({
title: page.data.title,
structuredData: page.data.exports.structuredData,
id: page.url,
url: page.url,
})),
});
================================================
FILE: apps/docs/app/docs/[[...slug]]/page.tsx
================================================
import { getPage, getPages } from "@/app/source";
import { DocsBody, DocsPage } from "fumadocs-ui/page";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
export default async function Page({
params,
}: {
params: { slug?: string[] };
}) {
const page = getPage(params.slug);
if (page == null) {
notFound();
}
const MDX = page.data.exports.default;
return (
{page.data.title}
);
}
export async function generateStaticParams() {
return getPages().map((page) => ({
slug: page.slugs,
}));
}
export function generateMetadata({ params }: { params: { slug?: string[] } }) {
const page = getPage(params.slug);
if (page == null) notFound();
return {
title: page.data.title,
description: page.data.description,
} satisfies Metadata;
}
================================================
FILE: apps/docs/app/docs/layout.tsx
================================================
import { DocsLayout } from "fumadocs-ui/layout";
import type { ReactNode } from "react";
import { baseOptions } from "../layout.config";
import { pageTree } from "../source";
export default function Layout({ children }: { children: ReactNode }) {
return (
{children}
);
}
================================================
FILE: apps/docs/app/global.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
================================================
FILE: apps/docs/app/layout.config.tsx
================================================
import { type BaseLayoutProps } from "fumadocs-ui/layout";
// basic configuration here
export const baseOptions: BaseLayoutProps = {
nav: {
title: "Cyberdesk Docs",
},
links: [
{
text: "Documentation",
url: "/docs",
active: "nested-url",
},
{
text: "Main Site",
url: "https://cyberdesk.io",
}
],
githubUrl: "https://github.com/cyberdesk-hq/cyberdesk",
};
================================================
FILE: apps/docs/app/layout.tsx
================================================
import "./global.css";
import { RootProvider } from "fumadocs-ui/provider";
import { Inter } from "next/font/google";
import type { ReactNode } from "react";
import { Analytics } from "@vercel/analytics/react";
const inter = Inter({
subsets: ["latin"],
});
export default function Layout({ children }: { children: ReactNode }) {
return (
{children}
);
}
================================================
FILE: apps/docs/app/page.tsx
================================================
import { redirect } from 'next/navigation';
export default function HomePage() {
redirect('/docs');
}
================================================
FILE: apps/docs/app/source.ts
================================================
import { map } from "@/.map";
import { loader } from "fumadocs-core/source";
import { createMDXSource } from "fumadocs-mdx";
export const { getPage, getPages, pageTree } = loader({
baseUrl: "/docs",
rootDir: "docs",
source: createMDXSource(map),
});
================================================
FILE: apps/docs/content/docs/api-reference.mdx
================================================
---
title: API Reference
description: API for Cyberdesk, to create, control, and manage virtual desktop instances.
full: true
toc: false
---
import { Root, API, APIInfo, APIExample, Responses, Response, ResponseTypes, ExampleResponse, TypeScriptResponse, Property, ObjectCollapsible, Requests, Request } from "fumadocs-ui/components/api";
## Get details of a specific desktop instance
Returns the ID, status, creation timestamp, and timeout timestamp for a given desktop instance.
### Path Parameters
The UUID of the desktop instance to retrieve
Example: `"a1b2c3d4-e5f6-7890-1234-567890abcdef"`Format: `"uuid"`
### Header Parameters
API key for authentication
Example: `"api_12345"`
| Status code | Description |
| ----------- | ----------- |
| `200` | Desktop instance details retrieved successfully |
| `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). |
| `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. |
| `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. |
| `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. |
| `409` | This response is sent when a request conflicts with the current state of the server. |
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
| `500` | The server has encountered a situation it does not know how to handle. |
| `502` | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. |
```bash
curl -X GET "https://api.cyberdesk.io/v1/desktop/:id" \
-H "x-api-key: api_12345"
```
```js
fetch("https://api.cyberdesk.io/v1/desktop/:id", {
method: "GET",
headers: {
"x-api-key": "api_12345"
}
});
```
```json
{
"id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"status": "running",
"stream_url": "https://cyberdesk.com/vnc/a1b2c3d4-e5f6-7890-1234-567890abcdef",
"created_at": "2023-10-27T10:00:00Z",
"timeout_at": "2023-10-28T10:00:00Z"
}
```
```ts
export interface Response {
/**
* Unique identifier for the desktop instance
*/
id: string;
/**
* Current status of the desktop instance
*/
status: "pending" | "running" | "terminated" | "error";
/**
* URL for the desktop stream (null if the desktop is not running)
*/
stream_url: string;
/**
* Timestamp when the instance was created
*/
created_at: string;
/**
* Timestamp when the instance will automatically time out
*/
timeout_at: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
## Create a new virtual desktop instance
Creates a new virtual desktop instance and returns its ID and stream URL
### Request Body (Optional)
Timeout in milliseconds for the desktop session
Example: `3600000`
### Header Parameters
API key for authentication
Example: `"api_12345"`
| Status code | Description |
| ----------- | ----------- |
| `200` | Desktop creation initiated successfully |
| `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). |
| `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. |
| `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. |
| `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. |
| `409` | This response is sent when a request conflicts with the current state of the server. |
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
| `500` | The server has encountered a situation it does not know how to handle. |
| `502` | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. |
```bash
curl -X POST "https://api.cyberdesk.io/v1/desktop" \
-H "x-api-key: api_12345" \
-d '{
"timeout_ms": 3600000
}'
```
```js
fetch("https://api.cyberdesk.io/v1/desktop", {
method: "POST",
headers: {
"x-api-key": "api_12345"
}
});
```
```json
{
"id": "desktop_12345",
"status": "pending"
}
```
```ts
export interface Response {
/**
* Unique identifier for the desktop instance
*/
id: string;
/**
* Initial status of the desktop instance after creation request
*/
status: "pending" | "running" | "terminated" | "error";
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
## Stop a running desktop instance
Stops a running desktop instance and cleans up resources
### Path Parameters
Desktop instance ID to stop
Example: `"desktop_12345"`
### Header Parameters
API key for authentication
Example: `"api_12345"`
| Status code | Description |
| ----------- | ----------- |
| `200` | Desktop stopped successfully |
| `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). |
| `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. |
| `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. |
| `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. |
| `409` | This response is sent when a request conflicts with the current state of the server. |
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
| `500` | The server has encountered a situation it does not know how to handle. |
| `502` | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. |
```bash
curl -X POST "https://api.cyberdesk.io/v1/desktop/:id/stop" \
-H "x-api-key: api_12345"
```
```js
fetch("https://api.cyberdesk.io/v1/desktop/:id/stop", {
method: "POST",
headers: {
"x-api-key": "api_12345"
}
});
```
```json
{
"status": "terminated"
}
```
```ts
export interface Response {
/**
* Status of the desktop instance after stopping
*/
status: "pending" | "running" | "terminated" | "error";
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
## Perform an action on the desktop
Executes a computer action such as mouse clicks, keyboard input, or screenshots on the desktop
### Request Body (Optional)
Perform a mouse action: click, press (down), or release (up). Defaults to a single left click at the current position.
Example: `"click_mouse"`Value in: `"click_mouse"`
X coordinate for the action (optional, uses current position if omitted)
Example: `500`
Y coordinate for the action (optional, uses current position if omitted)
Example: `300`
Mouse button to use (optional, defaults to 'left')
Example: `"left"`Value in: `"left" | "right" | "middle"`
Number of clicks to perform (optional, defaults to 1, only applicable for 'click' type)
Example: `1`Minimum: `0`
Type of mouse action (optional, defaults to 'click')
Example: `"click"`Value in: `"click" | "down" | "up"`
Scroll the mouse wheel in the specified direction
Example: `"scroll"`Value in: `"scroll"`
Direction to scroll
Example: `"down"`Value in: `"up" | "down" | "left" | "right"`
Amount to scroll in pixels
Example: `100`
Move the mouse cursor to the specified coordinates
Example: `"move_mouse"`Value in: `"move_mouse"`
X coordinate to move to
Example: `500`
Y coordinate to move to
Example: `300`
Drag the mouse from start to end coordinates
Example: `"drag_mouse"`Value in: `"drag_mouse"`
Starting coordinates for the drag operation
Example: `{"x":100,"y":100}`
X coordinate on the screen
Example: `500`
Y coordinate on the screen
Example: `300`
Ending coordinates for the drag operation
Example: `{"x":300,"y":300}`
X coordinate on the screen
Example: `500`
Y coordinate on the screen
Example: `300`
Type text at the current cursor position
Example: `"type"`Value in: `"type"`
Text to type
Example: `"Hello, World!"`
Press, hold down, or release one or more keyboard keys. Defaults to a single press and release.
Example: `"press_keys"`Value in: `"press_keys"`"} required={true} deprecated={undefined}>
"} required={false} deprecated={undefined}>
Multiple keys to press simultaneously
Example: `["Control","c"]`
Type of key action (optional, defaults to 'press' which is a down and up action)
Example: `"press"`Value in: `"press" | "down" | "up"`
Wait for the specified number of milliseconds
Example: `"wait"`Value in: `"wait"`
Time to wait in milliseconds
Example: `1000`
Take a screenshot of the desktop
Example: `"screenshot"`Value in: `"screenshot"`
Get the current mouse cursor position
Example: `"get_cursor_position"`Value in: `"get_cursor_position"`
### Path Parameters
Desktop instance ID to perform the action on
Example: `"desktop_12345"`
### Header Parameters
API key for authentication
Example: `"api_12345"`
| Status code | Description |
| ----------- | ----------- |
| `200` | Action executed successfully. Response may contain output or image data depending on the action. |
| `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). |
| `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. |
| `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. |
| `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. |
| `409` | This response is sent when a request conflicts with the current state of the server. |
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
| `500` | The server has encountered a situation it does not know how to handle. |
| `502` | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. |
```bash
curl -X POST "https://api.cyberdesk.io/v1/desktop/:id/computer-action" \
-H "x-api-key: api_12345" \
-d '{
"type": "click_mouse",
"x": 500,
"y": 300,
"button": "left",
"num_of_clicks": 1,
"click_type": "click"
}'
```
```js
fetch("https://api.cyberdesk.io/v1/desktop/:id/computer-action", {
method: "POST",
headers: {
"x-api-key": "api_12345"
}
});
```
```json
{
"output": "X=500 Y=300",
"error": "Command failed with code 1: xdotool: command not found",
"base64_image": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQ..."
}
```
```ts
export interface Response {
/**
* Raw string output from the executed command (if any)
*/
output?: string;
/**
* Error message if the operation failed (also indicated by non-2xx HTTP status)
*/
error?: string;
/**
* Base64 encoded JPEG image data (only returned for screenshot actions)
*/
base64_image?: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
## Execute a bash command on the desktop
Runs a bash command on the desktop and returns the command output
### Request Body (Optional)
Bash command to execute
Example: `"echo 'Hello, World!'"`
### Path Parameters
Desktop instance ID to run the command on
Example: `"desktop_12345"`
### Header Parameters
API key for authentication
Example: `"api_12345"`
| Status code | Description |
| ----------- | ----------- |
| `200` | Command executed successfully. Response contains command output. |
| `400` | The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing). |
| `401` | Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response. |
| `403` | The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. |
| `404` | The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. |
| `409` | This response is sent when a request conflicts with the current state of the server. |
| `429` | The user has sent too many requests in a given amount of time ("rate limiting") |
| `500` | The server has encountered a situation it does not know how to handle. |
| `502` | The server, while acting as a gateway or proxy, received an invalid response from the upstream server. |
```bash
curl -X POST "https://api.cyberdesk.io/v1/desktop/:id/bash-action" \
-H "x-api-key: api_12345" \
-d '{
"command": "echo 'Hello, World!'"
}'
```
```js
fetch("https://api.cyberdesk.io/v1/desktop/:id/bash-action", {
method: "POST",
headers: {
"x-api-key": "api_12345"
}
});
```
```json
{
"output": "X=500 Y=300",
"error": "Command failed with code 1: xdotool: command not found",
"base64_image": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQ..."
}
```
```ts
export interface Response {
/**
* Raw string output from the executed command (if any)
*/
output?: string;
/**
* Error message if the operation failed (also indicated by non-2xx HTTP status)
*/
error?: string;
/**
* Base64 encoded JPEG image data (only returned for screenshot actions)
*/
base64_image?: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
```json
{
"status": "error",
"error": "Instance not found or unauthorized"
}
```
```ts
export interface Response {
status: "error";
/**
* Error message detailing what went wrong
*/
error: string;
}
```
================================================
FILE: apps/docs/content/docs/conceptual-guide.mdx
================================================
---
title: Conceptual Guide
description: Understanding core concepts of Cyberdesk
---
Understanding the core concepts of Cyberdesk will help you build more effective applications.
## Architecture Overview
Cyberdesk is built on a microservices architecture that provides scalable, reliable access to virtual desktop environments. The system consists of:
- **API Service**: The backend service accessed via the SDK.
- **Desktop Pool**: Managed virtual desktop instances.
- **Stream Service**: Real-time visual streaming of desktop environments.
- **Database**: Persistent storage for desktop metadata and user information.
## Desktop Instances
A desktop instance is a virtual desktop environment running in the cloud. Each instance:
- Has a unique identifier (UUID format).
- Runs in an isolated environment for security.
- Can be controlled programmatically through the **SDK methods**.
- Provides a `stream_url` (obtained via SDK methods) for visual feedback.
- Persists until explicitly stopped (via `terminateDesktop`) or times out.
- Has configurable resources (CPU, memory, storage) - future feature.
When creating a desktop instance using `launchDesktop`, you can specify a custom timeout in milliseconds using the `body.timeout_ms` parameter. This allows you to control how long the desktop instance will remain active before being automatically terminated. If not specified, a default timeout is applied. Check your plan for maximum allowed timeout values.
## Computer Actions
Cyberdesk supports various computer actions to interact with the desktop via the **`executeComputerAction` SDK method**. Actions are specified using a `body` object containing a `type` field and associated parameters:
### Mouse Actions
- **`type: 'click_mouse'`**: Perform a mouse action (click, press down, or release up).
- `x`, `y` (optional): Coordinates for the click.
- `button` (optional): `left`, `right`, or `middle` (defaults to `left`).
- `num_of_clicks` (optional): Number of clicks (defaults to 1, for `click_type: 'click'`).
- `click_type` (optional): `click` (down and up), `down` (press), or `up` (release) (defaults to `click`).
- **`type: 'scroll'`**: Scroll the mouse wheel.
- `direction`: `up`, `down`, `left`, or `right`.
- `amount`: Amount to scroll in pixels.
- **`type: 'move_mouse'`**: Move the mouse cursor to specific coordinates.
- `x`, `y`: Target coordinates.
- **`type: 'drag_mouse'`**: Drag the mouse from a start point to an end point.
- `start`: `{ x, y }` starting coordinates.
- `end`: `{ x, y }` ending coordinates.
### Keyboard Actions
- **`type: 'type'`**: Type text at the current cursor position.
- `text`: The string to type.
- **`type: 'press_keys'`**: Press, hold down, or release keyboard keys.
- `keys`: A single key string (e.g., `'Enter'`) or an array of keys to press simultaneously (e.g., `['Control', 'c']`).
- `key_action_type` (optional): `press` (down and up), `down` (hold), or `up` (release) (defaults to `press`).
### Other Actions
- **`type: 'wait'`**: Pause execution for a specified duration.
- `ms`: Time to wait in milliseconds.
- **`type: 'screenshot'`**: Capture the current state of the desktop.
- Returns a `base64_image` field in the successful response object.
- **`type: 'get_cursor_position'`**: Get the current mouse cursor coordinates.
- Returns `x`, `y` coordinates in the successful response object.
## Bash Actions
Bash actions allow you to execute shell commands on the desktop instance via the **`executeBashAction` SDK method**. This is useful for:
- Installing software
- Running applications
- Manipulating files
- Executing scripts
- System configuration
The method accepts a `body.command` parameter containing the shell command to execute and returns the command `output` (stdout/stderr) as a string in the successful response object.
## Desktop Streaming
Cyberdesk provides real-time streaming of desktop visuals. The `stream_url` obtained from the `launchDesktop` or `getDesktop` SDK methods can be used for:
1. **Web-based Viewer**: Opening the URL in a browser.
2. **VNC Protocol**: Connecting with a VNC client (if the stream URL supports it).
3. **Embedded Iframe**: Embedding the desktop view directly in your web applications.
The streaming service aims for low-latency transmission suitable for real-time interaction.
## Authentication and Security
Cyberdesk uses API keys for authentication. The **SDK client is initialized with your API key**, which is then automatically included in all requests.
- API keys are associated with a specific user account.
- Rate limits apply to prevent abuse.
- Keys can be revoked or regenerated from your dashboard.
- Keep your API key secure.
Communication with the API is encrypted using TLS/SSL.
## Resource Management
Cyberdesk automatically manages resources:
- Desktop instances are cleaned up when stopped via the **`terminateDesktop` SDK method** or when they reach their timeout.
- Users are responsible for stopping instances when no longer needed to manage costs and resources.
- Resource quotas and limits may apply depending on your subscription plan.
## Error Handling
The Cyberdesk **SDK methods** simplify error handling. Each method returns a promise that resolves to an object. You should check if this object contains an `error` property:
```typescript
const result = await cyberdesk.someMethod(...);
if ('error' in result) {
// Handle the error
console.error('Cyberdesk SDK Error:', result.error);
// The 'error' property contains the descriptive error message from the API
} else {
// Process the successful result
console.log('Success:', result);
}
```
Common error scenarios include:
- Invalid parameters in the request body (`body` property of the method call).
- Missing or invalid API key (during client initialization or if the key is revoked).
- Attempting an action not allowed by your plan.
- Referencing a non-existent desktop instance ID (`path.id`).
- Rate limit exceeded.
- Server-side issues on the Cyberdesk platform.
Check the specific error message returned in the `error` property for details.
## Performance Considerations
When building applications with the Cyberdesk SDK, consider:
- **API Call Latency**: Network latency affects the time it takes for SDK methods to complete.
- **Action Duration**: Some actions (like `wait` or long bash commands) naturally take time.
- **Streaming Latency**: There will be some delay in the visual stream.
- **Error Retries**: For transient issues (like network errors or potential server-side hiccups), consider implementing retry logic around your SDK calls, possibly with exponential backoff.
## Billing and Usage
Cyberdesk billing is typically based on factors like:
- **Active Desktop Time**: The duration desktop instances are running (often billed per second or minute).
Monitor your usage and understand the pricing model via the Cyberdesk dashboard (coming soon!).
================================================
FILE: apps/docs/content/docs/index.mdx
================================================
---
title: Cyberdesk Documentation
description: Comprehensive documentation for Cyberdesk service
---
Welcome to the official documentation for Cyberdesk, a powerful service for creating, controlling, and managing virtual desktop instances in the cloud. This documentation focuses on using the **official TypeScript SDK**, the recommended way to interact with the Cyberdesk API. We also provide a Python SDK (docs coming soon!).
Whether you're building automation tools, testing applications, or creating AI agents that can interact with computers, the Cyberdesk SDK provides the tools you need.
## What's New
- **TypeScript + Python SDKs**: The easiest way to integrate Cyberdesk into your applications.
- **AI Integration Guide**: Learn how to integrate Cyberdesk with AI models like Claude to create agents that can use computers.
- **Responsive Desktop Viewer**: Implement a responsive viewer for desktop streams that works across all devices.
- **Enhanced API Reference**: Complete API documentation with examples and response formats
## Documentation Sections
- [Introduction](/docs/introduction) - Learn what Cyberdesk is and its key features
- [Quickstart](/docs/quickstart) - Get up and running with the Cyberdesk SDK in minutes
- [Tutorials](/docs/tutorials) - Step-by-step guides using the SDK for common use cases, including integration with AI agents
- [Conceptual Guide](/docs/conceptual-guide) - Understand the core concepts of Cyberdesk
- [API Reference](/docs/api-reference) - Detailed REST API documentation (primarily for reference, SDK usage is recommended)
## Key Features
- **Virtual Desktop Creation**: Create cloud-based desktop instances on demand
- **Programmatic Control**: Control mouse, keyboard, and system actions via API
- **Bash Command Execution**: Run shell commands on desktop instances
- **Visual Streaming**: Stream desktop visuals to your applications
- **AI Integration**: Easily integrate with AI models for computer control
- **Secure Authentication**: API key-based authentication for secure access
## Getting Started
The fastest way to get started with Cyberdesk is to follow our [Quickstart Guide](/docs/quickstart), which will walk you through setting up the SDK, creating your first desktop instance, and performing basic interactions.
For more detailed examples and use cases using the SDK, check out our [Tutorials](/docs/tutorials), which include step-by-step guides for common scenarios.
## Support
If you need help or have questions about Cyberdesk, you can:
- Join our [Discord community](https://discord.gg/ws5ddx5yZ8) for discussions and support
- Contact us at dev@cyberdesk.io, for issues and feature requests
We're constantly improving Cyberdesk based on user feedback, so please let us know how we can make it better!
================================================
FILE: apps/docs/content/docs/introduction.mdx
================================================
---
title: Introduction
description: Introduction to Cyberdesk
---
Cyberdesk provides a seamless way to create and interact with virtual desktop environments through a simple API. Whether you're building automation tools, testing applications, creating AI agents that can use computers, or developing remote desktop solutions, Cyberdesk offers a robust platform to handle your virtual desktop needs.
## What is Cyberdesk?
Cyberdesk is a cloud-based service that allows you to:
- Create virtual desktop instances on demand via the SDK
- Control desktop interactions programmatically (mouse movements, clicks, keyboard input) using SDK methods
- Execute bash commands remotely via the SDK
- Manage desktop lifecycle (creation, interaction, termination) through the SDK
- Stream desktop visuals to your applications or AI agents
## Key Features
- **Simple SDK**: An easy-to-use TypeScript SDK for Node.js and browser environments
- **Programmatic Control**: Full control over mouse, keyboard, and system actions via intuitive SDK functions
- **Secure Authentication**: Simple API key configuration within the SDK
- **Streaming Capability**: Stream desktop visuals to your applications with VNC support (SDK helps manage instance details)
- **Resource Management**: Efficient creation and cleanup of desktop instances managed via SDK calls
- **AI Integration**: Easily integrate with AI models like Claude or GPT to create agents that can use computers (see tutorials)
- **Cross-Platform**: Works across different operating systems and environments
- **Low Latency**: Fast response times for real-time interaction
## Use Cases
Cyberdesk is ideal for a variety of applications:
- **AI Agents**: Create AI assistants that can interact with desktop applications
- **Automated Testing**: Test desktop applications with programmatic control
- **Remote Automation**: Control desktop environments from anywhere
- **Demo Environments**: Create disposable demo environments for software showcases
- **Training Data Generation**: Generate training data for computer vision models
- **Virtual Workstations**: Provide remote access to virtual desktop environments
## Getting Started
To get started with Cyberdesk:
1. [Sign up](https://cyberdesk.io/signup) for an account
2. Obtain your API key from the dashboard
3. Follow our [Quickstart Guide](/docs/quickstart) to set up the SDK and create your first desktop instance
4. Explore our [Tutorials](/docs/tutorials) for common use cases
Ready to dive deeper? Check out our [Conceptual Guide](/docs/conceptual-guide) to understand the core concepts behind Cyberdesk.
================================================
FILE: apps/docs/content/docs/meta.json
================================================
{
"title": "Docs",
"pages": ["index", "introduction", "quickstart", "tutorials", "conceptual-guide", "api-reference"],
"defaultOpen": true,
"root": true
}
================================================
FILE: apps/docs/content/docs/quickstart.mdx
================================================
---
title: Quickstart
description: Get started quickly with Cyberdesk
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Get up and running with Cyberdesk in minutes.
## Fastest Start: Cyberdesk Starter Assistant UI
The quickest way to get started with Cyberdesk is to use our [AI SDK Starter](https://github.com/cyberdesk-hq/cyberdesk-ai-sdk-starter) template. This ready-to-use Next.js application demonstrates how to build an AI assistant with virtual desktop control capabilities using the Cyberdesk API and Anthropic's Claude AI model.
### Features
- Interactive virtual desktop with streaming capabilities
- AI assistant chat interface powered by Claude 3.7 Sonnet
- Desktop control via AI (mouse clicks, keyboard input, screenshots)
- Bash command execution in the virtual environment
### Quick Setup
1. Clone the repository (alternatively, click "Use this template" on GitHub):
```bash
git clone https://github.com/cyberdesk-hq/cyberdesk-ai-sdk-starter.git
cd cyberdesk-ai-sdk-starter
```
2. Install dependencies:
```bash
npm install
```
3. Create a `.env.local` file with your API keys:
```
CYBERDESK_API_KEY=your_cyberdesk_api_key_here
ANTHROPIC_API_KEY=your_anthropic_api_key_here
```
4. Start the development server:
```bash
npm run dev
```
5. Open [http://localhost:3000](http://localhost:3000) in your browser
This template provides a complete working example that you can customize for your specific use case. For more details, check out the [repository README](https://github.com/cyberdesk-hq/cyberdesk-ai-sdk-starter).
## Building a Custom Integration
If you want to build your own integration, you can use either the **official TypeScript SDK** or the **official Python SDK**. See the code tabs below for both options.
### Prerequisites
- Node.js or Python 3.8+
- A Cyberdesk API key (obtain from your [Cyberdesk dashboard](https://cyberdesk.io/dashboard))
### Installation
```bash
npm install cyberdesk
# or
yarn add cyberdesk
# or
pnpm add cyberdesk
```
```bash
pip install cyberdesk
```
### 1. Initialize the Client
```typescript
import { createCyberdeskClient } from 'cyberdesk';
const cyberdesk = createCyberdeskClient({
apiKey: 'YOUR_API_KEY',
// Optionally, you can override the baseUrl or provide a custom fetch implementation
});
```
```python
from cyberdesk import CyberdeskClient
client = CyberdeskClient(api_key="YOUR_API_KEY")
```
### 2. Launch a Desktop Instance
```typescript
const launchResult = await cyberdesk.launchDesktop({
body: { timeout_ms: 600000 } // Optional: 10-minute timeout
});
if ('error' in launchResult) {
throw new Error('Failed to launch desktop: ' + launchResult.error);
}
const desktopId = launchResult.id;
```
```python
result = client.launch_desktop(timeout_ms=10000) # Optional: set a timeout for the desktop session
if result.error:
raise Exception('Failed to launch desktop: ' + str(result.error))
desktop_id = result.id
```
### 3. Get Desktop Information (Including Stream URL)
```typescript
const info = await cyberdesk.getDesktop({ path: { id: desktopId } });
if ('error' in info) {
throw new Error('Failed to get desktop info: ' + info.error);
}
console.log('Desktop info:', info);
console.log('Stream URL:', info.stream_url);
```
```python
info = client.get_desktop(desktop_id)
if info.error:
raise Exception('Failed to get desktop info: ' + str(info.error))
print('Desktop info:', info)
# If available: print('Stream URL:', info.stream_url)
```
### 4. Control the Desktop
#### Perform a Mouse Click
```typescript
const actionResult = await cyberdesk.executeComputerAction({
path: { id: desktopId },
body: {
type: 'click_mouse',
x: 100,
y: 150,
button: 'left'
}
});
if ('error' in actionResult) {
throw new Error('Mouse click failed: ' + actionResult.error);
}
```
```python
from cyberdesk.actions import click_mouse, ClickMouseButton
action = click_mouse(x=100, y=150, button=ClickMouseButton.LEFT)
action_result = client.execute_computer_action(desktop_id, action)
if action_result.error:
raise Exception('Action failed: ' + str(action_result.error))
```
#### Run a Bash Command
```typescript
const bashResult = await cyberdesk.executeBashAction({
path: { id: desktopId },
body: {
command: 'ls -la /tmp'
}
});
if ('error' in bashResult) {
throw new Error('Bash command failed: ' + bashResult.error);
}
console.log('Bash command output:', bashResult.output);
```
```python
bash_result = client.execute_bash_action(
desktop_id,
"ls -la /tmp"
)
if bash_result.error:
raise Exception('Bash command failed: ' + str(bash_result.error))
print('Bash command output:', getattr(bash_result, 'output', bash_result))
```
### 5. Stop the Desktop Instance
```typescript
const terminateResult = await cyberdesk.terminateDesktop({
path: { id: desktopId }
});
if ('error' in terminateResult) {
// You might still want to proceed even if termination fails
}
```
```python
terminate_result = client.terminate_desktop(desktop_id)
if terminate_result.error:
print('Termination failed:', terminate_result.error)
```
### 6. Async Usage
```typescript
// All methods are available as async functions (see above)
```
```python
import asyncio
from cyberdesk import CyberdeskClient
from cyberdesk.actions import click_mouse, ClickMouseButton
async def main():
client = CyberdeskClient(api_key="YOUR_API_KEY")
result = await client.async_launch_desktop(timeout_ms=10000)
print(result)
action = click_mouse(x=100, y=200, button=ClickMouseButton.LEFT)
await client.async_execute_computer_action(desktop_id, action)
# ... use other async_ methods as needed
asyncio.run(main())
```
### 7. Type Hints and Models
```typescript
// All request/response types are available from the generated models
```
```python
from cyberdesk.actions import click_mouse, drag_mouse, type_text, wait, scroll, move_mouse, press_keys, screenshot, get_cursor_position, ClickMouseButton, ClickMouseActionType, PressKeyActionType, ScrollDirection
```
### 8. Available Computer Actions
| Action | Factory Function (Python Only) | Description |
|----------------|-------------------------|----------------------------|
| Click Mouse | `click_mouse` | Mouse click at (x, y) |
| Drag Mouse | `drag_mouse` | Mouse drag from/to (x, y) |
| Move Mouse | `move_mouse` | Move mouse to (x, y) |
| Scroll | `scroll` | Scroll by dx, dy |
| Type Text | `type_text` | Type text |
| Press Keys | `press_keys` | Press keyboard keys |
| Screenshot | `screenshot` | Take a screenshot |
| Wait | `wait` | Wait for ms milliseconds |
| Get Cursor Pos | `get_cursor_position` | Get mouse cursor position |
## Next Steps
- Explore the [Tutorials](/docs/tutorials) for more complex examples using the SDK.
- Understand core concepts in the [Conceptual Guide](/docs/conceptual-guide).
- Review the [API Reference](/docs/api-reference) if you need details on the underlying REST API.
================================================
FILE: apps/docs/content/docs/tutorials.mdx
================================================
---
title: Tutorials
description: Step-by-step tutorials for using the Cyberdesk TypeScript SDK
---
Learn how to use the Cyberdesk TypeScript SDK effectively with these step-by-step tutorials.
**Setup:** All examples assume you have initialized the SDK client as shown in the [Quickstart](/docs/quickstart):
```typescript
import { createCyberdeskClient } from 'cyberdesk';
const cyberdesk = createCyberdeskClient({
apiKey: 'YOUR_API_KEY',
});
```
## Tutorial 1: Creating and Interacting with a Desktop
This tutorial walks you through creating a desktop instance and performing basic interactions using the SDK.
### Step 1: Create a Desktop Instance
Use `launchDesktop`:
```typescript
async function createDesktop() {
console.log('Creating desktop...');
const launchResult = await cyberdesk.launchDesktop({
body: { timeout_ms: 3600000 } // Optional: 1-hour timeout
});
if ('error' in launchResult) {
console.error('Failed to create desktop:', launchResult.error);
throw new Error('Failed to create desktop: ' + launchResult.error);
}
console.log(`Desktop created with ID: ${launchResult.id}`);
// Note: You might need to wait or use getDesktop to get the streamUrl
return launchResult.id;
}
// Example usage:
// const desktopId = await createDesktop();
```
### Step 2: Perform Mouse Actions
Use `executeComputerAction` with `type: 'click_mouse'`:
```typescript
async function clickOnDesktop(desktopId: string) {
console.log(`Clicking on desktop ${desktopId}...`);
const clickResult = await cyberdesk.executeComputerAction({
path: { id: desktopId },
body: {
type: 'click_mouse',
x: 100,
y: 200,
button: 'left' // Optional
}
});
if ('error' in clickResult) {
console.error('Mouse click failed:', clickResult.error);
throw new Error('Mouse click failed: ' + clickResult.error);
}
console.log('Mouse click successful:', clickResult);
}
// Example usage:
// await clickOnDesktop(desktopId);
```
### Step 3: Type Text
Use `executeComputerAction` with `type: 'type'`:
```typescript
async function typeOnDesktop(desktopId: string) {
console.log(`Typing on desktop ${desktopId}...`);
const typeResult = await cyberdesk.executeComputerAction({
path: { id: desktopId },
body: {
type: 'type',
text: 'Hello, Cyberdesk SDK!'
}
});
if ('error' in typeResult) {
console.error('Typing failed:', typeResult.error);
throw new Error('Typing failed: ' + typeResult.error);
}
console.log('Typing successful:', typeResult);
}
// Example usage:
// await typeOnDesktop(desktopId);
```
### Step 4: Take a Screenshot
Use `executeComputerAction` with `type: 'screenshot'`:
```typescript
async function captureScreenshot(desktopId: string) {
console.log(`Taking screenshot of desktop ${desktopId}...`);
const screenshotResult = await cyberdesk.executeComputerAction({
path: { id: desktopId },
body: {
type: 'screenshot'
}
});
if ('error' in screenshotResult) {
console.error('Screenshot failed:', screenshotResult.error);
throw new Error('Screenshot failed: ' + screenshotResult.error);
}
console.log('Screenshot successful.');
// Access the image data:
// const base64Image = screenshotResult.base64_image;
// console.log('Base64 Image length:', base64Image?.length);
return screenshotResult.base64_image;
}
// Example usage:
// const imageData = await captureScreenshot(desktopId);
```
### Step 5: Execute a Bash Command
Use `executeBashAction`:
```typescript
async function runCommand(desktopId: string) {
console.log(`Running command on desktop ${desktopId}...`);
const bashResult = await cyberdesk.executeBashAction({
path: { id: desktopId },
body: {
command: 'ls -la /tmp'
}
});
if ('error' in bashResult) {
console.error('Bash command failed:', bashResult.error);
throw new Error('Bash command failed: ' + bashResult.error);
}
console.log('Command successful.');
console.log('Output:', bashResult.output);
return bashResult.output;
}
// Example usage:
// const commandOutput = await runCommand(desktopId);
```
### Step 6: Stop the Desktop
Use `terminateDesktop`:
```typescript
async function stopDesktop(desktopId: string) {
console.log(`Stopping desktop ${desktopId}...`);
const stopResult = await cyberdesk.terminateDesktop({
path: { id: desktopId }
});
if ('error' in stopResult) {
console.error('Failed to stop desktop:', stopResult.error);
// Decide if you need to throw an error or just log
}
console.log('Desktop stop requested.', stopResult);
}
// Example usage:
// await stopDesktop(desktopId);
```
## Tutorial 2: Automating Web Testing
This tutorial demonstrates how to use the Cyberdesk SDK to automate simple web browser tasks.
### Step 1: Create a Desktop Instance
Create a desktop instance using the `createDesktop` function from Tutorial 1.
```typescript
// const desktopId = await createDesktop();
```
### Step 2: Open a Web Browser and Wait
Use `executeBashAction` to open a browser (e.g., Firefox) in the background, then use `executeComputerAction` with `type: 'wait'` to allow time for it to load.
```typescript
async function openBrowserAndWait(desktopId: string, url: string, waitMs: number = 5000) {
console.log(`Opening ${url} on desktop ${desktopId}...`);
const openResult = await cyberdesk.executeBashAction({
path: { id: desktopId },
body: {
command: `firefox ${url} &` // Run in background
}
});
if ('error' in openResult) {
console.error('Failed to send open browser command:', openResult.error);
throw new Error('Failed to send open browser command: ' + openResult.error);
}
console.log('Browser opening command sent.');
console.log(`Waiting ${waitMs}ms for browser to load...`);
const waitResult = await cyberdesk.executeComputerAction({
path: { id: desktopId },
body: {
type: 'wait',
ms: waitMs
}
});
if ('error' in waitResult) {
console.error('Wait action failed:', waitResult.error);
throw new Error('Wait action failed: ' + waitResult.error);
}
console.log('Wait complete.');
}
// Example usage:
// await openBrowserAndWait(desktopId, 'https://example.com');
```
### Step 3: Take a Screenshot for Verification
Capture a screenshot using the `captureScreenshot` function from Tutorial 1 to verify the page loaded.
```typescript
async function verifyPageLoad(desktopId: string) {
console.log('Taking verification screenshot...');
const imageData = await captureScreenshot(desktopId);
if (imageData) {
console.log('Verification screenshot captured (length:', imageData.length, ')');
// Add logic here to analyze the screenshot if needed
} else {
console.warn('Could not capture verification screenshot.');
}
}
// Example usage:
// await verifyPageLoad(desktopId);
```
### Step 4: Stop the Desktop
Remember to stop the desktop instance using the `stopDesktop` function from Tutorial 1 when the test is complete.
```typescript
// await stopDesktop(desktopId);
```
## Tutorial 3: Integrating with AI Models for Computer Use
This tutorial demonstrates how to integrate the Cyberdesk SDK with AI models (like Anthropic's Claude) using the Vercel AI SDK to create agents that can use computers.
### Step 1: Prerequisites and Setup
Install the necessary dependencies:
```bash
npm install cyberdesk @ai-sdk/anthropic ai
```
Initialize the Cyberdesk SDK client (likely in a shared module, e.g., `@/lib/cyberdeskClient.ts`):
```typescript
// src/lib/cyberdeskClient.ts
import { createCyberdeskClient } from 'cyberdesk';
const client = createCyberdeskClient({
apiKey: process.env.CYBERDESK_API_KEY || 'YOUR_API_KEY', // Use environment variable
});
export default client;
```
Ensure you have `CYBERDESK_API_KEY` set in your environment variables.
### Step 2: Implement `executeComputerAction` Utility
Create a utility function that maps AI tool parameters to the Cyberdesk SDK's `executeComputerAction` method. This function handles the various action types supported by the AI tool.
```typescript
// src/utils/computer-use.ts
import client from '@/lib/cyberdeskClient';
import type { ExecuteComputerActionParams } from "cyberdesk"
// Define the action types your AI tool might use
export type ClaudeComputerActionType0124 = /* ... (action types like 'left_click', 'type', etc.) ... */
| "left_click"
| "right_click"
// ... (include all action types from the provided code) ...
| "screenshot";
export async function executeComputerAction(
action: ClaudeComputerActionType0124,
desktopId: string,
coordinate?: { x: number; y: number },
text?: string,
duration?: number,
scroll_amount?: number,
scroll_direction?: "left" | "right" | "down" | "up",
start_coordinate?: { x: number; y: number }
): Promise {
try {
let requestBody: ExecuteComputerActionParams['body'];
// Map the AI tool action to the Cyberdesk SDK's expected format
switch (action) {
case 'left_click':
requestBody = {
type: 'click_mouse',
x: coordinate?.x,
y: coordinate?.y,
button: 'left',
click_type: 'click',
num_of_clicks: 1
};
break;
// ... Map other actions (right_click, type, scroll, screenshot, etc.) ...
case 'type':
requestBody = {
type: 'type',
text: text || ''
};
break;
case 'screenshot':
requestBody = {
type: 'screenshot'
};
break;
// ... (Include all case mappings from the provided computer-use.ts code) ...
default: {
const _exhaustiveCheck: never = action;
throw new Error(`Unhandled action: ${action}`);
}
}
const clientParams: ExecuteComputerActionParams = {
path: { id: desktopId },
body: requestBody
};
// *** Use the Cyberdesk SDK client ***
const result = await client.executeComputerAction(clientParams);
// Check the raw response status embedded in the SDK result
if (result.response.status !== 200) {
let errorDetails = `Failed with status: ${result.response.status}`;
try {
// Attempt to parse error details from the response body
const errorBody = await result.response.json();
errorDetails = errorBody.message || errorBody.error || JSON.stringify(errorBody);
} catch (e) { /* Failed to parse body */ }
throw new Error(`Failed to execute computer action ${action}: ${errorDetails}`);
}
const data = result.data; // Access the parsed data from the SDK result
if (data?.base64_image) {
return {
type: "image",
data: data.base64_image
};
}
return data?.output || 'Action completed successfully';
} catch (error) {
console.error(`Error executing computer action ${action}:`, error);
throw error; // Re-throw to be handled by the AI SDK
}
}
```
*Note: The full mapping logic for all action types is omitted for brevity but should be included as shown in the `computer-use.ts` file.*
### Step 3: Implement `executeBashCommand` Utility
Create a similar utility for bash commands, calling the `executeBashAction` SDK method.
```typescript
// src/utils/bash.ts
import client from '@/lib/cyberdeskClient';
export async function executeBashCommand(
command: string,
desktopId: string
): Promise {
try {
// *** Use the Cyberdesk SDK client ***
const result = await client.executeBashAction({
path: { id: desktopId },
body: { command },
});
// Check the raw response status
if (result.response.status !== 200) {
let errorDetails = `Failed with status: ${result.response.status}`;
try {
const errorBody = await result.response.json();
errorDetails = errorBody.message || errorBody.error || JSON.stringify(errorBody);
} catch (e) { /* Failed to parse body */ }
throw new Error(`Failed to execute bash command: ${errorDetails}`);
}
const data = result.data; // Access the parsed data
return data?.output || ''; // Return output or empty string
} catch (error) {
console.error(`Error executing bash command "${command}":`, error);
// Return a meaningful error message for the AI to potentially see
return 'Error executing bash command: ' + (error as Error).message;
}
}
```
### Step 4: Set Up the AI Tools and API Route
Create an API route (e.g., `/api/chat`) that uses the Vercel AI SDK (`streamText`) and defines tools that call your utility functions.
```typescript
// src/app/api/chat/route.ts
import { anthropic } from '@ai-sdk/anthropic';
import { streamText } from 'ai';
import { executeComputerAction } from '../../../utils/computer-use'; // Adjust path
import { executeBashCommand } from '../../../utils/bash'; // Adjust path
// Define result types if needed
interface ComputerActionResult { type: "image"; data: string; }
export const maxDuration = 300;
export async function POST(req: Request) {
const desktopId = req.headers.get('X-Desktop-Id');
const { messages } = await req.json();
if (!desktopId) {
return Response.json({ error: "Desktop ID is required" }, { status: 400 });
}
const lastMessage = messages[messages.length - 1];
const userContent = /* ... logic to extract text from lastMessage.content ... */ "";
// Define the computer tool using the AI SDK
const computerTool = anthropic.tools.computer_20250124({
displayWidthPx: 1024,
displayHeightPx: 768,
// The execute function calls *your* utility function, which uses the Cyberdesk SDK
execute: async ({ action, coordinate, duration, scroll_amount, scroll_direction, start_coordinate, text }) => {
const coordinateObj = coordinate ? { x: coordinate[0], y: coordinate[1] } : undefined;
const startCoordinateObj = start_coordinate ? { x: start_coordinate[0], y: start_coordinate[1] } : undefined;
// *** Call your utility function ***
const result = await executeComputerAction(
action,
desktopId,
coordinateObj,
text,
duration,
scroll_amount,
scroll_direction,
startCoordinateObj
);
// Format the result for the AI SDK tool
return (typeof result === 'string')
? { type: "text" as const, text: result }
: { type: "image" as const, data: result.data };
},
// Optional: Format the tool result content for the AI model
experimental_toToolResultContent(result: { type: "text"; text: string } | ComputerActionResult) {
return result.type === 'text'
? [{ type: 'text', text: result.text }]
: [{ type: 'image', data: result.data, mimeType: 'image/jpeg' }];
},
});
// Define the bash tool using the AI SDK
const bashTool = anthropic.tools.bash_20250124({
// The execute function calls *your* utility function
execute: async ({ command }) => {
// *** Call your utility function ***
const output = await executeBashCommand(command, desktopId);
return output; // Return the string output directly
}
});
try {
// Call the AI model with the tools
const response = streamText({
model: anthropic("claude-3-7-sonnet-20250219"),
prompt: userContent,
system: "You are an AI assistant that can control a computer...", // Your system prompt
tools: {
computer: computerTool,
bash: bashTool
},
maxSteps: 100
});
return response.toDataStreamResponse();
} catch (error) {
console.error("Error calling Anthropic:", error);
return Response.json({ error: "Failed to process request" }, { status: 500 });
}
}
```
### Step 5: Frontend Implementation
A frontend application would:
1. Create a desktop instance (perhaps via another API route that uses `cyberdesk.launchDesktop`).
2. Render a chat interface.
3. Display the desktop stream using the `stream_url`.
4. Send user messages to the `/api/chat` endpoint, including the `desktopId` in the `X-Desktop-Id` header.
5. Process the streamed response from the API, updating the chat UI.
*(Refer to the Cyberdesk Starter Assistant UI template for a full frontend example)*.
### Conclusion
By wrapping the Cyberdesk SDK methods within utility functions called by your AI tool's `execute` logic, you can seamlessly integrate robust desktop control into your AI agents. The SDK handles the direct API communication, authentication, and response parsing, simplifying your integration code.
================================================
FILE: apps/docs/mdx-components.tsx
================================================
import defaultComponents from "fumadocs-ui/mdx";
import type { MDXComponents } from "mdx/types";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...defaultComponents,
...components,
};
}
================================================
FILE: apps/docs/next-env.d.ts
================================================
///
///
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
================================================
FILE: apps/docs/next.config.mjs
================================================
import createMDX from "fumadocs-mdx/config";
const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
};
export default withMDX(config);
================================================
FILE: apps/docs/package.json
================================================
{
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"generate": "node scripts/generate-docs.mjs",
"build": "next build",
"dev": "next dev",
"start": "next start"
},
"dependencies": {
"@vercel/analytics": "^1.5.0",
"fumadocs-core": "^12.1.2",
"fumadocs-mdx": "^8.2.32",
"fumadocs-openapi": "^3.0.0",
"fumadocs-typescript": "^2.0.1",
"fumadocs-ui": "^12.1.2",
"next": "^14.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
"@types/node": "^20.11.30",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4",
"typescript": "^5.4.5"
}
}
================================================
FILE: apps/docs/postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
================================================
FILE: apps/docs/scripts/generate-docs.mjs
================================================
import * as path from "node:path";
import * as OpenAPI from "fumadocs-openapi";
import * as Typescript from "fumadocs-typescript";
void OpenAPI.generateFiles({
input: ["../../sdks/openapi.json"],
output: "./content/docs/",
name: () => "api-reference",
frontmatter: (title) => ({
toc: false,
title: `${title[0].toUpperCase()}${title.slice(1)}`,
}),
});
void Typescript.generateFiles({
input: ["./content/docs/**/*.model.mdx"],
output: (file) =>
path.resolve(
path.dirname(file),
`${path.basename(file).split(".")[0]}.mdx`
),
});
================================================
FILE: apps/docs/tailwind.config.js
================================================
import { createPreset } from 'fumadocs-ui/tailwind-plugin';
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./content/**/*.{md,mdx}',
'./mdx-components.{ts,tsx}',
'./node_modules/fumadocs-ui/dist/**/*.js',
'./node_modules/fumadocs-openapi/dist/**/*.js',
],
presets: [createPreset()],
theme: {
extend: {
typography: {
DEFAULT: {
css: {
h1: {
letterSpacing: '-0.025em', // This is what tracking-tight does
fontWeight: '500', // font-medium (500) instead of bold (700)
},
h2: {
letterSpacing: '-0.025em',
fontWeight: '500',
},
h3: {
letterSpacing: '-0.025em',
fontWeight: '500',
},
h4: {
letterSpacing: '-0.025em',
fontWeight: '500',
},
h5: {
letterSpacing: '-0.025em',
fontWeight: '500',
},
h6: {
letterSpacing: '-0.025em',
fontWeight: '500',
},
},
},
},
},
},
};
================================================
FILE: apps/docs/tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: apps/web/.eslintrc.json
================================================
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@next/next/no-img-element": "off",
"@typescript-eslint/no-unused-vars": "warn"
}
}
================================================
FILE: apps/web/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
.env.local
# turbo
.turbo
================================================
FILE: apps/web/LICENSE.md
================================================
# Tailwind Plus License
## Personal License
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates.
You **can**:
- Use the Components and Templates to create unlimited End Products.
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
You **cannot**:
- Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates.
- Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets.
- Share your access to the Components and Templates with any other individuals.
- Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
### Example usage
Examples of usage **allowed** by the license:
- Creating a personal website by yourself.
- Creating a website or web application for a client that will be owned by that client.
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available.
Examples of usage **not allowed** by the license:
- Creating a repository of your favorite Tailwind Plus components or templates (or derivatives based on Tailwind Plus components or templates) and publishing it publicly.
- Creating a React or Vue version of Tailwind Plus and making it available either for sale or for free.
- Create a Figma or Sketch UI kit based on the Tailwind Plus component designs.
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind Plus.
- Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free.
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
In simple terms, use Tailwind Plus for anything you like as long as it doesn't compete with Tailwind Plus.
### Personal License Definitions
Licensee is the individual who has purchased a Personal License.
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind Plus license.
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
End User is a user of an End Product.
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
## Team License
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates.
You **can**:
- Use the Components and Templates to create unlimited End Products.
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
You **cannot**:
- Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates.
- Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product.
- Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee.
- Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
### Example usage
Examples of usage **allowed** by the license:
- Creating a website for your company.
- Creating a website or web application for a client that will be owned by that client.
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available.
Examples of use **not allowed** by the license:
- Creating a repository of your favorite Tailwind Plus components or template (or derivatives based on Tailwind Plus components or templates) and publishing it publicly.
- Creating a React or Vue version of Tailwind Plus and making it available either for sale or for free.
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind Plus.
- Creating a theme or template using the components or templates and making it available either for sale or for free.
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
- Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind Plus license to build their own websites or side projects.
### Team License Definitions
Licensee is the business entity who has purchased a Team License.
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind Plus license.
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
End User is a user of an End Product.
Employee is a full-time or part-time employee of the Licensee.
Contractor is an individual or business entity contracted to perform services for the Licensee.
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
## Enforcement
If you are found to be in violation of the license, access to your Tailwind Plus account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued.
The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license.
## Liability
Tailwind Labs Inc.’s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates.
This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order.
## Questions?
Unsure which license you need, or unsure if your use case is covered by our licenses?
Email us at [support@tailwindcss.com](mailto:support@tailwindcss.com) with your questions.
================================================
FILE: apps/web/README.md
================================================
## Getting started
To get started with this template, first install the npm dependencies:
```bash
npm install
```
Next, create a new Sanity project to power the blog within this template:
```bash
npm create sanity@latest -- --env=.env.local --create-project "Radiant Blog" --dataset production
```
This will prompt you to create a new Sanity account if you don't have one already. When asked "Would you like to add configuration files for a Sanity project in this Next.js folder?", choose "n".
Next, optionally import the demo seed data for the blog:
```bash
npx sanity@latest dataset import seed.tar.gz
```
Next, run the development server:
```bash
npm run dev
```
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
To manage your blog content, visit the embedded Sanity Studio at [http://localhost:3000/studio](http://localhost:3000/studio).
## License
This site template is a commercial product and is licensed under the [Tailwind Plus license](https://tailwindcss.com/plus/license).
## Learn more
To learn more about the technologies used in this site template, see the following resources:
- [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation
- [Next.js](https://nextjs.org/docs) - the official Next.js documentation
- [Headless UI](https://headlessui.dev) - the official Headless UI documentation
- [Sanity](https://www.sanity.io) - the Sanity website
================================================
FILE: apps/web/components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/tailwind.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
================================================
FILE: apps/web/config.ts
================================================
const CONFIG = {
docsURL: 'https://docs.cyberdesk.io',
subscriptionLimit: 1000
}
export default CONFIG;
================================================
FILE: apps/web/middleware.ts
================================================
import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
================================================
FILE: apps/web/next.config.mjs
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
async rewrites() {
return [
{
source: "/ingest/static/:path*",
destination: "https://us-assets.i.posthog.com/static/:path*",
},
{
source: "/ingest/:path*",
destination: "https://us.i.posthog.com/:path*",
},
{
source: "/ingest/decide",
destination: "https://us.i.posthog.com/decide",
},
];
},
// This is required to support PostHog trailing slash API requests
skipTrailingSlashRedirect: true,
};
export default nextConfig
================================================
FILE: apps/web/package.json
================================================
{
"name": "web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typegen": "sanity schema extract --path=src/sanity/extract.json && sanity typegen generate && rm ./src/sanity/extract.json"
},
"dependencies": {
"@ai-sdk/anthropic": "^1.2.2",
"@ai-sdk/openai": "^1.3.3",
"@anthropic-ai/sdk": "^0.39.0",
"@assistant-ui/react": "^0.8.6",
"@assistant-ui/react-ai-sdk": "^0.8.0",
"@assistant-ui/react-markdown": "^0.8.0",
"@headlessui/react": "^2.1.1",
"@heroicons/react": "^2.1.4",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@sanity/image-url": "^1.0.2",
"@sanity/vision": "^3.52.2",
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.49.4",
"@types/uuid": "^10.0.0",
"@unkey/api": "^0.34.0",
"ai": "^4.2.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cyberdesk": "^0.2.0",
"dayjs": "^1.11.12",
"feed": "^4.2.2",
"framer-motion": "^11.2.10",
"lucide-react": "^0.484.0",
"motion": "^12.11.0",
"next": "14.2.11",
"next-sanity": "^9.4.7",
"next-themes": "^0.4.6",
"openai": "^4.90.0",
"posthog-js": "^1.234.1",
"posthog-node": "^4.10.2",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.5.0",
"react-resizable-panels": "^3.0.2",
"react-use-measure": "^2.1.1",
"remark-gfm": "^4.0.1",
"sanity": "^3.55.0",
"sonner": "^2.0.3",
"stripe": "^17.7.0",
"tailwind-merge": "^3.0.2",
"tw-animate-css": "^1.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.6",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1",
"eslint": "^8",
"eslint-config-next": "14.2.11",
"postcss": "^8.4.40",
"prettier": "^3.3.2",
"prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-tailwindcss": "^0.6.10",
"tailwindcss": "^4.0.6",
"typescript": "^5"
},
"optionalDependencies": {
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
"@tailwindcss/oxide-win32-x64-msvc": "^4.0.17",
"lightningcss-linux-x64-gnu": "^1.29.1",
"lightningcss-win32-x64-msvc": "^1.29.3"
}
}
================================================
FILE: apps/web/postcss.config.js
================================================
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}
================================================
FILE: apps/web/prettier.config.js
================================================
/** @type {import('prettier').Options} */
module.exports = {
singleQuote: true,
semi: false,
plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],
tailwindFunctions: ['clsx'],
tailwindStylesheet: './src/styles/tailwind.css',
}
================================================
FILE: apps/web/radiant/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
/node_modules
/.pnp
.pnp.js
# Compiled Sanity Studio
/dist
# Temporary Sanity runtime, generated by the CLI on every dev server start
/.sanity
# Logs
/logs
*.log
# Coverage directory used by testing tools
/coverage
# Misc
.DS_Store
*.pem
# Typescript
*.tsbuildinfo
# Dotenv and similar local-only files
*.local
================================================
FILE: apps/web/radiant/README.md
================================================
# Sanity Clean Content Studio
Congratulations, you have now installed the Sanity Content Studio, an open-source real-time content editing environment connected to the Sanity backend.
Now you can do the following things:
- [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme)
- [Join the community Slack](https://slack.sanity.io/?utm_source=readme)
- [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme)
================================================
FILE: apps/web/radiant/eslint.config.mjs
================================================
import studio from '@sanity/eslint-config-studio'
export default [...studio]
================================================
FILE: apps/web/radiant/package.json
================================================
{
"name": "radiant",
"private": true,
"version": "1.0.0",
"main": "package.json",
"license": "UNLICENSED",
"scripts": {
"dev": "sanity dev",
"start": "sanity start",
"build": "sanity build",
"deploy": "sanity deploy",
"deploy-graphql": "sanity graphql deploy"
},
"keywords": [
"sanity"
],
"dependencies": {
"@sanity/vision": "^3.80.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sanity": "^3.80.0",
"styled-components": "^6.1.8"
},
"devDependencies": {
"@sanity/eslint-config-studio": "^5.0.2",
"@types/react": "^18.0.25",
"eslint": "^9.9.0",
"prettier": "^3.0.2",
"typescript": "^5.1.6"
},
"prettier": {
"semi": false,
"printWidth": 100,
"bracketSpacing": false,
"singleQuote": true
}
}
================================================
FILE: apps/web/radiant/sanity.cli.ts
================================================
import {defineCliConfig} from 'sanity/cli'
export default defineCliConfig({
api: {
projectId: '4hdczeqj',
dataset: 'production'
},
/**
* Enable auto-updates for studios.
* Learn more at https://www.sanity.io/docs/cli#auto-updates
*/
autoUpdates: true,
})
================================================
FILE: apps/web/radiant/sanity.config.ts
================================================
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {visionTool} from '@sanity/vision'
import {schemaTypes} from './schemaTypes'
export default defineConfig({
name: 'default',
title: 'Radiant',
projectId: '4hdczeqj',
dataset: 'production',
plugins: [structureTool(), visionTool()],
schema: {
types: schemaTypes,
},
})
================================================
FILE: apps/web/radiant/schemaTypes/index.ts
================================================
export const schemaTypes = []
================================================
FILE: apps/web/radiant/static/.gitkeep
================================================
Files placed here will be served by the Sanity server under the `/static`-prefix
================================================
FILE: apps/web/radiant/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "Preserve",
"moduleDetection": "force",
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
================================================
FILE: apps/web/sanity-typegen.json
================================================
{
"path": "./src/**/*.{ts,tsx,js,jsx}",
"schema": "./src/sanity/extract.json",
"generates": "./src/sanity/types.ts"
}
================================================
FILE: apps/web/sanity.cli.ts
================================================
import { defineCliConfig } from 'sanity/cli'
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET
export default defineCliConfig({ api: { projectId, dataset } })
================================================
FILE: apps/web/sanity.config.ts
================================================
'use client'
import { visionTool } from '@sanity/vision'
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { apiVersion, dataset, projectId } from './src/sanity/env'
import { schema } from './src/sanity/schema'
export default defineConfig({
basePath: '/studio',
projectId,
dataset,
schema,
plugins: [structureTool(), visionTool({ defaultApiVersion: apiVersion })],
})
================================================
FILE: apps/web/src/app/api/playground/chat/route.ts
================================================
import { anthropic } from "@ai-sdk/anthropic";
import { streamText, type UIMessage } from "ai";
import { prunedMessages } from "@/utils/playground/misc-demo-utils";
import { bashTool, computerTool } from "@/utils/playground/tools";
import client from "@/utils/playground/cyberdesk-client";
// Allow streaming responses up to 5 minutes
export const maxDuration = 300;
export async function POST(req: Request) {
const { messages, sandboxId }: { messages: UIMessage[]; sandboxId: string } =
await req.json();
try {
const result = streamText({
model: anthropic("claude-3-7-sonnet-20250219"), // Using Sonnet for computer use
system:
"You are a helpful assistant with access to a computer. " +
"Use the computer tool to help the user with their requests. " +
"Use the bash tool to execute commands on the computer. You can create files and folders using the bash tool. Always prefer the bash tool where it is viable for the task. " +
"Be sure to advise the user when waiting is necessary. " +
"If the browser opens with a setup wizard, YOU MUST IGNORE IT and move straight to the next step (e.g. input the url in the search bar)." +
"Use DuckDuckGo to search the web.",
messages: prunedMessages(messages),
tools: { computer: computerTool(sandboxId), bash: bashTool(sandboxId) },
providerOptions: {
anthropic: { cacheControl: { type: "ephemeral" } },
},
maxSteps: 100,
});
// Create response stream
const response = result.toDataStreamResponse({
// @ts-expect-error eheljfe
getErrorMessage(error) {
console.error("Error in streamText:", error);
return error;
},
});
return response;
} catch (error) {
console.error("Chat API error:", error);
await client.terminateDesktop({
path: {
id: sandboxId,
},
});
return new Response(JSON.stringify({ error: "Internal Server Error" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}
================================================
FILE: apps/web/src/app/api/playground/kill-desktop/route.ts
================================================
import client from "@/utils/playground/cyberdesk-client";
// Common handler for both GET and POST requests
async function handleKillDesktop(request: Request) {
// Enable CORS to ensure this works across all browsers
const { searchParams } = new URL(request.url);
const sandboxId = searchParams.get("sandboxId");
console.log(`Kill desktop request received via ${request.method} for ID: ${sandboxId}`);
if (!sandboxId) {
return new Response("No sandboxId provided", { status: 400 });
}
try {
await client.terminateDesktop({
path: {
id: sandboxId,
}
});
return new Response("Desktop killed successfully", { status: 200 });
} catch (error) {
console.error(`Failed to kill desktop with ID: ${sandboxId}`, error);
return new Response("Failed to kill desktop", { status: 500 });
}
}
// Handle POST requests
export async function POST(request: Request) {
return handleKillDesktop(request);
}
================================================
FILE: apps/web/src/app/api/stripe/checkout/route.ts
================================================
import { stripe, STRIPE_PRICE_ID } from '@/utils/stripe/stripe-server';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
try {
const { successUrl, cancelUrl, stripeCustomerId } = await req.json();
// Create a Stripe checkout session for the Pro plan
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: STRIPE_PRICE_ID,
quantity: 1,
},
],
mode: 'subscription',
success_url: successUrl || `${req.nextUrl.origin}/dashboard?payment=success`,
cancel_url: cancelUrl || `${req.nextUrl.origin}/pricing?payment=cancelled`,
metadata: {
plan: 'pro',
},
...(stripeCustomerId && { customer: stripeCustomerId }),
});
return NextResponse.json({ sessionId: session.id, url: session.url });
} catch (error) {
console.error('Error creating checkout session:', error);
return NextResponse.json(
{ error: 'Error creating checkout session' },
{ status: 500 }
);
}
}
================================================
FILE: apps/web/src/app/api/stripe/portal/route.ts
================================================
import { stripe } from '@/utils/stripe/stripe-server';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
try {
const { customerId, returnUrl } = await req.json();
if (!customerId) {
return NextResponse.json(
{ error: 'Customer ID is required' },
{ status: 400 }
);
}
// Create a billing portal session for the customer
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: returnUrl || `${req.nextUrl.origin}/dashboard`,
});
return NextResponse.json({ url: session.url });
} catch (error) {
console.error('Error creating portal session:', error);
return NextResponse.json(
{ error: 'Error creating portal session' },
{ status: 500 }
);
}
}
================================================
FILE: apps/web/src/app/api/stripe/webhook/route.ts
================================================
import { stripe } from '@/utils/stripe/stripe-server';
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { createClient } from '@/utils/supabase/server';
// Define subscription status type using Stripe's type
type SubscriptionStatus = Stripe.Subscription.Status;
// This is your Stripe webhook secret for testing your endpoint locally.
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
export async function POST(req: NextRequest) {
const body = await req.text();
const sig = headers().get('stripe-signature') as string;
const supabase = createClient();
let event;
try {
event = stripe.webhooks.constructEvent(body, sig, endpointSecret!);
} catch (err: unknown) {
let errorMessage = "An unknown error occurred";
if (err instanceof Error) {
errorMessage = err.message;
}
console.error(`Webhook Error: ${errorMessage}`);
return NextResponse.json(
{ error: `Webhook Error: ${errorMessage}` },
{ status: 400 }
);
}
// Helper function to update profile in Supabase
const updateProfile = async (customerId: string, data: {
subscription_status?: SubscriptionStatus;
updated_at?: Date;
stripe_subscription_id?: string | null;
current_period_end?: Date | null;
plan_id?: string | null;
cancel_at_period_end?: boolean | null;
email?: string | null;
}) => {
try {
// First, find the profile with this Stripe customer ID
const { data: profiles, error: fetchError } = await supabase
.from('profiles')
.select('*')
.eq('stripe_customer_id', customerId)
.limit(1);
if (fetchError) {
console.error('Error fetching profile:', fetchError);
return;
}
if (profiles && profiles.length > 0) {
const profile = profiles[0];
// Update the profile with new data
const { error: updateError } = await supabase
.from('profiles')
.update(data)
.eq('id', profile.id);
if (updateError) {
console.error('Error updating profile:', updateError);
} else {
console.log(`Profile ${profile.id} updated successfully`);
}
} else {
console.log(`No profile found for customer ID: ${customerId}`);
}
} catch (error) {
console.error('Error in updateProfile:', error);
}
};
// Handle the event
try {
switch (event.type) {
case 'checkout.session.async_payment_failed': {
const session = event.data.object;
console.log('Checkout session async payment failed:', session);
// Handle the failed async payment
const customerId = session.customer
? (typeof session.customer === 'string' ? session.customer : session.customer.id)
: null;
if (customerId) {
await updateProfile(customerId, {
subscription_status: 'past_due',
updated_at: new Date()
});
}
break;
}
case 'checkout.session.async_payment_succeeded': {
const session = event.data.object;
console.log('Checkout session async payment succeeded:', session);
// Similar handling to checkout.session.completed
const customerId = session.customer
? (typeof session.customer === 'string' ? session.customer : session.customer.id)
: null;
const subscriptionId = session.subscription
? (typeof session.subscription === 'string' ? session.subscription : session.subscription.id)
: null;
if (customerId && subscriptionId) {
// Get subscription details
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
// Update profile with subscription info
await updateProfile(customerId, {
stripe_subscription_id: subscriptionId,
subscription_status: subscription.status,
current_period_end: new Date(subscription.current_period_end * 1000),
plan_id: subscription.items.data[0].price.id,
cancel_at_period_end: subscription.cancel_at_period_end,
updated_at: new Date()
});
}
break;
}
case 'checkout.session.completed': {
const checkoutSession = event.data.object
console.log('Checkout session completed:', checkoutSession);
// Get customer ID and subscription ID from the session, handling possible null values
const customerId = checkoutSession.customer
? (typeof checkoutSession.customer === 'string' ? checkoutSession.customer : checkoutSession.customer.id)
: null;
const subscriptionId = checkoutSession.subscription
? (typeof checkoutSession.subscription === 'string' ? checkoutSession.subscription : checkoutSession.subscription.id)
: null;
if (customerId && subscriptionId) {
// Get subscription details
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
// Update profile with subscription info
await updateProfile(customerId, {
stripe_subscription_id: subscriptionId,
subscription_status: subscription.status,
current_period_end: new Date(subscription.current_period_end * 1000),
plan_id: subscription.items.data[0].price.id,
cancel_at_period_end: subscription.cancel_at_period_end,
updated_at: new Date()
});
}
break;
}
case 'customer.created': {
const customer = event.data.object;
console.log('Customer created:', customer);
// No specific action needed here as the customer ID will be associated with a profile
// when they complete checkout or when the user account is created
break;
}
case 'customer.deleted': {
const customer = event.data.object;
console.log('Customer deleted:', customer);
// Mark the customer as deleted in our system
await updateProfile(customer.id, {
subscription_status: 'canceled',
updated_at: new Date()
});
break;
}
case 'customer.updated': {
const customer = event.data.object;
console.log('Customer updated:', customer);
// Update basic customer information
await updateProfile(customer.id, {
email: customer.email,
updated_at: new Date()
});
break;
}
case 'customer.subscription.created': {
const subscription = event.data.object
console.log('Subscription created:', subscription);
const customerId = typeof subscription.customer === 'string'
? subscription.customer
: subscription.customer.id;
await updateProfile(customerId, {
stripe_subscription_id: subscription.id,
subscription_status: subscription.status,
current_period_end: new Date(subscription.current_period_end * 1000),
plan_id: subscription.items.data[0].price.id,
cancel_at_period_end: subscription.cancel_at_period_end,
updated_at: new Date()
});
break;
}
case 'customer.subscription.paused': {
const subscription = event.data.object;
console.log('Subscription paused:', subscription);
const customerId = typeof subscription.customer === 'string'
? subscription.customer
: subscription.customer.id;
await updateProfile(customerId, {
subscription_status: 'paused',
updated_at: new Date()
});
break;
}
case 'customer.subscription.resumed': {
const subscription = event.data.object;
console.log('Subscription resumed:', subscription);
const customerId = typeof subscription.customer === 'string'
? subscription.customer
: subscription.customer.id;
await updateProfile(customerId, {
subscription_status: subscription.status,
current_period_end: new Date(subscription.current_period_end * 1000),
updated_at: new Date()
});
break;
}
case 'customer.subscription.updated': {
const updatedSubscription = event.data.object as Stripe.Subscription;
console.log('Subscription updated:', updatedSubscription);
const customerId = typeof updatedSubscription.customer === 'string'
? updatedSubscription.customer
: updatedSubscription.customer.id;
await updateProfile(customerId, {
subscription_status: updatedSubscription.status,
current_period_end: new Date(updatedSubscription.current_period_end * 1000),
plan_id: updatedSubscription.items.data[0].price.id,
cancel_at_period_end: updatedSubscription.cancel_at_period_end,
updated_at: new Date()
});
break;
}
case 'customer.subscription.deleted': {
const deletedSubscription = event.data.object as Stripe.Subscription;
console.log('Subscription deleted:', deletedSubscription);
const customerId = typeof deletedSubscription.customer === 'string'
? deletedSubscription.customer
: deletedSubscription.customer.id;
await updateProfile(customerId, {
subscription_status: 'canceled',
cancel_at_period_end: false,
updated_at: new Date()
});
break;
}
case 'invoice.paid': {
const invoice = event.data.object as Stripe.Invoice;
console.log('Invoice paid:', invoice);
// Only update if this invoice is for a subscription
if (invoice.subscription && invoice.customer) {
const subscriptionId = typeof invoice.subscription === 'string'
? invoice.subscription
: invoice.subscription.id;
const customerId = typeof invoice.customer === 'string'
? invoice.customer
: invoice.customer.id;
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
await updateProfile(customerId, {
subscription_status: 'active',
current_period_end: new Date(subscription.current_period_end * 1000),
updated_at: new Date()
});
}
break;
}
case 'invoice.payment_failed': {
const failedInvoice = event.data.object as Stripe.Invoice;
console.log('Invoice payment failed:', failedInvoice);
if (failedInvoice.customer) {
const customerId = typeof failedInvoice.customer === 'string'
? failedInvoice.customer
: failedInvoice.customer.id;
await updateProfile(customerId, {
subscription_status: 'past_due',
updated_at: new Date()
});
}
break;
}
default:
console.log(`Unhandled event type ${event.type}`);
}
} catch (error) {
console.error(`Error processing webhook event ${event.type}:`, error);
}
return NextResponse.json({ received: true });
}
================================================
FILE: apps/web/src/app/api/unkey/route.ts
================================================
import { NextResponse } from 'next/server';
import { createClient } from '@/utils/supabase/server';
// Unkey API endpoints
const UNKEY_API_URL = 'https://api.unkey.dev/v1';
const UNKEY_API_ID = process.env.UNKEY_API_ID;
const UNKEY_ROOT_KEY = process.env.UNKEY_ROOT_KEY;
// Force dynamic rendering to ensure fresh data
export const dynamic = 'force-dynamic';
export const revalidate = 0;
export async function GET(request: Request) {
console.log('API route called');
// Get userId from query parameters
const url = new URL(request.url);
const userId = url.searchParams.get('userId');
console.log('Received userId from query:', userId);
if (!userId) {
return NextResponse.json({ error: 'UserId is required' }, { status: 400 });
}
// Create Supabase client
const supabase = createClient();
try {
// Check if user exists in profiles table
const { data: profileData, error: profileError } = await supabase
.from('profiles')
.select('id, unkey_key_id')
.eq('id', userId)
.single();
console.log('Profile data:', profileData);
if (profileError && profileError.code !== 'PGRST116') { // PGRST116 is the error code for 'no rows returned'
console.error('Error fetching profile:', profileError);
return NextResponse.json({ error: 'Failed to fetch user profile' }, { status: 500 });
}
// If user doesn't exist in profiles, create an entry
if (!profileData) {
console.log('Creating new profile for user:', userId);
const { error: insertError } = await supabase
.from('profiles')
.insert({ id: userId });
if (insertError) {
console.error('Error creating profile:', insertError);
return NextResponse.json({ error: 'Failed to create user profile' }, { status: 500 });
}
// Return that API key doesn't exist
return NextResponse.json({ exists: false });
}
// Only return exists: true if the user has a non-null unkey_key_id
if (!profileData.unkey_key_id) {
console.log('User exists but has no API key');
return NextResponse.json({ exists: false });
}
// At this point, we have a user with a non-null unkey_key_id
console.log('User has an API key ID:', profileData.unkey_key_id);
// Check if the key exists in Unkey
const getKeyResponse = await fetch(
`${UNKEY_API_URL}/keys.getKey?keyId=${profileData.unkey_key_id}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${UNKEY_ROOT_KEY}`,
},
}
);
// If key doesn't exist in Unkey, return that it doesn't exist
if (!getKeyResponse.ok) {
console.log('Key ID exists in profile but not in Unkey');
return NextResponse.json({ exists: false });
}
const keyData = await getKeyResponse.json();
console.log('Key found in Unkey');
return NextResponse.json({
exists: true,
key: keyData.key
});
} catch (error) {
console.error('Error checking API key:', error);
return NextResponse.json({ error: 'Failed to check API key' }, { status: 500 });
}
}
export async function POST(request: Request) {
// Parse the request body to get the userId
let userId;
try {
const body = await request.json();
userId = body.userId;
console.log('Received userId for key creation:', userId);
if (!userId) {
return NextResponse.json({ error: 'UserId is required' }, { status: 400 });
}
} catch (error) {
console.error('Error parsing request body:', error);
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
}
// Create Supabase client
const supabase = createClient();
try {
// Check if user exists in profiles table
const { error: profileError } = await supabase
.from('profiles')
.select('id, unkey_key_id')
.eq('id', userId)
.single();
if (profileError && profileError.code !== 'PGRST116') {
console.error('Error fetching profile:', profileError);
return NextResponse.json({ error: 'Failed to fetch user profile' }, { status: 500 });
}
// Create an API key for the user
const createKeyResponse = await fetch(`${UNKEY_API_URL}/keys.createKey`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${UNKEY_ROOT_KEY}`,
},
body: JSON.stringify({
apiId: UNKEY_API_ID,
prefix: 'cd',
byteLength: 16,
externalId: userId,
meta: {
createdAt: new Date().toISOString(),
userId
},
}),
});
if (!createKeyResponse.ok) {
const errorText = await createKeyResponse.text();
console.log('Error creating key:', errorText);
throw new Error(`Failed to create key: ${createKeyResponse.statusText}`);
}
const keyData = await createKeyResponse.json();
console.log('Key created:', keyData.keyId);
// Update the user's profile with the key ID
const { error: updateError } = await supabase
.from('profiles')
.update({ unkey_key_id: keyData.keyId })
.eq('id', userId);
if (updateError) {
console.error('Error updating profile with key ID:', updateError);
// We'll still return the key even if updating the profile fails
}
return NextResponse.json({
success: true,
key: keyData.key,
keyId: keyData.keyId,
});
} catch (error) {
console.error('Error creating API key:', error);
return NextResponse.json({ error: 'Failed to create API key' }, { status: 500 });
}
}
================================================
FILE: apps/web/src/app/auth/callback/route.ts
================================================
import { createClient } from '@/utils/supabase/server'
import { NextRequest, NextResponse } from 'next/server'
import PostHogClient from '@/utils/posthog/posthog'
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
const error = requestUrl.searchParams.get('error')
const errorDescription = requestUrl.searchParams.get('error_description')
// Create a response that will be used for redirecting
const redirectUrl = new URL('/dashboard', request.url)
if (error) {
console.error(`Auth error: ${error}, Description: ${errorDescription}`)
// If there's an error, redirect to login
redirectUrl.pathname = '/login'
// Add error information as query parameters
redirectUrl.searchParams.set('error', error)
if (errorDescription) {
redirectUrl.searchParams.set('error_description', errorDescription)
}
} else if (code) {
try {
const supabase = createClient()
// Exchange the code for a session
const { data } = await supabase.auth.exchangeCodeForSession(code)
// Identify the user in PostHog
if (data?.user) {
const posthog = PostHogClient()
// Use the correct identify method signature for posthog-node
posthog.identify({
distinctId: data.user.id,
properties: {
email: data.user.email,
name: data.user.user_metadata?.full_name || data.user.user_metadata?.name
}
})
}
} catch (err) {
console.error('Error exchanging code for session:', err)
// If there's an error, redirect to login
redirectUrl.pathname = '/login'
redirectUrl.searchParams.set('error', 'session_exchange_error')
}
}
// URL to redirect to after sign in process completes
return NextResponse.redirect(redirectUrl)
}
================================================
FILE: apps/web/src/app/blog/[slug]/page.tsx
================================================
import { Button } from '@/components/button'
import { Container } from '@/components/container'
import { Footer } from '@/components/footer'
import { GradientBackground } from '@/components/gradient'
import { Link } from '@/components/link'
import { Navbar } from '@/components/navbar'
import { Heading, Subheading } from '@/components/text'
import { image } from '@/sanity/image'
import { getPost } from '@/sanity/queries'
import { ChevronLeftIcon } from '@heroicons/react/16/solid'
import dayjs from 'dayjs'
import type { Metadata } from 'next'
import { PortableText } from 'next-sanity'
import { notFound } from 'next/navigation'
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): Promise {
const post = await getPost(params.slug)
return post ? { title: post.title, description: post.excerpt } : {}
}
export default async function BlogPost({
params,
}: {
params: { slug: string }
}) {
const post = (await getPost(params.slug)) || notFound()
return (
{dayjs(post.publishedAt).format('dddd, MMMM D, YYYY')}
{post.title}
)
}
export default async function Blog({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined }
}) {
const page =
'page' in searchParams
? typeof searchParams.page === 'string' && parseInt(searchParams.page) > 1
? parseInt(searchParams.page)
: notFound()
: 1
const category =
typeof searchParams.category === 'string'
? searchParams.category
: undefined
return (
Blog
What's happening at Cyberdesk.
Stay informed with product updates, company news, and insights on how
to build world class computer agents.
{page === 1 && !category && }
)
}
================================================
FILE: apps/web/src/app/company/page.tsx
================================================
import { AnimatedNumber } from '@/components/animated-number'
import { Button } from '@/components/button'
import { Container } from '@/components/container'
import { Footer } from '@/components/footer'
import { GradientBackground } from '@/components/gradient'
import { Navbar } from '@/components/navbar'
import { Heading, Lead, Subheading } from '@/components/text'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Company',
description:
'We’re on a mission to transform revenue organizations by harnessing vast amounts of illegally acquired customer data.',
}
function Header() {
return (
Helping companies generate revenue.
We’re on a mission to transform revenue organizations by harnessing vast
amounts of illegally acquired customer data.
Our mission
At Radiant, we are dedicated to transforming the way revenue
organizations source and close deals. Our mission is to provide our
customers with an unfair advantage over both their competitors and
potential customers through insight and analysis. We’ll stop at
nothing to get you the data you need to close a deal.
We’re customer-obsessed — putting the time in to build a detailed
financial picture of every one of our customers so that we know more
about your business than you do. We are in this together, mostly
because we are all implicated in large-scale financial crime. In our
history as a company, we’ve never lost a customer, because if any
one of us talks, we all go down.
)
}
function Team() {
return (
Meet the team
Founded by an all-star team.
Radiant is founded by two of the best sellers in the business and backed
by investors who look the other way.
Years ago, while working as sales associates at rival companies,
Thomas, Ben, and Natalie were discussing a big client they had all
been competing for. Joking about seeing the terms of each other’s
offers, they had an idea: what if they shared data to win deals and
split the commission behind their companies’ backs? It turned out to
be an incredible success, and that idea became the kernel for
Radiant.
Today, Radiant transforms revenue organizations by harnessing
illegally acquired customer and competitor data, using it to provide
extraordinary leverage. More than 30,000 companies rely on Radiant
to undercut their competitors and extort their customers, all
through a single integrated platform.
The team
)
}
function Investors() {
return (
Investors
Funded by industry-leaders.
We are fortunate to be backed by the best investors in the industry —
both literal and metaphorical partners in crime.
Venture Capital
Remington Schwartz has been a driving force in the tech industry,
backing bold entrepreneurs who explore grey areas in financial and
privacy law. Their deep industry expertise and extensive political
lobbying provide their portfolio companies with favorable regulation
and direct access to lawmakers.
Deccel has been at the forefront of innovation, investing in
pioneering companies across various sectors, including technology,
consumer goods, and healthcare. Their philosophy of ‘plausible
deniability’ and dedication to looking the other way have helped
produce some of the world’s most controversial companies.
Individual investors
)
}
function Testimonial() {
return (
We've managed to put two of our main competitors out of
business in 6 months.
Veronica Winton
CSO, Planeteria
)
}
function Careers() {
return (
Careers
Join our fully remote team.
We work together from all over the world, mainly from locations without
extradition agreements.
Open positions
Title
Location
Read more
Engineering
iOS Developer
Remote
Backend Engineer
Remote
Product Engineer
Remote
Design
Principal Designer
Remote
Designer
Remote
Senior Designer
Remote
)
}
export default function Company() {
return (
)
}
================================================
FILE: apps/web/src/app/dashboard/dashboard-content.tsx
================================================
'use client'
import { SubscriptionSection } from '@/components/dashboard/subscription-section'
import type { Profile } from '@/types/database'
import { ApiKeyManager } from '@/components/dashboard/api-key-manager'
import { VMInstancesManager } from '@/components/dashboard/vm-instances-manager'
import { supabase } from '@/utils/supabase/client'
import { ArrowRightOnRectangleIcon } from '@heroicons/react/24/outline'
import { Button } from '@/components/button'
import posthog from 'posthog-js'
// import { Subheading } from '@/components/text'
interface DashboardContentProps {
userEmail?: string;
userId?: string;
profile?: Profile | null;
}
export function DashboardContent({ userEmail, userId, profile }: DashboardContentProps) {
const isSubscriptionActive = profile?.subscription_status === "active";
// Removed fetching active subscriptions as pricing card is no longer displayed for non-subscribers
// Removed fetching active subscriptions as pricing card is no longer displayed for non-subscribers
const handleLogout = async () => {
// Reset PostHog user to anonymous before logging out
posthog.reset();
await supabase.auth.signOut();
window.location.href = '/';
};
// const isSoldOut = activeSubscriptionsCount !== null && activeSubscriptionsCount >= SUBSCRIPTION_LIMIT;
// If subscription is not active, show booking message instead of pricing/FAQ
if (!isSubscriptionActive) {
return (
Looks like we haven't set up your account yet.
Book a time here to get you up and running soon!
);
}
return (
{isSubscriptionActive ? (
<>
Dashboard
Welcome to your dashboard. This is where you can manage your virtual desktops, account settings, and more.
>
) : (
<>
Get Started with Cyberdesk
Unlock the full power of Cyberdesk by subscribing to our Pro plan.
>
)}
{/* Pricing and FAQ sections have been removed for non-subscribers */}
{/* For active subscribers, show API Key, VM Instances, and Subscription sections */}
{isSubscriptionActive && (
<>
{/* API Key Section */}
{/* VM Instances Section */}
{/* Subscription Management Section */}
>
)}
)
}
================================================
FILE: apps/web/src/app/dashboard/page.tsx
================================================
import { redirect } from 'next/navigation';
import { stripe } from '@/utils/stripe/stripe-server';
import type { Profile } from '@/types/database';
import { DashboardLayout } from '@/components/dashboard/dashboard-layout';
import { DashboardContent } from './dashboard-content';
import { createClient } from '@/utils/supabase/server';
export default async function Dashboard() {
const supabase = createClient();
// Check if user is authenticated - using getUser() for better security
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect('/login');
}
const userId = user.id;
// Query the profiles table for the user
const { data: dbProfile, error } = await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.single();
let profile: Profile | null = dbProfile;
if (error && error.code !== 'PGRST116') {
// PGRST116 is the error code for "no rows returned" - we handle this case separately
console.error('Error fetching profile:', error);
}
// If profile doesn't exist, create it along with a Stripe customer
if (!profile) {
try {
// Create a Stripe customer
const customer = await stripe.customers.create({
email: user.email,
metadata: {
userId: userId
}
});
// Create a profile entry with the Stripe customer ID
const newProfile: Profile = {
id: userId,
stripe_customer_id: customer.id,
subscription_status: 'inactive',
created_at: new Date(),
updated_at: new Date()
};
const { error: insertError } = await supabase
.from('profiles')
.insert(newProfile);
profile = newProfile;
if (insertError) {
console.error('Error creating profile:', insertError);
}
} catch (err) {
console.error('Error creating Stripe customer:', err);
}
}
// Get session for client components
// TODO: Utilize this for client components
// const { data: { session } } = await supabase.auth.getSession();
return (
);
}
================================================
FILE: apps/web/src/app/demo/page.tsx
================================================
'use client'
import { DemoSection } from '@/components/demo-section'
import { Thread } from '@/components/thread'
import { AssistantRuntimeProvider } from '@assistant-ui/react'
import { useChatRuntime } from '@assistant-ui/react-ai-sdk'
import {
ChatBubbleLeftIcon,
HeartIcon,
} from '@heroicons/react/24/outline'
import { useState } from 'react'
export default function PlaygroundDemo() {
// State to store the desktop ID
const [desktopId, setDesktopId] = useState(null)
// State to track if the demo has been finished
const [finishedDemo, setFinishedDemo] = useState(false)
// Update the chat runtime to support OpenAI Responses API features
const runtime = useChatRuntime({
api: '/api/chat',
// Use headers instead of body to pass the desktopId
headers: () => {
// Use the actual desktopId from state, or a fallback value if it's null
const currentDesktopId = desktopId || 'NO_DESKTOP_ID_YET'
console.log(
'[useChatRuntime] Sending request with desktopId in headers:',
currentDesktopId,
)
return Promise.resolve({
'Content-Type': 'application/json',
'X-Desktop-Id': currentDesktopId,
})
},
onFinish: (message) => {
const sources = message.metadata?.custom?.sources
if (sources) {
console.log('Web search sources:', sources)
}
},
})
// Function to handle when a desktop is deployed
const handleDesktopDeployed = (id: string) => {
// Set the desktop ID directly from the API response
setDesktopId(id)
}
const handleDesktopStopped = () => {
// Set the desktop ID directly from the API response
setDesktopId(null)
// Set finishedDemo to true when desktop is stopped
setFinishedDemo(true)
}
return (
{/* Thread Area */}
{desktopId ? (
{/* Add a button to stop the desktop on mobile and tablet only */}
) : (
{finishedDemo ? (
) : (
)}
{finishedDemo
? 'Thank you for trying our demo! Sign up to get started.'
: 'Launch the demo to start chatting with the assistant.'}
)}
{/* The mobile stop button is now handled by a reference to the DemoSection component */}
{/* Demo Section */}
)
}
================================================
FILE: apps/web/src/app/layout.tsx
================================================
import '@/styles/tailwind.css'
import type { Metadata } from 'next'
import { PostHogProvider } from '../components/PostHogProvider'
export const metadata: Metadata = {
title: {
template: '%s | Cyberdesk',
default: 'Cyberdesk | Virtual desktops for AI agents',
},
icons: {
icon: '/favicon.svg',
},
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
{children}
)
}
================================================
FILE: apps/web/src/app/login/login-form.d.ts
================================================
// Type declaration for login-form component
export function LoginForm(): JSX.Element;
================================================
FILE: apps/web/src/app/login/login-form.tsx
================================================
'use client';
import { Button } from '@/components/button'
import { Link } from '@/components/link'
import { Mark } from '@/components/logo'
import { supabase } from '@/utils/supabase/client'
import { useRouter } from 'next/navigation'
export function LoginForm() {
const router = useRouter()
const signInWithGoogle = async () => {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
})
if (error) {
console.error('Error signing in with Google:', error.message)
}
}
return (
<>
Continue with
>
)
}
================================================
FILE: apps/web/src/app/login/page.tsx
================================================
import { GradientBackground } from '@/components/gradient'
import type { Metadata } from 'next'
import { LoginForm } from './login-form'
export const metadata: Metadata = {
title: 'Login',
description: 'Sign in to your account to continue.',
}
export default function Login() {
return (
)
}
================================================
FILE: apps/web/src/app/page.tsx
================================================
import { Footer } from '@/components/footer'
import type { Metadata } from 'next'
import { Hero } from '../components/hero'
import { YCBanner } from '../components/yc-banner'
import Playground from './playground/page'
export const metadata: Metadata = {
description:
'Cyberdesk deploys virtual desktops for your computer agents with only in a few lines of code.',
}
export default function Home() {
return (
{/* */}
)
}
================================================
FILE: apps/web/src/app/playground/page.tsx
================================================
"use client";
import { PreviewMessage } from "@/components/playground/message";
import { getDesktopURL, startDesktop } from "@/utils/playground/server-actions";
import { useScrollToBottom } from "@/utils/playground/use-scroll-to-bottom";
import { useChat } from "@ai-sdk/react";
import { useEffect, useState } from "react";
import { Input } from "@/components/playground/input";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { ProjectInfo } from "@/components/playground/project-info";
import { PromptSuggestions } from "@/components/playground/prompt-suggestions";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { ABORTED } from "@/utils/playground/misc-demo-utils";
import { FaRocket } from "react-icons/fa";
import { ChatError } from "@/components/playground/chat-error";
// Shared polling helper for desktop URL
async function pollForDesktopURL(sandboxId: string | null | undefined) {
let delay = 1000; // Start with 1 second
const maxDelay = 5000; // Cap at 5 seconds
const maxTime = 180000; // 3 minutes in ms
const startTime = Date.now();
while (true) {
if (Date.now() - startTime > maxTime) {
throw new Error("Timed out after 3 minutes while getting desktop stream URL.");
}
const { streamUrl, id } = await getDesktopURL(sandboxId || undefined);
if (streamUrl) {
return { streamUrl, id };
}
delay = Math.min(maxDelay, Math.floor(delay * 1.5));
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
export default function Playground() {
// Create separate refs for mobile and desktop to ensure both scroll properly
const [desktopContainerRef, desktopEndRef] = useScrollToBottom();
const [mobileContainerRef, mobileEndRef] = useScrollToBottom();
const [isInitializing, setIsInitializing] = useState(false);
const [streamUrl, setStreamUrl] = useState(null);
const [sandboxId, setSandboxId] = useState(null);
const [hasStarted, setHasStarted] = useState(false);
const {
messages,
input,
handleInputChange,
handleSubmit,
error,
reload,
status,
stop: stopGeneration,
append,
setMessages,
} = useChat({
api: "/api/playground/chat",
id: sandboxId ?? undefined,
body: {
sandboxId,
},
onError: (error) => {
console.error(error);
toast.error("There was an error", {
description: "Please try again later.",
richColors: true,
position: "top-center",
});
},
});
const stop = () => {
stopGeneration();
const lastMessage = messages.at(-1);
const lastMessageLastPart = lastMessage?.parts.at(-1);
if (
lastMessage?.role === "assistant" &&
lastMessageLastPart?.type === "tool-invocation"
) {
setMessages((prev) => [
...prev.slice(0, -1),
{
...lastMessage,
parts: [
...lastMessage.parts.slice(0, -1),
{
...lastMessageLastPart,
toolInvocation: {
...lastMessageLastPart.toolInvocation,
state: "result",
result: ABORTED,
},
},
],
},
]);
}
};
const isLoading = status !== "ready";
const refreshDesktop = async () => {
try {
setIsInitializing(true);
const data = await startDesktop();
const id = data.id;
const { streamUrl } = await pollForDesktopURL(id);
setStreamUrl(streamUrl);
setSandboxId(id);
} catch (err) {
console.error("Failed to refresh desktop:", err);
} finally {
setIsInitializing(false);
}
};
// Handler for Start Desktop button
const handleStartDesktop = async () => {
setHasStarted(true);
setIsInitializing(true);
try {
const data = await startDesktop();
const id = data.id;
const { streamUrl } = await pollForDesktopURL(id);
setStreamUrl(streamUrl);
setSandboxId(id);
} catch (err) {
console.error("Failed to initialize desktop:", err);
toast.error("Failed to initialize desktop");
setHasStarted(false); // allow retry
} finally {
setIsInitializing(false);
}
};
// Kill desktop on page close
useEffect(() => {
if (!sandboxId) return;
// Function to kill the desktop - just one method to reduce duplicates
const killDesktop = () => {
if (!sandboxId) return;
// Use sendBeacon which is best supported across browsers
navigator.sendBeacon(
`/api/playground/kill-desktop?sandboxId=${encodeURIComponent(sandboxId)}`,
);
};
// Detect iOS / Safari
const isIOS =
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
// Choose exactly ONE event handler based on the browser
if (isIOS || isSafari) {
// For Safari on iOS, use pagehide which is most reliable
window.addEventListener("pagehide", killDesktop);
return () => {
window.removeEventListener("pagehide", killDesktop);
// Also kill desktop when component unmounts
killDesktop();
};
} else {
// For all other browsers, use beforeunload
window.addEventListener("beforeunload", killDesktop);
return () => {
window.removeEventListener("beforeunload", killDesktop);
// Also kill desktop when component unmounts
killDesktop();
};
}
}, [sandboxId]);
return (
{/* Starter Overlay */}
{!hasStarted && (
{/* Animated background shapes */}
Launch a demo computer agent
{/*
Your secure, cloud-powered development environment
*/}
Click below to start a Cyberdesk desktop. Once it launches, you can chat with a computer agent that can use the desktop.
);
}
================================================
FILE: apps/web/src/app/pricing/page.tsx
================================================
import { Button } from '@/components/button'
import { Container } from '@/components/container'
import { Footer } from '@/components/footer'
import { GradientBackground } from '@/components/gradient'
import { Link } from '@/components/link'
import { Navbar } from '@/components/navbar'
import { Heading, Subheading } from '@/components/text'
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import {
CheckIcon,
ChevronUpDownIcon,
MinusIcon,
} from '@heroicons/react/16/solid'
import type { Metadata } from 'next'
import { createClient } from '@/utils/supabase/server'
import type { Profile } from '@/types/database'
import { tiers } from '@/utils/stripe/tiers'
export const metadata: Metadata = {
title: 'Pricing',
description:
'Deploy AI agents on virtual desktops with a few lines of code.',
}
function Header() {
return (
Start deploying virtual desktops today
{/*
Companies all over the world have closed millions of deals with Radiant.
Sign up today and start selling smarter.
*/}
)
}
// This is now a server component
import ClientPricingCards from '../../components/stripe/client-pricing-cards';
// Moved to client-pricing-card.tsx
function PricingTable({
selectedTier,
}: {
selectedTier: (typeof tiers)[number]
}) {
return (
)
}
function PlusIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
)
}
function Testimonial() {
return (
Thanks to Radiant, we're finding new leads that we never
would have found with legal methods.
Tina Yards
VP of Sales, Protocol
)
}
export default async function Pricing({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined }
}) {
const selectedTierSlug =
typeof searchParams.tier === 'string' ? searchParams.tier : 'pro'
const selectedTier =
tiers.find((tier) => tier.slug === selectedTierSlug) ?? tiers[1]
// Fetch user and profile data on the server
const supabase = createClient();
let user = null;
let profile: Profile | null = null;
// Check if user is authenticated
const { data: { user: authUser } } = await supabase.auth.getUser();
if (authUser) {
user = authUser;
// Query the profiles table for the user
const { data: userProfile, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();
if (!error) {
profile = userProfile;
}
}
return (
{/* */}
{/* */}
)
}
================================================
FILE: apps/web/src/app/privacy/page.tsx
================================================
import { Container } from '@/components/container'
import { Footer } from '@/components/footer'
import { Gradient, GradientBackground } from '@/components/gradient'
import { Navbar } from '@/components/navbar'
import { Heading, Lead } from '@/components/text'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Privacy Policy | Cyberdesk',
description:
'Privacy Policy for Cyberdesk virtual desktop infrastructure and AI agents platform.',
}
function Header() {
return (
Privacy Policy
Last updated: March 27, 2025
)
}
export default function Privacy() {
return (
<>
1. Introduction
At Cyberdesk, we respect your privacy and are committed to protecting your personal data. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our virtual desktop infrastructure and AI agent services.
2. Information We Collect
We collect several types of information from and about users of our Services, including:
Personal Information: Name, email address, billing information, and other contact details you provide when registering for our Services.
Usage Data: Information about how you use our Services, including login times, features used, and interactions with our platform.
Device Information: Information about the devices you use to access our Services, including IP address, browser type, and operating system.
Virtual Desktop Content: Data generated or stored within your virtual desktop environment, including files, applications, and AI agent configurations.
3. How We Use Your Information
We use the information we collect for various purposes, including:
Providing, maintaining, and improving our Services
Processing your transactions and managing your account
Communicating with you about our Services, updates, and support
Analyzing usage patterns to enhance user experience
Detecting, preventing, and addressing technical issues or security breaches
Complying with legal obligations
4. Data Storage and Security
We implement appropriate technical and organizational measures to protect your personal data against unauthorized or unlawful processing, accidental loss, destruction, or damage. However, no method of transmission over the Internet or electronic storage is 100% secure, and we cannot guarantee absolute security.
5. Data Retention
We retain your personal information for as long as necessary to fulfill the purposes outlined in this Privacy Policy, unless a longer retention period is required or permitted by law. When determining how long to retain data, we consider the amount, nature, and sensitivity of the data, potential risk of harm from unauthorized use or disclosure, and applicable legal requirements.
6. Sharing Your Information
We may share your information in the following circumstances:
Service Providers: We may share your information with third-party vendors who provide services on our behalf, such as payment processing, data analysis, and customer service.
Business Transfers: If we are involved in a merger, acquisition, or sale of assets, your information may be transferred as part of that transaction.
Legal Requirements: We may disclose your information if required to do so by law or in response to valid requests by public authorities.
With Your Consent: We may share your information with third parties when you have given us your consent to do so.
7. Your Rights
Depending on your location, you may have certain rights regarding your personal data, including:
The right to access and receive a copy of your personal data
The right to rectify or update your personal data
The right to erase your personal data
The right to restrict processing of your personal data
The right to data portability
The right to object to processing of your personal data
The right to withdraw consent
8. Children's Privacy
Our Services are not intended for children under the age of 16, and we do not knowingly collect personal information from children under 16. If we learn we have collected or received personal information from a child under 16, we will delete that information.
9. International Data Transfers
Your information may be transferred to, and maintained on, computers located outside of your state, province, country, or other governmental jurisdiction where the data protection laws may differ from those of your jurisdiction. If you are located outside the United States and choose to provide information to us, please note that we transfer the data to the United States and process it there.
10. Cookies and Similar Technologies
We use cookies and similar tracking technologies to track activity on our Services and hold certain information. Cookies are files with a small amount of data which may include an anonymous unique identifier. You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent.
11. Changes to This Privacy Policy
We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last updated" date. You are advised to review this Privacy Policy periodically for any changes.
12. Contact Us
If you have any questions about this Privacy Policy, please contact us at mahmoud@cyberdesk.io.
>
)
}
================================================
FILE: apps/web/src/app/studio/[[...tool]]/page.tsx
================================================
/**
* This route is responsible for the built-in authoring environment using Sanity Studio.
* All routes under your studio path is handled by this file using Next.js' catch-all routes:
* https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes
*
* You can learn more about the next-sanity package here:
* https://github.com/sanity-io/next-sanity
*/
import { NextStudio } from 'next-sanity/studio'
import config from '../../../../sanity.config'
export const dynamic = 'force-static'
export { metadata, viewport } from 'next-sanity/studio'
export default function StudioPage() {
return
}
================================================
FILE: apps/web/src/app/terms/page.tsx
================================================
import { Container } from '@/components/container'
import { Footer } from '@/components/footer'
import { Gradient, GradientBackground } from '@/components/gradient'
import { Navbar } from '@/components/navbar'
import { Heading, Lead } from '@/components/text'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Terms of Service | Cyberdesk',
description:
'Terms of Service for Cyberdesk virtual desktop infrastructure and AI agents platform.',
}
function Header() {
return (
Terms of Service
Last updated: March 27, 2025
)
}
export default function Terms() {
return (
<>
1. Acceptance of Terms
By accessing or using Cyberdesk's virtual desktop infrastructure and AI agent services ("Services"), you agree to be bound by these Terms of Service. If you do not agree to these terms, please do not use our Services.
2. Description of Services
Cyberdesk provides cloud-based virtual desktop infrastructure and AI agent deployment services. Our platform allows users to deploy, manage, and interact with autonomous AI agents on virtual desktops.
3. Account Registration
To access certain features of our Services, you must register for an account. You agree to provide accurate, current, and complete information during the registration process and to update such information to keep it accurate, current, and complete.
4. User Responsibilities
You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account. You agree to notify us immediately of any unauthorized use of your account or any other breach of security.
5. Subscription and Billing
Certain aspects of our Services require payment of fees. All fees are specified on our pricing page and are subject to change with notice. You agree to pay all fees in accordance with the billing terms in effect at the time a fee is due.
6. Usage Limitations
Your use of the Services must comply with all applicable laws and regulations. You may not use the Services for any illegal or unauthorized purpose, including but not limited to violating any intellectual property rights.
7. Data and Security
We implement reasonable security measures to protect your data, but we cannot guarantee absolute security. You are responsible for backing up your data and ensuring that your use of our Services does not expose your systems to security risks.
8. Intellectual Property
All content, features, and functionality of our Services, including but not limited to text, graphics, logos, and software, are owned by Cyberdesk and are protected by intellectual property laws. You may not reproduce, distribute, or create derivative works without our prior written consent.
9. Termination
We reserve the right to suspend or terminate your access to the Services at any time for any reason, including but not limited to a violation of these Terms. Upon termination, your right to use the Services will immediately cease.
10. Disclaimer of Warranties
THE SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. WE DO NOT WARRANT THAT THE SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE.
11. Limitation of Liability
IN NO EVENT SHALL CYBERDESK BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES, WHETHER INCURRED DIRECTLY OR INDIRECTLY.
12. Indemnification
You agree to indemnify and hold harmless Cyberdesk and its officers, directors, employees, and agents, from and against any claims, liabilities, damages, losses, and expenses arising out of or in any way connected with your use of the Services.
13. Governing Law
These Terms shall be governed by and construed in accordance with the laws of the State of California, without regard to its conflict of law provisions.
14. Changes to Terms
We reserve the right to modify these Terms at any time. We will provide notice of any material changes by posting the new Terms on our website. Your continued use of the Services after such modifications constitutes your acceptance of the revised Terms.
15. Contact Information
If you have any questions about these Terms, please contact us at mahmoud@cyberdesk.io.
>
)
}
================================================
FILE: apps/web/src/components/LogoText.tsx
================================================
import React from 'react';
import { cn } from '@/utils/misc-utils'; // Assuming you use shadcn/ui or similar for utils
interface LogoTextProps extends React.HTMLAttributes {}
export const LogoText: React.FC = ({ className, ...props }) => {
return (
Cyberdesk
);
};
================================================
FILE: apps/web/src/components/PostHogProvider.tsx
================================================
"use client"
import posthog from "posthog-js"
import { PostHogProvider as PHProvider, usePostHog } from "posthog-js/react"
import { Suspense, useEffect } from "react"
import { usePathname, useSearchParams } from "next/navigation"
export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: "/ingest",
ui_host: "https://us.posthog.com",
capture_pageview: false, // We capture pageviews manually
capture_pageleave: true, // Enable pageleave capture
})
}, [])
return (
{children}
)
}
function PostHogPageView() {
const pathname = usePathname()
const searchParams = useSearchParams()
const posthog = usePostHog()
useEffect(() => {
if (pathname && posthog) {
let url = window.origin + pathname
const search = searchParams.toString()
if (search) {
url += "?" + search
}
posthog.capture("$pageview", { "$current_url": url })
}
}, [pathname, searchParams, posthog])
return null
}
function SuspendedPostHogPageView() {
return (
)
}
================================================
FILE: apps/web/src/components/animated-number.tsx
================================================
'use client'
import {
motion,
useInView,
useMotionValue,
useSpring,
useTransform,
} from 'framer-motion'
import { useEffect, useRef } from 'react'
export function AnimatedNumber({
start,
end,
decimals = 0,
}: {
start: number
end: number
decimals?: number
}) {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, amount: 0.5 })
const value = useMotionValue(start)
const spring = useSpring(value, { damping: 30, stiffness: 100 })
const display = useTransform(spring, (num) => num.toFixed(decimals))
useEffect(() => {
value.set(isInView ? end : start)
}, [start, end, isInView, value])
return {display}
}
================================================
FILE: apps/web/src/components/bento-card.tsx
================================================
'use client'
import { clsx } from 'clsx'
import { motion } from 'framer-motion'
import { Subheading } from './text'
export function BentoCard({
dark = false,
className = '',
eyebrow,
title,
description,
graphic,
fade = [],
}: {
dark?: boolean
className?: string
eyebrow: React.ReactNode
title: React.ReactNode
description: React.ReactNode
graphic: React.ReactNode
fade?: ('top' | 'bottom')[]
}) {
return (
)
}
================================================
FILE: apps/web/src/components/dark-bento-section.tsx
================================================
import { BentoCard } from '@/components/bento-card'
import { Container } from '@/components/container'
import { LinkedAvatars } from '@/components/linked-avatars'
import { LogoTimeline } from '@/components/logo-timeline'
import { Heading, Subheading } from '@/components/text'
export function DarkBentoSection() {
return (
Customization
Tailor your virtual desktops to your needs
}
fade={['top']}
className="max-lg:rounded-t-4xl lg:col-span-4 lg:rounded-tl-4xl"
/>
}
// `overflow-visible!` is needed to work around a Chrome bug that disables the mask on the graphic.
className="z-10 overflow-visible! lg:col-span-2 lg:rounded-tr-4xl"
/>
}
className="lg:col-span-2 lg:rounded-bl-4xl"
/>
}
fade={['top']}
className="max-lg:rounded-b-4xl lg:col-span-4 lg:rounded-br-4xl"
/>
)
}
================================================
FILE: apps/web/src/components/dashboard/api-key-manager.tsx
================================================
'use client'
import { useState, useEffect } from 'react'
import { Button } from '@/components/button'
import { ClipboardIcon, KeyIcon, ArrowPathIcon } from '@heroicons/react/24/outline'
import { CheckIcon } from '@heroicons/react/24/solid'
import { supabase } from '@/utils/supabase/client'
interface ApiKey {
id: string
key: string
createdAt: string
}
export function ApiKeyManager() {
const [apiKey, setApiKey] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
const [copied, setCopied] = useState(false)
const [isGenerating, setIsGenerating] = useState(false)
const [keyExists, setKeyExists] = useState(false)
useEffect(() => {
fetchApiKey()
}, [])
const fetchApiKey = async () => {
setIsLoading(true)
setError(null)
try {
// Get the current user session
const { data, error } = await supabase.auth.getSession();
if (error || !data.session) {
console.error('Authentication error:', error)
setError('Authentication error. Please sign in again.')
setIsLoading(false)
return
}
const userId = data.session.user.id
console.log('Checking if API key exists for user:', userId)
// Call our API endpoint with the userId as a query parameter
const response = await fetch(`/api/unkey?userId=${userId}`, {
method: 'GET',
})
console.log('API response status:', response.status)
const responseData = await response.json()
if (!response.ok) {
throw new Error(responseData.error || 'Failed to fetch API key')
}
console.log('API key check response:', responseData)
setKeyExists(responseData.exists)
if (responseData.exists && responseData.key) {
// We have a key, set it
setApiKey({
id: responseData.keyId || 'unknown',
key: responseData.key,
createdAt: new Date().toISOString(),
})
} else {
setApiKey(null)
}
} catch (err) {
console.error('Error fetching API key:', err)
setError('Failed to load your API key. Please try again.')
} finally {
setIsLoading(false)
}
}
const generateApiKey = async () => {
setIsGenerating(true)
setError(null)
try {
// Get the current user session
const { data, error } = await supabase.auth.getSession();
if (error || !data.session) {
console.error('Authentication error:', error)
setError('Authentication error. Please sign in again.')
setIsGenerating(false)
return
}
const userId = data.session.user.id
console.log('Generating API key for user:', userId)
// Call our API endpoint with the userId in the body
const response = await fetch('/api/unkey', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId }),
})
const responseData = await response.json()
if (!response.ok) {
throw new Error(responseData.error || 'Failed to generate API key')
}
if (responseData.key) {
setApiKey({
id: responseData.keyId || 'unknown',
key: responseData.key,
createdAt: new Date().toISOString(),
})
setKeyExists(true)
} else {
throw new Error('No key returned from API')
}
} catch (err) {
console.error('Error generating API key:', err)
setError('Failed to generate a new API key. Please try again.')
} finally {
setIsGenerating(false)
}
}
const copyToClipboard = () => {
if (apiKey) {
navigator.clipboard.writeText(apiKey.key)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
return (
Your API Key
Use this key to authenticate API requests
{isLoading ? (
) : error ? (
{error}
) : apiKey ? (
API Key Active
You have an active API key that you can use to authenticate your API requests.
{apiKey.key}
Keep this key secret. Do not share it in client-side code.
This is your current API key. If you've lost access to it, you can generate a new one below.
) : keyExists ? (
API Key Already Created
You've already created an API key.
If you lost your API key, you can generate a new key below, which will replace your existing key.
) : (
No API Key Found
You don't have an API key yet. Generate one to start using our API.
)}
)
}
================================================
FILE: apps/web/src/components/dashboard/api-key-section.tsx
================================================
'use client'
import { useState, useEffect } from 'react'
import { Button } from '@/components/button'
import { CheckIcon, ClipboardIcon, KeyIcon } from '@heroicons/react/24/outline'
import { ArrowPathIcon } from '@heroicons/react/24/outline'
interface ApiKeyData {
key: string
keyId: string
}
export function ApiKeySection() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [identityExists, setIdentityExists] = useState(false)
const [apiKey, setApiKey] = useState(null)
const [copied, setCopied] = useState(false)
const [isCreating, setIsCreating] = useState(false)
// Toast notification function
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
// In a real implementation, this would use a toast library
console.log(`Toast (${type}):`, message)
}
useEffect(() => {
checkIdentity()
}, [])
const checkIdentity = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch('/api/unkey')
if (!response.ok) {
throw new Error('Failed to check identity')
}
const data = await response.json()
setIdentityExists(data.exists)
if (data.exists && data.keys && data.keys.length > 0) {
// Sort keys by creation date and get the most recent one
const sortedKeys = [...data.keys].sort((a, b) => {
return new Date(b.meta?.createdAt || 0).getTime() -
new Date(a.meta?.createdAt || 0).getTime()
})
setApiKey({
key: sortedKeys[0].key,
keyId: sortedKeys[0].id
})
}
} catch (err) {
console.error('Error checking identity:', err)
setError('Failed to check your API key status')
} finally {
setLoading(false)
}
}
const createApiKey = async () => {
setIsCreating(true)
setError(null)
try {
const response = await fetch('/api/unkey', {
method: 'POST'
})
if (!response.ok) {
throw new Error('Failed to create API key')
}
const data = await response.json()
setIdentityExists(true)
setApiKey({
key: data.key,
keyId: data.keyId
})
showToast("Your new API key has been generated successfully.", "success")
} catch (err) {
console.error('Error creating API key:', err)
setError('Failed to create your API key')
showToast("Failed to create your API key. Please try again.", "error")
} finally {
setIsCreating(false)
}
}
const copyToClipboard = () => {
if (apiKey?.key) {
navigator.clipboard.writeText(apiKey.key)
setCopied(true)
showToast("API key copied to clipboard", "success")
setTimeout(() => setCopied(false), 2000)
}
}
if (loading) {
return (
)
}
if (error) {
return (
API Key
There was a problem loading your API key information
{error}
)
}
return (
API Key
{identityExists
? "Use this key to authenticate API requests to our service"
: "Generate an API key to start using our service"}
{identityExists && apiKey ? (
{apiKey.key}
Keep this key secure. Never share it in client-side code or public repositories.
) : (
You don't have an API key yet
)}
)
}
================================================
FILE: apps/web/src/components/dashboard/dashboard-layout.tsx
================================================
'use client'
import { useState, type ReactNode } from 'react'
import { MobileSidebar } from './mobile-sidebar'
import { DesktopSidebar } from './desktop-sidebar'
import { MobileHeader } from './mobile-header'
interface DashboardLayoutProps {
children: ReactNode;
userEmail?: string;
}
export function DashboardLayout({ children, userEmail }: DashboardLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false)
return (
{/* Sidebar component, swap this element with another sidebar if you like */}
)
}
================================================
FILE: apps/web/src/components/dashboard/faq-section.tsx
================================================
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
import { MinusSmallIcon, PlusSmallIcon } from '@heroicons/react/24/outline'
const faqs = [
{
question: "How do I get started with Cyberdesk?",
answer:
"Simply subscribe to our Pro plan, and you'll immediately gain access to all features. You can then create your API key and start deploying virtual desktops that your computer agents can seamlessly control like a human.",
},
{
question: "Is there a money back guarantee?",
answer:
"Yes, we offer a 30-day money-back guarantee. If you're not satisfied with our service, you can let us know and we'll process a refund. You can cancel your subscription at any time.",
},
{
question: "Can I increase my limits?",
answer:
"Yes, you can increase your limits by upgrading to a higher-tier plan. Contact us for more information.",
},
{
question: "What is the operating system of the virtual desktop?",
answer:
"The virtual desktop is based on Ubuntu. This ensures a stable and secure environment for your AI agents.",
},
{
question: "What programming languages and frameworks are supported?",
answer:
"Our REST API can be called from anywhere, regardless of the programming language you're using. SDK support for popular languages is coming soon.",
},
{
question: "How do I connect my AI agent to the virtual desktop?",
answer:
"Our API has endpoints for computer and bash actions, specifically tailored for CUA (Computer-Using Agent) agents. These endpoints allow your AI to interact with the virtual desktop just like a human would.",
},
{
question: "Is there a free trial available?",
answer:
"Contact us about this, we'd love to chat about your specific needs and how we can help you get started with Cyberdesk.",
}
]
export function FAQSection() {
return (
Spin up thousands of concurrent desktops in seconds
)
}
================================================
FILE: apps/web/src/components/footer.tsx
================================================
'use client'
import { PlusGrid, PlusGridItem, PlusGridRow } from '@/components/plus-grid'
import { Button } from './button'
import { Container } from './container'
import { Gradient } from './gradient'
import { Link } from './link'
import { LogoText } from './LogoText' // Import the new component
import { Subheading } from './text'
import { useState, useEffect } from 'react'
import { supabase } from '@/utils/supabase/client'
import { AppLogo } from './shared/app-logo'
function CallToAction() {
return (
Book a demo
Dealing with high-volume
repetitive computer tasks?
We'd love to chat and see how we can help
)
}
function SitemapHeading({ children }: { children: React.ReactNode }) {
return
{children}
}
function SitemapLinks({ children }: { children: React.ReactNode }) {
return
{children}
}
function SitemapLink(props: React.ComponentPropsWithoutRef) {
return (
)
}
function Sitemap() {
return (
<>
{/*
ProductPricingDocsAPI
*/}
LegalTerms of ServicePrivacy Policy
{/*
CompanyCareersBlogCompany
SupportHelp centerCommunity
CompanyTerms of servicePrivacy policy
*/}
>
)
}
function SocialIconX(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
)
}
function SocialIconFacebook(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
)
}
function SocialIconLinkedIn(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
)
}
// Social links currently not in use.
function Copyright() {
return (
);
};
export const PreviewMessage = memo(
PurePreviewMessage,
(prevProps, nextProps) => {
if (prevProps.status !== nextProps.status) return false;
if (prevProps.message.annotations !== nextProps.message.annotations)
return false;
// if (prevProps.message.content !== nextProps.message.content) return false;
if (!equal(prevProps.message.parts, nextProps.message.parts)) return false;
return true;
},
);
================================================
FILE: apps/web/src/components/playground/project-info.tsx
================================================
import { motion } from "motion/react";
import { VercelIcon } from "@/components/playground/icons";
import { ComputerIcon } from "lucide-react";
import Link from "next/link";
import { AppLogo } from "../shared/app-logo";
export const ProjectInfo = () => {
return (
+
Cyberdesk Agent
This demo showcases a Computer Agent built with the{" "}
Anthropic Claude Sonnet 3.7
and Cyberdesk.
{" "}
The code for this demo is open source on{" "}
GitHub
.
);
};
const StyledLink = ({
children,
href,
}: {
children: React.ReactNode;
href: string;
}) => {
return (
{children}
);
};
// const Code = ({ text }: { text: string }) => {
// return {text};
// };
================================================
FILE: apps/web/src/components/playground/prompt-suggestions.tsx
================================================
import { ArrowUpRight } from "lucide-react";
import { Button } from "@/components/ui/button";
const suggestions = [
{
text: "Create a website about big floppas",
prompt: "Create a website about big floppas using Next.js, Tailwind CSS, and TypeScript",
},
{
text: "Create a new text file",
prompt: "Open a text editor and create a new file called notes.txt and write 'let's go cyberdesk!'",
},
{
text: "Research the latest trends in AI and write a report",
prompt: "Research the latest trends in AI and write a report",
},
];
export const PromptSuggestions = ({
submitPrompt,
disabled,
}: {
submitPrompt: (prompt: string) => void;
disabled: boolean;
}) => {
return (
);
}
================================================
FILE: apps/web/src/components/stripe/client-pricing-cards.tsx
================================================
"use client";
import { Container } from '@/components/container';
import { Gradient } from '@/components/gradient';
import type { User } from '@supabase/supabase-js';
import type { Profile } from '@/types/database';
import { PricingCard } from './client-pricing-card';
import { useEffect, useState } from 'react';
import { supabase } from '@/utils/supabase/client';
import { Subheading } from '@/components/text';
import CONFIG from '../../../config';
// Define the tier type
interface Tier {
name: string;
slug: string;
description: string;
priceMonthly: number;
href: string;
highlights: { description: string; disabled?: boolean }[];
features: { section: string; name: string; value: string | number | boolean; }[];
}
export default function ClientPricingCards({
tiers,
user,
profile
}: {
tiers: Tier[];
user: User | null;
profile: Profile | null;
}) {
const [activeSubscriptionsCount, setActiveSubscriptionsCount] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const SUBSCRIPTION_LIMIT = CONFIG.subscriptionLimit;
useEffect(() => {
// Only check subscription count for logged-in users
if (user) {
async function fetchActiveSubscriptions() {
try {
setIsLoading(true);
const { count, error } = await supabase
.from('profiles')
.select('*', { count: 'exact', head: true })
.eq('subscription_status', 'active');
if (error) {
console.error('Error fetching active subscriptions:', error);
} else {
setActiveSubscriptionsCount(count);
}
} catch (err) {
console.error('Error in fetchActiveSubscriptions:', err);
} finally {
setIsLoading(false);
}
}
fetchActiveSubscriptions();
}
}, [user]);
const isSoldOut = user && activeSubscriptionsCount !== null && activeSubscriptionsCount >= SUBSCRIPTION_LIMIT;
// Custom component for sold-out tier card
const SoldOutPricingCard = ({ tier }: { tier: Tier }) => (
{tier.name}
{tier.description}
${tier.priceMonthly}
USD
per month
Thank you for your interest!
We're sold out but we're working on adding more capacity!
You get access to:
{tier.highlights.map((props, featureIndex) => (
{props.disabled && Not included:}
{props.description}
))}
);
return (
{tiers.map((tier, tierIndex) => {
// If user is logged in and subscriptions are sold out, show sold out message
if (isSoldOut) {
return ;
}
// Otherwise show normal pricing card
return (
);
})}
);
}
================================================
FILE: apps/web/src/components/stripe/payment-success.tsx
================================================
import { Button } from '@/components/button';
import { Link } from '@/components/link';
import { CheckIcon } from '@heroicons/react/24/outline';
interface PaymentSuccessProps {
className?: string;
}
export function PaymentSuccess({ className }: PaymentSuccessProps) {
return (
Payment Successful!
Thank you for your subscription. Your account has been upgraded and you now have access to all features.
)
}
================================================
FILE: apps/web/src/sanity/client.ts
================================================
import { createClient, type QueryParams } from 'next-sanity'
import { apiVersion, dataset, projectId } from './env'
const isDevelopment = process.env.NODE_ENV === 'development'
export const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: isDevelopment ? false : true,
})
export async function sanityFetch({
query,
params = {},
revalidate = 60,
tags = [],
}: {
query: QueryString
params?: QueryParams
revalidate?: number | false
tags?: string[]
}) {
return client.fetch(query, params, {
next: {
revalidate: isDevelopment || tags.length ? false : revalidate,
tags,
},
})
}
================================================
FILE: apps/web/src/sanity/env.ts
================================================
export const apiVersion =
process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2024-07-25'
export const dataset = assertValue(
process.env.NEXT_PUBLIC_SANITY_DATASET,
'Missing environment variable: NEXT_PUBLIC_SANITY_DATASET',
)
export const projectId = assertValue(
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
'Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID',
)
function assertValue(v: T | undefined, errorMessage: string): T {
if (v === undefined) {
throw new Error(errorMessage)
}
return v
}
================================================
FILE: apps/web/src/sanity/image.ts
================================================
import createImageUrlBuilder from '@sanity/image-url'
import type { SanityImageSource } from '@sanity/image-url/lib/types/types'
import { dataset, projectId } from './env'
const builder = createImageUrlBuilder({ projectId, dataset })
export function image(source: SanityImageSource) {
return builder.image(source).auto('format')
}
================================================
FILE: apps/web/src/sanity/queries.ts
================================================
import { defineQuery } from 'next-sanity'
import { sanityFetch } from './client'
const TOTAL_POSTS_QUERY = defineQuery(/* groq */ `count(*[
_type == "post"
&& defined(slug.current)
&& (isFeatured != true || defined($category))
&& select(defined($category) => $category in categories[]->slug.current, true)
])`)
export async function getPostsCount(category?: string) {
return await sanityFetch({
query: TOTAL_POSTS_QUERY,
params: { category: category ?? null },
})
}
const POSTS_QUERY = defineQuery(/* groq */ `*[
_type == "post"
&& defined(slug.current)
&& (isFeatured != true || defined($category))
&& select(defined($category) => $category in categories[]->slug.current, true)
]|order(publishedAt desc)[$startIndex...$endIndex]{
title,
"slug": slug.current,
publishedAt,
excerpt,
author->{
name,
image,
},
}`)
export async function getPosts(
startIndex: number,
endIndex: number,
category?: string,
) {
return await sanityFetch({
query: POSTS_QUERY,
params: {
startIndex,
endIndex,
category: category ?? null,
},
})
}
const FEATURED_POSTS_QUERY = defineQuery(/* groq */ `*[
_type == "post"
&& isFeatured == true
&& defined(slug.current)
]|order(publishedAt desc)[0...$quantity]{
title,
"slug": slug.current,
publishedAt,
mainImage,
excerpt,
author->{
name,
image,
},
}`)
export async function getFeaturedPosts(quantity: number) {
return await sanityFetch({
query: FEATURED_POSTS_QUERY,
params: { quantity },
})
}
const FEED_POSTS_QUERY = defineQuery(/* groq */ `*[
_type == "post"
&& defined(slug.current)
]|order(isFeatured, publishedAt desc){
title,
"slug": slug.current,
publishedAt,
mainImage,
excerpt,
author->{
name,
},
}`)
export async function getPostsForFeed() {
return await sanityFetch({
query: FEED_POSTS_QUERY,
})
}
const POST_QUERY = defineQuery(/* groq */ `*[
_type == "post"
&& slug.current == $slug
][0]{
publishedAt,
title,
mainImage,
excerpt,
body,
author->{
name,
image,
},
categories[]->{
title,
"slug": slug.current,
}
}
`)
export async function getPost(slug: string) {
return await sanityFetch({
query: POST_QUERY,
params: { slug },
})
}
const CATEGORIES_QUERY = defineQuery(/* groq */ `*[
_type == "category"
&& count(*[_type == "post" && defined(slug.current) && ^._id in categories[]._ref]) > 0
]|order(title asc){
title,
"slug": slug.current,
}`)
export async function getCategories() {
return await sanityFetch({
query: CATEGORIES_QUERY,
})
}
================================================
FILE: apps/web/src/sanity/schema.ts
================================================
import type { SchemaTypeDefinition } from 'sanity'
import { authorType } from './types/author'
import { blockContentType } from './types/block-content'
import { categoryType } from './types/category'
import { postType } from './types/post'
export const schema: { types: SchemaTypeDefinition[] } = {
types: [blockContentType, categoryType, postType, authorType],
}
================================================
FILE: apps/web/src/sanity/types/author.ts
================================================
import { UserIcon } from '@heroicons/react/16/solid'
import { defineField, defineType } from 'sanity'
export const authorType = defineType({
name: 'author',
title: 'Author',
type: 'document',
icon: UserIcon,
fields: [
defineField({
name: 'name',
type: 'string',
}),
defineField({
name: 'slug',
type: 'slug',
options: {
source: 'name',
maxLength: 96,
},
}),
defineField({
name: 'image',
type: 'image',
options: {
hotspot: true,
},
}),
],
preview: {
select: {
title: 'name',
media: 'image',
},
},
})
================================================
FILE: apps/web/src/sanity/types/block-content.ts
================================================
import { ImageIcon } from '@sanity/icons'
import { defineArrayMember, defineType } from 'sanity'
export const blockContentType = defineType({
title: 'Block Content',
name: 'blockContent',
type: 'array',
of: [
defineArrayMember({
type: 'block',
styles: [
{ title: 'Normal', value: 'normal' },
{ title: 'H2', value: 'h2' },
{ title: 'H3', value: 'h3' },
{ title: 'Quote', value: 'blockquote' },
],
marks: {
decorators: [
{ title: 'Strong', value: 'strong' },
{ title: 'Emphasis', value: 'em' },
{ title: 'Code', value: 'code' },
],
annotations: [
{
title: 'URL',
name: 'link',
type: 'object',
fields: [
{
title: 'URL',
name: 'href',
type: 'url',
},
],
},
],
},
}),
defineArrayMember({
title: 'Separator',
name: 'separator',
type: 'object',
fields: [
{
name: 'style',
title: 'Style',
type: 'string',
options: {
list: [
{ title: 'Line', value: 'line' },
{ title: 'Space', value: 'space' },
],
},
},
],
}),
defineArrayMember({
type: 'image',
icon: ImageIcon,
options: { hotspot: true },
fields: [
{
name: 'alt',
type: 'string',
title: 'Alternative Text',
},
],
}),
],
})
================================================
FILE: apps/web/src/sanity/types/category.ts
================================================
import { TagIcon } from '@heroicons/react/16/solid'
import { defineField, defineType } from 'sanity'
export const categoryType = defineType({
name: 'category',
type: 'document',
icon: TagIcon,
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'slug',
type: 'slug',
options: {
source: 'title',
},
}),
],
})
================================================
FILE: apps/web/src/sanity/types/post.ts
================================================
import { DocumentIcon } from '@heroicons/react/16/solid'
import { groq } from 'next-sanity'
import { defineField, defineType } from 'sanity'
import { apiVersion } from '../env'
export const postType = defineType({
name: 'post',
title: 'Post',
type: 'document',
icon: DocumentIcon,
fields: [
defineField({
name: 'title',
type: 'string',
validation: (Rule) => Rule.required(),
}),
defineField({
name: 'slug',
type: 'slug',
options: {
source: 'title',
},
validation: (Rule) =>
Rule.required().error('A slug is required for the post URL.'),
}),
defineField({
name: 'publishedAt',
type: 'datetime',
validation: (Rule) =>
Rule.required().error(
'A publication date is required for ordering posts.',
),
}),
defineField({
name: 'isFeatured',
type: 'boolean',
initialValue: false,
validation: (Rule) =>
Rule.custom(async (isFeatured, { getClient }) => {
if (isFeatured !== true) {
return true
}
const featuredPosts = await getClient({ apiVersion })
.withConfig({ perspective: 'previewDrafts' })
.fetch(
groq`count(*[_type == 'post' && isFeatured == true])`,
)
return featuredPosts > 3
? 'Only 3 posts can be featured at a time.'
: true
}),
}),
defineField({
name: 'author',
type: 'reference',
to: { type: 'author' },
}),
defineField({
name: 'mainImage',
type: 'image',
options: {
hotspot: true,
},
fields: [
{
name: 'alt',
type: 'string',
title: 'Alternative text',
},
],
}),
defineField({
name: 'categories',
type: 'array',
of: [{ type: 'reference', to: { type: 'category' } }],
}),
defineField({
name: 'excerpt',
type: 'text',
rows: 3,
}),
defineField({
name: 'body',
type: 'blockContent',
}),
],
preview: {
select: {
title: 'title',
media: 'mainImage',
author: 'author.name',
isFeatured: 'isFeatured',
},
prepare({ title, author, media, isFeatured }) {
return {
title,
subtitle: [isFeatured && 'Featured', author && `By ${author}`]
.filter(Boolean)
.join(' | '),
media,
}
},
},
orderings: [
{
name: 'isFeaturedAndPublishedAtDesc',
title: 'Featured & Latest Published',
by: [
{ field: 'isFeatured', direction: 'desc' },
{ field: 'publishedAt', direction: 'desc' },
],
},
],
})
================================================
FILE: apps/web/src/sanity/types.ts
================================================
/**
* ---------------------------------------------------------------------------------
* This file has been generated by Sanity TypeGen.
* Command: `sanity typegen generate`
*
* Any modifications made directly to this file will be overwritten the next time
* the TypeScript definitions are generated. Please make changes to the Sanity
* schema definitions and/or GROQ queries if you need to update these types.
*
* For more information on how to use Sanity TypeGen, visit the official documentation:
* https://www.sanity.io/docs/sanity-typegen
* ---------------------------------------------------------------------------------
*/
// Source: schema.json
export type SanityImagePaletteSwatch = {
_type: 'sanity.imagePaletteSwatch'
background?: string
foreground?: string
population?: number
title?: string
}
export type SanityImagePalette = {
_type: 'sanity.imagePalette'
darkMuted?: SanityImagePaletteSwatch
lightVibrant?: SanityImagePaletteSwatch
darkVibrant?: SanityImagePaletteSwatch
vibrant?: SanityImagePaletteSwatch
dominant?: SanityImagePaletteSwatch
lightMuted?: SanityImagePaletteSwatch
muted?: SanityImagePaletteSwatch
}
export type SanityImageDimensions = {
_type: 'sanity.imageDimensions'
height?: number
width?: number
aspectRatio?: number
}
export type SanityFileAsset = {
_id: string
_type: 'sanity.fileAsset'
_createdAt: string
_updatedAt: string
_rev: string
originalFilename?: string
label?: string
title?: string
description?: string
altText?: string
sha1hash?: string
extension?: string
mimeType?: string
size?: number
assetId?: string
uploadId?: string
path?: string
url?: string
source?: SanityAssetSourceData
}
export type Geopoint = {
_type: 'geopoint'
lat?: number
lng?: number
alt?: number
}
export type Post = {
_id: string
_type: 'post'
_createdAt: string
_updatedAt: string
_rev: string
title?: string
slug?: Slug
publishedAt?: string
isFeatured?: boolean
author?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'author'
}
mainImage?: {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
alt?: string
_type: 'image'
}
categories?: Array<{
_ref: string
_type: 'reference'
_weak?: boolean
_key: string
[internalGroqTypeReferenceTo]?: 'category'
}>
excerpt?: string
body?: Array<
| {
children?: Array<{
marks?: Array
text?: string
_type: 'span'
_key: string
}>
style?: 'normal' | 'h2' | 'h3' | 'blockquote'
listItem?: 'bullet' | 'number'
markDefs?: Array<{
href?: string
_type: 'link'
_key: string
}>
level?: number
_type: 'block'
_key: string
}
| {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
alt?: string
_type: 'image'
_key: string
}
>
}
export type Author = {
_id: string
_type: 'author'
_createdAt: string
_updatedAt: string
_rev: string
name?: string
slug?: Slug
image?: {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
_type: 'image'
}
}
export type Category = {
_id: string
_type: 'category'
_createdAt: string
_updatedAt: string
_rev: string
title?: string
slug?: Slug
}
export type Slug = {
_type: 'slug'
current?: string
source?: string
}
export type BlockContent = Array<
| {
children?: Array<{
marks?: Array
text?: string
_type: 'span'
_key: string
}>
style?: 'normal' | 'h2' | 'h3' | 'blockquote'
listItem?: 'bullet' | 'number'
markDefs?: Array<{
href?: string
_type: 'link'
_key: string
}>
level?: number
_type: 'block'
_key: string
}
| {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
alt?: string
_type: 'image'
_key: string
}
>
export type SanityImageCrop = {
_type: 'sanity.imageCrop'
top?: number
bottom?: number
left?: number
right?: number
}
export type SanityImageHotspot = {
_type: 'sanity.imageHotspot'
x?: number
y?: number
height?: number
width?: number
}
export type SanityImageAsset = {
_id: string
_type: 'sanity.imageAsset'
_createdAt: string
_updatedAt: string
_rev: string
originalFilename?: string
label?: string
title?: string
description?: string
altText?: string
sha1hash?: string
extension?: string
mimeType?: string
size?: number
assetId?: string
uploadId?: string
path?: string
url?: string
metadata?: SanityImageMetadata
source?: SanityAssetSourceData
}
export type SanityAssetSourceData = {
_type: 'sanity.assetSourceData'
name?: string
id?: string
url?: string
}
export type SanityImageMetadata = {
_type: 'sanity.imageMetadata'
location?: Geopoint
dimensions?: SanityImageDimensions
palette?: SanityImagePalette
lqip?: string
blurHash?: string
hasAlpha?: boolean
isOpaque?: boolean
}
export type AllSanitySchemaTypes =
| SanityImagePaletteSwatch
| SanityImagePalette
| SanityImageDimensions
| SanityFileAsset
| Geopoint
| Post
| Author
| Category
| Slug
| BlockContent
| SanityImageCrop
| SanityImageHotspot
| SanityImageAsset
| SanityAssetSourceData
| SanityImageMetadata
export declare const internalGroqTypeReferenceTo: unique symbol
// Source: ./src/sanity/queries.ts
// Variable: TOTAL_POSTS_QUERY
// Query: count(*[ _type == "post" && defined(slug.current) && (isFeatured != true || defined($category)) && select(defined($category) => $category in categories[]->slug.current, true)])
export type TOTAL_POSTS_QUERYResult = number
// Variable: POSTS_QUERY
// Query: *[ _type == "post" && defined(slug.current) && (isFeatured != true || defined($category)) && select(defined($category) => $category in categories[]->slug.current, true)]|order(publishedAt desc)[$startIndex...$endIndex]{ title, "slug": slug.current, publishedAt, excerpt, author->{ name, image, },}
export type POSTS_QUERYResult = Array<{
title: string | null
slug: string | null
publishedAt: string | null
excerpt: string | null
author: {
name: string | null
image: {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
_type: 'image'
} | null
} | null
}>
// Variable: FEATURED_POSTS_QUERY
// Query: *[ _type == "post" && isFeatured == true && defined(slug.current)]|order(publishedAt desc)[0...$quantity]{ title, "slug": slug.current, publishedAt, mainImage, excerpt, author->{ name, image, },}
export type FEATURED_POSTS_QUERYResult = Array<{
title: string | null
slug: string | null
publishedAt: string | null
mainImage: {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
alt?: string
_type: 'image'
} | null
excerpt: string | null
author: {
name: string | null
image: {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
_type: 'image'
} | null
} | null
}>
// Variable: FEED_POSTS_QUERY
// Query: *[ _type == "post" && defined(slug.current)]|order(isFeatured, publishedAt desc){ title, "slug": slug.current, publishedAt, mainImage, excerpt, author->{ name, },}
export type FEED_POSTS_QUERYResult = Array<{
title: string | null
slug: string | null
publishedAt: string | null
mainImage: {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
alt?: string
_type: 'image'
} | null
excerpt: string | null
author: {
name: string | null
} | null
}>
// Variable: POST_QUERY
// Query: *[ _type == "post" && slug.current == $slug][0]{ publishedAt, title, mainImage, excerpt, body, author->{ name, image, }, categories[]->{ title, "slug": slug.current, }}
export type POST_QUERYResult = {
publishedAt: string | null
title: string | null
mainImage: {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
alt?: string
_type: 'image'
} | null
excerpt: string | null
body: Array<
| {
children?: Array<{
marks?: Array
text?: string
_type: 'span'
_key: string
}>
style?: 'blockquote' | 'h2' | 'h3' | 'normal'
listItem?: 'bullet' | 'number'
markDefs?: Array<{
href?: string
_type: 'link'
_key: string
}>
level?: number
_type: 'block'
_key: string
}
| {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
alt?: string
_type: 'image'
_key: string
}
> | null
author: {
name: string | null
image: {
asset?: {
_ref: string
_type: 'reference'
_weak?: boolean
[internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
}
hotspot?: SanityImageHotspot
crop?: SanityImageCrop
_type: 'image'
} | null
} | null
categories: Array<{
title: string | null
slug: string | null
}> | null
} | null
// Variable: CATEGORIES_QUERY
// Query: *[ _type == "category" && count(*[_type == "post" && defined(slug.current) && ^._id in categories[]._ref]) > 0]|order(title asc){ title, "slug": slug.current,}
export type CATEGORIES_QUERYResult = Array<{
title: string | null
slug: string | null
}>
// Query TypeMap
import '@sanity/client'
declare module '@sanity/client' {
interface SanityQueries {
'count(*[\n _type == "post"\n && defined(slug.current)\n && (isFeatured != true || defined($category))\n && select(defined($category) => $category in categories[]->slug.current, true)\n])': TOTAL_POSTS_QUERYResult
'*[\n _type == "post"\n && defined(slug.current)\n && (isFeatured != true || defined($category))\n && select(defined($category) => $category in categories[]->slug.current, true)\n]|order(publishedAt desc)[$startIndex...$endIndex]{\n title,\n "slug": slug.current,\n publishedAt,\n excerpt,\n author->{\n name,\n image,\n },\n}': POSTS_QUERYResult
'*[\n _type == "post"\n && isFeatured == true\n && defined(slug.current)\n]|order(publishedAt desc)[0...$quantity]{\n title,\n "slug": slug.current,\n publishedAt,\n mainImage,\n excerpt,\n author->{\n name,\n image,\n },\n}': FEATURED_POSTS_QUERYResult
'*[\n _type == "post"\n && defined(slug.current)\n]|order(isFeatured, publishedAt desc){\n title,\n "slug": slug.current,\n publishedAt,\n mainImage,\n excerpt,\n author->{\n name,\n },\n}': FEED_POSTS_QUERYResult
'*[\n _type == "post"\n && slug.current == $slug\n][0]{\n publishedAt,\n title,\n mainImage,\n excerpt,\n body,\n author->{\n name,\n image,\n },\n categories[]->{\n title,\n "slug": slug.current,\n }\n}\n': POST_QUERYResult
'*[\n _type == "category"\n && count(*[_type == "post" && defined(slug.current) && ^._id in categories[]._ref]) > 0\n]|order(title asc){\n title,\n "slug": slug.current,\n}': CATEGORIES_QUERYResult
}
}
================================================
FILE: apps/web/src/styles/tailwind.css
================================================
@import 'tailwindcss';
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans: Switzer, system-ui, sans-serif;
--radius-4xl: 2rem;
}
@keyframes move-x {
0% {
transform: translateX(var(--move-x-from));
}
100% {
transform: translateX(var(--move-x-to));
}
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
.b {
@apply border-2 border-red-500;
}
}
================================================
FILE: apps/web/src/types/database.ts
================================================
export interface Profile {
id: string;
unkey_key_id?: string;
stripe_customer_id?: string;
stripe_subscription_id?: string;
current_period_end?: Date;
subscription_status?: string;
plan_id?: string;
cancel_at_period_end?: boolean;
created_at?: Date;
updated_at?: Date;
}
// This interface matches the backend schema for cyberdesk_instances (see apps/api/src/db/schema.ts)
export interface CyberdeskInstance {
id: string;
user_id: string;
created_at: string; // ISO string from DB
updated_at?: string | null; // nullable
status: 'pending' | 'running' | 'terminated' | 'error';
timeout_at: string; // ISO string from DB
stream_url?: string | null;
}
================================================
FILE: apps/web/src/utils/misc-utils.ts
================================================
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
================================================
FILE: apps/web/src/utils/playground/cyberdesk-client.ts
================================================
import { createCyberdeskClient } from "cyberdesk";
const client = createCyberdeskClient({
apiKey: process.env.CYBERDESK_API_KEY || '',
baseUrl: process.env.CYBERDESK_API_BASE_URL || 'https://api.cyberdesk.io'
});
export default client;
================================================
FILE: apps/web/src/utils/playground/misc-demo-utils.ts
================================================
import type { UIMessage } from "ai";
export const ABORTED = "User aborted";
export const prunedMessages = (messages: UIMessage[]): UIMessage[] => {
if (messages.at(-1)?.role === "assistant") {
return messages;
}
return messages.map((message) => {
// check if last message part is a tool invocation in a call state, then append a part with the tool result
message.parts = message.parts.map((part) => {
if (part.type === "tool-invocation") {
if (
part.toolInvocation.toolName === "computer" &&
part.toolInvocation.args.action === "screenshot"
) {
return {
...part,
toolInvocation: {
...part.toolInvocation,
result: {
type: "text",
text: "Image redacted to save input tokens",
},
},
};
}
return part;
}
return part;
});
return message;
});
};
================================================
FILE: apps/web/src/utils/playground/server-actions.ts
================================================
"use server";
import client from "@/utils/playground/cyberdesk-client";
export const getDesktopURL = async (id?: string) => {
if (!id) throw new Error("Sandbox ID required for getDesktopURL");
try {
const response = await client.getDesktop({
path: {
id,
},
});
const streamUrl = response.data?.stream_url;
return { streamUrl, id };
} catch (error) {
console.error("Error in getDesktopURL:", error);
throw error;
}
};
export const startDesktop = async () => {
try {
const response = await client.launchDesktop({
body: {
timeout_ms: 86400000,
}
});
if (!response.data || !response.data.id) {
throw new Error("Failed to start desktop: No ID returned from API");
}
return response.data;
} catch (error) {
console.error("Error in startDesktop:", error);
throw error;
}
};
export const killDesktop = async (id?: string) => {
if (!id) throw new Error("Sandbox ID required for killDesktop");
try {
const response = await client.terminateDesktop({
path: {
id,
},
});
if (!response.data) {
throw new Error("Failed to kill desktop: No data returned from API");
}
return response.data;
} catch (error) {
console.error("Error in killDesktop:", error);
throw error;
}
};
================================================
FILE: apps/web/src/utils/playground/tools.ts
================================================
import { anthropic } from "@ai-sdk/anthropic";
import client from "@/utils/playground/cyberdesk-client";
const wait = async (seconds: number) => {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
};
export const resolution = { x: 1024, y: 768 };
export const computerTool = (sandboxId: string) =>
anthropic.tools.computer_20250124({
displayWidthPx: resolution.x,
displayHeightPx: resolution.y,
displayNumber: 1,
execute: async ({
action,
coordinate,
text,
duration,
scroll_amount,
scroll_direction,
start_coordinate,
}) => {
// console.log("action", action);
// console.log("coordinate", coordinate);
// console.log("text", text);
// console.log("duration", duration);
// console.log("scroll_amount", scroll_amount);
// console.log("scroll_direction", scroll_direction);
// console.log("start_coordinate", start_coordinate);
switch (action) {
case "screenshot": {
const response = await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "screenshot",
},
});
const base64Image = response.data?.base64_image;
if (!base64Image) throw new Error("No image data received");
return {
type: "image" as const,
data: base64Image,
};
}
case "wait": {
if (!duration) throw new Error("Duration required for wait action");
const actualDuration = Math.min(duration, 2);
await wait(actualDuration);
return {
type: "text" as const,
text: `Waited for ${actualDuration} seconds`,
};
}
case "left_click": {
if (!coordinate)
throw new Error("Coordinate required for left click action");
const [x, y] = coordinate;
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "click_mouse",
x,
y,
num_of_clicks: 1,
button: "left",
click_type: "click",
},
});
return {
type: "text" as const,
text: `Left clicked at ${x}, ${y}`,
};
}
case "double_click": {
if (!coordinate)
throw new Error("Coordinate required for double click action");
const [x, y] = coordinate;
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "click_mouse",
x,
y,
num_of_clicks: 2,
},
});
return {
type: "text" as const,
text: `Double clicked at ${x}, ${y}`,
};
}
case "triple_click": {
if (!coordinate)
throw new Error("Coordinate required for triple click action");
const [x, y] = coordinate;
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "click_mouse",
x,
y,
num_of_clicks: 3,
},
});
return {
type: "text" as const,
text: `Triple clicked at ${x}, ${y}`,
};
}
case "right_click": {
if (!coordinate)
throw new Error("Coordinate required for right click action");
const [x, y] = coordinate;
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "click_mouse",
x,
y,
num_of_clicks: 1,
button: "right",
click_type: "click",
},
});
return {
type: "text" as const,
text: `Right clicked at ${x}, ${y}`,
};
}
case "mouse_move": {
if (!coordinate)
throw new Error("Coordinate required for mouse move action");
const [x, y] = coordinate;
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "move_mouse",
x,
y,
},
});
return {
type: "text" as const,
text: `Moved mouse to ${x}, ${y}`,
};
}
case "type": {
if (!text) throw new Error("Text required for type action");
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "type",
text,
}
})
return { type: "text" as const, text: `Typed: ${text}` };
}
case "key": {
if (!text) throw new Error("Key required for key action");
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "press_keys",
keys: text
},
});
return { type: "text" as const, text: `Pressed key: ${text}` };
}
case "scroll": {
if (!scroll_direction)
throw new Error("Scroll direction required for scroll action");
if (!scroll_amount)
throw new Error("Scroll amount required for scroll action");
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "scroll",
direction: scroll_direction,
amount: scroll_amount,
},
});
return { type: "text" as const, text: `Scrolled ${text}` };
}
case "left_click_drag": {
if (!start_coordinate || !coordinate)
throw new Error("Coordinate required for mouse move action");
const [startX, startY] = start_coordinate;
const [endX, endY] = coordinate;
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "drag_mouse",
start: { x: startX, y: startY },
end: { x: endX, y: endY },
},
});
return {
type: "text" as const,
text: `Dragged mouse from ${startX}, ${startY} to ${endX}, ${endY}`,
};
}
case "cursor_position": {
const response = await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "get_cursor_position",
},
});
if (!response.data?.output) throw new Error("No output received");
return {
type: "text" as const,
text: `Cursor position data: ${response.data?.output}`,
};
}
case "hold_key": {
if (!text) throw new Error("Key required for hold key action");
if (!duration) throw new Error("Duration required for hold key action");
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "press_keys",
keys: text,
key_action_type: "down",
},
});
await wait(duration);
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "press_keys",
keys: text,
key_action_type: "up",
},
});
return { type: "text" as const, text: `Held key ${text} for ${duration} seconds` };
}
case "left_mouse_down": {
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "click_mouse",
button: "left",
click_type: "down",
},
});
return { type: "text" as const, text: `Left mouse button down` };
}
case "left_mouse_up": {
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "click_mouse",
button: "left",
click_type: "up",
},
});
return { type: "text" as const, text: `Left mouse button up` };
}
case "middle_click": {
await client.executeComputerAction({
path: {
id: sandboxId,
},
body: {
type: "click_mouse",
button: "middle",
click_type: "click",
},
});
return { type: "text" as const, text: `Middle mouse button clicked` };
}
default:
throw new Error(`Unsupported action: ${action}`);
}
},
experimental_toToolResultContent(result) {
if (typeof result === "string") {
return [{ type: "text", text: result }];
}
if (result.type === "image" && result.data) {
return [
{
type: "image",
data: result.data,
mimeType: "image/jpeg",
},
];
}
if (result.type === "text" && result.text) {
return [{ type: "text", text: result.text }];
}
throw new Error("Invalid result format");
},
});
export const bashTool = (sandboxId?: string) =>
anthropic.tools.bash_20250124({
execute: async ({ command }) => {
if (!sandboxId) throw new Error("Sandbox ID required for bash action");
try {
const result = await client.executeBashAction({
path: {
id: sandboxId,
},
body: {
command,
},
});
if (result.data?.error) {
throw new Error(result.data.error);
}
return (
result.data?.output || "(Command executed successfully with no output)"
);
} catch (error) {
console.error("Bash command failed:", error);
if (error instanceof Error) {
return `Error executing command: ${error.message}`;
} else {
return `Error executing command: ${String(error)}`;
}
}
},
});
================================================
FILE: apps/web/src/utils/playground/use-scroll-to-bottom.ts
================================================
import { useEffect, useRef, type RefObject } from 'react';
export function useScrollToBottom(): [
RefObject,
RefObject,
] {
const containerRef = useRef(null);
const endRef = useRef(null);
useEffect(() => {
const container = containerRef.current;
if (container) {
const observer = new MutationObserver(() => {
// Directly set scrollTop to scrollHeight to scroll to the bottom of the container.
// This manipulation is localized to the 'container' element.
container.scrollTop = container.scrollHeight;
});
// Observe changes that would typically require scrolling in a chat interface
observer.observe(container, {
childList: true, // For new messages being added
subtree: true, // For changes within messages (e.g., streaming content updating)
characterData: true // Specifically for text changes during streaming
});
// Initial scroll to bottom in case there's pre-existing content
// that might not trigger an immediate mutation but should be scrolled past.
// Use a microtask to ensure layout has been calculated.
queueMicrotask(() => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
});
return () => observer.disconnect();
}
}, []); // Effect runs once on mount
return [containerRef, endRef];
}
================================================
FILE: apps/web/src/utils/posthog/posthog.ts
================================================
import { PostHog } from "posthog-node"
export default function PostHogClient() {
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
host: "https://us.i.posthog.com",
flushAt: 1,
flushInterval: 0,
})
return posthogClient
}
================================================
FILE: apps/web/src/utils/stripe/stripe-server.ts
================================================
import Stripe from 'stripe';
// This file should only be imported in server components or API routes
if (typeof window !== 'undefined') {
throw new Error('This file should only be imported in server components or API routes');
}
// Initialize Stripe with the secret key from environment variables
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2025-02-24.acacia', // Use the latest API version
});
// Price ID for the Pro subscription tier
export const STRIPE_PRICE_ID = process.env.STRIPE_PRICE_ID_PRO || '';
================================================
FILE: apps/web/src/utils/stripe/stripe.ts
================================================
// Helper function for client-side use
export const formatPrice = (price: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(price / 100);
};
================================================
FILE: apps/web/src/utils/stripe/tiers.ts
================================================
// Define the tier type
export interface Tier {
name: string;
slug: string;
description: string;
priceMonthly: number;
href: string;
highlights: { description: string; disabled?: boolean }[];
features: {
section: string;
name: string;
value: string | number | boolean;
}[];
}
// Define the tiers configuration
export const tiers: Tier[] = [
{
name: 'Pro',
slug: 'pro',
description: 'Get started with deploying AI computer agents on up to 20 concurrent desktops',
priceMonthly: 19.99,
href: '#',
highlights: [
{ description: '24 hour session per desktop launch' },
{ description: 'Up to 20 concurrent desktops' },
{ description: 'Unlimited desktop actions' },
// { description: 'RadiantAI integrations', disabled: true },
// { description: 'Competitor analysis', disabled: true },
],
features: [
{ section: 'Features', name: 'Desktops', value: 3 },
{ section: 'Features', name: 'Deal progress boards', value: 5 },
{ section: 'Features', name: 'Sourcing platforms', value: 'Select' },
{ section: 'Features', name: 'Contacts', value: 100 },
{ section: 'Features', name: 'AI assisted outreach', value: false },
{ section: 'Analysis', name: 'Competitor analysis', value: false },
{ section: 'Analysis', name: 'Dashboard reporting', value: false },
{ section: 'Analysis', name: 'Community insights', value: false },
{ section: 'Analysis', name: 'Performance analysis', value: false },
{ section: 'Support', name: 'Email support', value: true },
{ section: 'Support', name: '24 / 7 call center support', value: false },
{ section: 'Support', name: 'Dedicated account manager', value: false },
],
}
];
================================================
FILE: apps/web/src/utils/supabase/client.ts
================================================
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || '',
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''
)
}
// For backward compatibility with existing code
export const supabase = createClient()
================================================
FILE: apps/web/src/utils/supabase/middleware.ts
================================================
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// Do not run code between createServerClient and
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
// IMPORTANT: DO NOT REMOVE auth.getUser()
const {
data: { user },
} = await supabase.auth.getUser()
if (
!user &&
!request.nextUrl.pathname.startsWith('/login') &&
!request.nextUrl.pathname.startsWith('/auth')
) {
// no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
// IMPORTANT: You *must* return the supabaseResponse object as it is.
// If you're creating a new response object with NextResponse.next() make sure to:
// 1. Pass the request in it, like so:
// const myNewResponse = NextResponse.next({ request })
// 2. Copy over the cookies, like so:
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
// 3. Change the myNewResponse object to fit your needs, but avoid changing
// the cookies!
// 4. Finally:
// return myNewResponse
// If this is not done, you may be causing the browser and server to go out
// of sync and terminate the user's session prematurely!
return supabaseResponse
}
================================================
FILE: apps/web/src/utils/supabase/server.ts
================================================
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || '',
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '',
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}
================================================
FILE: apps/web/src/utils/supabase/supabaseClient.js
================================================
// supabaseClient.js
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || '',
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''
)
}
================================================
FILE: apps/web/src/utils/supabase/supabaseServerClient.ts
================================================
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}
================================================
FILE: apps/web/tsconfig.json
================================================
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
},
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: cyberdesk-architecture.md
================================================
# Cyberdesk Architecture
## System Overview
This document outlines the architecture for Cyberdesk,a scalable cloud desktop platform that allows users to create and interact with isolated virtual machine environments. The system is designed to support thousands of concurrent VMs while maintaining security, performance, and ease of use.
## Architecture Components
```
┌───────────────┐ ┌───────────────┐ ┌─────────────────────────────────────────┐
│ Frontend │ │ API Backend │ │ Kubernetes Cluster │
│ (Next.js on │───▶│ (Node.js │───▶│┌───────────┐┌──────────┐┌─────────────┐ │
│ Vercel) │ │ on │ ││ Gateway ││ Instance ││ Kubevirt │ │
└───────────────┘ │ Fly.io) │ ││ Service ││ Operator ││ VMs │ │
└───────────────┘ │└───────────┘└──────────┘└─────────────┘ │
└─────────────────────────────────────────┘
```
### 1. Frontend (Next.js on Vercel)
- Landing page
- Docs
- User authentication and account management
- Desktop creation and management interface
- Playground
### 2. API Backend (Node.js on Fly.io)
- RESTful API for desktop management and control (makes calls to Gateway Service)
- API key auth provided by Unkey
- Billing with Stripe
- Usage tracking and limit enforcing
- Event logging and analytics with Posthog
### 3. Kubernetes Cluster (AKS with Kubevirt)
- **Gateway Service**
- Single entry point into the cluster
- CRUD endpoints for CyberdeskInstance K8s resources
- Endpoint to route commands to running VMs
- Exposed for public access from the FastAPI backend, via K8s External Service
- **Instance Operator**
- Listens to changes to CyberdeskInstances and does all relevant business logic to create the underlying VirtualMachineInstance using Kubevirt
- Enforce timeout and compute hour credits
- Apply snapshots, etc
- **Kubevirt VMs**
- Isolated sandbox environments
- Each running the custom execD service, which accepts requests from Gateway Service
- Network isolation via Kubernetes networking
## Other Key Components
### 1. execD Service (Inside Each VM)
#### Implementation
- Custom lightweight HTTP service
- Written in FastAPI
- Runs on private-internal-ip:port inside each VM, only Gateway Service can access
- Starts automatically when VM boots
#### Features
- Command execution
- File system operations
### 2. CyberdeskInstance K8s Custom Resource Definition
#### Implementation
- YAML file that defines a desktop instance and it's parameters
- Instances of this custom resource are listened to by Instance Operator
### 3. JS / Python SDKs
#### Implementation
- Automatically generated from OpenAPI spec
### 4. Supabase Database
- Handles all database functionalities, authentication
## Scaling Considerations
### Horizontal Scaling
- Frontend: Automatic scaling via Vercel
- API Backend: Auto-scaling on Fly.io
- Gateway Service: Kubernetes HPA up to 20+ pods
- Kubernetes Cluster: Multiple node pools, auto-scaling
================================================
FILE: infra/README.md
================================================
# Cyberdesk AKS/KubeVirt Deployment Guide
This guide walks you through deploying Cyberdesk on Azure Kubernetes Service (AKS) with KubeVirt, including all required infrastructure, snapshotting, ingress, and certificate management. **Follow the steps in order for a successful deployment.**
---
## Prerequisites
- [Terraform](https://www.terraform.io/downloads.html)
- [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli)
- [kubectl](https://kubernetes.io/docs/tasks/tools/)
- [kubelogin](https://github.com/Azure/kubelogin)
- [virtctl](https://kubevirt.io/user-guide/operations/virtctl_client_tool/)
- Azure subscription with permissions
---
## 1. Authenticate and Set Up Terraform Workspaces
### 1.1 Authenticate with Azure
Before running any Terraform commands, authenticate with Azure CLI:
```bash
az login
az account set --subscription
```
Replace `` with the correct Azure subscription for your environment (this may be auto-set if you're using the Azure CLI to login).
### 1.2 Initialize Terraform
Navigate to the terraform directory and initialize:
```bash
cd ./terraform
terraform init
```
### 1.3 Using Workspaces for Dev and Prod
Terraform workspaces allow you to manage separate state for different environments (e.g., dev and prod).
#### **Create and Use the Dev Workspace**
```bash
# Create the dev workspace if it doesn't exist
terraform workspace new dev
# Switch to the dev workspace
terraform workspace select dev
# Apply the dev configuration
terraform apply -var-file="dev.tfvars" -auto-approve
```
#### **Create and Use the Prod Workspace**
```bash
# Create the prod workspace if it doesn't exist
terraform workspace new prod
# Switch to the prod workspace
terraform workspace select prod
# Apply the prod configuration
terraform apply -var-file="prod.tfvars" -auto-approve
```
- Always make sure you are in the correct workspace before running `plan` or `apply`.
- Each workspace maintains its own state, so dev and prod resources are managed separately.
---
## 2. Configure kubectl Access
After deploying your environment, configure kubectl access to your AKS cluster. You will need the resource group name and AKS cluster name, which are defined in your tfvars file for the environment you just deployed (either dev.tfvars or prod.tfvars).
- For **dev**:
- Use the values of `resource_group_name` and `aks_cluster_name` from `dev.tfvars`.
- For **prod**:
- Use the values of `resource_group_name` and `aks_cluster_name` from `prod.tfvars`.
Example:
```bash
az aks get-credentials --resource-group --name
```
Replace the placeholders with the actual values from your tfvars file.
---
## 3. Deploy KubeVirt Operator and CR
Note: From now on, you'll want to navigate to the correct folders to apply the correct YAMLs. For the most part, you'll be in the `infra/kubernetes` folder.
```bash
kubectl apply -f kubevirt-operator.yaml
kubectl apply -f kubevirt-cr.yaml
```
---
## 4. Deploy Containerized Data Importer (CDI)
CDI is required for KubeVirt features like cloning PVCs and importing disk images. This is the recommended way to create VM root disks with specific sizes.
```bash
kubectl apply -f cdi-operator.yaml
kubectl apply -f cdi-cr.yaml
# Wait for CDI pods to be ready (optional check)
kubectl wait --for=condition=Ready pod -l cdi.kubevirt.io -n cdi --timeout=300s
```
---
## 5. Apply Azure Disk Snapshot Class (REQUIRED for KubeVirt Snapshots)
```bash
kubectl apply -f azure-snapshot-class.yaml
```
This enables snapshotting for Azure disks and is required for KubeVirt VM snapshots and cloning.
---
## 6. Create the Golden VM and Snapshot (REQUIRED for Cyberdesk Operator)
> **Note:** The golden VM manifest is gitignored. Reference Notion or ask a team member for `golden-vm-deploy.yaml`
1. **Apply the Golden VM:**
```bash
kubectl apply -f golden-vm-deploy.yaml
```
2. **Wait for the VM to fully boot and complete cloud-init.**
- Check with:
```bash
# Wait for the VM to be Running
kubectl get vmi -n kubevirt
# Access the VM console (to login, reference the golden-vm-deploy.yaml file to see the credentials)
virtctl console golden-vm -n kubevirt
# Inside the VM, if there doesn't seem to be any cloud-init logs, run this and see if "status:done" is printed
cloud-init status
# Once done, exit the VM console with ctrl / cmd + ]
# Then, VNC into the VM
virtctl port-forward golden-vm 5900:5900 -n kubevirt
# Use RealVNC (download if you don't have it for free) and connect to localhost:5900
# Install any applications and set any settings you'd like in all VMs, such as DuckDuckGo for search, VSCode, etc.
# Current list:
# - DuckDuckGo as the default search engine
# - VSCode
# - Change desktop background to something nice
# - Delete browser history and cookies
```
- Once done, exit RealVNC and stop the port-forward:
3. **Stop the Golden VM:**
```bash
virtctl stop golden-vm -n kubevirt
# Wait for the VM to be fully stopped
kubectl get vm golden-vm -n kubevirt
```
4. **Create the Golden Snapshot:**
```bash
kubectl apply -f golden-vm-snapshot-request.yaml
```
- Wait for the snapshot to be ready:
```bash
kubectl get vmsnapshot -n kubevirt
```
---
## 7. Install ingress-nginx (YAML, NOT Helm)
```bash
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.1/deploy/static/provider/cloud/deploy.yaml
```
- Wait for all pods in `ingress-nginx` namespace to be Running.
```bash
kubectl get pods -n ingress-nginx
```
---
## 8. Deploy the Default Backend and Catch-All Ingress
1. **Apply the default-backend DaemonSet, Service, and catch-all Ingress:**
(See `default-backend.yaml` in this repo or request from a team member)
This ensures that Azure Load Balancer health probes are healthy for all nodes, and that unmatched traffic is routed to the default backend. If you don't do this, the Gateway will not be accessible via https.
```bash
kubectl apply -f default-backend.yaml
```
2. **Optional: Verify Azure Load Balancer health probes are healthy for all nodes** (in Azure Portal).
- They probably are, but if you're having trouble connecting to the Gateway, check this.
---
## 9. Install cert-manager (YAML, NOT Helm)
```bash
kubectl apply --validate=false -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.1/cert-manager.yaml
```
- Wait for all pods in `cert-manager` namespace to be Running.
```bash
kubectl get pods -n cert-manager
```
---
## 10. Build and Push Operator and Gateway Images
If you have made changes to the operator or gateway code, you need to rebuild and push the images with a **unique, specific tag** (e.g., using Semantic Versioning like `v1.2.3` or a Git commit SHA like `a1b2c3d4`). **Avoid using the `:latest` tag**.
### Cyberdesk Operator
**Bash:**
```bash
# Navigate to the operator directory
cd ../../services/cyberdesk-operator
# Build the docker image with a specific tag (ensure Docker daemon is running)
docker build -t cyberdesk/cyberdesk-operator:NEW_VERSION_TAG_HERE .
# Push the image to Docker Hub
docker push cyberdesk/cyberdesk-operator:NEW_VERSION_TAG_HERE
```
**PowerShell:**
```powershell
cd ../services/cyberdesk-operator
docker build -t "cyberdesk/cyberdesk-operator:NEW_VERSION_TAG_HERE" .
docker push cyberdesk/cyberdesk-operator:NEW_VERSION_TAG_HERE
```
**Important:** After building and pushing, you **must update** the `image:` field in `infra/kubernetes/cyberdesk-operator.yaml` to reference the **specific tag** you just used.
### Gateway
**Bash:**
```bash
cd ../services/gateway
docker build -t cyberdesk/gateway:NEW_GATEWAY_TAG_HERE .
docker push cyberdesk/gateway:NEW_GATEWAY_TAG_HERE
```
**PowerShell:**
```powershell
cd ../services/gateway
docker build -t "cyberdesk/gateway:NEW_GATEWAY_TAG_HERE" .
docker push cyberdesk/gateway:NEW_GATEWAY_TAG_HERE
```
**Important:** After building and pushing, you **must update** the `image:` field in `infra/kubernetes/gateway-deploy.yaml` to reference the **specific tag** you just used.
---
## 11. Deploy Cyberdesk Operator and Gateway
1. **Apply the Supabase Secret:**
(Ask a team member for `cyberdesk-secret.yaml`)
```bash
kubectl apply -f cyberdesk-secret.yaml
```
2. **Deploy the Cyberdesk Operator:**
```bash
kubectl apply -f cyberdesk-operator.yaml
```
3. **Deploy the Gateway:**
```bash
kubectl apply -f gateway-deploy.yaml
```
4. **Trigger Operator Setup:**
```bash
kubectl apply -f start-cyberdesk-operator-cr.yaml
```
---
## 12. Apply the ClusterIssuer and Gateway Ingress
Before applying your Ingress resources, you must first create the ClusterIssuer, which is defined in a separate file (`cluster-issuer.yaml`). This ClusterIssuer is required for cert-manager to issue certificates for your Ingress resources.
- **Step 1: Apply the ClusterIssuer**
```bash
kubectl apply -f cluster-issuer.yaml
```
- **Step 2: Apply the Gateway Ingress**
For **dev** (in your dev cluster):
```bash
kubectl apply -f gateway-ingress-dev.yaml
```
For **prod** (in your prod cluster):
```bash
kubectl apply -f gateway-ingress-prod.yaml
```
Wait for the Ingress to be assigned an external IP:
```bash
kubectl get ingress -n cyberdesk-system
```
---
## 13. Set Up DNS for Gateway
Once your Ingress has an external IP, set up your DNS records (Cyberdesk manages it in Cloudflare, see credentials in Notion):
- For **dev**:
Create an A record for `dev-gateway.cyberdesk.io` pointing to your dev cluster's Ingress external IP. Disable proxy on the record, if it's enabled.
- For **prod**:
Create an A record for `gateway.cyberdesk.io` pointing to your prod cluster's Ingress external IP. Disable proxy on the record, if it's enabled.
Update your DNS provider with the correct IP.
Your DNS is ready when nslookup returns the correct IP (should be relatively quick).
```bash
nslookup gateway.cyberdesk.io (or dev-gateway.cyberdesk.io)
```
---
## 14. Verify Ingress and Certificate
Check that your Ingress and certificate resources are created and progressing:
```bash
kubectl get ingress -n cyberdesk-system
kubectl get clusterissuer
kubectl get certificate -n cyberdesk-system
kubectl describe certificate -n cyberdesk-system
```
- The certificate may take a few minutes to become `READY` after DNS is set up.
- If it does not, check DNS, Ingress, and cert-manager logs for troubleshooting.
---
## 15. Initiate Virtual Machine Warm Pool
Before starting the warm pool, you must update `warm-pool.yaml` to reference the correct snapshot name for the golden VM's root disk.
1. **Get the name of the new root disk snapshot:**
```bash
kubectl get volumesnapshots -n kubevirt
```
- Look for the snapshot associated with your golden VM (it will look like `vmsnapshot--volume-rootdisk`).
2. **Edit `warm-pool.yaml`:**
- Update the `snapshot: name:` field to match the name you found above.
- Check the YAML to see if the desired amount of replicas are set. In dev, usually 1 is enough. In prod, this should probably be much, much higher.
3. **Apply the warm pool:**
```bash
kubectl apply -f warm-pool.yaml
```
## 16. Verify Everything
- Check pod status in all namespaces:
```bash
kubectl get pods -A
```
- Check if the warm pool is running:
```bash
kubectl get vms -n kubevirt # should see 1 or more warm pool VMs starting up and eventually running
```
- Check if the Gateway is accessible:
```bash
curl -k https://gateway.cyberdesk.io/healthz # (or dev-gateway.cyberdesk.io if you're in dev)
```
## 17. Delete the Golden VM
Since we've snapshotted it, we can delete it now.
```bash
kubectl delete vm golden-vm -n kubevirt
```
## 18. Local Development
- Head to /apps/api, and make sure your .env contains the correct values, but most importantly, make sure the `GATEWAY_URL` is set to the correct URL (dev-gateway.cyberdesk.io or gateway.cyberdesk.io). Note: if set to dev-gateway.cyberdesk.io, stream URL's returned from the Gateway service will be overridden to use the dev-gateway.cyberdesk.io domain.
- Run `npm run dev` to start the API.
- Head to /apps/web, and make sure your .env contains the correct values, but most importantly, make sure the `CYBERDESK_API_BASE_URL` is set to http://localhost:3001 (or whatever port your API is running on).
- Run `npm run dev` to start the web app.
You're now set to start developing! Make sure you branch off from `dev` and create a new branch for your work.
## 19. Deploying to Prod
- Make a PR to merge your changes into `dev`.
- Once approved, merge your PR into `dev`.
- Carefully plan how you will bring the changes you made into the production cluster. Make sure to switch kubeconfig to prod before you start, using `az aks get-credentials --resource-group --name `.
- Make final tests in the dev cluster to ensure everything is working as expected.
- Once you're ready, make a PR to merge your changes into `prod`.
- Once approved, merge your PR into `prod`.
- Apply the changes to the production cluster using kubectl (for example, if the Gateway has a new image, you can do kubectl rollout restart deployment gateway -n cyberdesk-system).
- Make sure corresponding changes to the developer API / web app / docs are also being pushed to live via Vercel / Fly.io.
TODO: Figure out how to orchestrate this better. Right now, there is discrepency between when cluster changes are pushed vs when the developer API / web app are pushed to live (since we have so many different hosting environments).
================================================
FILE: infra/kubernetes/azure-snapshot-class.yaml
================================================
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-azure-disk-snapshot-class
annotations:
snapshot.storage.kubernetes.io/is-default-class: "true" # Make this the default
driver: disk.csi.azure.com
deletionPolicy: Delete # Options: Delete, Retain. Delete removes the underlying snapshot when this object is deleted. Retain keeps it.
================================================
FILE: infra/kubernetes/cdi-cr.yaml
================================================
apiVersion: cdi.kubevirt.io/v1beta1
kind: CDI
metadata:
name: cdi
spec:
config:
featureGates:
- HonorWaitForFirstConsumer
imagePullPolicy: IfNotPresent
infra:
nodeSelector:
kubernetes.io/os: linux
tolerations:
- key: CriticalAddonsOnly
operator: Exists
workload:
nodeSelector:
kubernetes.io/os: linux
================================================
FILE: infra/kubernetes/cdi-operator.yaml
================================================
apiVersion: v1
kind: Namespace
metadata:
labels:
cdi.kubevirt.io: ""
name: cdi
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
name: cdis.cdi.kubevirt.io
spec:
group: cdi.kubevirt.io
names:
kind: CDI
listKind: CDIList
plural: cdis
shortNames:
- cdi
- cdis
singular: cdi
scope: Cluster
versions:
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- jsonPath: .status.phase
name: Phase
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: CDI is the CDI Operator CRD
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: CDISpec defines our specification for the CDI installation
properties:
certConfig:
description: certificate configuration
properties:
ca:
description: |-
CA configuration
CA certs are kept in the CA bundle as long as they are valid
properties:
duration:
description: The requested 'duration' (i.e. lifetime) of the
Certificate.
type: string
renewBefore:
description: |-
The amount of time before the currently issued certificate's `notAfter`
time that we will begin to attempt to renew the certificate.
type: string
type: object
client:
description: |-
Client configuration
Certs are rotated and discarded
properties:
duration:
description: The requested 'duration' (i.e. lifetime) of the
Certificate.
type: string
renewBefore:
description: |-
The amount of time before the currently issued certificate's `notAfter`
time that we will begin to attempt to renew the certificate.
type: string
type: object
server:
description: |-
Server configuration
Certs are rotated and discarded
properties:
duration:
description: The requested 'duration' (i.e. lifetime) of the
Certificate.
type: string
renewBefore:
description: |-
The amount of time before the currently issued certificate's `notAfter`
time that we will begin to attempt to renew the certificate.
type: string
type: object
type: object
cloneStrategyOverride:
description: 'Clone strategy override: should we use a host-assisted
copy even if snapshots are available?'
enum:
- copy
- snapshot
- csi-clone
type: string
config:
description: CDIConfig at CDI level
properties:
dataVolumeTTLSeconds:
description: |-
DataVolumeTTLSeconds is the time in seconds after DataVolume completion it can be garbage collected. Disabled by default.
Deprecated: Removed in v1.62.
format: int32
type: integer
featureGates:
description: FeatureGates are a list of specific enabled feature
gates
items:
type: string
type: array
filesystemOverhead:
description: FilesystemOverhead describes the space reserved for
overhead when using Filesystem volumes. A value is between 0
and 1, if not defined it is 0.055 (5.5% overhead)
properties:
global:
description: Global is how much space of a Filesystem volume
should be reserved for overhead. This value is used unless
overridden by a more specific value (per storageClass)
pattern: ^(0(?:\.\d{1,3})?|1)$
type: string
storageClass:
additionalProperties:
description: |-
Percent is a string that can only be a value between [0,1)
(Note: we actually rely on reconcile to reject invalid values)
pattern: ^(0(?:\.\d{1,3})?|1)$
type: string
description: StorageClass specifies how much space of a Filesystem
volume should be reserved for safety. The keys are the storageClass
and the values are the overhead. This value overrides the
global value
type: object
type: object
imagePullSecrets:
description: The imagePullSecrets used to pull the container images
items:
description: |-
LocalObjectReference contains enough information to let you locate the
referenced object inside the same namespace.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
TODO: Add other useful fields. apiVersion, kind, uid?
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
type: string
type: object
x-kubernetes-map-type: atomic
type: array
importProxy:
description: ImportProxy contains importer pod proxy configuration.
properties:
HTTPProxy:
description: HTTPProxy is the URL http://:@:
of the import proxy for HTTP requests. Empty means unset
and will not result in the import pod env var.
type: string
HTTPSProxy:
description: HTTPSProxy is the URL https://:@:
of the import proxy for HTTPS requests. Empty means unset
and will not result in the import pod env var.
type: string
noProxy:
description: NoProxy is a comma-separated list of hostnames
and/or CIDRs for which the proxy should not be used. Empty
means unset and will not result in the import pod env var.
type: string
trustedCAProxy:
description: "TrustedCAProxy is the name of a ConfigMap in
the cdi namespace that contains a user-provided trusted
certificate authority (CA) bundle.\nThe TrustedCAProxy ConfigMap
is consumed by the DataImportCron controller for creating
cronjobs, and by the import controller referring a copy
of the ConfigMap in the import namespace.\nHere is an example
of the ConfigMap (in yaml):\n\n\napiVersion: v1\nkind: ConfigMap\nmetadata:\n
\ name: my-ca-proxy-cm\n namespace: cdi\ndata:\n ca.pem:
|\n -----BEGIN CERTIFICATE-----\n\t ... ...\n\t -----END CERTIFICATE-----"
type: string
type: object
insecureRegistries:
description: InsecureRegistries is a list of TLS disabled registries
items:
type: string
type: array
logVerbosity:
description: LogVerbosity overrides the default verbosity level
used to initialize loggers
format: int32
type: integer
podResourceRequirements:
description: ResourceRequirements describes the compute resource
requirements.
properties:
claims:
description: |-
Claims lists the names of resources, defined in spec.resourceClaims,
that are used by this container.
This is an alpha field and requires enabling the
DynamicResourceAllocation feature gate.
This field is immutable. It can only be set for containers.
items:
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
properties:
name:
description: |-
Name must match the name of one entry in pod.spec.resourceClaims of
the Pod where this field is used. It makes that resource available
inside a container.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Limits describes the maximum amount of compute resources allowed.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
requests:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Requests describes the minimum amount of compute resources required.
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
otherwise to an implementation-defined value. Requests cannot exceed Limits.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
preallocation:
description: Preallocation controls whether storage for DataVolumes
should be allocated in advance.
type: boolean
scratchSpaceStorageClass:
description: 'Override the storage class to used for scratch space
during transfer operations. The scratch space storage class
is determined in the following order: 1. value of scratchSpaceStorageClass,
if that doesn''t exist, use the default storage class, if there
is no default storage class, use the storage class of the DataVolume,
if no storage class specified, use no storage class for scratch
space'
type: string
tlsSecurityProfile:
description: TLSSecurityProfile is used by operators to apply
cluster-wide TLS security settings to operands.
properties:
custom:
description: |-
custom is a user-defined TLS security profile. Be extremely careful using a custom
profile as invalid configurations can be catastrophic. An example custom profile
looks like this:
ciphers:
- ECDHE-ECDSA-CHACHA20-POLY1305
- ECDHE-RSA-CHACHA20-POLY1305
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES128-GCM-SHA256
minTLSVersion: VersionTLS11
nullable: true
properties:
ciphers:
description: |-
ciphers is used to specify the cipher algorithms that are negotiated
during the TLS handshake. Operators may remove entries their operands
do not support. For example, to use DES-CBC3-SHA (yaml):
ciphers:
- DES-CBC3-SHA
items:
type: string
type: array
minTLSVersion:
description: |-
minTLSVersion is used to specify the minimal version of the TLS protocol
that is negotiated during the TLS handshake. For example, to use TLS
versions 1.1, 1.2 and 1.3 (yaml):
minTLSVersion: VersionTLS11
NOTE: currently the highest minTLSVersion allowed is VersionTLS12
enum:
- VersionTLS10
- VersionTLS11
- VersionTLS12
- VersionTLS13
type: string
required:
- ciphers
- minTLSVersion
type: object
intermediate:
description: |-
intermediate is a TLS security profile based on:
https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28recommended.29
and looks like this (yaml):
ciphers:
- TLS_AES_128_GCM_SHA256
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384
- ECDHE-ECDSA-CHACHA20-POLY1305
- ECDHE-RSA-CHACHA20-POLY1305
- DHE-RSA-AES128-GCM-SHA256
- DHE-RSA-AES256-GCM-SHA384
minTLSVersion: VersionTLS12
nullable: true
type: object
modern:
description: |-
modern is a TLS security profile based on:
https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility
and looks like this (yaml):
ciphers:
- TLS_AES_128_GCM_SHA256
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
minTLSVersion: VersionTLS13
NOTE: Currently unsupported.
nullable: true
type: object
old:
description: |-
old is a TLS security profile based on:
https://wiki.mozilla.org/Security/Server_Side_TLS#Old_backward_compatibility
and looks like this (yaml):
ciphers:
- TLS_AES_128_GCM_SHA256
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384
- ECDHE-ECDSA-CHACHA20-POLY1305
- ECDHE-RSA-CHACHA20-POLY1305
- DHE-RSA-AES128-GCM-SHA256
- DHE-RSA-AES256-GCM-SHA384
- DHE-RSA-CHACHA20-POLY1305
- ECDHE-ECDSA-AES128-SHA256
- ECDHE-RSA-AES128-SHA256
- ECDHE-ECDSA-AES128-SHA
- ECDHE-RSA-AES128-SHA
- ECDHE-ECDSA-AES256-SHA384
- ECDHE-RSA-AES256-SHA384
- ECDHE-ECDSA-AES256-SHA
- ECDHE-RSA-AES256-SHA
- DHE-RSA-AES128-SHA256
- DHE-RSA-AES256-SHA256
- AES128-GCM-SHA256
- AES256-GCM-SHA384
- AES128-SHA256
- AES256-SHA256
- AES128-SHA
- AES256-SHA
- DES-CBC3-SHA
minTLSVersion: VersionTLS10
nullable: true
type: object
type:
description: |-
type is one of Old, Intermediate, Modern or Custom. Custom provides
the ability to specify individual TLS security profile parameters.
Old, Intermediate and Modern are TLS security profiles based on:
https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations
The profiles are intent based, so they may change over time as new ciphers are developed and existing ciphers
are found to be insecure. Depending on precisely which ciphers are available to a process, the list may be
reduced.
Note that the Modern profile is currently not supported because it is not
yet well adopted by common software libraries.
enum:
- Old
- Intermediate
- Modern
- Custom
type: string
type: object
uploadProxyURLOverride:
description: Override the URL used when uploading to a DataVolume
type: string
type: object
customizeComponents:
description: CustomizeComponents defines patches for components deployed
by the CDI operator.
properties:
flags:
description: Configure the value used for deployment and daemonset
resources
properties:
api:
additionalProperties:
type: string
type: object
controller:
additionalProperties:
type: string
type: object
uploadProxy:
additionalProperties:
type: string
type: object
type: object
patches:
items:
description: CustomizeComponentsPatch defines a patch for some
resource.
properties:
patch:
type: string
resourceName:
minLength: 1
type: string
resourceType:
minLength: 1
type: string
type:
description: PatchType defines the patch type.
type: string
required:
- patch
- resourceName
- resourceType
- type
type: object
type: array
x-kubernetes-list-type: atomic
type: object
imagePullPolicy:
description: PullPolicy describes a policy for if/when to pull a container
image
enum:
- Always
- IfNotPresent
- Never
type: string
infra:
description: Selectors and tolerations that should apply to cdi infrastructure
components
properties:
affinity:
description: |-
affinity enables pod affinity/anti-affinity placement expanding the types of constraints
that can be expressed with nodeSelector.
affinity is going to be applied to the relevant kind of pods in parallel with nodeSelector
See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity
properties:
nodeAffinity:
description: Describes node affinity scheduling rules for
the pod.
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node matches the corresponding matchExpressions; the
node(s) with the highest sum are the most preferred.
items:
description: |-
An empty preferred scheduling term matches all objects with implicit weight 0
(i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).
properties:
preference:
description: A node selector term, associated with
the corresponding weight.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
weight:
description: Weight associated with matching the
corresponding nodeSelectorTerm, in the range 1-100.
format: int32
type: integer
required:
- preference
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to an update), the system
may or may not try to eventually evict the pod from its node.
properties:
nodeSelectorTerms:
description: Required. A list of node selector terms.
The terms are ORed.
items:
description: |-
A null or empty node selector term matches no objects. The requirements of
them are ANDed.
The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
type: array
x-kubernetes-list-type: atomic
required:
- nodeSelectorTerms
type: object
x-kubernetes-map-type: atomic
type: object
podAffinity:
description: Describes pod affinity scheduling rules (e.g.
co-locate this pod in the same node, zone, etc. as some
other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term, associated
with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
podAntiAffinity:
description: Describes pod anti-affinity scheduling rules
(e.g. avoid putting this pod in the same node, zone, etc.
as some other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the anti-affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling anti-affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term, associated
with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the anti-affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the anti-affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: object
apiServerReplicas:
description: ApiserverReplicas set Replicas for cdi-apiserver
format: int32
type: integer
deploymentReplicas:
description: DeploymentReplicas set Replicas for cdi-deployment
format: int32
type: integer
nodeSelector:
additionalProperties:
type: string
description: |-
nodeSelector is the node selector applied to the relevant kind of pods
It specifies a map of key-value pairs: for the pod to be eligible to run on a node,
the node must have each of the indicated key-value pairs as labels
(it can have additional labels as well).
See https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
type: object
tolerations:
description: |-
tolerations is a list of tolerations applied to the relevant kind of pods
See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ for more info.
These are additional tolerations other than default ones.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple using the matching operator .
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
uploadProxyReplicas:
description: UploadproxyReplicas set Replicas for cdi-uploadproxy
format: int32
type: integer
type: object
priorityClass:
description: PriorityClass of the CDI control plane
type: string
uninstallStrategy:
description: CDIUninstallStrategy defines the state to leave CDI on
uninstall
enum:
- RemoveWorkloads
- BlockUninstallIfWorkloadsExist
type: string
workload:
description: Restrict on which nodes CDI workload pods will be scheduled
properties:
affinity:
description: |-
affinity enables pod affinity/anti-affinity placement expanding the types of constraints
that can be expressed with nodeSelector.
affinity is going to be applied to the relevant kind of pods in parallel with nodeSelector
See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity
properties:
nodeAffinity:
description: Describes node affinity scheduling rules for
the pod.
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node matches the corresponding matchExpressions; the
node(s) with the highest sum are the most preferred.
items:
description: |-
An empty preferred scheduling term matches all objects with implicit weight 0
(i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).
properties:
preference:
description: A node selector term, associated with
the corresponding weight.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
weight:
description: Weight associated with matching the
corresponding nodeSelectorTerm, in the range 1-100.
format: int32
type: integer
required:
- preference
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to an update), the system
may or may not try to eventually evict the pod from its node.
properties:
nodeSelectorTerms:
description: Required. A list of node selector terms.
The terms are ORed.
items:
description: |-
A null or empty node selector term matches no objects. The requirements of
them are ANDed.
The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
type: array
x-kubernetes-list-type: atomic
required:
- nodeSelectorTerms
type: object
x-kubernetes-map-type: atomic
type: object
podAffinity:
description: Describes pod affinity scheduling rules (e.g.
co-locate this pod in the same node, zone, etc. as some
other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term, associated
with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
podAntiAffinity:
description: Describes pod anti-affinity scheduling rules
(e.g. avoid putting this pod in the same node, zone, etc.
as some other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the anti-affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling anti-affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term, associated
with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the anti-affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the anti-affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: object
nodeSelector:
additionalProperties:
type: string
description: |-
nodeSelector is the node selector applied to the relevant kind of pods
It specifies a map of key-value pairs: for the pod to be eligible to run on a node,
the node must have each of the indicated key-value pairs as labels
(it can have additional labels as well).
See https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
type: object
tolerations:
description: |-
tolerations is a list of tolerations applied to the relevant kind of pods
See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ for more info.
These are additional tolerations other than default ones.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple using the matching operator .
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
type: object
type: object
status:
description: CDIStatus defines the status of the installation
properties:
conditions:
description: A list of current conditions of the resource
items:
description: |-
Condition represents the state of the operator's
reconciliation functionality.
properties:
lastHeartbeatTime:
format: date-time
type: string
lastTransitionTime:
format: date-time
type: string
message:
type: string
reason:
type: string
status:
type: string
type:
description: ConditionType is the state of the operator's reconciliation
functionality.
type: string
required:
- status
- type
type: object
type: array
observedVersion:
description: The observed version of the resource
type: string
operatorVersion:
description: The version of the resource as defined by the operator
type: string
phase:
description: Phase is the current phase of the deployment
type: string
targetVersion:
description: The desired version of the resource
type: string
type: object
required:
- spec
type: object
served: true
storage: false
subresources: {}
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- jsonPath: .status.phase
name: Phase
type: string
name: v1beta1
schema:
openAPIV3Schema:
description: CDI is the CDI Operator CRD
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: CDISpec defines our specification for the CDI installation
properties:
certConfig:
description: certificate configuration
properties:
ca:
description: |-
CA configuration
CA certs are kept in the CA bundle as long as they are valid
properties:
duration:
description: The requested 'duration' (i.e. lifetime) of the
Certificate.
type: string
renewBefore:
description: |-
The amount of time before the currently issued certificate's `notAfter`
time that we will begin to attempt to renew the certificate.
type: string
type: object
client:
description: |-
Client configuration
Certs are rotated and discarded
properties:
duration:
description: The requested 'duration' (i.e. lifetime) of the
Certificate.
type: string
renewBefore:
description: |-
The amount of time before the currently issued certificate's `notAfter`
time that we will begin to attempt to renew the certificate.
type: string
type: object
server:
description: |-
Server configuration
Certs are rotated and discarded
properties:
duration:
description: The requested 'duration' (i.e. lifetime) of the
Certificate.
type: string
renewBefore:
description: |-
The amount of time before the currently issued certificate's `notAfter`
time that we will begin to attempt to renew the certificate.
type: string
type: object
type: object
cloneStrategyOverride:
description: 'Clone strategy override: should we use a host-assisted
copy even if snapshots are available?'
enum:
- copy
- snapshot
- csi-clone
type: string
config:
description: CDIConfig at CDI level
properties:
dataVolumeTTLSeconds:
description: |-
DataVolumeTTLSeconds is the time in seconds after DataVolume completion it can be garbage collected. Disabled by default.
Deprecated: Removed in v1.62.
format: int32
type: integer
featureGates:
description: FeatureGates are a list of specific enabled feature
gates
items:
type: string
type: array
filesystemOverhead:
description: FilesystemOverhead describes the space reserved for
overhead when using Filesystem volumes. A value is between 0
and 1, if not defined it is 0.055 (5.5% overhead)
properties:
global:
description: Global is how much space of a Filesystem volume
should be reserved for overhead. This value is used unless
overridden by a more specific value (per storageClass)
pattern: ^(0(?:\.\d{1,3})?|1)$
type: string
storageClass:
additionalProperties:
description: |-
Percent is a string that can only be a value between [0,1)
(Note: we actually rely on reconcile to reject invalid values)
pattern: ^(0(?:\.\d{1,3})?|1)$
type: string
description: StorageClass specifies how much space of a Filesystem
volume should be reserved for safety. The keys are the storageClass
and the values are the overhead. This value overrides the
global value
type: object
type: object
imagePullSecrets:
description: The imagePullSecrets used to pull the container images
items:
description: |-
LocalObjectReference contains enough information to let you locate the
referenced object inside the same namespace.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
TODO: Add other useful fields. apiVersion, kind, uid?
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896.
type: string
type: object
x-kubernetes-map-type: atomic
type: array
importProxy:
description: ImportProxy contains importer pod proxy configuration.
properties:
HTTPProxy:
description: HTTPProxy is the URL http://:@:
of the import proxy for HTTP requests. Empty means unset
and will not result in the import pod env var.
type: string
HTTPSProxy:
description: HTTPSProxy is the URL https://:@:
of the import proxy for HTTPS requests. Empty means unset
and will not result in the import pod env var.
type: string
noProxy:
description: NoProxy is a comma-separated list of hostnames
and/or CIDRs for which the proxy should not be used. Empty
means unset and will not result in the import pod env var.
type: string
trustedCAProxy:
description: "TrustedCAProxy is the name of a ConfigMap in
the cdi namespace that contains a user-provided trusted
certificate authority (CA) bundle.\nThe TrustedCAProxy ConfigMap
is consumed by the DataImportCron controller for creating
cronjobs, and by the import controller referring a copy
of the ConfigMap in the import namespace.\nHere is an example
of the ConfigMap (in yaml):\n\n\napiVersion: v1\nkind: ConfigMap\nmetadata:\n
\ name: my-ca-proxy-cm\n namespace: cdi\ndata:\n ca.pem:
|\n -----BEGIN CERTIFICATE-----\n\t ... ...\n\t -----END CERTIFICATE-----"
type: string
type: object
insecureRegistries:
description: InsecureRegistries is a list of TLS disabled registries
items:
type: string
type: array
logVerbosity:
description: LogVerbosity overrides the default verbosity level
used to initialize loggers
format: int32
type: integer
podResourceRequirements:
description: ResourceRequirements describes the compute resource
requirements.
properties:
claims:
description: |-
Claims lists the names of resources, defined in spec.resourceClaims,
that are used by this container.
This is an alpha field and requires enabling the
DynamicResourceAllocation feature gate.
This field is immutable. It can only be set for containers.
items:
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
properties:
name:
description: |-
Name must match the name of one entry in pod.spec.resourceClaims of
the Pod where this field is used. It makes that resource available
inside a container.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Limits describes the maximum amount of compute resources allowed.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
requests:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Requests describes the minimum amount of compute resources required.
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
otherwise to an implementation-defined value. Requests cannot exceed Limits.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
preallocation:
description: Preallocation controls whether storage for DataVolumes
should be allocated in advance.
type: boolean
scratchSpaceStorageClass:
description: 'Override the storage class to used for scratch space
during transfer operations. The scratch space storage class
is determined in the following order: 1. value of scratchSpaceStorageClass,
if that doesn''t exist, use the default storage class, if there
is no default storage class, use the storage class of the DataVolume,
if no storage class specified, use no storage class for scratch
space'
type: string
tlsSecurityProfile:
description: TLSSecurityProfile is used by operators to apply
cluster-wide TLS security settings to operands.
properties:
custom:
description: |-
custom is a user-defined TLS security profile. Be extremely careful using a custom
profile as invalid configurations can be catastrophic. An example custom profile
looks like this:
ciphers:
- ECDHE-ECDSA-CHACHA20-POLY1305
- ECDHE-RSA-CHACHA20-POLY1305
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES128-GCM-SHA256
minTLSVersion: VersionTLS11
nullable: true
properties:
ciphers:
description: |-
ciphers is used to specify the cipher algorithms that are negotiated
during the TLS handshake. Operators may remove entries their operands
do not support. For example, to use DES-CBC3-SHA (yaml):
ciphers:
- DES-CBC3-SHA
items:
type: string
type: array
minTLSVersion:
description: |-
minTLSVersion is used to specify the minimal version of the TLS protocol
that is negotiated during the TLS handshake. For example, to use TLS
versions 1.1, 1.2 and 1.3 (yaml):
minTLSVersion: VersionTLS11
NOTE: currently the highest minTLSVersion allowed is VersionTLS12
enum:
- VersionTLS10
- VersionTLS11
- VersionTLS12
- VersionTLS13
type: string
required:
- ciphers
- minTLSVersion
type: object
intermediate:
description: |-
intermediate is a TLS security profile based on:
https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28recommended.29
and looks like this (yaml):
ciphers:
- TLS_AES_128_GCM_SHA256
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384
- ECDHE-ECDSA-CHACHA20-POLY1305
- ECDHE-RSA-CHACHA20-POLY1305
- DHE-RSA-AES128-GCM-SHA256
- DHE-RSA-AES256-GCM-SHA384
minTLSVersion: VersionTLS12
nullable: true
type: object
modern:
description: |-
modern is a TLS security profile based on:
https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility
and looks like this (yaml):
ciphers:
- TLS_AES_128_GCM_SHA256
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
minTLSVersion: VersionTLS13
NOTE: Currently unsupported.
nullable: true
type: object
old:
description: |-
old is a TLS security profile based on:
https://wiki.mozilla.org/Security/Server_Side_TLS#Old_backward_compatibility
and looks like this (yaml):
ciphers:
- TLS_AES_128_GCM_SHA256
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384
- ECDHE-ECDSA-CHACHA20-POLY1305
- ECDHE-RSA-CHACHA20-POLY1305
- DHE-RSA-AES128-GCM-SHA256
- DHE-RSA-AES256-GCM-SHA384
- DHE-RSA-CHACHA20-POLY1305
- ECDHE-ECDSA-AES128-SHA256
- ECDHE-RSA-AES128-SHA256
- ECDHE-ECDSA-AES128-SHA
- ECDHE-RSA-AES128-SHA
- ECDHE-ECDSA-AES256-SHA384
- ECDHE-RSA-AES256-SHA384
- ECDHE-ECDSA-AES256-SHA
- ECDHE-RSA-AES256-SHA
- DHE-RSA-AES128-SHA256
- DHE-RSA-AES256-SHA256
- AES128-GCM-SHA256
- AES256-GCM-SHA384
- AES128-SHA256
- AES256-SHA256
- AES128-SHA
- AES256-SHA
- DES-CBC3-SHA
minTLSVersion: VersionTLS10
nullable: true
type: object
type:
description: |-
type is one of Old, Intermediate, Modern or Custom. Custom provides
the ability to specify individual TLS security profile parameters.
Old, Intermediate and Modern are TLS security profiles based on:
https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations
The profiles are intent based, so they may change over time as new ciphers are developed and existing ciphers
are found to be insecure. Depending on precisely which ciphers are available to a process, the list may be
reduced.
Note that the Modern profile is currently not supported because it is not
yet well adopted by common software libraries.
enum:
- Old
- Intermediate
- Modern
- Custom
type: string
type: object
uploadProxyURLOverride:
description: Override the URL used when uploading to a DataVolume
type: string
type: object
customizeComponents:
description: CustomizeComponents defines patches for components deployed
by the CDI operator.
properties:
flags:
description: Configure the value used for deployment and daemonset
resources
properties:
api:
additionalProperties:
type: string
type: object
controller:
additionalProperties:
type: string
type: object
uploadProxy:
additionalProperties:
type: string
type: object
type: object
patches:
items:
description: CustomizeComponentsPatch defines a patch for some
resource.
properties:
patch:
type: string
resourceName:
minLength: 1
type: string
resourceType:
minLength: 1
type: string
type:
description: PatchType defines the patch type.
type: string
required:
- patch
- resourceName
- resourceType
- type
type: object
type: array
x-kubernetes-list-type: atomic
type: object
imagePullPolicy:
description: PullPolicy describes a policy for if/when to pull a container
image
enum:
- Always
- IfNotPresent
- Never
type: string
infra:
description: Selectors and tolerations that should apply to cdi infrastructure
components
properties:
affinity:
description: |-
affinity enables pod affinity/anti-affinity placement expanding the types of constraints
that can be expressed with nodeSelector.
affinity is going to be applied to the relevant kind of pods in parallel with nodeSelector
See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity
properties:
nodeAffinity:
description: Describes node affinity scheduling rules for
the pod.
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node matches the corresponding matchExpressions; the
node(s) with the highest sum are the most preferred.
items:
description: |-
An empty preferred scheduling term matches all objects with implicit weight 0
(i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).
properties:
preference:
description: A node selector term, associated with
the corresponding weight.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
weight:
description: Weight associated with matching the
corresponding nodeSelectorTerm, in the range 1-100.
format: int32
type: integer
required:
- preference
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to an update), the system
may or may not try to eventually evict the pod from its node.
properties:
nodeSelectorTerms:
description: Required. A list of node selector terms.
The terms are ORed.
items:
description: |-
A null or empty node selector term matches no objects. The requirements of
them are ANDed.
The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
type: array
x-kubernetes-list-type: atomic
required:
- nodeSelectorTerms
type: object
x-kubernetes-map-type: atomic
type: object
podAffinity:
description: Describes pod affinity scheduling rules (e.g.
co-locate this pod in the same node, zone, etc. as some
other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term, associated
with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
podAntiAffinity:
description: Describes pod anti-affinity scheduling rules
(e.g. avoid putting this pod in the same node, zone, etc.
as some other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the anti-affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling anti-affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term, associated
with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the anti-affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the anti-affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: object
apiServerReplicas:
description: ApiserverReplicas set Replicas for cdi-apiserver
format: int32
type: integer
deploymentReplicas:
description: DeploymentReplicas set Replicas for cdi-deployment
format: int32
type: integer
nodeSelector:
additionalProperties:
type: string
description: |-
nodeSelector is the node selector applied to the relevant kind of pods
It specifies a map of key-value pairs: for the pod to be eligible to run on a node,
the node must have each of the indicated key-value pairs as labels
(it can have additional labels as well).
See https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
type: object
tolerations:
description: |-
tolerations is a list of tolerations applied to the relevant kind of pods
See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ for more info.
These are additional tolerations other than default ones.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple using the matching operator .
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
uploadProxyReplicas:
description: UploadproxyReplicas set Replicas for cdi-uploadproxy
format: int32
type: integer
type: object
priorityClass:
description: PriorityClass of the CDI control plane
type: string
uninstallStrategy:
description: CDIUninstallStrategy defines the state to leave CDI on
uninstall
enum:
- RemoveWorkloads
- BlockUninstallIfWorkloadsExist
type: string
workload:
description: Restrict on which nodes CDI workload pods will be scheduled
properties:
affinity:
description: |-
affinity enables pod affinity/anti-affinity placement expanding the types of constraints
that can be expressed with nodeSelector.
affinity is going to be applied to the relevant kind of pods in parallel with nodeSelector
See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity
properties:
nodeAffinity:
description: Describes node affinity scheduling rules for
the pod.
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node matches the corresponding matchExpressions; the
node(s) with the highest sum are the most preferred.
items:
description: |-
An empty preferred scheduling term matches all objects with implicit weight 0
(i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).
properties:
preference:
description: A node selector term, associated with
the corresponding weight.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
weight:
description: Weight associated with matching the
corresponding nodeSelectorTerm, in the range 1-100.
format: int32
type: integer
required:
- preference
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to an update), the system
may or may not try to eventually evict the pod from its node.
properties:
nodeSelectorTerms:
description: Required. A list of node selector terms.
The terms are ORed.
items:
description: |-
A null or empty node selector term matches no objects. The requirements of
them are ANDed.
The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the selector
applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
type: array
x-kubernetes-list-type: atomic
required:
- nodeSelectorTerms
type: object
x-kubernetes-map-type: atomic
type: object
podAffinity:
description: Describes pod affinity scheduling rules (e.g.
co-locate this pod in the same node, zone, etc. as some
other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term, associated
with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
podAntiAffinity:
description: Describes pod anti-affinity scheduling rules
(e.g. avoid putting this pod in the same node, zone, etc.
as some other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the anti-affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling anti-affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term, associated
with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the anti-affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the anti-affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)`
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate.
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list of label
selector requirements. The requirements are
ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that
the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: object
nodeSelector:
additionalProperties:
type: string
description: |-
nodeSelector is the node selector applied to the relevant kind of pods
It specifies a map of key-value pairs: for the pod to be eligible to run on a node,
the node must have each of the indicated key-value pairs as labels
(it can have additional labels as well).
See https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
type: object
tolerations:
description: |-
tolerations is a list of tolerations applied to the relevant kind of pods
See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ for more info.
These are additional tolerations other than default ones.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple using the matching operator .
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
type: object
type: object
status:
description: CDIStatus defines the status of the installation
properties:
conditions:
description: A list of current conditions of the resource
items:
description: |-
Condition represents the state of the operator's
reconciliation functionality.
properties:
lastHeartbeatTime:
format: date-time
type: string
lastTransitionTime:
format: date-time
type: string
message:
type: string
reason:
type: string
status:
type: string
type:
description: ConditionType is the state of the operator's reconciliation
functionality.
type: string
required:
- status
- type
type: object
type: array
observedVersion:
description: The observed version of the resource
type: string
operatorVersion:
description: The version of the resource as defined by the operator
type: string
phase:
description: Phase is the current phase of the deployment
type: string
targetVersion:
description: The desired version of the resource
type: string
type: object
required:
- spec
type: object
served: true
storage: true
subresources: {}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
operator.cdi.kubevirt.io: ""
name: cdi-operator-cluster
rules:
- apiGroups:
- rbac.authorization.k8s.io
resources:
- clusterrolebindings
- clusterroles
verbs:
- get
- list
- watch
- create
- update
- delete
- apiGroups:
- security.openshift.io
resources:
- securitycontextconstraints
verbs:
- get
- list
- watch
- update
- create
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
- customresourcedefinitions/status
verbs:
- get
- list
- watch
- create
- update
- delete
- apiGroups:
- cdi.kubevirt.io
- upload.cdi.kubevirt.io
resources:
- '*'
verbs:
- '*'
- apiGroups:
- admissionregistration.k8s.io
resources:
- validatingwebhookconfigurations
- mutatingwebhookconfigurations
verbs:
- create
- list
- watch
- apiGroups:
- admissionregistration.k8s.io
resourceNames:
- cdi-api-dataimportcron-validate
- cdi-api-populator-validate
- cdi-api-datavolume-validate
- cdi-api-validate
- objecttransfer-api-validate
resources:
- validatingwebhookconfigurations
verbs:
- get
- update
- delete
- apiGroups:
- admissionregistration.k8s.io
resourceNames:
- cdi-api-datavolume-mutate
- cdi-api-pvc-mutate
resources:
- mutatingwebhookconfigurations
verbs:
- get
- update
- delete
- apiGroups:
- apiregistration.k8s.io
resources:
- apiservices
verbs:
- get
- list
- watch
- create
- update
- delete
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- persistentvolumeclaims
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- persistentvolumes
verbs:
- get
- list
- watch
- apiGroups:
- storage.k8s.io
resources:
- storageclasses
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- apiGroups:
- snapshot.storage.k8s.io
resources:
- volumesnapshots
verbs:
- get
- list
- watch
- apiGroups:
- cdi.kubevirt.io
resources:
- datavolumes
verbs:
- list
- get
- apiGroups:
- cdi.kubevirt.io
resources:
- datasources
verbs:
- get
- apiGroups:
- cdi.kubevirt.io
resources:
- volumeclonesources
verbs:
- get
- list
- watch
- apiGroups:
- cdi.kubevirt.io
resources:
- storageprofiles
verbs:
- get
- list
- watch
- apiGroups:
- cdi.kubevirt.io
resources:
- cdis
verbs:
- get
- list
- watch
- apiGroups:
- cdi.kubevirt.io
resources:
- cdiconfigs
verbs:
- get
- list
- watch
- apiGroups:
- cdi.kubevirt.io
resources:
- cdis/finalizers
verbs:
- update
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
- persistentvolumeclaims
verbs:
- get
- list
- watch
- create
- update
- delete
- deletecollection
- patch
- apiGroups:
- ""
resources:
- persistentvolumes
verbs:
- get
- list
- watch
- update
- apiGroups:
- ""
resources:
- persistentvolumeclaims/finalizers
- pods/finalizers
verbs:
- update
- apiGroups:
- ""
resources:
- pods
- services
verbs:
- get
- list
- watch
- create
- delete
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- create
- apiGroups:
- storage.k8s.io
resources:
- storageclasses
- csidrivers
verbs:
- get
- list
- watch
- apiGroups:
- config.openshift.io
resources:
- proxies
- infrastructures
verbs:
- get
- list
- watch
- apiGroups:
- config.openshift.io
resources:
- clusterversions
verbs:
- get
- apiGroups:
- cdi.kubevirt.io
resources:
- '*'
verbs:
- '*'
- apiGroups:
- snapshot.storage.k8s.io
resources:
- volumesnapshots
- volumesnapshotclasses
- volumesnapshotcontents
verbs:
- get
- list
- watch
- create
- delete
- apiGroups:
- snapshot.storage.k8s.io
resources:
- volumesnapshots
verbs:
- update
- deletecollection
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- apiGroups:
- scheduling.k8s.io
resources:
- priorityclasses
verbs:
- get
- list
- watch
- apiGroups:
- image.openshift.io
resources:
- imagestreams
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- apiGroups:
- kubevirt.io
resources:
- virtualmachines/finalizers
verbs:
- update
- apiGroups:
- forklift.cdi.kubevirt.io
resources:
- ovirtvolumepopulators
- openstackvolumepopulators
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- persistentvolumeclaims
verbs:
- get
- apiGroups:
- cdi.kubevirt.io
resources:
- dataimportcrons
verbs:
- get
- list
- update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
operator.cdi.kubevirt.io: ""
name: cdi-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cdi-operator-cluster
subjects:
- kind: ServiceAccount
name: cdi-operator
namespace: cdi
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
operator.cdi.kubevirt.io: ""
name: cdi-operator
namespace: cdi
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
app: containerized-data-importer
app.kubernetes.io/component: storage
app.kubernetes.io/managed-by: cdi-operator
cdi.kubevirt.io: ""
name: cdi-operator
namespace: cdi
rules:
- apiGroups:
- rbac.authorization.k8s.io
resources:
- rolebindings
- roles
verbs:
- get
- list
- watch
- create
- update
- delete
- apiGroups:
- ""
resources:
- serviceaccounts
- configmaps
- events
- secrets
- services
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- apps
resources:
- deployments
- deployments/finalizers
verbs:
- get
- list
- watch
- create
- update
- delete
- apiGroups:
- route.openshift.io
resources:
- routes
- routes/custom-host
verbs:
- get
- list
- watch
- create
- update
- apiGroups:
- config.openshift.io
resources:
- proxies
verbs:
- get
- list
- watch
- apiGroups:
- monitoring.coreos.com
resources:
- servicemonitors
- prometheusrules
verbs:
- get
- list
- watch
- create
- delete
- update
- patch
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- create
- update
- apiGroups:
- ""
resources:
- secrets
- configmaps
verbs:
- get
- list
- watch
- create
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- create
- update
- delete
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- list
- watch
- apiGroups:
- batch
resources:
- cronjobs
verbs:
- get
- list
- watch
- create
- update
- deletecollection
- apiGroups:
- batch
resources:
- jobs
verbs:
- create
- deletecollection
- list
- watch
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- create
- update
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- route.openshift.io
resources:
- routes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- apiGroups:
- ""
resources:
- services
- endpoints
- pods
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app: containerized-data-importer
app.kubernetes.io/component: storage
app.kubernetes.io/managed-by: cdi-operator
cdi.kubevirt.io: ""
name: cdi-operator
namespace: cdi
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: cdi-operator
subjects:
- kind: ServiceAccount
name: cdi-operator
namespace: cdi
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
cdi.kubevirt.io: cdi-operator
name: cdi-operator
operator.cdi.kubevirt.io: ""
prometheus.cdi.kubevirt.io: "true"
name: cdi-operator
namespace: cdi
spec:
replicas: 1
selector:
matchLabels:
name: cdi-operator
operator.cdi.kubevirt.io: ""
strategy: {}
template:
metadata:
annotations:
openshift.io/required-scc: restricted-v2
labels:
cdi.kubevirt.io: cdi-operator
name: cdi-operator
operator.cdi.kubevirt.io: ""
prometheus.cdi.kubevirt.io: "true"
spec:
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: cdi.kubevirt.io
operator: In
values:
- cdi-operator
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- env:
- name: DEPLOY_CLUSTER_RESOURCES
value: "true"
- name: OPERATOR_VERSION
value: v1.62.0
- name: CONTROLLER_IMAGE
value: quay.io/kubevirt/cdi-controller:v1.62.0
- name: IMPORTER_IMAGE
value: quay.io/kubevirt/cdi-importer:v1.62.0
- name: CLONER_IMAGE
value: quay.io/kubevirt/cdi-cloner:v1.62.0
- name: OVIRT_POPULATOR_IMAGE
value: quay.io/kubevirt/cdi-importer:v1.62.0
- name: APISERVER_IMAGE
value: quay.io/kubevirt/cdi-apiserver:v1.62.0
- name: UPLOAD_SERVER_IMAGE
value: quay.io/kubevirt/cdi-uploadserver:v1.62.0
- name: UPLOAD_PROXY_IMAGE
value: quay.io/kubevirt/cdi-uploadproxy:v1.62.0
- name: VERBOSITY
value: "1"
- name: PULL_POLICY
value: IfNotPresent
- name: MONITORING_NAMESPACE
image: quay.io/kubevirt/cdi-operator:v1.62.0
imagePullPolicy: IfNotPresent
name: cdi-operator
ports:
- containerPort: 8080
name: metrics
protocol: TCP
resources:
requests:
cpu: 100m
memory: 150Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
nodeSelector:
kubernetes.io/os: linux
securityContext:
runAsNonRoot: true
serviceAccountName: cdi-operator
tolerations:
- key: CriticalAddonsOnly
operator: Exists
================================================
FILE: infra/kubernetes/cluster-issuer.yaml
================================================
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: dev@cyberdesk.io
privateKeySecretRef:
name: le-prod-key
solvers:
- http01:
ingress:
class: nginx
================================================
FILE: infra/kubernetes/cyberdesk-cr-v2.yaml
================================================
apiVersion: cyberdesk.io/v1alpha1
kind: Cyberdesk
metadata:
name: 3840362a-cbc5-4f35-ab21-cccfa9275ce4
namespace: cyberdesk-system
spec:
timeoutMs: 86400000 # 24 hour timeout@
================================================
FILE: infra/kubernetes/cyberdesk-cr.yaml
================================================
apiVersion: cyberdesk.io/v1alpha1
kind: Cyberdesk
metadata:
name: 6ebf5303-1e81-4203-b870-ccfeb590d02f
namespace: cyberdesk-system
spec:
timeoutMs: 86400000 # 24 hour timeout
================================================
FILE: infra/kubernetes/cyberdesk-operator.yaml
================================================
apiVersion: v1
kind: Namespace
metadata:
name: cyberdesk-system
labels:
app.kubernetes.io/name: cyberdesk-system
app.kubernetes.io/part-of: cyberdesk
app.kubernetes.io/component: operator
annotations:
description: "Dedicated namespace for the Cyberdesk Operator and related resources"
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: cyberdesk-operator
namespace: cyberdesk-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cyberdesk-operator
rules:
# Kopf framework requires these permissions
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch", "update"]
- apiGroups: ["", "coordination.k8s.io"]
resources: ["configmaps", "leases"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Access to the trigger CRD
- apiGroups: ["cyberdesk.io"]
resources: ["startcyberdeskoperators"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Access to Cyberdesk CRDs
- apiGroups: ["cyberdesk.io"]
resources: ["cyberdesks", "cyberdesks/status"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Access to KubeVirt resources
- apiGroups: ["kubevirt.io"]
resources: ["virtualmachines", "virtualmachineinstances"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# NEW: Access to KubeVirt Snapshot resources
- apiGroups: ["snapshot.kubevirt.io"]
resources: ["virtualmachinesnapshots"]
verbs: ["get", "list", "watch"]
# NEW: Access to KubeVirt Clone resources
- apiGroups: ["clone.kubevirt.io"]
resources: ["virtualmachineclones"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# Permission to manage CRDs
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cyberdesk-operator
subjects:
- kind: ServiceAccount
name: cyberdesk-operator
namespace: cyberdesk-system
roleRef:
kind: ClusterRole
name: cyberdesk-operator
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cyberdesk-operator
namespace: cyberdesk-system
labels:
app: cyberdesk-operator
spec:
replicas: 1
selector:
matchLabels:
app: cyberdesk-operator
template:
metadata:
labels:
app: cyberdesk-operator
spec:
serviceAccountName: cyberdesk-operator
containers:
- name: operator
image: cyberdesk/cyberdesk-operator:v0.2.2
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: health
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 15
periodSeconds: 30
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "200m"
memory: "256Mi"
envFrom:
- secretRef:
name: supabase-credentials
---
# CRD for the trigger resource
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: startcyberdeskoperators.cyberdesk.io
spec:
group: cyberdesk.io
names:
kind: StartCyberdeskOperator
plural: startcyberdeskoperators
singular: startcyberdeskoperator
shortNames:
- sco
scope: Namespaced # Keeping it namespaced as requested for minimal scope
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
description: "Specification for triggering the Cyberdesk operator setup. Currently holds no fields."
# No specific fields needed for now, just the presence triggers the action
================================================
FILE: infra/kubernetes/default-backend.yaml
================================================
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: default-backend
namespace: ingress-nginx
spec:
selector:
matchLabels:
app: default-backend
template:
metadata:
labels:
app: default-backend
spec:
containers:
- name: default-backend
image: k8s.gcr.io/defaultbackend-amd64:1.5
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: default-backend
namespace: ingress-nginx
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: default-backend
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: default-backend
namespace: ingress-nginx
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: default-http-backend
port:
number: 80
================================================
FILE: infra/kubernetes/gateway-deploy.yaml
================================================
apiVersion: v1
kind: ServiceAccount
metadata:
name: gateway
namespace: cyberdesk-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gateway-vnc-access
rules:
- apiGroups: ["subresources.kubevirt.io"]
resources: ["virtualmachineinstances/vnc"]
verbs: ["get"]
- apiGroups: [""] # Core API group
resources: ["services", "services/status"] # Combined service permissions
verbs: ["get", "list", "watch"] # Permissions needed for service status and IP lookup
- apiGroups: ["cyberdesk.io"] # Add permissions for Cyberdesk CRs
resources: ["cyberdesks"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
- apiGroups: ["kubevirt.io"]
resources: ["virtualmachineinstances"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gateway-vnc-binding
namespace: cyberdesk-system
subjects:
- kind: ServiceAccount
name: gateway
namespace: cyberdesk-system
roleRef:
kind: ClusterRole
name: gateway-vnc-access
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway
namespace: cyberdesk-system
spec:
replicas: 1
selector:
matchLabels:
app: gateway
template:
metadata:
labels:
app: gateway
spec:
serviceAccountName: gateway
containers:
- name: gateway
image: cyberdesk/gateway:v0.2.1
envFrom:
- secretRef:
name: supabase-credentials
ports:
- containerPort: 80
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: gateway
namespace: cyberdesk-system
spec:
type: LoadBalancer
selector:
app: gateway
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http
================================================
FILE: infra/kubernetes/gateway-ingress-dev.yaml
================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dev-gateway-ingress
namespace: cyberdesk-system
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- dev-gateway.cyberdesk.io
secretName: gateway-tls
rules:
- host: dev-gateway.cyberdesk.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gateway
port:
number: 80
================================================
FILE: infra/kubernetes/gateway-ingress-prod.yaml
================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gateway-ingress
namespace: cyberdesk-system
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- gateway.cyberdesk.io
secretName: gateway-tls
rules:
- host: gateway.cyberdesk.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gateway
port:
number: 80
================================================
FILE: infra/kubernetes/golden-vm-snapshot-request.yaml
================================================
apiVersion: snapshot.kubevirt.io/v1beta1
kind: VirtualMachineSnapshot
metadata:
name: snapshot-golden-vm
namespace: kubevirt # Assuming golden-vm is in the kubevirt namespace
spec:
source:
apiGroup: kubevirt.io
kind: VirtualMachine
name: golden-vm
================================================
FILE: infra/kubernetes/kubevirt-cr.yaml
================================================
---
apiVersion: kubevirt.io/v1
kind: KubeVirt
metadata:
name: kubevirt
namespace: kubevirt
spec:
certificateRotateStrategy: {}
configuration:
developerConfiguration:
featureGates: [
"Snapshot"
]
customizeComponents: {}
imagePullPolicy: IfNotPresent
workloadUpdateStrategy: {}
infra:
nodePlacement:
tolerations:
- operator: Exists
================================================
FILE: infra/kubernetes/kubevirt-operator.yaml
================================================
---
apiVersion: v1
kind: Namespace
metadata:
labels:
kubevirt.io: ""
pod-security.kubernetes.io/enforce: "privileged"
name: kubevirt
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
labels:
operator.kubevirt.io: ""
name: kubevirts.kubevirt.io
spec:
group: kubevirt.io
names:
categories:
- all
kind: KubeVirt
plural: kubevirts
shortNames:
- kv
- kvs
singular: kubevirt
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- jsonPath: .status.phase
name: Phase
type: string
name: v1
schema:
openAPIV3Schema:
description: KubeVirt represents the object deploying all KubeVirt resources
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
certificateRotateStrategy:
properties:
selfSigned:
properties:
ca:
description: |-
CA configuration
CA certs are kept in the CA bundle as long as they are valid
properties:
duration:
description: The requested 'duration' (i.e. lifetime)
of the Certificate.
type: string
renewBefore:
description: |-
The amount of time before the currently issued certificate's "notAfter"
time that we will begin to attempt to renew the certificate.
type: string
type: object
caOverlapInterval:
description: Deprecated. Use CA.Duration and CA.RenewBefore
instead
type: string
caRotateInterval:
description: Deprecated. Use CA.Duration instead
type: string
certRotateInterval:
description: Deprecated. Use Server.Duration instead
type: string
server:
description: |-
Server configuration
Certs are rotated and discarded
properties:
duration:
description: The requested 'duration' (i.e. lifetime)
of the Certificate.
type: string
renewBefore:
description: |-
The amount of time before the currently issued certificate's "notAfter"
time that we will begin to attempt to renew the certificate.
type: string
type: object
type: object
type: object
configuration:
description: |-
holds kubevirt configurations.
same as the virt-configMap
properties:
additionalGuestMemoryOverheadRatio:
description: |-
AdditionalGuestMemoryOverheadRatio can be used to increase the virtualization infrastructure
overhead. This is useful, since the calculation of this overhead is not accurate and cannot
be entirely known in advance. The ratio that is being set determines by which factor to increase
the overhead calculated by Kubevirt. A higher ratio means that the VMs would be less compromised
by node pressures, but would mean that fewer VMs could be scheduled to a node.
If not set, the default is 1.
type: string
apiConfiguration:
description: |-
ReloadableComponentConfiguration holds all generic k8s configuration options which can
be reloaded by components without requiring a restart.
properties:
restClient:
description: RestClient can be used to tune certain aspects
of the k8s client in use.
properties:
rateLimiter:
description: RateLimiter allows selecting and configuring
different rate limiters for the k8s client.
properties:
tokenBucketRateLimiter:
properties:
burst:
description: |-
Maximum burst for throttle.
If it's zero, the component default will be used
type: integer
qps:
description: |-
QPS indicates the maximum QPS to the apiserver from this client.
If it's zero, the component default will be used
type: number
required:
- burst
- qps
type: object
type: object
type: object
type: object
architectureConfiguration:
properties:
amd64:
properties:
emulatedMachines:
items:
type: string
type: array
x-kubernetes-list-type: atomic
machineType:
type: string
ovmfPath:
type: string
type: object
arm64:
properties:
emulatedMachines:
items:
type: string
type: array
x-kubernetes-list-type: atomic
machineType:
type: string
ovmfPath:
type: string
type: object
defaultArchitecture:
type: string
ppc64le:
properties:
emulatedMachines:
items:
type: string
type: array
x-kubernetes-list-type: atomic
machineType:
type: string
ovmfPath:
type: string
type: object
type: object
autoCPULimitNamespaceLabelSelector:
description: |-
When set, AutoCPULimitNamespaceLabelSelector will set a CPU limit on virt-launcher for VMIs running inside
namespaces that match the label selector.
The CPU limit will equal the number of requested vCPUs.
This setting does not apply to VMIs with dedicated CPUs.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
commonInstancetypesDeployment:
description: CommonInstancetypesDeployment controls the deployment
of common-instancetypes resources
nullable: true
properties:
enabled:
description: Enabled controls the deployment of common-instancetypes
resources, defaults to True.
nullable: true
type: boolean
type: object
controllerConfiguration:
description: |-
ReloadableComponentConfiguration holds all generic k8s configuration options which can
be reloaded by components without requiring a restart.
properties:
restClient:
description: RestClient can be used to tune certain aspects
of the k8s client in use.
properties:
rateLimiter:
description: RateLimiter allows selecting and configuring
different rate limiters for the k8s client.
properties:
tokenBucketRateLimiter:
properties:
burst:
description: |-
Maximum burst for throttle.
If it's zero, the component default will be used
type: integer
qps:
description: |-
QPS indicates the maximum QPS to the apiserver from this client.
If it's zero, the component default will be used
type: number
required:
- burst
- qps
type: object
type: object
type: object
type: object
cpuModel:
type: string
cpuRequest:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
defaultRuntimeClass:
type: string
developerConfiguration:
description: DeveloperConfiguration holds developer options
properties:
cpuAllocationRatio:
description: |-
For each requested virtual CPU, CPUAllocationRatio defines how much physical CPU to request per VMI
from the hosting node. The value is in fraction of a CPU thread (or core on non-hyperthreaded nodes).
For example, a value of 1 means 1 physical CPU thread per VMI CPU thread.
A value of 100 would be 1% of a physical thread allocated for each requested VMI thread.
This option has no effect on VMIs that request dedicated CPUs. More information at:
https://kubevirt.io/user-guide/operations/node_overcommit/#node-cpu-allocation-ratio
Defaults to 10
type: integer
diskVerification:
description: DiskVerification holds container disks verification
limits
properties:
memoryLimit:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
required:
- memoryLimit
type: object
featureGates:
description: FeatureGates is the list of experimental features
to enable. Defaults to none
items:
type: string
type: array
logVerbosity:
description: LogVerbosity sets log verbosity level of various
components
properties:
nodeVerbosity:
additionalProperties:
type: integer
description: NodeVerbosity represents a map of nodes with
a specific verbosity level
type: object
virtAPI:
type: integer
virtController:
type: integer
virtHandler:
type: integer
virtLauncher:
type: integer
virtOperator:
type: integer
type: object
memoryOvercommit:
description: |-
MemoryOvercommit is the percentage of memory we want to give VMIs compared to the amount
given to its parent pod (virt-launcher). For example, a value of 102 means the VMI will
"see" 2% more memory than its parent pod. Values under 100 are effectively "undercommits".
Overcommits can lead to memory exhaustion, which in turn can lead to crashes. Use carefully.
Defaults to 100
type: integer
minimumClusterTSCFrequency:
description: |-
Allow overriding the automatically determined minimum TSC frequency of the cluster
and fixate the minimum to this frequency.
format: int64
type: integer
minimumReservePVCBytes:
description: |-
MinimumReservePVCBytes is the amount of space, in bytes, to leave unused on disks.
Defaults to 131072 (128KiB)
format: int64
type: integer
nodeSelectors:
additionalProperties:
type: string
description: |-
NodeSelectors allows restricting VMI creation to nodes that match a set of labels.
Defaults to none
type: object
pvcTolerateLessSpaceUpToPercent:
description: |-
LessPVCSpaceToleration determines how much smaller, in percentage, disk PVCs are
allowed to be compared to the requested size (to account for various overheads).
Defaults to 10
type: integer
useEmulation:
description: |-
UseEmulation can be set to true to allow fallback to software emulation
in case hardware-assisted emulation is not available. Defaults to false
type: boolean
type: object
emulatedMachines:
description: Deprecated. Use architectureConfiguration instead.
items:
type: string
type: array
evictionStrategy:
description: |-
EvictionStrategy defines at the cluster level if the VirtualMachineInstance should be
migrated instead of shut-off in case of a node drain. If the VirtualMachineInstance specific
field is set it overrides the cluster level one.
type: string
handlerConfiguration:
description: |-
ReloadableComponentConfiguration holds all generic k8s configuration options which can
be reloaded by components without requiring a restart.
properties:
restClient:
description: RestClient can be used to tune certain aspects
of the k8s client in use.
properties:
rateLimiter:
description: RateLimiter allows selecting and configuring
different rate limiters for the k8s client.
properties:
tokenBucketRateLimiter:
properties:
burst:
description: |-
Maximum burst for throttle.
If it's zero, the component default will be used
type: integer
qps:
description: |-
QPS indicates the maximum QPS to the apiserver from this client.
If it's zero, the component default will be used
type: number
required:
- burst
- qps
type: object
type: object
type: object
type: object
imagePullPolicy:
description: PullPolicy describes a policy for if/when to pull
a container image
type: string
instancetype:
description: Instancetype configuration
nullable: true
properties:
referencePolicy:
description: |-
ReferencePolicy defines how an instance type or preference should be referenced by the VM after submission, supported values are:
reference (default) - Where a copy of the original object is stashed in a ControllerRevision and referenced by the VM.
expand - Where the instance type or preference are expanded into the VM if no revisionNames have been populated.
expandAll - Where the instance type or preference are expanded into the VM regardless of revisionNames previously being populated.
enum:
- reference
- expand
- expandAll
nullable: true
type: string
type: object
ksmConfiguration:
description: KSMConfiguration holds the information regarding
the enabling the KSM in the nodes (if available).
properties:
nodeLabelSelector:
description: |-
NodeLabelSelector is a selector that filters in which nodes the KSM will be enabled.
Empty NodeLabelSelector will enable ksm for every node.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: object
liveUpdateConfiguration:
description: LiveUpdateConfiguration holds defaults for live update
features
properties:
maxCpuSockets:
description: |-
MaxCpuSockets provides a MaxSockets value for VMs that do not provide their own.
For VMs with more sockets than maximum the MaxSockets will be set to equal number of sockets.
format: int32
type: integer
maxGuest:
anyOf:
- type: integer
- type: string
description: |-
MaxGuest defines the maximum amount memory that can be allocated
to the guest using hotplug.
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
maxHotplugRatio:
description: |-
MaxHotplugRatio is the ratio used to define the max amount
of a hotplug resource that can be made available to a VM
when the specific Max* setting is not defined (MaxCpuSockets, MaxGuest)
Example: VM is configured with 512Mi of guest memory, if MaxGuest is not
defined and MaxHotplugRatio is 2 then MaxGuest = 1Gi
defaults to 4
format: int32
type: integer
type: object
machineType:
description: Deprecated. Use architectureConfiguration instead.
type: string
mediatedDevicesConfiguration:
description: MediatedDevicesConfiguration holds information about
MDEV types to be defined, if available
properties:
mediatedDeviceTypes:
items:
type: string
type: array
x-kubernetes-list-type: atomic
mediatedDevicesTypes:
description: Deprecated. Use mediatedDeviceTypes instead.
items:
type: string
type: array
x-kubernetes-list-type: atomic
nodeMediatedDeviceTypes:
items:
description: NodeMediatedDeviceTypesConfig holds information
about MDEV types to be defined in a specific node that
matches the NodeSelector field.
properties:
mediatedDeviceTypes:
items:
type: string
type: array
x-kubernetes-list-type: atomic
mediatedDevicesTypes:
description: Deprecated. Use mediatedDeviceTypes instead.
items:
type: string
type: array
x-kubernetes-list-type: atomic
nodeSelector:
additionalProperties:
type: string
description: |-
NodeSelector is a selector which must be true for the vmi to fit on a node.
Selector which must match a node's labels for the vmi to be scheduled on that node.
More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/
type: object
required:
- nodeSelector
type: object
type: array
x-kubernetes-list-type: atomic
type: object
memBalloonStatsPeriod:
format: int32
type: integer
migrations:
description: |-
MigrationConfiguration holds migration options.
Can be overridden for specific groups of VMs though migration policies.
Visit https://kubevirt.io/user-guide/operations/migration_policies/ for more information.
properties:
allowAutoConverge:
description: |-
AllowAutoConverge allows the platform to compromise performance/availability of VMIs to
guarantee successful VMI live migrations. Defaults to false
type: boolean
allowPostCopy:
description: |-
AllowPostCopy enables post-copy live migrations. Such migrations allow even the busiest VMIs
to successfully live-migrate. However, events like a network failure can cause a VMI crash.
If set to true, migrations will still start in pre-copy, but switch to post-copy when
CompletionTimeoutPerGiB triggers. Defaults to false
type: boolean
allowWorkloadDisruption:
description: |-
AllowWorkloadDisruption indicates that the migration shouldn't be
canceled after acceptableCompletionTime is exceeded. Instead, if
permitted, migration will be switched to post-copy or the VMI will be
paused to allow the migration to complete
type: boolean
bandwidthPerMigration:
anyOf:
- type: integer
- type: string
description: |-
BandwidthPerMigration limits the amount of network bandwidth live migrations are allowed to use.
The value is in quantity per second. Defaults to 0 (no limit)
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
completionTimeoutPerGiB:
description: |-
CompletionTimeoutPerGiB is the maximum number of seconds per GiB a migration is allowed to take.
If the timeout is reached, the migration will be either paused, switched
to post-copy or cancelled depending on other settings. Defaults to 150
format: int64
type: integer
disableTLS:
description: |-
When set to true, DisableTLS will disable the additional layer of live migration encryption
provided by KubeVirt. This is usually a bad idea. Defaults to false
type: boolean
matchSELinuxLevelOnMigration:
description: |-
By default, the SELinux level of target virt-launcher pods is forced to the level of the source virt-launcher.
When set to true, MatchSELinuxLevelOnMigration lets the CRI auto-assign a random level to the target.
That will ensure the target virt-launcher doesn't share categories with another pod on the node.
However, migrations will fail when using RWX volumes that don't automatically deal with SELinux levels.
type: boolean
network:
description: |-
Network is the name of the CNI network to use for live migrations. By default, migrations go
through the pod network.
type: string
nodeDrainTaintKey:
description: |-
NodeDrainTaintKey defines the taint key that indicates a node should be drained.
Note: this option relies on the deprecated node taint feature. Default: kubevirt.io/drain
type: string
parallelMigrationsPerCluster:
description: |-
ParallelMigrationsPerCluster is the total number of concurrent live migrations
allowed cluster-wide. Defaults to 5
format: int32
type: integer
parallelOutboundMigrationsPerNode:
description: |-
ParallelOutboundMigrationsPerNode is the maximum number of concurrent outgoing live migrations
allowed per node. Defaults to 2
format: int32
type: integer
progressTimeout:
description: |-
ProgressTimeout is the maximum number of seconds a live migration is allowed to make no progress.
Hitting this timeout means a migration transferred 0 data for that many seconds. The migration is
then considered stuck and therefore cancelled. Defaults to 150
format: int64
type: integer
unsafeMigrationOverride:
description: |-
UnsafeMigrationOverride allows live migrations to occur even if the compatibility check
indicates the migration will be unsafe to the guest. Defaults to false
type: boolean
type: object
minCPUModel:
type: string
network:
description: NetworkConfiguration holds network options
properties:
binding:
additionalProperties:
properties:
computeResourceOverhead:
description: |-
ComputeResourceOverhead specifies the resource overhead that should be added to the compute container when using the binding.
version: v1alphav1
properties:
limits:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Limits describes the maximum amount of compute resources allowed.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
requests:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Requests describes the minimum amount of compute resources required.
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
otherwise to an implementation-defined value. Requests cannot exceed Limits.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
domainAttachmentType:
description: |-
DomainAttachmentType is a standard domain network attachment method kubevirt supports.
Supported values: "tap", "managedTap" (since v1.4).
The standard domain attachment can be used instead or in addition to the sidecarImage.
version: 1alphav1
type: string
downwardAPI:
description: |-
DownwardAPI specifies what kind of data should be exposed to the binding plugin sidecar.
Supported values: "device-info"
version: v1alphav1
type: string
migration:
description: |-
Migration means the VM using the plugin can be safely migrated
version: 1alphav1
properties:
method:
description: |-
Method defines a pre-defined migration methodology
version: 1alphav1
type: string
type: object
networkAttachmentDefinition:
description: |-
NetworkAttachmentDefinition references to a NetworkAttachmentDefinition CR object.
Format: , /.
If namespace is not specified, VMI namespace is assumed.
version: 1alphav1
type: string
sidecarImage:
description: |-
SidecarImage references a container image that runs in the virt-launcher pod.
The sidecar handles (libvirt) domain configuration and optional services.
version: 1alphav1
type: string
type: object
type: object
defaultNetworkInterface:
type: string
permitBridgeInterfaceOnPodNetwork:
type: boolean
permitSlirpInterface:
description: |-
DeprecatedPermitSlirpInterface is an alias for the deprecated PermitSlirpInterface.
Deprecated: Removed in v1.3.
type: boolean
type: object
obsoleteCPUModels:
additionalProperties:
type: boolean
type: object
ovmfPath:
description: Deprecated. Use architectureConfiguration instead.
type: string
permittedHostDevices:
description: PermittedHostDevices holds information about devices
allowed for passthrough
properties:
mediatedDevices:
items:
description: MediatedHostDevice represents a host mediated
device allowed for passthrough
properties:
externalResourceProvider:
type: boolean
mdevNameSelector:
type: string
resourceName:
type: string
required:
- mdevNameSelector
- resourceName
type: object
type: array
x-kubernetes-list-type: atomic
pciHostDevices:
items:
description: PciHostDevice represents a host PCI device
allowed for passthrough
properties:
externalResourceProvider:
description: |-
If true, KubeVirt will leave the allocation and monitoring to an
external device plugin
type: boolean
pciVendorSelector:
description: The vendor_id:product_id tuple of the PCI
device
type: string
resourceName:
description: |-
The name of the resource that is representing the device. Exposed by
a device plugin and requested by VMs. Typically of the form
vendor.com/product_name
type: string
required:
- pciVendorSelector
- resourceName
type: object
type: array
x-kubernetes-list-type: atomic
usb:
items:
properties:
externalResourceProvider:
description: |-
If true, KubeVirt will leave the allocation and monitoring to an
external device plugin
type: boolean
resourceName:
description: |-
Identifies the list of USB host devices.
e.g: kubevirt.io/storage, kubevirt.io/bootable-usb, etc
type: string
selectors:
items:
properties:
product:
type: string
vendor:
type: string
required:
- product
- vendor
type: object
type: array
x-kubernetes-list-type: atomic
required:
- resourceName
type: object
type: array
x-kubernetes-list-type: atomic
type: object
seccompConfiguration:
description: SeccompConfiguration holds Seccomp configuration
for Kubevirt components
properties:
virtualMachineInstanceProfile:
description: VirtualMachineInstanceProfile defines what profile
should be used with virt-launcher. Defaults to none
properties:
customProfile:
description: CustomProfile allows to request arbitrary
profile for virt-launcher
properties:
localhostProfile:
type: string
runtimeDefaultProfile:
type: boolean
type: object
type: object
type: object
selinuxLauncherType:
type: string
smbios:
properties:
family:
type: string
manufacturer:
type: string
product:
type: string
sku:
type: string
version:
type: string
type: object
supportContainerResources:
description: SupportContainerResources specifies the resource
requirements for various types of supporting containers such
as container disks/virtiofs/sidecars and hotplug attachment
pods. If omitted a sensible default will be supplied.
items:
description: SupportContainerResources are used to specify the
cpu/memory request and limits for the containers that support
various features of Virtual Machines. These containers are
usually idle and don't require a lot of memory or cpu.
properties:
resources:
description: |-
ResourceRequirementsWithoutClaims describes the compute resource requirements.
This struct was taken from the k8s.ResourceRequirements and cleaned up the 'Claims' field.
properties:
limits:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Limits describes the maximum amount of compute resources allowed.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
requests:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Requests describes the minimum amount of compute resources required.
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
otherwise to an implementation-defined value. Requests cannot exceed Limits.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
type:
type: string
required:
- resources
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
supportedGuestAgentVersions:
description: deprecated
items:
type: string
type: array
tlsConfiguration:
description: TLSConfiguration holds TLS options
properties:
ciphers:
items:
type: string
type: array
x-kubernetes-list-type: set
minTLSVersion:
description: |-
MinTLSVersion is a way to specify the minimum protocol version that is acceptable for TLS connections.
Protocol versions are based on the following most common TLS configurations:
https://ssl-config.mozilla.org/
Note that SSLv3.0 is not a supported protocol version due to well known
vulnerabilities such as POODLE: https://en.wikipedia.org/wiki/POODLE
enum:
- VersionTLS10
- VersionTLS11
- VersionTLS12
- VersionTLS13
type: string
type: object
virtualMachineInstancesPerNode:
type: integer
virtualMachineOptions:
description: VirtualMachineOptions holds the cluster level information
regarding the virtual machine.
properties:
disableFreePageReporting:
description: |-
DisableFreePageReporting disable the free page reporting of
memory balloon device https://libvirt.org/formatdomain.html#memory-balloon-device.
This will have effect only if AutoattachMemBalloon is not false and the vmi is not
requesting any high performance feature (dedicatedCPU/realtime/hugePages), in which free page reporting is always disabled.
type: object
disableSerialConsoleLog:
description: |-
DisableSerialConsoleLog disables logging the auto-attached default serial console.
If not set, serial console logs will be written to a file and then streamed from a container named 'guest-console-log'.
The value can be individually overridden for each VM, not relevant if AutoattachSerialConsole is disabled.
type: object
type: object
vmRolloutStrategy:
description: |-
VMRolloutStrategy defines how live-updatable fields, like CPU sockets, memory,
tolerations, and affinity, are propagated from a VM to its VMI.
enum:
- Stage
- LiveUpdate
nullable: true
type: string
vmStateStorageClass:
description: VMStateStorageClass is the name of the storage class
to use for the PVCs created to preserve VM state, like TPM.
type: string
webhookConfiguration:
description: |-
ReloadableComponentConfiguration holds all generic k8s configuration options which can
be reloaded by components without requiring a restart.
properties:
restClient:
description: RestClient can be used to tune certain aspects
of the k8s client in use.
properties:
rateLimiter:
description: RateLimiter allows selecting and configuring
different rate limiters for the k8s client.
properties:
tokenBucketRateLimiter:
properties:
burst:
description: |-
Maximum burst for throttle.
If it's zero, the component default will be used
type: integer
qps:
description: |-
QPS indicates the maximum QPS to the apiserver from this client.
If it's zero, the component default will be used
type: number
required:
- burst
- qps
type: object
type: object
type: object
type: object
type: object
customizeComponents:
properties:
flags:
description: Configure the value used for deployment and daemonset
resources
properties:
api:
additionalProperties:
type: string
type: object
controller:
additionalProperties:
type: string
type: object
handler:
additionalProperties:
type: string
type: object
type: object
patches:
items:
properties:
patch:
type: string
resourceName:
minLength: 1
type: string
resourceType:
minLength: 1
type: string
type:
type: string
required:
- patch
- resourceName
- resourceType
- type
type: object
type: array
x-kubernetes-list-type: atomic
type: object
imagePullPolicy:
description: The ImagePullPolicy to use.
type: string
imagePullSecrets:
description: |-
The imagePullSecrets to pull the container images from
Defaults to none
items:
description: |-
LocalObjectReference contains enough information to let you locate the
referenced object inside the same namespace.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
type: array
x-kubernetes-list-type: atomic
imageRegistry:
description: |-
The image registry to pull the container images from
Defaults to the same registry the operator's container image is pulled from.
type: string
imageTag:
description: |-
The image tag to use for the continer images installed.
Defaults to the same tag as the operator's container image.
type: string
infra:
description: selectors and tolerations that should apply to KubeVirt
infrastructure components
properties:
nodePlacement:
description: |-
nodePlacement describes scheduling configuration for specific
KubeVirt components
properties:
affinity:
description: |-
affinity enables pod affinity/anti-affinity placement expanding the types of constraints
that can be expressed with nodeSelector.
affinity is going to be applied to the relevant kind of pods in parallel with nodeSelector
See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity
properties:
nodeAffinity:
description: Describes node affinity scheduling rules
for the pod.
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node matches the corresponding matchExpressions; the
node(s) with the highest sum are the most preferred.
items:
description: |-
An empty preferred scheduling term matches all objects with implicit weight 0
(i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).
properties:
preference:
description: A node selector term, associated
with the corresponding weight.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
weight:
description: Weight associated with matching
the corresponding nodeSelectorTerm, in the
range 1-100.
format: int32
type: integer
required:
- preference
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to an update), the system
may or may not try to eventually evict the pod from its node.
properties:
nodeSelectorTerms:
description: Required. A list of node selector
terms. The terms are ORed.
items:
description: |-
A null or empty node selector term matches no objects. The requirements of
them are ANDed.
The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
type: array
x-kubernetes-list-type: atomic
required:
- nodeSelectorTerms
type: object
x-kubernetes-map-type: atomic
type: object
podAffinity:
description: Describes pod affinity scheduling rules (e.g.
co-locate this pod in the same node, zone, etc. as some
other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term,
associated with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
podAntiAffinity:
description: Describes pod anti-affinity scheduling rules
(e.g. avoid putting this pod in the same node, zone,
etc. as some other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the anti-affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling anti-affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term,
associated with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the anti-affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the anti-affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: object
nodeSelector:
additionalProperties:
type: string
description: |-
nodeSelector is the node selector applied to the relevant kind of pods
It specifies a map of key-value pairs: for the pod to be eligible to run on a node,
the node must have each of the indicated key-value pairs as labels
(it can have additional labels as well).
See https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
type: object
tolerations:
description: |-
tolerations is a list of tolerations applied to the relevant kind of pods
See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ for more info.
These are additional tolerations other than default ones.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple using the matching operator .
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
type: object
replicas:
description: |-
replicas indicates how many replicas should be created for each KubeVirt infrastructure
component (like virt-api or virt-controller). Defaults to 2.
WARNING: this is an advanced feature that prevents auto-scaling for core kubevirt components. Please use with caution!
type: integer
type: object
monitorAccount:
description: |-
The name of the Prometheus service account that needs read-access to KubeVirt endpoints
Defaults to prometheus-k8s
type: string
monitorNamespace:
description: |-
The namespace Prometheus is deployed in
Defaults to openshift-monitor
type: string
productComponent:
description: |-
Designate the apps.kubevirt.io/component label for KubeVirt components.
Useful if KubeVirt is included as part of a product.
If ProductComponent is not specified, the component label default value is kubevirt.
type: string
productName:
description: |-
Designate the apps.kubevirt.io/part-of label for KubeVirt components.
Useful if KubeVirt is included as part of a product.
If ProductName is not specified, the part-of label will be omitted.
type: string
productVersion:
description: |-
Designate the apps.kubevirt.io/version label for KubeVirt components.
Useful if KubeVirt is included as part of a product.
If ProductVersion is not specified, KubeVirt's version will be used.
type: string
serviceMonitorNamespace:
description: |-
The namespace the service monitor will be deployed
When ServiceMonitorNamespace is set, then we'll install the service monitor object in that namespace
otherwise we will use the monitoring namespace.
type: string
uninstallStrategy:
description: |-
Specifies if kubevirt can be deleted if workloads are still present.
This is mainly a precaution to avoid accidental data loss
type: string
workloadUpdateStrategy:
description: |-
WorkloadUpdateStrategy defines at the cluster level how to handle
automated workload updates
properties:
batchEvictionInterval:
description: |-
BatchEvictionInterval Represents the interval to wait before issuing the next
batch of shutdowns
Defaults to 1 minute
type: string
batchEvictionSize:
description: |-
BatchEvictionSize Represents the number of VMIs that can be forced updated per
the BatchShutdownInteral interval
Defaults to 10
type: integer
workloadUpdateMethods:
description: |-
WorkloadUpdateMethods defines the methods that can be used to disrupt workloads
during automated workload updates.
When multiple methods are present, the least disruptive method takes
precedence over more disruptive methods. For example if both LiveMigrate and Shutdown
methods are listed, only VMs which are not live migratable will be restarted/shutdown
An empty list defaults to no automated workload updating
items:
type: string
type: array
x-kubernetes-list-type: atomic
type: object
workloads:
description: selectors and tolerations that should apply to KubeVirt
workloads
properties:
nodePlacement:
description: |-
nodePlacement describes scheduling configuration for specific
KubeVirt components
properties:
affinity:
description: |-
affinity enables pod affinity/anti-affinity placement expanding the types of constraints
that can be expressed with nodeSelector.
affinity is going to be applied to the relevant kind of pods in parallel with nodeSelector
See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity
properties:
nodeAffinity:
description: Describes node affinity scheduling rules
for the pod.
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node matches the corresponding matchExpressions; the
node(s) with the highest sum are the most preferred.
items:
description: |-
An empty preferred scheduling term matches all objects with implicit weight 0
(i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).
properties:
preference:
description: A node selector term, associated
with the corresponding weight.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
weight:
description: Weight associated with matching
the corresponding nodeSelectorTerm, in the
range 1-100.
format: int32
type: integer
required:
- preference
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to an update), the system
may or may not try to eventually evict the pod from its node.
properties:
nodeSelectorTerms:
description: Required. A list of node selector
terms. The terms are ORed.
items:
description: |-
A null or empty node selector term matches no objects. The requirements of
them are ANDed.
The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
type: array
x-kubernetes-list-type: atomic
required:
- nodeSelectorTerms
type: object
x-kubernetes-map-type: atomic
type: object
podAffinity:
description: Describes pod affinity scheduling rules (e.g.
co-locate this pod in the same node, zone, etc. as some
other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term,
associated with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
podAntiAffinity:
description: Describes pod anti-affinity scheduling rules
(e.g. avoid putting this pod in the same node, zone,
etc. as some other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the anti-affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling anti-affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term,
associated with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the anti-affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the anti-affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: object
nodeSelector:
additionalProperties:
type: string
description: |-
nodeSelector is the node selector applied to the relevant kind of pods
It specifies a map of key-value pairs: for the pod to be eligible to run on a node,
the node must have each of the indicated key-value pairs as labels
(it can have additional labels as well).
See https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
type: object
tolerations:
description: |-
tolerations is a list of tolerations applied to the relevant kind of pods
See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ for more info.
These are additional tolerations other than default ones.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple using the matching operator .
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
type: object
replicas:
description: |-
replicas indicates how many replicas should be created for each KubeVirt infrastructure
component (like virt-api or virt-controller). Defaults to 2.
WARNING: this is an advanced feature that prevents auto-scaling for core kubevirt components. Please use with caution!
type: integer
type: object
type: object
status:
description: KubeVirtStatus represents information pertaining to a KubeVirt
deployment.
properties:
conditions:
items:
description: KubeVirtCondition represents a condition of a KubeVirt
deployment
properties:
lastProbeTime:
format: date-time
nullable: true
type: string
lastTransitionTime:
format: date-time
nullable: true
type: string
message:
type: string
reason:
type: string
status:
type: string
type:
type: string
required:
- status
- type
type: object
type: array
defaultArchitecture:
type: string
generations:
items:
description: GenerationStatus keeps track of the generation for
a given resource so that decisions about forced updates can be
made.
properties:
group:
description: group is the group of the thing you're tracking
type: string
hash:
description: hash is an optional field set for resources without
generation that are content sensitive like secrets and configmaps
type: string
lastGeneration:
description: lastGeneration is the last generation of the workload
controller involved
format: int64
type: integer
name:
description: name is the name of the thing you're tracking
type: string
namespace:
description: namespace is where the thing you're tracking is
type: string
resource:
description: resource is the resource type of the thing you're
tracking
type: string
required:
- group
- lastGeneration
- name
- resource
type: object
type: array
x-kubernetes-list-type: atomic
observedDeploymentConfig:
type: string
observedDeploymentID:
type: string
observedGeneration:
format: int64
type: integer
observedKubeVirtRegistry:
type: string
observedKubeVirtVersion:
type: string
operatorVersion:
type: string
outdatedVirtualMachineInstanceWorkloads:
type: integer
phase:
description: KubeVirtPhase is a label for the phase of a KubeVirt
deployment at the current time.
type: string
targetDeploymentConfig:
type: string
targetDeploymentID:
type: string
targetKubeVirtRegistry:
type: string
targetKubeVirtVersion:
type: string
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- jsonPath: .status.phase
name: Phase
type: string
deprecated: true
deprecationWarning: kubevirt.io/v1alpha3 is now deprecated and will be removed
in a future release.
name: v1alpha3
schema:
openAPIV3Schema:
description: KubeVirt represents the object deploying all KubeVirt resources
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
certificateRotateStrategy:
properties:
selfSigned:
properties:
ca:
description: |-
CA configuration
CA certs are kept in the CA bundle as long as they are valid
properties:
duration:
description: The requested 'duration' (i.e. lifetime)
of the Certificate.
type: string
renewBefore:
description: |-
The amount of time before the currently issued certificate's "notAfter"
time that we will begin to attempt to renew the certificate.
type: string
type: object
caOverlapInterval:
description: Deprecated. Use CA.Duration and CA.RenewBefore
instead
type: string
caRotateInterval:
description: Deprecated. Use CA.Duration instead
type: string
certRotateInterval:
description: Deprecated. Use Server.Duration instead
type: string
server:
description: |-
Server configuration
Certs are rotated and discarded
properties:
duration:
description: The requested 'duration' (i.e. lifetime)
of the Certificate.
type: string
renewBefore:
description: |-
The amount of time before the currently issued certificate's "notAfter"
time that we will begin to attempt to renew the certificate.
type: string
type: object
type: object
type: object
configuration:
description: |-
holds kubevirt configurations.
same as the virt-configMap
properties:
additionalGuestMemoryOverheadRatio:
description: |-
AdditionalGuestMemoryOverheadRatio can be used to increase the virtualization infrastructure
overhead. This is useful, since the calculation of this overhead is not accurate and cannot
be entirely known in advance. The ratio that is being set determines by which factor to increase
the overhead calculated by Kubevirt. A higher ratio means that the VMs would be less compromised
by node pressures, but would mean that fewer VMs could be scheduled to a node.
If not set, the default is 1.
type: string
apiConfiguration:
description: |-
ReloadableComponentConfiguration holds all generic k8s configuration options which can
be reloaded by components without requiring a restart.
properties:
restClient:
description: RestClient can be used to tune certain aspects
of the k8s client in use.
properties:
rateLimiter:
description: RateLimiter allows selecting and configuring
different rate limiters for the k8s client.
properties:
tokenBucketRateLimiter:
properties:
burst:
description: |-
Maximum burst for throttle.
If it's zero, the component default will be used
type: integer
qps:
description: |-
QPS indicates the maximum QPS to the apiserver from this client.
If it's zero, the component default will be used
type: number
required:
- burst
- qps
type: object
type: object
type: object
type: object
architectureConfiguration:
properties:
amd64:
properties:
emulatedMachines:
items:
type: string
type: array
x-kubernetes-list-type: atomic
machineType:
type: string
ovmfPath:
type: string
type: object
arm64:
properties:
emulatedMachines:
items:
type: string
type: array
x-kubernetes-list-type: atomic
machineType:
type: string
ovmfPath:
type: string
type: object
defaultArchitecture:
type: string
ppc64le:
properties:
emulatedMachines:
items:
type: string
type: array
x-kubernetes-list-type: atomic
machineType:
type: string
ovmfPath:
type: string
type: object
type: object
autoCPULimitNamespaceLabelSelector:
description: |-
When set, AutoCPULimitNamespaceLabelSelector will set a CPU limit on virt-launcher for VMIs running inside
namespaces that match the label selector.
The CPU limit will equal the number of requested vCPUs.
This setting does not apply to VMIs with dedicated CPUs.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
commonInstancetypesDeployment:
description: CommonInstancetypesDeployment controls the deployment
of common-instancetypes resources
nullable: true
properties:
enabled:
description: Enabled controls the deployment of common-instancetypes
resources, defaults to True.
nullable: true
type: boolean
type: object
controllerConfiguration:
description: |-
ReloadableComponentConfiguration holds all generic k8s configuration options which can
be reloaded by components without requiring a restart.
properties:
restClient:
description: RestClient can be used to tune certain aspects
of the k8s client in use.
properties:
rateLimiter:
description: RateLimiter allows selecting and configuring
different rate limiters for the k8s client.
properties:
tokenBucketRateLimiter:
properties:
burst:
description: |-
Maximum burst for throttle.
If it's zero, the component default will be used
type: integer
qps:
description: |-
QPS indicates the maximum QPS to the apiserver from this client.
If it's zero, the component default will be used
type: number
required:
- burst
- qps
type: object
type: object
type: object
type: object
cpuModel:
type: string
cpuRequest:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
defaultRuntimeClass:
type: string
developerConfiguration:
description: DeveloperConfiguration holds developer options
properties:
cpuAllocationRatio:
description: |-
For each requested virtual CPU, CPUAllocationRatio defines how much physical CPU to request per VMI
from the hosting node. The value is in fraction of a CPU thread (or core on non-hyperthreaded nodes).
For example, a value of 1 means 1 physical CPU thread per VMI CPU thread.
A value of 100 would be 1% of a physical thread allocated for each requested VMI thread.
This option has no effect on VMIs that request dedicated CPUs. More information at:
https://kubevirt.io/user-guide/operations/node_overcommit/#node-cpu-allocation-ratio
Defaults to 10
type: integer
diskVerification:
description: DiskVerification holds container disks verification
limits
properties:
memoryLimit:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
required:
- memoryLimit
type: object
featureGates:
description: FeatureGates is the list of experimental features
to enable. Defaults to none
items:
type: string
type: array
logVerbosity:
description: LogVerbosity sets log verbosity level of various
components
properties:
nodeVerbosity:
additionalProperties:
type: integer
description: NodeVerbosity represents a map of nodes with
a specific verbosity level
type: object
virtAPI:
type: integer
virtController:
type: integer
virtHandler:
type: integer
virtLauncher:
type: integer
virtOperator:
type: integer
type: object
memoryOvercommit:
description: |-
MemoryOvercommit is the percentage of memory we want to give VMIs compared to the amount
given to its parent pod (virt-launcher). For example, a value of 102 means the VMI will
"see" 2% more memory than its parent pod. Values under 100 are effectively "undercommits".
Overcommits can lead to memory exhaustion, which in turn can lead to crashes. Use carefully.
Defaults to 100
type: integer
minimumClusterTSCFrequency:
description: |-
Allow overriding the automatically determined minimum TSC frequency of the cluster
and fixate the minimum to this frequency.
format: int64
type: integer
minimumReservePVCBytes:
description: |-
MinimumReservePVCBytes is the amount of space, in bytes, to leave unused on disks.
Defaults to 131072 (128KiB)
format: int64
type: integer
nodeSelectors:
additionalProperties:
type: string
description: |-
NodeSelectors allows restricting VMI creation to nodes that match a set of labels.
Defaults to none
type: object
pvcTolerateLessSpaceUpToPercent:
description: |-
LessPVCSpaceToleration determines how much smaller, in percentage, disk PVCs are
allowed to be compared to the requested size (to account for various overheads).
Defaults to 10
type: integer
useEmulation:
description: |-
UseEmulation can be set to true to allow fallback to software emulation
in case hardware-assisted emulation is not available. Defaults to false
type: boolean
type: object
emulatedMachines:
description: Deprecated. Use architectureConfiguration instead.
items:
type: string
type: array
evictionStrategy:
description: |-
EvictionStrategy defines at the cluster level if the VirtualMachineInstance should be
migrated instead of shut-off in case of a node drain. If the VirtualMachineInstance specific
field is set it overrides the cluster level one.
type: string
handlerConfiguration:
description: |-
ReloadableComponentConfiguration holds all generic k8s configuration options which can
be reloaded by components without requiring a restart.
properties:
restClient:
description: RestClient can be used to tune certain aspects
of the k8s client in use.
properties:
rateLimiter:
description: RateLimiter allows selecting and configuring
different rate limiters for the k8s client.
properties:
tokenBucketRateLimiter:
properties:
burst:
description: |-
Maximum burst for throttle.
If it's zero, the component default will be used
type: integer
qps:
description: |-
QPS indicates the maximum QPS to the apiserver from this client.
If it's zero, the component default will be used
type: number
required:
- burst
- qps
type: object
type: object
type: object
type: object
imagePullPolicy:
description: PullPolicy describes a policy for if/when to pull
a container image
type: string
instancetype:
description: Instancetype configuration
nullable: true
properties:
referencePolicy:
description: |-
ReferencePolicy defines how an instance type or preference should be referenced by the VM after submission, supported values are:
reference (default) - Where a copy of the original object is stashed in a ControllerRevision and referenced by the VM.
expand - Where the instance type or preference are expanded into the VM if no revisionNames have been populated.
expandAll - Where the instance type or preference are expanded into the VM regardless of revisionNames previously being populated.
enum:
- reference
- expand
- expandAll
nullable: true
type: string
type: object
ksmConfiguration:
description: KSMConfiguration holds the information regarding
the enabling the KSM in the nodes (if available).
properties:
nodeLabelSelector:
description: |-
NodeLabelSelector is a selector that filters in which nodes the KSM will be enabled.
Empty NodeLabelSelector will enable ksm for every node.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: object
liveUpdateConfiguration:
description: LiveUpdateConfiguration holds defaults for live update
features
properties:
maxCpuSockets:
description: |-
MaxCpuSockets provides a MaxSockets value for VMs that do not provide their own.
For VMs with more sockets than maximum the MaxSockets will be set to equal number of sockets.
format: int32
type: integer
maxGuest:
anyOf:
- type: integer
- type: string
description: |-
MaxGuest defines the maximum amount memory that can be allocated
to the guest using hotplug.
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
maxHotplugRatio:
description: |-
MaxHotplugRatio is the ratio used to define the max amount
of a hotplug resource that can be made available to a VM
when the specific Max* setting is not defined (MaxCpuSockets, MaxGuest)
Example: VM is configured with 512Mi of guest memory, if MaxGuest is not
defined and MaxHotplugRatio is 2 then MaxGuest = 1Gi
defaults to 4
format: int32
type: integer
type: object
machineType:
description: Deprecated. Use architectureConfiguration instead.
type: string
mediatedDevicesConfiguration:
description: MediatedDevicesConfiguration holds information about
MDEV types to be defined, if available
properties:
mediatedDeviceTypes:
items:
type: string
type: array
x-kubernetes-list-type: atomic
mediatedDevicesTypes:
description: Deprecated. Use mediatedDeviceTypes instead.
items:
type: string
type: array
x-kubernetes-list-type: atomic
nodeMediatedDeviceTypes:
items:
description: NodeMediatedDeviceTypesConfig holds information
about MDEV types to be defined in a specific node that
matches the NodeSelector field.
properties:
mediatedDeviceTypes:
items:
type: string
type: array
x-kubernetes-list-type: atomic
mediatedDevicesTypes:
description: Deprecated. Use mediatedDeviceTypes instead.
items:
type: string
type: array
x-kubernetes-list-type: atomic
nodeSelector:
additionalProperties:
type: string
description: |-
NodeSelector is a selector which must be true for the vmi to fit on a node.
Selector which must match a node's labels for the vmi to be scheduled on that node.
More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/
type: object
required:
- nodeSelector
type: object
type: array
x-kubernetes-list-type: atomic
type: object
memBalloonStatsPeriod:
format: int32
type: integer
migrations:
description: |-
MigrationConfiguration holds migration options.
Can be overridden for specific groups of VMs though migration policies.
Visit https://kubevirt.io/user-guide/operations/migration_policies/ for more information.
properties:
allowAutoConverge:
description: |-
AllowAutoConverge allows the platform to compromise performance/availability of VMIs to
guarantee successful VMI live migrations. Defaults to false
type: boolean
allowPostCopy:
description: |-
AllowPostCopy enables post-copy live migrations. Such migrations allow even the busiest VMIs
to successfully live-migrate. However, events like a network failure can cause a VMI crash.
If set to true, migrations will still start in pre-copy, but switch to post-copy when
CompletionTimeoutPerGiB triggers. Defaults to false
type: boolean
allowWorkloadDisruption:
description: |-
AllowWorkloadDisruption indicates that the migration shouldn't be
canceled after acceptableCompletionTime is exceeded. Instead, if
permitted, migration will be switched to post-copy or the VMI will be
paused to allow the migration to complete
type: boolean
bandwidthPerMigration:
anyOf:
- type: integer
- type: string
description: |-
BandwidthPerMigration limits the amount of network bandwidth live migrations are allowed to use.
The value is in quantity per second. Defaults to 0 (no limit)
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
completionTimeoutPerGiB:
description: |-
CompletionTimeoutPerGiB is the maximum number of seconds per GiB a migration is allowed to take.
If the timeout is reached, the migration will be either paused, switched
to post-copy or cancelled depending on other settings. Defaults to 150
format: int64
type: integer
disableTLS:
description: |-
When set to true, DisableTLS will disable the additional layer of live migration encryption
provided by KubeVirt. This is usually a bad idea. Defaults to false
type: boolean
matchSELinuxLevelOnMigration:
description: |-
By default, the SELinux level of target virt-launcher pods is forced to the level of the source virt-launcher.
When set to true, MatchSELinuxLevelOnMigration lets the CRI auto-assign a random level to the target.
That will ensure the target virt-launcher doesn't share categories with another pod on the node.
However, migrations will fail when using RWX volumes that don't automatically deal with SELinux levels.
type: boolean
network:
description: |-
Network is the name of the CNI network to use for live migrations. By default, migrations go
through the pod network.
type: string
nodeDrainTaintKey:
description: |-
NodeDrainTaintKey defines the taint key that indicates a node should be drained.
Note: this option relies on the deprecated node taint feature. Default: kubevirt.io/drain
type: string
parallelMigrationsPerCluster:
description: |-
ParallelMigrationsPerCluster is the total number of concurrent live migrations
allowed cluster-wide. Defaults to 5
format: int32
type: integer
parallelOutboundMigrationsPerNode:
description: |-
ParallelOutboundMigrationsPerNode is the maximum number of concurrent outgoing live migrations
allowed per node. Defaults to 2
format: int32
type: integer
progressTimeout:
description: |-
ProgressTimeout is the maximum number of seconds a live migration is allowed to make no progress.
Hitting this timeout means a migration transferred 0 data for that many seconds. The migration is
then considered stuck and therefore cancelled. Defaults to 150
format: int64
type: integer
unsafeMigrationOverride:
description: |-
UnsafeMigrationOverride allows live migrations to occur even if the compatibility check
indicates the migration will be unsafe to the guest. Defaults to false
type: boolean
type: object
minCPUModel:
type: string
network:
description: NetworkConfiguration holds network options
properties:
binding:
additionalProperties:
properties:
computeResourceOverhead:
description: |-
ComputeResourceOverhead specifies the resource overhead that should be added to the compute container when using the binding.
version: v1alphav1
properties:
limits:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Limits describes the maximum amount of compute resources allowed.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
requests:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Requests describes the minimum amount of compute resources required.
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
otherwise to an implementation-defined value. Requests cannot exceed Limits.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
domainAttachmentType:
description: |-
DomainAttachmentType is a standard domain network attachment method kubevirt supports.
Supported values: "tap", "managedTap" (since v1.4).
The standard domain attachment can be used instead or in addition to the sidecarImage.
version: 1alphav1
type: string
downwardAPI:
description: |-
DownwardAPI specifies what kind of data should be exposed to the binding plugin sidecar.
Supported values: "device-info"
version: v1alphav1
type: string
migration:
description: |-
Migration means the VM using the plugin can be safely migrated
version: 1alphav1
properties:
method:
description: |-
Method defines a pre-defined migration methodology
version: 1alphav1
type: string
type: object
networkAttachmentDefinition:
description: |-
NetworkAttachmentDefinition references to a NetworkAttachmentDefinition CR object.
Format: , /.
If namespace is not specified, VMI namespace is assumed.
version: 1alphav1
type: string
sidecarImage:
description: |-
SidecarImage references a container image that runs in the virt-launcher pod.
The sidecar handles (libvirt) domain configuration and optional services.
version: 1alphav1
type: string
type: object
type: object
defaultNetworkInterface:
type: string
permitBridgeInterfaceOnPodNetwork:
type: boolean
permitSlirpInterface:
description: |-
DeprecatedPermitSlirpInterface is an alias for the deprecated PermitSlirpInterface.
Deprecated: Removed in v1.3.
type: boolean
type: object
obsoleteCPUModels:
additionalProperties:
type: boolean
type: object
ovmfPath:
description: Deprecated. Use architectureConfiguration instead.
type: string
permittedHostDevices:
description: PermittedHostDevices holds information about devices
allowed for passthrough
properties:
mediatedDevices:
items:
description: MediatedHostDevice represents a host mediated
device allowed for passthrough
properties:
externalResourceProvider:
type: boolean
mdevNameSelector:
type: string
resourceName:
type: string
required:
- mdevNameSelector
- resourceName
type: object
type: array
x-kubernetes-list-type: atomic
pciHostDevices:
items:
description: PciHostDevice represents a host PCI device
allowed for passthrough
properties:
externalResourceProvider:
description: |-
If true, KubeVirt will leave the allocation and monitoring to an
external device plugin
type: boolean
pciVendorSelector:
description: The vendor_id:product_id tuple of the PCI
device
type: string
resourceName:
description: |-
The name of the resource that is representing the device. Exposed by
a device plugin and requested by VMs. Typically of the form
vendor.com/product_name
type: string
required:
- pciVendorSelector
- resourceName
type: object
type: array
x-kubernetes-list-type: atomic
usb:
items:
properties:
externalResourceProvider:
description: |-
If true, KubeVirt will leave the allocation and monitoring to an
external device plugin
type: boolean
resourceName:
description: |-
Identifies the list of USB host devices.
e.g: kubevirt.io/storage, kubevirt.io/bootable-usb, etc
type: string
selectors:
items:
properties:
product:
type: string
vendor:
type: string
required:
- product
- vendor
type: object
type: array
x-kubernetes-list-type: atomic
required:
- resourceName
type: object
type: array
x-kubernetes-list-type: atomic
type: object
seccompConfiguration:
description: SeccompConfiguration holds Seccomp configuration
for Kubevirt components
properties:
virtualMachineInstanceProfile:
description: VirtualMachineInstanceProfile defines what profile
should be used with virt-launcher. Defaults to none
properties:
customProfile:
description: CustomProfile allows to request arbitrary
profile for virt-launcher
properties:
localhostProfile:
type: string
runtimeDefaultProfile:
type: boolean
type: object
type: object
type: object
selinuxLauncherType:
type: string
smbios:
properties:
family:
type: string
manufacturer:
type: string
product:
type: string
sku:
type: string
version:
type: string
type: object
supportContainerResources:
description: SupportContainerResources specifies the resource
requirements for various types of supporting containers such
as container disks/virtiofs/sidecars and hotplug attachment
pods. If omitted a sensible default will be supplied.
items:
description: SupportContainerResources are used to specify the
cpu/memory request and limits for the containers that support
various features of Virtual Machines. These containers are
usually idle and don't require a lot of memory or cpu.
properties:
resources:
description: |-
ResourceRequirementsWithoutClaims describes the compute resource requirements.
This struct was taken from the k8s.ResourceRequirements and cleaned up the 'Claims' field.
properties:
limits:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Limits describes the maximum amount of compute resources allowed.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
requests:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Requests describes the minimum amount of compute resources required.
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
otherwise to an implementation-defined value. Requests cannot exceed Limits.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
type:
type: string
required:
- resources
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
supportedGuestAgentVersions:
description: deprecated
items:
type: string
type: array
tlsConfiguration:
description: TLSConfiguration holds TLS options
properties:
ciphers:
items:
type: string
type: array
x-kubernetes-list-type: set
minTLSVersion:
description: |-
MinTLSVersion is a way to specify the minimum protocol version that is acceptable for TLS connections.
Protocol versions are based on the following most common TLS configurations:
https://ssl-config.mozilla.org/
Note that SSLv3.0 is not a supported protocol version due to well known
vulnerabilities such as POODLE: https://en.wikipedia.org/wiki/POODLE
enum:
- VersionTLS10
- VersionTLS11
- VersionTLS12
- VersionTLS13
type: string
type: object
virtualMachineInstancesPerNode:
type: integer
virtualMachineOptions:
description: VirtualMachineOptions holds the cluster level information
regarding the virtual machine.
properties:
disableFreePageReporting:
description: |-
DisableFreePageReporting disable the free page reporting of
memory balloon device https://libvirt.org/formatdomain.html#memory-balloon-device.
This will have effect only if AutoattachMemBalloon is not false and the vmi is not
requesting any high performance feature (dedicatedCPU/realtime/hugePages), in which free page reporting is always disabled.
type: object
disableSerialConsoleLog:
description: |-
DisableSerialConsoleLog disables logging the auto-attached default serial console.
If not set, serial console logs will be written to a file and then streamed from a container named 'guest-console-log'.
The value can be individually overridden for each VM, not relevant if AutoattachSerialConsole is disabled.
type: object
type: object
vmRolloutStrategy:
description: |-
VMRolloutStrategy defines how live-updatable fields, like CPU sockets, memory,
tolerations, and affinity, are propagated from a VM to its VMI.
enum:
- Stage
- LiveUpdate
nullable: true
type: string
vmStateStorageClass:
description: VMStateStorageClass is the name of the storage class
to use for the PVCs created to preserve VM state, like TPM.
type: string
webhookConfiguration:
description: |-
ReloadableComponentConfiguration holds all generic k8s configuration options which can
be reloaded by components without requiring a restart.
properties:
restClient:
description: RestClient can be used to tune certain aspects
of the k8s client in use.
properties:
rateLimiter:
description: RateLimiter allows selecting and configuring
different rate limiters for the k8s client.
properties:
tokenBucketRateLimiter:
properties:
burst:
description: |-
Maximum burst for throttle.
If it's zero, the component default will be used
type: integer
qps:
description: |-
QPS indicates the maximum QPS to the apiserver from this client.
If it's zero, the component default will be used
type: number
required:
- burst
- qps
type: object
type: object
type: object
type: object
type: object
customizeComponents:
properties:
flags:
description: Configure the value used for deployment and daemonset
resources
properties:
api:
additionalProperties:
type: string
type: object
controller:
additionalProperties:
type: string
type: object
handler:
additionalProperties:
type: string
type: object
type: object
patches:
items:
properties:
patch:
type: string
resourceName:
minLength: 1
type: string
resourceType:
minLength: 1
type: string
type:
type: string
required:
- patch
- resourceName
- resourceType
- type
type: object
type: array
x-kubernetes-list-type: atomic
type: object
imagePullPolicy:
description: The ImagePullPolicy to use.
type: string
imagePullSecrets:
description: |-
The imagePullSecrets to pull the container images from
Defaults to none
items:
description: |-
LocalObjectReference contains enough information to let you locate the
referenced object inside the same namespace.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
type: array
x-kubernetes-list-type: atomic
imageRegistry:
description: |-
The image registry to pull the container images from
Defaults to the same registry the operator's container image is pulled from.
type: string
imageTag:
description: |-
The image tag to use for the continer images installed.
Defaults to the same tag as the operator's container image.
type: string
infra:
description: selectors and tolerations that should apply to KubeVirt
infrastructure components
properties:
nodePlacement:
description: |-
nodePlacement describes scheduling configuration for specific
KubeVirt components
properties:
affinity:
description: |-
affinity enables pod affinity/anti-affinity placement expanding the types of constraints
that can be expressed with nodeSelector.
affinity is going to be applied to the relevant kind of pods in parallel with nodeSelector
See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity
properties:
nodeAffinity:
description: Describes node affinity scheduling rules
for the pod.
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node matches the corresponding matchExpressions; the
node(s) with the highest sum are the most preferred.
items:
description: |-
An empty preferred scheduling term matches all objects with implicit weight 0
(i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).
properties:
preference:
description: A node selector term, associated
with the corresponding weight.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
weight:
description: Weight associated with matching
the corresponding nodeSelectorTerm, in the
range 1-100.
format: int32
type: integer
required:
- preference
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to an update), the system
may or may not try to eventually evict the pod from its node.
properties:
nodeSelectorTerms:
description: Required. A list of node selector
terms. The terms are ORed.
items:
description: |-
A null or empty node selector term matches no objects. The requirements of
them are ANDed.
The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
type: array
x-kubernetes-list-type: atomic
required:
- nodeSelectorTerms
type: object
x-kubernetes-map-type: atomic
type: object
podAffinity:
description: Describes pod affinity scheduling rules (e.g.
co-locate this pod in the same node, zone, etc. as some
other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term,
associated with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
podAntiAffinity:
description: Describes pod anti-affinity scheduling rules
(e.g. avoid putting this pod in the same node, zone,
etc. as some other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the anti-affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling anti-affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term,
associated with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the anti-affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the anti-affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: object
nodeSelector:
additionalProperties:
type: string
description: |-
nodeSelector is the node selector applied to the relevant kind of pods
It specifies a map of key-value pairs: for the pod to be eligible to run on a node,
the node must have each of the indicated key-value pairs as labels
(it can have additional labels as well).
See https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
type: object
tolerations:
description: |-
tolerations is a list of tolerations applied to the relevant kind of pods
See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ for more info.
These are additional tolerations other than default ones.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple using the matching operator .
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
type: object
replicas:
description: |-
replicas indicates how many replicas should be created for each KubeVirt infrastructure
component (like virt-api or virt-controller). Defaults to 2.
WARNING: this is an advanced feature that prevents auto-scaling for core kubevirt components. Please use with caution!
type: integer
type: object
monitorAccount:
description: |-
The name of the Prometheus service account that needs read-access to KubeVirt endpoints
Defaults to prometheus-k8s
type: string
monitorNamespace:
description: |-
The namespace Prometheus is deployed in
Defaults to openshift-monitor
type: string
productComponent:
description: |-
Designate the apps.kubevirt.io/component label for KubeVirt components.
Useful if KubeVirt is included as part of a product.
If ProductComponent is not specified, the component label default value is kubevirt.
type: string
productName:
description: |-
Designate the apps.kubevirt.io/part-of label for KubeVirt components.
Useful if KubeVirt is included as part of a product.
If ProductName is not specified, the part-of label will be omitted.
type: string
productVersion:
description: |-
Designate the apps.kubevirt.io/version label for KubeVirt components.
Useful if KubeVirt is included as part of a product.
If ProductVersion is not specified, KubeVirt's version will be used.
type: string
serviceMonitorNamespace:
description: |-
The namespace the service monitor will be deployed
When ServiceMonitorNamespace is set, then we'll install the service monitor object in that namespace
otherwise we will use the monitoring namespace.
type: string
uninstallStrategy:
description: |-
Specifies if kubevirt can be deleted if workloads are still present.
This is mainly a precaution to avoid accidental data loss
type: string
workloadUpdateStrategy:
description: |-
WorkloadUpdateStrategy defines at the cluster level how to handle
automated workload updates
properties:
batchEvictionInterval:
description: |-
BatchEvictionInterval Represents the interval to wait before issuing the next
batch of shutdowns
Defaults to 1 minute
type: string
batchEvictionSize:
description: |-
BatchEvictionSize Represents the number of VMIs that can be forced updated per
the BatchShutdownInteral interval
Defaults to 10
type: integer
workloadUpdateMethods:
description: |-
WorkloadUpdateMethods defines the methods that can be used to disrupt workloads
during automated workload updates.
When multiple methods are present, the least disruptive method takes
precedence over more disruptive methods. For example if both LiveMigrate and Shutdown
methods are listed, only VMs which are not live migratable will be restarted/shutdown
An empty list defaults to no automated workload updating
items:
type: string
type: array
x-kubernetes-list-type: atomic
type: object
workloads:
description: selectors and tolerations that should apply to KubeVirt
workloads
properties:
nodePlacement:
description: |-
nodePlacement describes scheduling configuration for specific
KubeVirt components
properties:
affinity:
description: |-
affinity enables pod affinity/anti-affinity placement expanding the types of constraints
that can be expressed with nodeSelector.
affinity is going to be applied to the relevant kind of pods in parallel with nodeSelector
See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity
properties:
nodeAffinity:
description: Describes node affinity scheduling rules
for the pod.
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node matches the corresponding matchExpressions; the
node(s) with the highest sum are the most preferred.
items:
description: |-
An empty preferred scheduling term matches all objects with implicit weight 0
(i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op).
properties:
preference:
description: A node selector term, associated
with the corresponding weight.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
weight:
description: Weight associated with matching
the corresponding nodeSelectorTerm, in the
range 1-100.
format: int32
type: integer
required:
- preference
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to an update), the system
may or may not try to eventually evict the pod from its node.
properties:
nodeSelectorTerms:
description: Required. A list of node selector
terms. The terms are ORed.
items:
description: |-
A null or empty node selector term matches no objects. The requirements of
them are ANDed.
The TopologySelectorTerm type implements a subset of the NodeSelectorTerm.
properties:
matchExpressions:
description: A list of node selector requirements
by node's labels.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchFields:
description: A list of node selector requirements
by node's fields.
items:
description: |-
A node selector requirement is a selector that contains values, a key, and an operator
that relates the key and values.
properties:
key:
description: The label key that the
selector applies to.
type: string
operator:
description: |-
Represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt.
type: string
values:
description: |-
An array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. If the operator is Gt or Lt, the values
array must have a single element, which will be interpreted as an integer.
This array is replaced during a strategic merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
type: object
x-kubernetes-map-type: atomic
type: array
x-kubernetes-list-type: atomic
required:
- nodeSelectorTerms
type: object
x-kubernetes-map-type: atomic
type: object
podAffinity:
description: Describes pod affinity scheduling rules (e.g.
co-locate this pod in the same node, zone, etc. as some
other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term,
associated with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
podAntiAffinity:
description: Describes pod anti-affinity scheduling rules
(e.g. avoid putting this pod in the same node, zone,
etc. as some other pod(s)).
properties:
preferredDuringSchedulingIgnoredDuringExecution:
description: |-
The scheduler will prefer to schedule pods to nodes that satisfy
the anti-affinity expressions specified by this field, but it may choose
a node that violates one or more of the expressions. The node that is
most preferred is the one with the greatest sum of weights, i.e.
for each node that meets all of the scheduling requirements (resource
request, requiredDuringScheduling anti-affinity expressions, etc.),
compute a sum by iterating through the elements of this field and adding
"weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the
node(s) with the highest sum are the most preferred.
items:
description: The weights of all of the matched WeightedPodAffinityTerm
fields are added per-node to find the most preferred
node(s)
properties:
podAffinityTerm:
description: Required. A pod affinity term,
associated with the corresponding weight.
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The
requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label
key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
weight:
description: |-
weight associated with matching the corresponding podAffinityTerm,
in the range 1-100.
format: int32
type: integer
required:
- podAffinityTerm
- weight
type: object
type: array
x-kubernetes-list-type: atomic
requiredDuringSchedulingIgnoredDuringExecution:
description: |-
If the anti-affinity requirements specified by this field are not met at
scheduling time, the pod will not be scheduled onto the node.
If the anti-affinity requirements specified by this field cease to be met
at some point during pod execution (e.g. due to a pod label update), the
system may or may not try to eventually evict the pod from its node.
When there are multiple elements, the lists of nodes corresponding to each
podAffinityTerm are intersected, i.e. all terms must be satisfied.
items:
description: |-
Defines a set of pods (namely those matching the labelSelector
relative to the given namespace(s)) that this pod should be
co-located (affinity) or not co-located (anti-affinity) with,
where co-located is defined as running on a node whose value of
the label with key matches that of any node on which
a pod of the set of pods is running
properties:
labelSelector:
description: |-
A label query over a set of resources, in this case pods.
If it's null, this PodAffinityTerm matches with no Pods.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key in (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both matchLabelKeys and labelSelector.
Also, matchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
mismatchLabelKeys:
description: |-
MismatchLabelKeys is a set of pod label keys to select which pods will
be taken into consideration. The keys are used to lookup values from the
incoming pod labels, those key-value labels are merged with 'labelSelector' as 'key notin (value)'
to select the group of existing pods which pods will be taken into consideration
for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming
pod labels will be ignored. The default value is empty.
The same key is forbidden to exist in both mismatchLabelKeys and labelSelector.
Also, mismatchLabelKeys cannot be set when labelSelector isn't set.
This is a beta field and requires enabling MatchLabelKeysInPodAffinity feature gate (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
namespaceSelector:
description: |-
A label query over the set of namespaces that the term applies to.
The term is applied to the union of the namespaces selected by this field
and the ones listed in the namespaces field.
null selector and null or empty namespaces list means "this pod's namespace".
An empty selector ({}) matches all namespaces.
properties:
matchExpressions:
description: matchExpressions is a list
of label selector requirements. The requirements
are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key
that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
namespaces:
description: |-
namespaces specifies a static list of namespace names that the term applies to.
The term is applied to the union of the namespaces listed in this field
and the ones selected by namespaceSelector.
null or empty namespaces list and null namespaceSelector means "this pod's namespace".
items:
type: string
type: array
x-kubernetes-list-type: atomic
topologyKey:
description: |-
This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching
the labelSelector in the specified namespaces, where co-located is defined as running on a node
whose value of the label with key topologyKey matches that of any node on which any of the
selected pods is running.
Empty topologyKey is not allowed.
type: string
required:
- topologyKey
type: object
type: array
x-kubernetes-list-type: atomic
type: object
type: object
nodeSelector:
additionalProperties:
type: string
description: |-
nodeSelector is the node selector applied to the relevant kind of pods
It specifies a map of key-value pairs: for the pod to be eligible to run on a node,
the node must have each of the indicated key-value pairs as labels
(it can have additional labels as well).
See https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
type: object
tolerations:
description: |-
tolerations is a list of tolerations applied to the relevant kind of pods
See https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ for more info.
These are additional tolerations other than default ones.
items:
description: |-
The pod this Toleration is attached to tolerates any taint that matches
the triple using the matching operator .
properties:
effect:
description: |-
Effect indicates the taint effect to match. Empty means match all taint effects.
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: |-
Key is the taint key that the toleration applies to. Empty means match all taint keys.
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: |-
Operator represents a key's relationship to the value.
Valid operators are Exists and Equal. Defaults to Equal.
Exists is equivalent to wildcard for value, so that a pod can
tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: |-
TolerationSeconds represents the period of time the toleration (which must be
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
it is not set, which means tolerate the taint forever (do not evict). Zero and
negative values will be treated as 0 (evict immediately) by the system.
format: int64
type: integer
value:
description: |-
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
type: object
type: array
type: object
replicas:
description: |-
replicas indicates how many replicas should be created for each KubeVirt infrastructure
component (like virt-api or virt-controller). Defaults to 2.
WARNING: this is an advanced feature that prevents auto-scaling for core kubevirt components. Please use with caution!
type: integer
type: object
type: object
status:
description: KubeVirtStatus represents information pertaining to a KubeVirt
deployment.
properties:
conditions:
items:
description: KubeVirtCondition represents a condition of a KubeVirt
deployment
properties:
lastProbeTime:
format: date-time
nullable: true
type: string
lastTransitionTime:
format: date-time
nullable: true
type: string
message:
type: string
reason:
type: string
status:
type: string
type:
type: string
required:
- status
- type
type: object
type: array
defaultArchitecture:
type: string
generations:
items:
description: GenerationStatus keeps track of the generation for
a given resource so that decisions about forced updates can be
made.
properties:
group:
description: group is the group of the thing you're tracking
type: string
hash:
description: hash is an optional field set for resources without
generation that are content sensitive like secrets and configmaps
type: string
lastGeneration:
description: lastGeneration is the last generation of the workload
controller involved
format: int64
type: integer
name:
description: name is the name of the thing you're tracking
type: string
namespace:
description: namespace is where the thing you're tracking is
type: string
resource:
description: resource is the resource type of the thing you're
tracking
type: string
required:
- group
- lastGeneration
- name
- resource
type: object
type: array
x-kubernetes-list-type: atomic
observedDeploymentConfig:
type: string
observedDeploymentID:
type: string
observedGeneration:
format: int64
type: integer
observedKubeVirtRegistry:
type: string
observedKubeVirtVersion:
type: string
operatorVersion:
type: string
outdatedVirtualMachineInstanceWorkloads:
type: integer
phase:
description: KubeVirtPhase is a label for the phase of a KubeVirt
deployment at the current time.
type: string
targetDeploymentConfig:
type: string
targetDeploymentID:
type: string
targetKubeVirtRegistry:
type: string
targetKubeVirtVersion:
type: string
type: object
required:
- spec
type: object
served: true
storage: false
subresources:
status: {}
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: kubevirt-cluster-critical
value: 1000000000
globalDefault: false
description: "This priority class should be used for core kubevirt components only."
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kubevirt.io:operator
labels:
operator.kubevirt.io: ""
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rules:
- apiGroups:
- kubevirt.io
resources:
- kubevirts
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- deletecollection
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
kubevirt.io: ""
name: kubevirt-operator
namespace: kubevirt
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
kubevirt.io: ""
name: kubevirt-operator
namespace: kubevirt
rules:
- apiGroups:
- ""
resourceNames:
- kubevirt-ca
- kubevirt-export-ca
- kubevirt-virt-handler-certs
- kubevirt-virt-handler-server-certs
- kubevirt-operator-certs
- kubevirt-virt-api-certs
- kubevirt-controller-certs
- kubevirt-exportproxy-certs
resources:
- secrets
verbs:
- create
- get
- list
- watch
- patch
- delete
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- get
- list
- watch
- patch
- delete
- apiGroups:
- route.openshift.io
resources:
- routes
verbs:
- create
- get
- list
- watch
- patch
- delete
- apiGroups:
- route.openshift.io
resources:
- routes/custom-host
verbs:
- create
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- delete
- update
- create
- patch
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- apiGroups:
- route.openshift.io
resources:
- routes
verbs:
- list
- get
- watch
- apiGroups:
- ""
resources:
- secrets
verbs:
- list
- get
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- list
- get
- watch
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- get
- list
- watch
- delete
- update
- create
- patch
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- apiGroups:
- ""
resourceNames:
- kubevirt-export-ca
resources:
- configmaps
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
kubevirt.io: ""
name: kubevirt-operator-rolebinding
namespace: kubevirt
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kubevirt-operator
subjects:
- kind: ServiceAccount
name: kubevirt-operator
namespace: kubevirt
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
kubevirt.io: ""
name: kubevirt-operator
rules:
- apiGroups:
- kubevirt.io
resources:
- kubevirts
verbs:
- get
- list
- watch
- patch
- update
- patch
- apiGroups:
- ""
resources:
- serviceaccounts
- services
- endpoints
- pods/exec
verbs:
- get
- list
- watch
- create
- update
- delete
- patch
- apiGroups:
- ""
resources:
- configmaps
verbs:
- patch
- delete
- apiGroups:
- batch
resources:
- jobs
verbs:
- get
- list
- watch
- create
- delete
- patch
- apiGroups:
- apps
resources:
- controllerrevisions
verbs:
- watch
- list
- create
- delete
- patch
- apiGroups:
- apps
resources:
- deployments
- daemonsets
verbs:
- get
- list
- watch
- create
- delete
- patch
- apiGroups:
- rbac.authorization.k8s.io
resources:
- clusterroles
- clusterrolebindings
- roles
- rolebindings
verbs:
- get
- list
- watch
- create
- delete
- patch
- update
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- create
- delete
- patch
- apiGroups:
- security.openshift.io
resources:
- securitycontextconstraints
verbs:
- create
- get
- list
- watch
- apiGroups:
- security.openshift.io
resourceNames:
- privileged
resources:
- securitycontextconstraints
verbs:
- get
- patch
- update
- apiGroups:
- security.openshift.io
resourceNames:
- kubevirt-handler
- kubevirt-controller
resources:
- securitycontextconstraints
verbs:
- get
- list
- watch
- update
- delete
- apiGroups:
- admissionregistration.k8s.io
resources:
- validatingwebhookconfigurations
- mutatingwebhookconfigurations
- validatingadmissionpolicybindings
- validatingadmissionpolicies
verbs:
- get
- list
- watch
- create
- delete
- update
- patch
- apiGroups:
- apiregistration.k8s.io
resources:
- apiservices
verbs:
- get
- list
- watch
- create
- delete
- update
- patch
- apiGroups:
- monitoring.coreos.com
resources:
- servicemonitors
- prometheusrules
verbs:
- get
- list
- watch
- create
- delete
- update
- patch
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch
- patch
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- list
- delete
- patch
- apiGroups:
- kubevirt.io
resources:
- virtualmachines
- virtualmachineinstances
verbs:
- get
- list
- watch
- patch
- update
- apiGroups:
- ""
resources:
- persistentvolumeclaims
verbs:
- get
- apiGroups:
- kubevirt.io
resources:
- virtualmachines/status
verbs:
- patch
- apiGroups:
- kubevirt.io
resources:
- virtualmachineinstancemigrations
verbs:
- create
- get
- list
- watch
- patch
- apiGroups:
- kubevirt.io
resources:
- virtualmachineinstancepresets
verbs:
- watch
- list
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- limitranges
verbs:
- watch
- list
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- apiGroups:
- kubevirt.io
resources:
- kubevirts
verbs:
- get
- list
- watch
- apiGroups:
- snapshot.kubevirt.io
resources:
- virtualmachinesnapshots
- virtualmachinerestores
- virtualmachinesnapshotcontents
verbs:
- get
- list
- watch
- apiGroups:
- cdi.kubevirt.io
resources:
- datasources
- datavolumes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch
- apiGroups:
- instancetype.kubevirt.io
resources:
- virtualmachineinstancetypes
- virtualmachineclusterinstancetypes
- virtualmachinepreferences
- virtualmachineclusterpreferences
verbs:
- get
- list
- watch
- apiGroups:
- migrations.kubevirt.io
resources:
- migrationpolicies
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
- controllerrevisions
verbs:
- create
- list
- get
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch
- patch
- apiGroups:
- policy
resources:
- poddisruptionbudgets
verbs:
- get
- list
- watch
- delete
- create
- patch
- apiGroups:
- ""
resources:
- pods
- configmaps
- endpoints
- services
verbs:
- get
- list
- watch
- delete
- update
- create
- patch
- apiGroups:
- ""
resources:
- events
verbs:
- update
- create
- patch
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- apiGroups:
- ""
resources:
- pods/finalizers
verbs:
- update
- apiGroups:
- ""
resources:
- pods/eviction
verbs:
- create
- apiGroups:
- ""
resources:
- pods/status
verbs:
- patch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
- update
- patch
- apiGroups:
- apps
resources:
- daemonsets
verbs:
- list
- apiGroups:
- apps
resources:
- controllerrevisions
verbs:
- watch
- list
- create
- delete
- get
- update
- apiGroups:
- ""
resources:
- persistentvolumeclaims
verbs:
- get
- list
- watch
- create
- update
- delete
- patch
- apiGroups:
- snapshot.kubevirt.io
resources:
- virtualmachinesnapshots
- virtualmachinesnapshots/status
- virtualmachinesnapshots/finalizers
- virtualmachinesnapshotcontents
- virtualmachinesnapshotcontents/status
- virtualmachinesnapshotcontents/finalizers
- virtualmachinerestores
- virtualmachinerestores/status
verbs:
- get
- list
- watch
- create
- update
- delete
- patch
- apiGroups:
- export.kubevirt.io
resources:
- virtualmachineexports
- virtualmachineexports/status
- virtualmachineexports/finalizers
verbs:
- get
- list
- watch
- create
- update
- delete
- patch
- apiGroups:
- pool.kubevirt.io
resources:
- virtualmachinepools
- virtualmachinepools/finalizers
- virtualmachinepools/status
- virtualmachinepools/scale
verbs:
- watch
- list
- create
- delete
- update
- patch
- get
- apiGroups:
- kubevirt.io
resources:
- '*'
verbs:
- '*'
- apiGroups:
- kubevirt.io
resources:
- virtualmachines/finalizers
- virtualmachineinstances/finalizers
verbs:
- update
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachines/stop
- virtualmachineinstances/addvolume
- virtualmachineinstances/removevolume
- virtualmachineinstances/freeze
- virtualmachineinstances/unfreeze
- virtualmachineinstances/reset
- virtualmachineinstances/softreboot
- virtualmachineinstances/sev/setupsession
- virtualmachineinstances/sev/injectlaunchsecret
verbs:
- update
- apiGroups:
- cdi.kubevirt.io
resources:
- '*'
verbs:
- '*'
- apiGroups:
- k8s.cni.cncf.io
resources:
- network-attachment-definitions
verbs:
- get
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
- apiGroups:
- snapshot.storage.k8s.io
resources:
- volumesnapshotclasses
verbs:
- get
- list
- watch
- apiGroups:
- snapshot.storage.k8s.io
resources:
- volumesnapshots
verbs:
- get
- list
- watch
- create
- update
- delete
- apiGroups:
- storage.k8s.io
resources:
- storageclasses
verbs:
- get
- list
- watch
- apiGroups:
- instancetype.kubevirt.io
resources:
- virtualmachineinstancetypes
- virtualmachineclusterinstancetypes
- virtualmachinepreferences
- virtualmachineclusterpreferences
verbs:
- get
- list
- watch
- apiGroups:
- migrations.kubevirt.io
resources:
- migrationpolicies
verbs:
- get
- list
- watch
- apiGroups:
- clone.kubevirt.io
resources:
- virtualmachineclones
- virtualmachineclones/status
- virtualmachineclones/finalizers
verbs:
- get
- list
- watch
- update
- patch
- delete
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- apiGroups:
- ""
resources:
- resourcequotas
verbs:
- list
- watch
- apiGroups:
- batch
resources:
- jobs
verbs:
- create
- get
- delete
- apiGroups:
- kubevirt.io
resources:
- virtualmachineinstances
verbs:
- update
- list
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- patch
- list
- watch
- get
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- apiGroups:
- kubevirt.io
resources:
- kubevirts
verbs:
- get
- list
- watch
- apiGroups:
- migrations.kubevirt.io
resources:
- migrationpolicies
verbs:
- get
- list
- watch
- apiGroups:
- export.kubevirt.io
resources:
- virtualmachineexports
verbs:
- get
- list
- watch
- apiGroups:
- kubevirt.io
resources:
- kubevirts
verbs:
- list
- watch
- apiGroups:
- kubevirt.io
resources:
- kubevirts
verbs:
- get
- list
- apiGroups:
- subresources.kubevirt.io
resources:
- version
- guestfs
verbs:
- get
- list
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachineinstances/console
- virtualmachineinstances/vnc
- virtualmachineinstances/vnc/screenshot
- virtualmachineinstances/portforward
- virtualmachineinstances/guestosinfo
- virtualmachineinstances/filesystemlist
- virtualmachineinstances/userlist
- virtualmachineinstances/sev/fetchcertchain
- virtualmachineinstances/sev/querylaunchmeasurement
- virtualmachineinstances/usbredir
verbs:
- get
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachineinstances/pause
- virtualmachineinstances/unpause
- virtualmachineinstances/addvolume
- virtualmachineinstances/removevolume
- virtualmachineinstances/freeze
- virtualmachineinstances/unfreeze
- virtualmachineinstances/softreboot
- virtualmachineinstances/reset
- virtualmachineinstances/sev/setupsession
- virtualmachineinstances/sev/injectlaunchsecret
verbs:
- update
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachines/expand-spec
- virtualmachines/portforward
verbs:
- get
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachines/start
- virtualmachines/stop
- virtualmachines/restart
- virtualmachines/addvolume
- virtualmachines/removevolume
- virtualmachines/memorydump
verbs:
- update
- apiGroups:
- subresources.kubevirt.io
resources:
- expand-vm-spec
verbs:
- update
- apiGroups:
- kubevirt.io
resources:
- virtualmachines
- virtualmachineinstances
- virtualmachineinstancepresets
- virtualmachineinstancereplicasets
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- deletecollection
- apiGroups:
- kubevirt.io
resources:
- virtualmachineinstancemigrations
verbs:
- get
- list
- watch
- apiGroups:
- snapshot.kubevirt.io
resources:
- virtualmachinesnapshots
- virtualmachinesnapshotcontents
- virtualmachinerestores
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- deletecollection
- apiGroups:
- export.kubevirt.io
resources:
- virtualmachineexports
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- deletecollection
- apiGroups:
- clone.kubevirt.io
resources:
- virtualmachineclones
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- deletecollection
- apiGroups:
- instancetype.kubevirt.io
resources:
- virtualmachineinstancetypes
- virtualmachineclusterinstancetypes
- virtualmachinepreferences
- virtualmachineclusterpreferences
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- deletecollection
- apiGroups:
- pool.kubevirt.io
resources:
- virtualmachinepools
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- deletecollection
- apiGroups:
- migrations.kubevirt.io
resources:
- migrationpolicies
verbs:
- get
- list
- watch
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachineinstances/console
- virtualmachineinstances/vnc
- virtualmachineinstances/vnc/screenshot
- virtualmachineinstances/portforward
- virtualmachineinstances/guestosinfo
- virtualmachineinstances/filesystemlist
- virtualmachineinstances/userlist
- virtualmachineinstances/sev/fetchcertchain
- virtualmachineinstances/sev/querylaunchmeasurement
- virtualmachineinstances/usbredir
verbs:
- get
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachineinstances/pause
- virtualmachineinstances/unpause
- virtualmachineinstances/addvolume
- virtualmachineinstances/removevolume
- virtualmachineinstances/freeze
- virtualmachineinstances/unfreeze
- virtualmachineinstances/softreboot
- virtualmachineinstances/reset
- virtualmachineinstances/sev/setupsession
- virtualmachineinstances/sev/injectlaunchsecret
verbs:
- update
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachines/expand-spec
- virtualmachines/portforward
verbs:
- get
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachines/start
- virtualmachines/stop
- virtualmachines/restart
- virtualmachines/addvolume
- virtualmachines/removevolume
- virtualmachines/memorydump
verbs:
- update
- apiGroups:
- subresources.kubevirt.io
resources:
- expand-vm-spec
verbs:
- update
- apiGroups:
- kubevirt.io
resources:
- virtualmachines
- virtualmachineinstances
- virtualmachineinstancepresets
- virtualmachineinstancereplicasets
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- apiGroups:
- kubevirt.io
resources:
- virtualmachineinstancemigrations
verbs:
- get
- list
- watch
- apiGroups:
- snapshot.kubevirt.io
resources:
- virtualmachinesnapshots
- virtualmachinesnapshotcontents
- virtualmachinerestores
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- apiGroups:
- export.kubevirt.io
resources:
- virtualmachineexports
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- apiGroups:
- clone.kubevirt.io
resources:
- virtualmachineclones
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- apiGroups:
- instancetype.kubevirt.io
resources:
- virtualmachineinstancetypes
- virtualmachineclusterinstancetypes
- virtualmachinepreferences
- virtualmachineclusterpreferences
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- apiGroups:
- pool.kubevirt.io
resources:
- virtualmachinepools
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- apiGroups:
- kubevirt.io
resources:
- kubevirts
verbs:
- get
- list
- apiGroups:
- migrations.kubevirt.io
resources:
- migrationpolicies
verbs:
- get
- list
- watch
- apiGroups:
- kubevirt.io
resources:
- kubevirts
verbs:
- get
- list
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachines/expand-spec
- virtualmachineinstances/guestosinfo
- virtualmachineinstances/filesystemlist
- virtualmachineinstances/userlist
- virtualmachineinstances/sev/fetchcertchain
- virtualmachineinstances/sev/querylaunchmeasurement
verbs:
- get
- apiGroups:
- subresources.kubevirt.io
resources:
- expand-vm-spec
verbs:
- update
- apiGroups:
- kubevirt.io
resources:
- virtualmachines
- virtualmachineinstances
- virtualmachineinstancepresets
- virtualmachineinstancereplicasets
- virtualmachineinstancemigrations
verbs:
- get
- list
- watch
- apiGroups:
- snapshot.kubevirt.io
resources:
- virtualmachinesnapshots
- virtualmachinesnapshotcontents
- virtualmachinerestores
verbs:
- get
- list
- watch
- apiGroups:
- export.kubevirt.io
resources:
- virtualmachineexports
verbs:
- get
- list
- watch
- apiGroups:
- clone.kubevirt.io
resources:
- virtualmachineclones
verbs:
- get
- list
- watch
- apiGroups:
- instancetype.kubevirt.io
resources:
- virtualmachineinstancetypes
- virtualmachineclusterinstancetypes
- virtualmachinepreferences
- virtualmachineclusterpreferences
verbs:
- get
- list
- watch
- apiGroups:
- pool.kubevirt.io
resources:
- virtualmachinepools
verbs:
- get
- list
- watch
- apiGroups:
- migrations.kubevirt.io
resources:
- migrationpolicies
verbs:
- get
- list
- watch
- apiGroups:
- instancetype.kubevirt.io
resources:
- virtualmachineclusterinstancetypes
- virtualmachineclusterpreferences
verbs:
- get
- list
- watch
- apiGroups:
- subresources.kubevirt.io
resources:
- virtualmachines/migrate
verbs:
- update
- apiGroups:
- kubevirt.io
resources:
- virtualmachineinstancemigrations
verbs:
- get
- delete
- create
- update
- patch
- list
- watch
- deletecollection
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
kubevirt.io: ""
name: kubevirt-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kubevirt-operator
subjects:
- kind: ServiceAccount
name: kubevirt-operator
namespace: kubevirt
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
kubevirt.io: virt-operator
name: virt-operator
namespace: kubevirt
spec:
replicas: 2
selector:
matchLabels:
kubevirt.io: virt-operator
strategy:
type: RollingUpdate
template:
metadata:
annotations:
openshift.io/required-scc: restricted-v2
labels:
kubevirt.io: virt-operator
name: virt-operator
prometheus.kubevirt.io: "true"
name: virt-operator
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: kubevirt.io
operator: In
values:
- virt-operator
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- args:
- --port
- "8443"
- -v
- "2"
command:
- virt-operator
env:
- name: VIRT_OPERATOR_IMAGE
value: quay.io/kubevirt/virt-operator:v1.5.0
- name: WATCH_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.annotations['olm.targetNamespaces']
- name: KUBEVIRT_VERSION
value: v1.5.0
image: quay.io/kubevirt/virt-operator:v1.5.0
imagePullPolicy: IfNotPresent
name: virt-operator
ports:
- containerPort: 8443
name: metrics
protocol: TCP
- containerPort: 8444
name: webhooks
protocol: TCP
readinessProbe:
httpGet:
path: /metrics
port: 8443
scheme: HTTPS
initialDelaySeconds: 5
timeoutSeconds: 10
resources:
requests:
cpu: 10m
memory: 450Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts:
- mountPath: /etc/virt-operator/certificates
name: kubevirt-operator-certs
readOnly: true
- mountPath: /profile-data
name: profile-data
nodeSelector:
kubernetes.io/os: linux
priorityClassName: kubevirt-cluster-critical
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
serviceAccountName: kubevirt-operator
tolerations:
- key: CriticalAddonsOnly
operator: Exists
volumes:
- name: kubevirt-operator-certs
secret:
optional: true
secretName: kubevirt-operator-certs
- emptyDir: {}
name: profile-data
================================================
FILE: infra/kubernetes/readme-todo.txt
================================================
================================================
FILE: infra/kubernetes/start-cyberdesk-operator-cr.yaml
================================================
apiVersion: cyberdesk.io/v1alpha1
kind: StartCyberdeskOperator
metadata:
name: bootstrap-cyberdesk-setup
namespace: cyberdesk-system
spec:
{}
================================================
FILE: infra/kubernetes/warm-pool.yaml
================================================
apiVersion: pool.kubevirt.io/v1alpha1
kind: VirtualMachinePool
metadata:
name: cyberdesk-warm-pool
namespace: kubevirt
spec:
replicas: 1
selector:
matchLabels:
pool.kubevirt.io/warm: ready # <- selector
virtualMachineTemplate:
metadata:
labels:
pool.kubevirt.io/warm: ready # <- matches selector while warm
spec:
runStrategy: Always # already running
dataVolumeTemplates:
- metadata:
name: rootdisk
spec:
source:
snapshot:
name: vmsnapshot-fa8369cc-fcb9-4bb2-babf-e19baca0b227-volume-rootdisk # Make sure this is updated to the latest snapshot
namespace: kubevirt
pvc:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 20Gi
volumeMode: Filesystem
template:
metadata:
labels:
pool.kubevirt.io/warm: ready
app: cyberdesk
spec:
domain:
cpu:
cores: 1
resources:
requests:
memory: 2Gi
devices:
disks:
- name: rootdisk
disk:
bus: virtio
- name: cloudinit
disk:
bus: virtio
interfaces:
- name: default
masquerade: {}
networks:
- name: default
pod: {}
volumes:
- name: rootdisk
dataVolume:
name: rootdisk
- name: cloudinit
cloudInitNoCloud:
secretRef:
name: cloud-init-golden-vm
================================================
FILE: infra/terraform/.terraform.lock.hcl
================================================
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/azurerm" {
version = "4.27.0"
constraints = "~> 4.27.0"
hashes = [
"h1:hmAzHk4XVbrGQ5iJJj1QdFx0aWNW9Hjh+GIE6S8G5I8=",
"h1:nvIa9Z9GdI3/J7U4oMR2w/h/JZ66M1ozUPh/jHn5pXU=",
"zh:0c69edea1995bd3bd9e61980757169c35bf22281b660b5c755b6cb13d08d29d2",
"zh:25b86bf7b9678371d8573983954c571696f3e64a3967133be3b835da36307106",
"zh:49921cff4f26a49bafada60cd07dabb52c5eb35231059ed928a4f4722e269c82",
"zh:4b986166531f9fd1289f01d8220519443e74888a21da512c1b841b006dad6215",
"zh:53fb65b2ca4df637f03e4748a100a7d7fc77249e307c03e294d6259cec0310f6",
"zh:5c0d021a387ca4e2a5a01da009746a08c45f08e971c10d9bda54539d7264d671",
"zh:600043f2b20dc5a45275e43f175c19fe8b6e8e9557a0c884aef018f1f63de90e",
"zh:a0284f6f38912f67bb4cb7829fda3fa75be81fea6a9b21119965c2a839430092",
"zh:a7ac0576e2069ef77557042c6b5157ded364fbd355b2f9bf7f5441622424086e",
"zh:c5db0bcafe986868e28cc6225b68b2d1cf4bf631939d260ca845f17a9aa1677d",
"zh:ce620c0eb71b1fdd925828b30cf232a869abccf1c459180f2f991c4166315251",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}
================================================
FILE: infra/terraform/main.tf
================================================
#########################################
# main.tf
#########################################
terraform {
required_version = ">= 1.11.3"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.27.0"
}
}
}
#############################
# Providers Configuration
#############################
provider "azurerm" {
features {}
subscription_id = var.subscription_id
}
#############################
# Networking & IAM Setup
#############################
# Resource Group
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location
}
# Virtual Network
resource "azurerm_virtual_network" "vnet" {
name = "vnet-kubevirt-demo"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
depends_on = [azurerm_resource_group.rg]
}
# AKS Subnet
resource "azurerm_subnet" "aks" {
name = "subnet-aks"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.1.0/24"]
depends_on = [azurerm_resource_group.rg]
}
# NSG for AKS Subnet
resource "azurerm_network_security_group" "aks_nsg" {
name = "nsg-aks-subnet"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
depends_on = [azurerm_resource_group.rg]
}
# Outbound Internet (always allowed)
resource "azurerm_network_security_rule" "allow_outbound" {
name = "AllowOutboundInternet"
priority = 300
direction = "Outbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "VirtualNetwork"
destination_address_prefix = "Internet"
resource_group_name = azurerm_resource_group.rg.name
network_security_group_name = azurerm_network_security_group.aks_nsg.name
depends_on = [azurerm_resource_group.rg]
}
# Inbound HTTP from your developers (0.0.0.0/0)
resource "azurerm_network_security_rule" "allow_inbound_http" {
name = "AllowInboundHttpFromTrusted"
priority = 200
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefixes = concat(var.developer_api_ips, var.developer_vpn_ips)
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.rg.name
network_security_group_name = azurerm_network_security_group.aks_nsg.name
depends_on = [azurerm_resource_group.rg]
}
# Inbound HTTPS from your developers (0.0.0.0/0)
resource "azurerm_network_security_rule" "allow_inbound_https" {
name = "AllowInboundHttpsFromTrusted"
priority = 210
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefixes = concat(var.developer_api_ips, var.developer_vpn_ips)
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.rg.name
network_security_group_name = azurerm_network_security_group.aks_nsg.name
depends_on = [azurerm_resource_group.rg]
}
# ─────────── NEW RULES ───────────
# 1) Allow Azure Load Balancer health probes on 80 & 443
resource "azurerm_network_security_rule" "allow_lb_probes_http_https" {
name = "AllowAzureLBProbes"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
source_address_prefix = "AzureLoadBalancer"
destination_port_ranges = ["80", "443"]
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.rg.name
network_security_group_name = azurerm_network_security_group.aks_nsg.name
depends_on = [azurerm_resource_group.rg]
}
# 2) Allow Azure Load Balancer to probe Kubernetes NodePorts
resource "azurerm_network_security_rule" "allow_lb_probes_nodeport" {
name = "AllowAzureLBNodePorts"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
source_address_prefix = "AzureLoadBalancer"
destination_port_range = "30000-32767"
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.rg.name
network_security_group_name = azurerm_network_security_group.aks_nsg.name
depends_on = [azurerm_resource_group.rg]
}
# Associate NSG with the subnet
resource "azurerm_subnet_network_security_group_association" "aks_nsg_assoc" {
subnet_id = azurerm_subnet.aks.id
network_security_group_id = azurerm_network_security_group.aks_nsg.id
depends_on = [azurerm_resource_group.rg]
}
# AKS Cluster
resource "azurerm_kubernetes_cluster" "aks" {
name = var.aks_cluster_name
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
dns_prefix = var.aks_cluster_name
default_node_pool {
name = "default"
vm_size = var.aks_default_node_pool_vm_size
temporary_name_for_rotation = "tempnodepool"
auto_scaling_enabled = true
min_count = var.aks_default_node_pool_min_count
max_count = var.aks_default_node_pool_max_count
vnet_subnet_id = azurerm_subnet.aks.id
}
identity {
type = "SystemAssigned"
}
network_profile {
network_plugin = "azure"
network_policy = "azure"
service_cidr = "10.2.0.0/16"
dns_service_ip = "10.2.0.10"
}
depends_on = [azurerm_resource_group.rg]
}
# Assign AKS identity rights
resource "azurerm_role_assignment" "aks_subnet_role" {
scope = azurerm_subnet.aks.id
role_definition_name = "Network Contributor"
principal_id = azurerm_kubernetes_cluster.aks.identity[0].principal_id
depends_on = [azurerm_resource_group.rg]
}
resource "azurerm_role_assignment" "aks_routetable_role" {
scope = azurerm_resource_group.rg.id
role_definition_name = "Network Contributor"
principal_id = azurerm_kubernetes_cluster.aks.identity[0].principal_id
depends_on = [azurerm_resource_group.rg]
}
================================================
FILE: infra/terraform/variables.tf
================================================
variable "subscription_id" {
description = "The Azure Subscription ID where resources will be created."
type = string
}
variable "resource_group_name" {
description = "The name of the resource group in which to create resources."
type = string
}
variable "location" {
description = "Azure region for the resources."
type = string
}
variable "aks_cluster_name" {
description = "The name to use for the AKS cluster."
type = string
}
variable "aks_default_node_pool_vm_size" {
description = "The VM size for the default AKS node pool."
type = string
default = "Standard_D8ds_v5" # Keeping the original as default
}
variable "aks_default_node_pool_min_count" {
description = "The minimum number of nodes for the default AKS node pool autoscaler."
type = number
default = 1
}
variable "aks_default_node_pool_max_count" {
description = "The maximum number of nodes for the default AKS node pool autoscaler."
type = number
default = 2
}
variable "developer_api_ips" {
type = list(string)
description = "List of dedicated public IP addresses/CIDRs for the Developer-facing API app."
default = ["0.0.0.0/0"]
}
variable "developer_vpn_ips" {
type = list(string)
description = "List of public IP addresses/CIDRs for the developer VPN (for secure local access to the cluster)"
default = ["0.0.0.0/0"]
}
================================================
FILE: sdks/openapi.json
================================================
{
"openapi": "3.1.0",
"info": {
"title": "API Reference",
"version": "1.2.1",
"description": "API for Cyberdesk, to create, control, and manage virtual desktop instances."
},
"servers": [
{
"url": "https://api.cyberdesk.io",
"description": "Production server"
}
],
"components": {
"securitySchemes": {
"apiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "x-api-key"
}
},
"schemas": {},
"parameters": {}
},
"paths": {
"/v1/desktop/{id}": {
"get": {
"tags": [
"Desktop"
],
"summary": "Get details of a specific desktop instance",
"description": "Returns the ID, status, creation timestamp, and timeout timestamp for a given desktop instance.",
"parameters": [
{
"schema": {
"type": "string",
"format": "uuid",
"description": "The UUID of the desktop instance to retrieve",
"example": "a1b2c3d4-e5f6-7890-1234-567890abcdef"
},
"required": true,
"name": "id",
"in": "path"
},
{
"schema": {
"type": "string",
"description": "API key for authentication",
"example": "api_12345"
},
"required": true,
"name": "x-api-key",
"in": "header"
}
],
"responses": {
"200": {
"description": "Desktop instance details retrieved successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for the desktop instance",
"example": "a1b2c3d4-e5f6-7890-1234-567890abcdef"
},
"status": {
"type": "string",
"enum": [
"pending",
"running",
"terminated",
"error"
],
"description": "Current status of the desktop instance",
"example": "running"
},
"stream_url": {
"type": "string",
"nullable": true,
"description": "URL for the desktop stream (null if the desktop is not running)",
"example": "https://cyberdesk.com/vnc/a1b2c3d4-e5f6-7890-1234-567890abcdef"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "Timestamp when the instance was created",
"example": "2023-10-27T10:00:00Z"
},
"timeout_at": {
"type": "string",
"format": "date-time",
"description": "Timestamp when the instance will automatically time out",
"example": "2023-10-28T10:00:00Z"
}
},
"required": [
"id",
"status",
"stream_url",
"created_at",
"timeout_at"
]
}
}
}
},
"400": {
"description": "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"401": {
"description": "Although the HTTP standard specifies \"unauthorized\", semantically this response means \"unauthenticated\". That is, the client must authenticate itself to get the requested response.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"403": {
"description": "The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"404": {
"description": "The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"409": {
"description": "This response is sent when a request conflicts with the current state of the server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"429": {
"description": "The user has sent too many requests in a given amount of time (\"rate limiting\")",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"500": {
"description": "The server has encountered a situation it does not know how to handle.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"502": {
"description": "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
}
}
}
},
"/v1/desktop": {
"post": {
"tags": [
"Desktop"
],
"summary": "Create a new virtual desktop instance",
"description": "Creates a new virtual desktop instance and returns its ID and stream URL",
"parameters": [
{
"schema": {
"type": "string",
"description": "API key for authentication",
"example": "api_12345"
},
"required": true,
"name": "x-api-key",
"in": "header"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"timeout_ms": {
"type": "integer",
"description": "Timeout in milliseconds for the desktop session",
"example": 3600000
}
}
}
}
}
},
"responses": {
"200": {
"description": "Desktop creation initiated successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the desktop instance",
"example": "desktop_12345"
},
"status": {
"type": "string",
"enum": [
"pending",
"running",
"terminated",
"error"
],
"description": "Initial status of the desktop instance after creation request",
"example": "pending"
}
},
"required": [
"id",
"status"
]
}
}
}
},
"400": {
"description": "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"401": {
"description": "Although the HTTP standard specifies \"unauthorized\", semantically this response means \"unauthenticated\". That is, the client must authenticate itself to get the requested response.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"403": {
"description": "The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"404": {
"description": "The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"409": {
"description": "This response is sent when a request conflicts with the current state of the server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"429": {
"description": "The user has sent too many requests in a given amount of time (\"rate limiting\")",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"500": {
"description": "The server has encountered a situation it does not know how to handle.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"502": {
"description": "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
}
}
}
},
"/v1/desktop/{id}/stop": {
"post": {
"tags": [
"Desktop"
],
"summary": "Stop a running desktop instance",
"description": "Stops a running desktop instance and cleans up resources",
"parameters": [
{
"schema": {
"type": "string",
"description": "Desktop instance ID to stop",
"example": "desktop_12345"
},
"required": true,
"name": "id",
"in": "path"
},
{
"schema": {
"type": "string",
"description": "API key for authentication",
"example": "api_12345"
},
"required": true,
"name": "x-api-key",
"in": "header"
}
],
"responses": {
"200": {
"description": "Desktop stopped successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"pending",
"running",
"terminated",
"error"
],
"description": "Status of the desktop instance after stopping",
"example": "terminated"
}
},
"required": [
"status"
]
}
}
}
},
"400": {
"description": "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"401": {
"description": "Although the HTTP standard specifies \"unauthorized\", semantically this response means \"unauthenticated\". That is, the client must authenticate itself to get the requested response.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"403": {
"description": "The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"404": {
"description": "The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"409": {
"description": "This response is sent when a request conflicts with the current state of the server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"429": {
"description": "The user has sent too many requests in a given amount of time (\"rate limiting\")",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"500": {
"description": "The server has encountered a situation it does not know how to handle.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"502": {
"description": "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
}
}
}
},
"/v1/desktop/{id}/computer-action": {
"post": {
"tags": [
"Desktop"
],
"summary": "Perform an action on the desktop",
"description": "Executes a computer action such as mouse clicks, keyboard input, or screenshots on the desktop",
"parameters": [
{
"schema": {
"type": "string",
"description": "Desktop instance ID to perform the action on",
"example": "desktop_12345"
},
"required": true,
"name": "id",
"in": "path"
},
{
"schema": {
"type": "string",
"description": "API key for authentication",
"example": "api_12345"
},
"required": true,
"name": "x-api-key",
"in": "header"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"click_mouse"
],
"description": "Perform a mouse action: click, press (down), or release (up). Defaults to a single left click at the current position.",
"example": "click_mouse"
},
"x": {
"type": "integer",
"description": "X coordinate for the action (optional, uses current position if omitted)",
"example": 500
},
"y": {
"type": "integer",
"description": "Y coordinate for the action (optional, uses current position if omitted)",
"example": 300
},
"button": {
"type": "string",
"enum": [
"left",
"right",
"middle"
],
"description": "Mouse button to use (optional, defaults to 'left')",
"example": "left"
},
"num_of_clicks": {
"type": "integer",
"minimum": 0,
"description": "Number of clicks to perform (optional, defaults to 1, only applicable for 'click' type)",
"example": 1
},
"click_type": {
"type": "string",
"enum": [
"click",
"down",
"up"
],
"description": "Type of mouse action (optional, defaults to 'click')",
"example": "click"
}
},
"required": [
"type"
],
"title": "Click Mouse Action"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"scroll"
],
"description": "Scroll the mouse wheel in the specified direction",
"example": "scroll"
},
"direction": {
"type": "string",
"enum": [
"up",
"down",
"left",
"right"
],
"description": "Direction to scroll",
"example": "down"
},
"amount": {
"type": "integer",
"description": "Amount to scroll in pixels",
"example": 100
}
},
"required": [
"type",
"direction",
"amount"
],
"title": "Scroll Action"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"move_mouse"
],
"description": "Move the mouse cursor to the specified coordinates",
"example": "move_mouse"
},
"x": {
"type": "integer",
"description": "X coordinate to move to",
"example": 500
},
"y": {
"type": "integer",
"description": "Y coordinate to move to",
"example": 300
}
},
"required": [
"type",
"x",
"y"
],
"title": "Move Mouse Action"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"drag_mouse"
],
"description": "Drag the mouse from start to end coordinates",
"example": "drag_mouse"
},
"start": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"description": "X coordinate on the screen",
"example": 500
},
"y": {
"type": "integer",
"description": "Y coordinate on the screen",
"example": 300
}
},
"required": [
"x",
"y"
],
"description": "Starting coordinates for the drag operation",
"example": {
"x": 100,
"y": 100
}
},
"end": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"description": "X coordinate on the screen",
"example": 500
},
"y": {
"type": "integer",
"description": "Y coordinate on the screen",
"example": 300
}
},
"required": [
"x",
"y"
],
"description": "Ending coordinates for the drag operation",
"example": {
"x": 300,
"y": 300
}
}
},
"required": [
"type",
"start",
"end"
],
"title": "Drag Mouse Action"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"type"
],
"description": "Type text at the current cursor position",
"example": "type"
},
"text": {
"type": "string",
"description": "Text to type",
"example": "Hello, World!"
}
},
"required": [
"type",
"text"
],
"title": "Type Text Action"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"press_keys"
],
"description": "Press, hold down, or release one or more keyboard keys. Defaults to a single press and release.",
"example": "press_keys"
},
"keys": {
"anyOf": [
{
"type": "string",
"description": "Single key to press",
"example": "Enter"
},
{
"type": "array",
"items": {
"type": "string"
},
"description": "Multiple keys to press simultaneously",
"example": [
"Control",
"c"
]
}
]
},
"key_action_type": {
"type": "string",
"enum": [
"press",
"down",
"up"
],
"description": "Type of key action (optional, defaults to 'press' which is a down and up action)",
"example": "press"
}
},
"required": [
"type",
"keys"
],
"title": "Press Keys Action"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"wait"
],
"description": "Wait for the specified number of milliseconds",
"example": "wait"
},
"ms": {
"type": "integer",
"description": "Time to wait in milliseconds",
"example": 1000
}
},
"required": [
"type",
"ms"
],
"title": "Wait Action"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"screenshot"
],
"description": "Take a screenshot of the desktop",
"example": "screenshot"
}
},
"required": [
"type"
],
"title": "Screenshot Action"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"get_cursor_position"
],
"description": "Get the current mouse cursor position",
"example": "get_cursor_position"
}
},
"required": [
"type"
],
"title": "Get Cursor Position Action"
}
]
}
}
}
},
"responses": {
"200": {
"description": "Action executed successfully. Response may contain output or image data depending on the action.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"output": {
"type": "string",
"description": "Raw string output from the executed command (if any)",
"example": "X=500 Y=300"
},
"error": {
"type": "string",
"description": "Error message if the operation failed (also indicated by non-2xx HTTP status)",
"example": "Command failed with code 1: xdotool: command not found"
},
"base64_image": {
"type": "string",
"description": "Base64 encoded JPEG image data (only returned for screenshot actions)",
"example": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQ..."
}
}
}
}
}
},
"400": {
"description": "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"401": {
"description": "Although the HTTP standard specifies \"unauthorized\", semantically this response means \"unauthenticated\". That is, the client must authenticate itself to get the requested response.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"403": {
"description": "The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"404": {
"description": "The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"409": {
"description": "This response is sent when a request conflicts with the current state of the server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"429": {
"description": "The user has sent too many requests in a given amount of time (\"rate limiting\")",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"500": {
"description": "The server has encountered a situation it does not know how to handle.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"502": {
"description": "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
}
}
}
},
"/v1/desktop/{id}/bash-action": {
"post": {
"tags": [
"Desktop"
],
"summary": "Execute a bash command on the desktop",
"description": "Runs a bash command on the desktop and returns the command output",
"parameters": [
{
"schema": {
"type": "string",
"description": "Desktop instance ID to run the command on",
"example": "desktop_12345"
},
"required": true,
"name": "id",
"in": "path"
},
{
"schema": {
"type": "string",
"description": "API key for authentication",
"example": "api_12345"
},
"required": true,
"name": "x-api-key",
"in": "header"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Bash command to execute",
"example": "echo 'Hello, World!'"
}
},
"required": [
"command"
]
}
}
}
},
"responses": {
"200": {
"description": "Command executed successfully. Response contains command output.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"output": {
"type": "string",
"description": "Raw string output from the executed command (if any)",
"example": "X=500 Y=300"
},
"error": {
"type": "string",
"description": "Error message if the operation failed (also indicated by non-2xx HTTP status)",
"example": "Command failed with code 1: xdotool: command not found"
},
"base64_image": {
"type": "string",
"description": "Base64 encoded JPEG image data (only returned for screenshot actions)",
"example": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQ..."
}
}
}
}
}
},
"400": {
"description": "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"401": {
"description": "Although the HTTP standard specifies \"unauthorized\", semantically this response means \"unauthenticated\". That is, the client must authenticate itself to get the requested response.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"403": {
"description": "The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"404": {
"description": "The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"409": {
"description": "This response is sent when a request conflicts with the current state of the server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"429": {
"description": "The user has sent too many requests in a given amount of time (\"rate limiting\")",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"500": {
"description": "The server has encountered a situation it does not know how to handle.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
},
"502": {
"description": "The server, while acting as a gateway or proxy, received an invalid response from the upstream server.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
],
"example": "error"
},
"error": {
"type": "string",
"description": "Error message detailing what went wrong",
"example": "Instance not found or unauthorized"
}
},
"required": [
"status",
"error"
]
}
}
}
}
}
}
}
}
}
================================================
FILE: sdks/py-sdk/.gitignore
================================================
# Python cache
__pycache__/
*.pyc
# Build artifacts
dist/
build/
*.egg-info/
================================================
FILE: sdks/py-sdk/LICENSE
================================================
MIT License
Copyright (c) 2024 Cyberdesk Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: sdks/py-sdk/README.md
================================================
# cyberdesk
[](https://badge.fury.io/py/cyberdesk)
The official Python SDK for Cyberdesk.
## Installation
```bash
pip install cyberdesk
```
## Usage
First, create a Cyberdesk client instance with your API key:
```python
from cyberdesk import CyberdeskClient
client = CyberdeskClient(api_key="YOUR_API_KEY")
```
---
### Launch a Desktop
```python
result = client.launch_desktop(timeout_ms=10000) # Optional: set a timeout for the desktop session
# Error handling example
if hasattr(result, 'error') and result.error:
raise Exception('Failed to launch desktop: ' + str(result.error))
# Success
if hasattr(result, 'id'):
desktop_id = result.id
print('Launched desktop with ID:', desktop_id)
```
---
### Get Desktop Info
```python
info = client.get_desktop("your-desktop-id")
if hasattr(info, 'error') and info.error:
raise Exception('Failed to get desktop info: ' + str(info.error))
print('Desktop info:', info)
```
---
### Perform a Computer Action (e.g., Mouse Click)
```python
from cyberdesk.actions import click_mouse, ClickMouseButton
action = click_mouse(x=100, y=150, button=ClickMouseButton.LEFT)
action_result = client.execute_computer_action("your-desktop-id", action)
if hasattr(action_result, 'error') and action_result.error:
raise Exception('Action failed: ' + str(action_result.error))
print('Action result:', action_result)
```
---
### Run a Bash Command
```python
bash_result = client.execute_bash_action(
"your-desktop-id",
"echo Hello, world!"
)
if hasattr(bash_result, 'error') and bash_result.error:
raise Exception('Bash command failed: ' + str(bash_result.error))
print('Bash output:', getattr(bash_result, 'output', bash_result))
```
---
## Ergonomic, Type-Safe Actions
To create computer actions (mouse, keyboard, etc.), use the factory functions in `cyberdesk.actions`. These provide full type hints and IDE autocompletion for all required and optional fields.
**Example:**
```python
from cyberdesk.actions import click_mouse, type_text, ClickMouseButton
action1 = click_mouse(x=100, y=200, button=ClickMouseButton.LEFT)
action2 = type_text(text="Hello, world!")
client.execute_computer_action("your-desktop-id", action1)
client.execute_computer_action("your-desktop-id", action2)
```
| Action | Factory Function | Description |
|----------------|-------------------------|----------------------------|
| Click Mouse | `click_mouse` | Mouse click at (x, y) |
| Drag Mouse | `drag_mouse` | Mouse drag from/to (x, y) |
| Move Mouse | `move_mouse` | Move mouse to (x, y) |
| Scroll | `scroll` | Scroll by dx, dy |
| Type Text | `type_text` | Type text |
| Press Keys | `press_keys` | Press keyboard keys |
| Screenshot | `screenshot` | Take a screenshot |
| Wait | `wait` | Wait for ms milliseconds |
| Get Cursor Pos | `get_cursor_position` | Get mouse cursor position |
---
## Async Usage
All methods are also available as async variants (prefixed with `async_`). Example:
```python
import asyncio
from cyberdesk import CyberdeskClient
from cyberdesk.actions import click_mouse, ClickMouseButton
async def main():
client = CyberdeskClient(api_key="YOUR_API_KEY")
result = await client.async_launch_desktop(timeout_ms=10000)
print(result)
# Example async computer action
action = click_mouse(x=100, y=200, button=ClickMouseButton.LEFT)
await client.async_execute_computer_action("your-desktop-id", action)
# ... use other async_ methods as needed
asyncio.run(main())
```
---
## Type Hints and Models
All request/response types are available from the generated models, and all computer actions are available as factory functions in `cyberdesk.actions` for ergonomic, type-safe usage.
```python
from cyberdesk.actions import click_mouse, drag_mouse, type_text, wait, scroll, move_mouse, press_keys, screenshot, get_cursor_position, ClickMouseButton, ClickMouseActionType, PressKeyActionType, ScrollDirection
```
# Note: All action enums (e.g., ClickMouseButton, ClickMouseActionType, PressKeyActionType, ScrollDirection, etc.) are available from cyberdesk.actions for type-safe usage.
---
## For Cyberdesk Team: Publishing to PyPI
**Recommended:** Always use a [virtual environment](https://docs.python.org/3/library/venv.html) (venv) for building and publishing to avoid dependency conflicts.
To build and publish this package to [PyPI](https://pypi.org/project/cyberdesk/):
1. **Log into PyPI** (get credentials from the Cyberdesk team).
2. **Install dev dependencies** (in a clean venv):
```bash
pip install .[dev]
# or
uv pip install .[dev]
```
3. **Bump the version number** in `pyproject.toml` (e.g., `version = "0.2.4"`).
4. **Clean your `dist/` directory** before building to avoid 'File already exists' errors:
```bash
rm -rf dist/*
# On Windows PowerShell:
Remove-Item dist\* -Force
```
5. **Build the package:**
```bash
python -m build
```
This creates a `dist/` directory with `.whl` and `.tar.gz` files.
6. **(Recommended) Set up a `.pypirc` file for easy publishing:**
- Create a file named `.pypirc` in your home directory (e.g., `C:\Users\yourname\.pypirc` on Windows or `~/.pypirc` on Linux/macOS).
- Add:
```ini
[distutils]
index-servers =
pypi
[pypi]
username = __token__
password = pypi-AgEIcH... # <-- paste your API token here
```
7. **Publish to PyPI:**
```bash
twine upload dist/*
```
- If you set up `.pypirc`, you won't be prompted for credentials.
- If not, enter `__token__` as the username and paste your API token as the password.
8. **Verify:**
- Visit https://pypi.org/project/cyberdesk/ to see your published package.
- Try installing it in a fresh environment:
```bash
pip install cyberdesk
```
---
## License
[MIT](LICENSE)
================================================
FILE: sdks/py-sdk/cyberdesk/__init__.py
================================================
from .client import CyberdeskClient
from .types import (
GetDesktopParams,
LaunchDesktopParams,
TerminateDesktopParams,
ExecuteBashActionParams,
)
================================================
FILE: sdks/py-sdk/cyberdesk/actions.py
================================================
from typing import Union, List, Optional
from openapi_client.api_reference_client.types import UNSET
from openapi_client.api_reference_client.models import (
PostV1DesktopIdComputerActionClickMouseAction,
PostV1DesktopIdComputerActionClickMouseActionType,
PostV1DesktopIdComputerActionClickMouseActionButton as ClickMouseButton,
PostV1DesktopIdComputerActionClickMouseActionClickType as ClickMouseActionType,
PostV1DesktopIdComputerActionDragMouseAction,
PostV1DesktopIdComputerActionDragMouseActionType,
PostV1DesktopIdComputerActionDragMouseActionStart,
PostV1DesktopIdComputerActionDragMouseActionEnd,
PostV1DesktopIdComputerActionGetCursorPositionAction,
PostV1DesktopIdComputerActionGetCursorPositionActionType,
PostV1DesktopIdComputerActionMoveMouseAction,
PostV1DesktopIdComputerActionMoveMouseActionType,
PostV1DesktopIdComputerActionPressKeysAction,
PostV1DesktopIdComputerActionPressKeysActionType,
PostV1DesktopIdComputerActionPressKeysActionKeyActionType as PressKeyActionType,
PostV1DesktopIdComputerActionScreenshotAction,
PostV1DesktopIdComputerActionScreenshotActionType,
PostV1DesktopIdComputerActionScrollAction,
PostV1DesktopIdComputerActionScrollActionType,
PostV1DesktopIdComputerActionScrollActionDirection as ScrollDirection,
PostV1DesktopIdComputerActionTypeTextAction,
PostV1DesktopIdComputerActionTypeTextActionType,
PostV1DesktopIdComputerActionWaitAction,
PostV1DesktopIdComputerActionWaitActionType,
)
# Re-export the original Enum types under their aliased names for user convenience if they need to import them
# This makes `from cyberdesk.actions import ClickMouseButton` possible.
def click_mouse(
x: Optional[int] = None,
y: Optional[int] = None,
button: Optional[ClickMouseButton] = None,
num_of_clicks: Optional[int] = None,
click_type: Optional[ClickMouseActionType] = None,
) -> PostV1DesktopIdComputerActionClickMouseAction:
return PostV1DesktopIdComputerActionClickMouseAction(
type_=PostV1DesktopIdComputerActionClickMouseActionType.CLICK_MOUSE,
x=x if x is not None else UNSET,
y=y if y is not None else UNSET,
button=button if button is not None else UNSET,
num_of_clicks=num_of_clicks if num_of_clicks is not None else UNSET,
click_type=click_type if click_type is not None else UNSET
)
def drag_mouse(
start_x: int,
start_y: int,
end_x: int,
end_y: int
) -> PostV1DesktopIdComputerActionDragMouseAction:
start_model = PostV1DesktopIdComputerActionDragMouseActionStart(x=start_x, y=start_y)
end_model = PostV1DesktopIdComputerActionDragMouseActionEnd(x=end_x, y=end_y)
return PostV1DesktopIdComputerActionDragMouseAction(
type_=PostV1DesktopIdComputerActionDragMouseActionType.DRAG_MOUSE,
start=start_model,
end=end_model
)
def get_cursor_position() -> PostV1DesktopIdComputerActionGetCursorPositionAction:
return PostV1DesktopIdComputerActionGetCursorPositionAction(
type_=PostV1DesktopIdComputerActionGetCursorPositionActionType.GET_CURSOR_POSITION
)
def move_mouse(
x: int,
y: int
) -> PostV1DesktopIdComputerActionMoveMouseAction:
return PostV1DesktopIdComputerActionMoveMouseAction(
type_=PostV1DesktopIdComputerActionMoveMouseActionType.MOVE_MOUSE,
x=x,
y=y
)
def press_keys(
keys: Optional[Union[str, List[str]]] = None,
key_action_type: Optional[PressKeyActionType] = None
) -> PostV1DesktopIdComputerActionPressKeysAction:
return PostV1DesktopIdComputerActionPressKeysAction(
type_=PostV1DesktopIdComputerActionPressKeysActionType.PRESS_KEYS,
keys=keys if keys is not None else UNSET,
key_action_type=key_action_type if key_action_type is not None else UNSET
)
def screenshot() -> PostV1DesktopIdComputerActionScreenshotAction:
return PostV1DesktopIdComputerActionScreenshotAction(
type_=PostV1DesktopIdComputerActionScreenshotActionType.SCREENSHOT
)
def scroll(
direction: ScrollDirection,
amount: int
) -> PostV1DesktopIdComputerActionScrollAction:
return PostV1DesktopIdComputerActionScrollAction(
type_=PostV1DesktopIdComputerActionScrollActionType.SCROLL,
direction=direction,
amount=amount
)
def type_text(
text: str
) -> PostV1DesktopIdComputerActionTypeTextAction:
return PostV1DesktopIdComputerActionTypeTextAction(
type_=PostV1DesktopIdComputerActionTypeTextActionType.TYPE,
text=text
)
def wait(
ms: int
) -> PostV1DesktopIdComputerActionWaitAction:
return PostV1DesktopIdComputerActionWaitAction(
type_=PostV1DesktopIdComputerActionWaitActionType.WAIT,
ms=ms
)
================================================
FILE: sdks/py-sdk/cyberdesk/client.py
================================================
"""
Cyberdesk Python SDK wrapper client.
"""
from .types import (
GetDesktopParams,
TerminateDesktopParams,
ExecuteBashActionParams,
ComputerActionModel,
)
from openapi_client.api_reference_client.client import Client
from openapi_client.api_reference_client.api.desktop import (
get_v1_desktop_id,
post_v1_desktop,
post_v1_desktop_id_stop,
post_v1_desktop_id_computer_action,
post_v1_desktop_id_bash_action,
)
from openapi_client.api_reference_client.models import (
PostV1DesktopBody,
PostV1DesktopIdBashActionBody,
)
class CyberdeskClient:
"""
Wrapper client for the Cyberdesk API.
Provides both synchronous and asynchronous methods.
"""
def __init__(self, api_key: str, base_url: str = "https://api.cyberdesk.io"):
self.api_key = api_key
self.client = Client(base_url=base_url, headers={"x-api-key": api_key})
def get_desktop(self, id: GetDesktopParams):
"""Synchronous: Get details of a specific desktop instance."""
return get_v1_desktop_id.sync(id=id, client=self.client, x_api_key=self.api_key)
async def async_get_desktop(self, id: GetDesktopParams):
"""Async: Get details of a specific desktop instance. Use with 'await'."""
return await get_v1_desktop_id.asyncio(id=id, client=self.client, x_api_key=self.api_key)
def launch_desktop(self, timeout_ms: int = None):
"""Synchronous: Create a new virtual desktop instance."""
body = PostV1DesktopBody(timeout_ms=timeout_ms) if timeout_ms is not None else PostV1DesktopBody()
return post_v1_desktop.sync(client=self.client, body=body, x_api_key=self.api_key)
async def async_launch_desktop(self, timeout_ms: int = None):
"""Async: Create a new virtual desktop instance. Use with 'await'."""
body = PostV1DesktopBody(timeout_ms=timeout_ms) if timeout_ms is not None else PostV1DesktopBody()
return await post_v1_desktop.asyncio(client=self.client, body=body, x_api_key=self.api_key)
def terminate_desktop(self, id: TerminateDesktopParams):
"""Synchronous: Stop a running desktop instance."""
return post_v1_desktop_id_stop.sync(id=id, client=self.client, x_api_key=self.api_key)
async def async_terminate_desktop(self, id: TerminateDesktopParams):
"""Async: Stop a running desktop instance. Use with 'await'."""
return await post_v1_desktop_id_stop.asyncio(id=id, client=self.client, x_api_key=self.api_key)
def execute_computer_action(self, id: GetDesktopParams, action: ComputerActionModel):
"""Synchronous: Perform an action on the desktop (mouse, keyboard, etc)."""
return post_v1_desktop_id_computer_action.sync(id=id, client=self.client, body=action, x_api_key=self.api_key)
async def async_execute_computer_action(self, id: GetDesktopParams, action: ComputerActionModel):
"""Async: Perform an action on the desktop (mouse, keyboard, etc). Use with 'await'."""
return await post_v1_desktop_id_computer_action.asyncio(id=id, client=self.client, body=action, x_api_key=self.api_key)
def execute_bash_action(self, id: GetDesktopParams, command: ExecuteBashActionParams):
"""Synchronous: Execute a bash command on the desktop."""
body = PostV1DesktopIdBashActionBody(command=command)
return post_v1_desktop_id_bash_action.sync(id=id, client=self.client, body=body, x_api_key=self.api_key)
async def async_execute_bash_action(self, id: GetDesktopParams, command: ExecuteBashActionParams):
"""Async: Execute a bash command on the desktop. Use with 'await'."""
body = PostV1DesktopIdBashActionBody(command=command)
return await post_v1_desktop_id_bash_action.asyncio(id=id, client=self.client, body=body, x_api_key=self.api_key)
================================================
FILE: sdks/py-sdk/cyberdesk/types.py
================================================
from openapi_client.api_reference_client.models import (
PostV1DesktopBody,
PostV1DesktopIdComputerActionClickMouseAction,
PostV1DesktopIdComputerActionDragMouseAction,
PostV1DesktopIdComputerActionGetCursorPositionAction,
PostV1DesktopIdComputerActionMoveMouseAction,
PostV1DesktopIdComputerActionPressKeysAction,
PostV1DesktopIdComputerActionScreenshotAction,
PostV1DesktopIdComputerActionScrollAction,
PostV1DesktopIdComputerActionTypeTextAction,
PostV1DesktopIdComputerActionWaitAction,
)
from typing import Union
# Named parameter types for SDK methods
GetDesktopParams = str # Desktop ID
LaunchDesktopParams = PostV1DesktopBody
TerminateDesktopParams = str # Desktop ID
ExecuteBashActionParams = str # Command string
# Strongly-typed union for all computer action models
ComputerActionModel = Union[
PostV1DesktopIdComputerActionClickMouseAction,
PostV1DesktopIdComputerActionDragMouseAction,
PostV1DesktopIdComputerActionGetCursorPositionAction,
PostV1DesktopIdComputerActionMoveMouseAction,
PostV1DesktopIdComputerActionPressKeysAction,
PostV1DesktopIdComputerActionScreenshotAction,
PostV1DesktopIdComputerActionScrollAction,
PostV1DesktopIdComputerActionTypeTextAction,
PostV1DesktopIdComputerActionWaitAction,
]
# Re-export action models for ergonomic imports
__all__ = [
"GetDesktopParams",
"LaunchDesktopParams",
"TerminateDesktopParams",
"ExecuteBashActionParams",
"ComputerActionModel",
"PostV1DesktopIdComputerActionClickMouseAction",
"PostV1DesktopIdComputerActionDragMouseAction",
"PostV1DesktopIdComputerActionGetCursorPositionAction",
"PostV1DesktopIdComputerActionMoveMouseAction",
"PostV1DesktopIdComputerActionPressKeysAction",
"PostV1DesktopIdComputerActionScreenshotAction",
"PostV1DesktopIdComputerActionScrollAction",
"PostV1DesktopIdComputerActionTypeTextAction",
"PostV1DesktopIdComputerActionWaitAction",
]
================================================
FILE: sdks/py-sdk/openapi_client/.gitignore
================================================
__pycache__/
build/
dist/
*.egg-info/
.pytest_cache/
# pyenv
.python-version
# Environments
.env
.venv
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# JetBrains
.idea/
/coverage.xml
/.coverage
================================================
FILE: sdks/py-sdk/openapi_client/README.md
================================================
# api-reference-client
A client library for accessing API Reference
## Usage
First, create a client:
```python
from api_reference_client import Client
client = Client(base_url="https://api.example.com")
```
If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead:
```python
from api_reference_client import AuthenticatedClient
client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken")
```
Now call your endpoint and use your models:
```python
from api_reference_client.models import MyDataModel
from api_reference_client.api.my_tag import get_my_data_model
from api_reference_client.types import Response
with client as client:
my_data: MyDataModel = get_my_data_model.sync(client=client)
# or if you need more info (e.g. status_code)
response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client)
```
Or do the same thing with an async version:
```python
from api_reference_client.models import MyDataModel
from api_reference_client.api.my_tag import get_my_data_model
from api_reference_client.types import Response
async with client as client:
my_data: MyDataModel = await get_my_data_model.asyncio(client=client)
response: Response[MyDataModel] = await get_my_data_model.asyncio_detailed(client=client)
```
By default, when you're calling an HTTPS API it will attempt to verify that SSL is working correctly. Using certificate verification is highly recommended most of the time, but sometimes you may need to authenticate to a server (especially an internal server) using a custom certificate bundle.
```python
client = AuthenticatedClient(
base_url="https://internal_api.example.com",
token="SuperSecretToken",
verify_ssl="/path/to/certificate_bundle.pem",
)
```
You can also disable certificate validation altogether, but beware that **this is a security risk**.
```python
client = AuthenticatedClient(
base_url="https://internal_api.example.com",
token="SuperSecretToken",
verify_ssl=False
)
```
Things to know:
1. Every path/method combo becomes a Python module with four functions:
1. `sync`: Blocking request that returns parsed data (if successful) or `None`
1. `sync_detailed`: Blocking request that always returns a `Request`, optionally with `parsed` set if the request was successful.
1. `asyncio`: Like `sync` but async instead of blocking
1. `asyncio_detailed`: Like `sync_detailed` but async instead of blocking
1. All path/query params, and bodies become method arguments.
1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above)
1. Any endpoint which did not have a tag will be in `api_reference_client.api.default`
## Advanced customizations
There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case):
```python
from api_reference_client import Client
def log_request(request):
print(f"Request event hook: {request.method} {request.url} - Waiting for response")
def log_response(response):
request = response.request
print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")
client = Client(
base_url="https://api.example.com",
httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}},
)
# Or get the underlying httpx client to modify directly with client.get_httpx_client() or client.get_async_httpx_client()
```
You can even set the httpx client directly, but beware that this will override any existing settings (e.g., base_url):
```python
import httpx
from api_reference_client import Client
client = Client(
base_url="https://api.example.com",
)
# Note that base_url needs to be re-set, as would any shared cookies, headers, etc.
client.set_httpx_client(httpx.Client(base_url="https://api.example.com", proxies="http://localhost:8030"))
```
## Building / publishing this package
This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics:
1. Update the metadata in pyproject.toml (e.g. authors, version)
1. If you're using a private repository, configure it with Poetry
1. `poetry config repositories.`
1. `poetry config http-basic.`
1. Publish the client with `poetry publish --build -r ` or, if for public PyPI, just `poetry publish --build`
If you want to install this client into another project without publishing it (e.g. for development) then:
1. If that project **is using Poetry**, you can simply do `poetry add ` from that project
1. If that project is not using Poetry:
1. Build a wheel with `poetry build -f wheel`
1. Install that wheel from the other project `pip install `
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/__init__.py
================================================
"""A client library for accessing API Reference"""
from .client import AuthenticatedClient, Client
__all__ = (
"AuthenticatedClient",
"Client",
)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/api/__init__.py
================================================
"""Contains methods for accessing the API"""
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/api/desktop/__init__.py
================================================
"""Contains endpoint functions for accessing the API"""
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/api/desktop/get_v1_desktop_id.py
================================================
from http import HTTPStatus
from typing import Any, Optional, Union
from uuid import UUID
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.get_v1_desktop_id_response_200 import GetV1DesktopIdResponse200
from ...models.get_v1_desktop_id_response_400 import GetV1DesktopIdResponse400
from ...models.get_v1_desktop_id_response_401 import GetV1DesktopIdResponse401
from ...models.get_v1_desktop_id_response_403 import GetV1DesktopIdResponse403
from ...models.get_v1_desktop_id_response_404 import GetV1DesktopIdResponse404
from ...models.get_v1_desktop_id_response_409 import GetV1DesktopIdResponse409
from ...models.get_v1_desktop_id_response_429 import GetV1DesktopIdResponse429
from ...models.get_v1_desktop_id_response_500 import GetV1DesktopIdResponse500
from ...models.get_v1_desktop_id_response_502 import GetV1DesktopIdResponse502
from ...types import Response
def _get_kwargs(
id: UUID,
*,
x_api_key: str,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
headers["x-api-key"] = x_api_key
_kwargs: dict[str, Any] = {
"method": "get",
"url": f"/v1/desktop/{id}",
}
_kwargs["headers"] = headers
return _kwargs
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[
Union[
GetV1DesktopIdResponse200,
GetV1DesktopIdResponse400,
GetV1DesktopIdResponse401,
GetV1DesktopIdResponse403,
GetV1DesktopIdResponse404,
GetV1DesktopIdResponse409,
GetV1DesktopIdResponse429,
GetV1DesktopIdResponse500,
GetV1DesktopIdResponse502,
]
]:
if response.status_code == 200:
response_200 = GetV1DesktopIdResponse200.from_dict(response.json())
return response_200
if response.status_code == 400:
response_400 = GetV1DesktopIdResponse400.from_dict(response.json())
return response_400
if response.status_code == 401:
response_401 = GetV1DesktopIdResponse401.from_dict(response.json())
return response_401
if response.status_code == 403:
response_403 = GetV1DesktopIdResponse403.from_dict(response.json())
return response_403
if response.status_code == 404:
response_404 = GetV1DesktopIdResponse404.from_dict(response.json())
return response_404
if response.status_code == 409:
response_409 = GetV1DesktopIdResponse409.from_dict(response.json())
return response_409
if response.status_code == 429:
response_429 = GetV1DesktopIdResponse429.from_dict(response.json())
return response_429
if response.status_code == 500:
response_500 = GetV1DesktopIdResponse500.from_dict(response.json())
return response_500
if response.status_code == 502:
response_502 = GetV1DesktopIdResponse502.from_dict(response.json())
return response_502
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[
Union[
GetV1DesktopIdResponse200,
GetV1DesktopIdResponse400,
GetV1DesktopIdResponse401,
GetV1DesktopIdResponse403,
GetV1DesktopIdResponse404,
GetV1DesktopIdResponse409,
GetV1DesktopIdResponse429,
GetV1DesktopIdResponse500,
GetV1DesktopIdResponse502,
]
]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
id: UUID,
*,
client: Union[AuthenticatedClient, Client],
x_api_key: str,
) -> Response[
Union[
GetV1DesktopIdResponse200,
GetV1DesktopIdResponse400,
GetV1DesktopIdResponse401,
GetV1DesktopIdResponse403,
GetV1DesktopIdResponse404,
GetV1DesktopIdResponse409,
GetV1DesktopIdResponse429,
GetV1DesktopIdResponse500,
GetV1DesktopIdResponse502,
]
]:
"""Get details of a specific desktop instance
Returns the ID, status, creation timestamp, and timeout timestamp for a given desktop instance.
Args:
id (UUID): The UUID of the desktop instance to retrieve Example:
a1b2c3d4-e5f6-7890-1234-567890abcdef.
x_api_key (str): API key for authentication Example: api_12345.
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Union[GetV1DesktopIdResponse200, GetV1DesktopIdResponse400, GetV1DesktopIdResponse401, GetV1DesktopIdResponse403, GetV1DesktopIdResponse404, GetV1DesktopIdResponse409, GetV1DesktopIdResponse429, GetV1DesktopIdResponse500, GetV1DesktopIdResponse502]]
"""
kwargs = _get_kwargs(
id=id,
x_api_key=x_api_key,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
id: UUID,
*,
client: Union[AuthenticatedClient, Client],
x_api_key: str,
) -> Optional[
Union[
GetV1DesktopIdResponse200,
GetV1DesktopIdResponse400,
GetV1DesktopIdResponse401,
GetV1DesktopIdResponse403,
GetV1DesktopIdResponse404,
GetV1DesktopIdResponse409,
GetV1DesktopIdResponse429,
GetV1DesktopIdResponse500,
GetV1DesktopIdResponse502,
]
]:
"""Get details of a specific desktop instance
Returns the ID, status, creation timestamp, and timeout timestamp for a given desktop instance.
Args:
id (UUID): The UUID of the desktop instance to retrieve Example:
a1b2c3d4-e5f6-7890-1234-567890abcdef.
x_api_key (str): API key for authentication Example: api_12345.
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Union[GetV1DesktopIdResponse200, GetV1DesktopIdResponse400, GetV1DesktopIdResponse401, GetV1DesktopIdResponse403, GetV1DesktopIdResponse404, GetV1DesktopIdResponse409, GetV1DesktopIdResponse429, GetV1DesktopIdResponse500, GetV1DesktopIdResponse502]
"""
return sync_detailed(
id=id,
client=client,
x_api_key=x_api_key,
).parsed
async def asyncio_detailed(
id: UUID,
*,
client: Union[AuthenticatedClient, Client],
x_api_key: str,
) -> Response[
Union[
GetV1DesktopIdResponse200,
GetV1DesktopIdResponse400,
GetV1DesktopIdResponse401,
GetV1DesktopIdResponse403,
GetV1DesktopIdResponse404,
GetV1DesktopIdResponse409,
GetV1DesktopIdResponse429,
GetV1DesktopIdResponse500,
GetV1DesktopIdResponse502,
]
]:
"""Get details of a specific desktop instance
Returns the ID, status, creation timestamp, and timeout timestamp for a given desktop instance.
Args:
id (UUID): The UUID of the desktop instance to retrieve Example:
a1b2c3d4-e5f6-7890-1234-567890abcdef.
x_api_key (str): API key for authentication Example: api_12345.
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Union[GetV1DesktopIdResponse200, GetV1DesktopIdResponse400, GetV1DesktopIdResponse401, GetV1DesktopIdResponse403, GetV1DesktopIdResponse404, GetV1DesktopIdResponse409, GetV1DesktopIdResponse429, GetV1DesktopIdResponse500, GetV1DesktopIdResponse502]]
"""
kwargs = _get_kwargs(
id=id,
x_api_key=x_api_key,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
id: UUID,
*,
client: Union[AuthenticatedClient, Client],
x_api_key: str,
) -> Optional[
Union[
GetV1DesktopIdResponse200,
GetV1DesktopIdResponse400,
GetV1DesktopIdResponse401,
GetV1DesktopIdResponse403,
GetV1DesktopIdResponse404,
GetV1DesktopIdResponse409,
GetV1DesktopIdResponse429,
GetV1DesktopIdResponse500,
GetV1DesktopIdResponse502,
]
]:
"""Get details of a specific desktop instance
Returns the ID, status, creation timestamp, and timeout timestamp for a given desktop instance.
Args:
id (UUID): The UUID of the desktop instance to retrieve Example:
a1b2c3d4-e5f6-7890-1234-567890abcdef.
x_api_key (str): API key for authentication Example: api_12345.
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Union[GetV1DesktopIdResponse200, GetV1DesktopIdResponse400, GetV1DesktopIdResponse401, GetV1DesktopIdResponse403, GetV1DesktopIdResponse404, GetV1DesktopIdResponse409, GetV1DesktopIdResponse429, GetV1DesktopIdResponse500, GetV1DesktopIdResponse502]
"""
return (
await asyncio_detailed(
id=id,
client=client,
x_api_key=x_api_key,
)
).parsed
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/api/desktop/post_v1_desktop.py
================================================
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.post_v1_desktop_body import PostV1DesktopBody
from ...models.post_v1_desktop_response_200 import PostV1DesktopResponse200
from ...models.post_v1_desktop_response_400 import PostV1DesktopResponse400
from ...models.post_v1_desktop_response_401 import PostV1DesktopResponse401
from ...models.post_v1_desktop_response_403 import PostV1DesktopResponse403
from ...models.post_v1_desktop_response_404 import PostV1DesktopResponse404
from ...models.post_v1_desktop_response_409 import PostV1DesktopResponse409
from ...models.post_v1_desktop_response_429 import PostV1DesktopResponse429
from ...models.post_v1_desktop_response_500 import PostV1DesktopResponse500
from ...models.post_v1_desktop_response_502 import PostV1DesktopResponse502
from ...types import Response
def _get_kwargs(
*,
body: PostV1DesktopBody,
x_api_key: str,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
headers["x-api-key"] = x_api_key
_kwargs: dict[str, Any] = {
"method": "post",
"url": "/v1/desktop",
}
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[
Union[
PostV1DesktopResponse200,
PostV1DesktopResponse400,
PostV1DesktopResponse401,
PostV1DesktopResponse403,
PostV1DesktopResponse404,
PostV1DesktopResponse409,
PostV1DesktopResponse429,
PostV1DesktopResponse500,
PostV1DesktopResponse502,
]
]:
if response.status_code == 200:
response_200 = PostV1DesktopResponse200.from_dict(response.json())
return response_200
if response.status_code == 400:
response_400 = PostV1DesktopResponse400.from_dict(response.json())
return response_400
if response.status_code == 401:
response_401 = PostV1DesktopResponse401.from_dict(response.json())
return response_401
if response.status_code == 403:
response_403 = PostV1DesktopResponse403.from_dict(response.json())
return response_403
if response.status_code == 404:
response_404 = PostV1DesktopResponse404.from_dict(response.json())
return response_404
if response.status_code == 409:
response_409 = PostV1DesktopResponse409.from_dict(response.json())
return response_409
if response.status_code == 429:
response_429 = PostV1DesktopResponse429.from_dict(response.json())
return response_429
if response.status_code == 500:
response_500 = PostV1DesktopResponse500.from_dict(response.json())
return response_500
if response.status_code == 502:
response_502 = PostV1DesktopResponse502.from_dict(response.json())
return response_502
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[
Union[
PostV1DesktopResponse200,
PostV1DesktopResponse400,
PostV1DesktopResponse401,
PostV1DesktopResponse403,
PostV1DesktopResponse404,
PostV1DesktopResponse409,
PostV1DesktopResponse429,
PostV1DesktopResponse500,
PostV1DesktopResponse502,
]
]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: Union[AuthenticatedClient, Client],
body: PostV1DesktopBody,
x_api_key: str,
) -> Response[
Union[
PostV1DesktopResponse200,
PostV1DesktopResponse400,
PostV1DesktopResponse401,
PostV1DesktopResponse403,
PostV1DesktopResponse404,
PostV1DesktopResponse409,
PostV1DesktopResponse429,
PostV1DesktopResponse500,
PostV1DesktopResponse502,
]
]:
"""Create a new virtual desktop instance
Creates a new virtual desktop instance and returns its ID and stream URL
Args:
x_api_key (str): API key for authentication Example: api_12345.
body (PostV1DesktopBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Union[PostV1DesktopResponse200, PostV1DesktopResponse400, PostV1DesktopResponse401, PostV1DesktopResponse403, PostV1DesktopResponse404, PostV1DesktopResponse409, PostV1DesktopResponse429, PostV1DesktopResponse500, PostV1DesktopResponse502]]
"""
kwargs = _get_kwargs(
body=body,
x_api_key=x_api_key,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: Union[AuthenticatedClient, Client],
body: PostV1DesktopBody,
x_api_key: str,
) -> Optional[
Union[
PostV1DesktopResponse200,
PostV1DesktopResponse400,
PostV1DesktopResponse401,
PostV1DesktopResponse403,
PostV1DesktopResponse404,
PostV1DesktopResponse409,
PostV1DesktopResponse429,
PostV1DesktopResponse500,
PostV1DesktopResponse502,
]
]:
"""Create a new virtual desktop instance
Creates a new virtual desktop instance and returns its ID and stream URL
Args:
x_api_key (str): API key for authentication Example: api_12345.
body (PostV1DesktopBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Union[PostV1DesktopResponse200, PostV1DesktopResponse400, PostV1DesktopResponse401, PostV1DesktopResponse403, PostV1DesktopResponse404, PostV1DesktopResponse409, PostV1DesktopResponse429, PostV1DesktopResponse500, PostV1DesktopResponse502]
"""
return sync_detailed(
client=client,
body=body,
x_api_key=x_api_key,
).parsed
async def asyncio_detailed(
*,
client: Union[AuthenticatedClient, Client],
body: PostV1DesktopBody,
x_api_key: str,
) -> Response[
Union[
PostV1DesktopResponse200,
PostV1DesktopResponse400,
PostV1DesktopResponse401,
PostV1DesktopResponse403,
PostV1DesktopResponse404,
PostV1DesktopResponse409,
PostV1DesktopResponse429,
PostV1DesktopResponse500,
PostV1DesktopResponse502,
]
]:
"""Create a new virtual desktop instance
Creates a new virtual desktop instance and returns its ID and stream URL
Args:
x_api_key (str): API key for authentication Example: api_12345.
body (PostV1DesktopBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Union[PostV1DesktopResponse200, PostV1DesktopResponse400, PostV1DesktopResponse401, PostV1DesktopResponse403, PostV1DesktopResponse404, PostV1DesktopResponse409, PostV1DesktopResponse429, PostV1DesktopResponse500, PostV1DesktopResponse502]]
"""
kwargs = _get_kwargs(
body=body,
x_api_key=x_api_key,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: Union[AuthenticatedClient, Client],
body: PostV1DesktopBody,
x_api_key: str,
) -> Optional[
Union[
PostV1DesktopResponse200,
PostV1DesktopResponse400,
PostV1DesktopResponse401,
PostV1DesktopResponse403,
PostV1DesktopResponse404,
PostV1DesktopResponse409,
PostV1DesktopResponse429,
PostV1DesktopResponse500,
PostV1DesktopResponse502,
]
]:
"""Create a new virtual desktop instance
Creates a new virtual desktop instance and returns its ID and stream URL
Args:
x_api_key (str): API key for authentication Example: api_12345.
body (PostV1DesktopBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Union[PostV1DesktopResponse200, PostV1DesktopResponse400, PostV1DesktopResponse401, PostV1DesktopResponse403, PostV1DesktopResponse404, PostV1DesktopResponse409, PostV1DesktopResponse429, PostV1DesktopResponse500, PostV1DesktopResponse502]
"""
return (
await asyncio_detailed(
client=client,
body=body,
x_api_key=x_api_key,
)
).parsed
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/api/desktop/post_v1_desktop_id_bash_action.py
================================================
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.post_v1_desktop_id_bash_action_body import PostV1DesktopIdBashActionBody
from ...models.post_v1_desktop_id_bash_action_response_200 import PostV1DesktopIdBashActionResponse200
from ...models.post_v1_desktop_id_bash_action_response_400 import PostV1DesktopIdBashActionResponse400
from ...models.post_v1_desktop_id_bash_action_response_401 import PostV1DesktopIdBashActionResponse401
from ...models.post_v1_desktop_id_bash_action_response_403 import PostV1DesktopIdBashActionResponse403
from ...models.post_v1_desktop_id_bash_action_response_404 import PostV1DesktopIdBashActionResponse404
from ...models.post_v1_desktop_id_bash_action_response_409 import PostV1DesktopIdBashActionResponse409
from ...models.post_v1_desktop_id_bash_action_response_429 import PostV1DesktopIdBashActionResponse429
from ...models.post_v1_desktop_id_bash_action_response_500 import PostV1DesktopIdBashActionResponse500
from ...models.post_v1_desktop_id_bash_action_response_502 import PostV1DesktopIdBashActionResponse502
from ...types import Response
def _get_kwargs(
id: str,
*,
body: PostV1DesktopIdBashActionBody,
x_api_key: str,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
headers["x-api-key"] = x_api_key
_kwargs: dict[str, Any] = {
"method": "post",
"url": f"/v1/desktop/{id}/bash-action",
}
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[
Union[
PostV1DesktopIdBashActionResponse200,
PostV1DesktopIdBashActionResponse400,
PostV1DesktopIdBashActionResponse401,
PostV1DesktopIdBashActionResponse403,
PostV1DesktopIdBashActionResponse404,
PostV1DesktopIdBashActionResponse409,
PostV1DesktopIdBashActionResponse429,
PostV1DesktopIdBashActionResponse500,
PostV1DesktopIdBashActionResponse502,
]
]:
if response.status_code == 200:
response_200 = PostV1DesktopIdBashActionResponse200.from_dict(response.json())
return response_200
if response.status_code == 400:
response_400 = PostV1DesktopIdBashActionResponse400.from_dict(response.json())
return response_400
if response.status_code == 401:
response_401 = PostV1DesktopIdBashActionResponse401.from_dict(response.json())
return response_401
if response.status_code == 403:
response_403 = PostV1DesktopIdBashActionResponse403.from_dict(response.json())
return response_403
if response.status_code == 404:
response_404 = PostV1DesktopIdBashActionResponse404.from_dict(response.json())
return response_404
if response.status_code == 409:
response_409 = PostV1DesktopIdBashActionResponse409.from_dict(response.json())
return response_409
if response.status_code == 429:
response_429 = PostV1DesktopIdBashActionResponse429.from_dict(response.json())
return response_429
if response.status_code == 500:
response_500 = PostV1DesktopIdBashActionResponse500.from_dict(response.json())
return response_500
if response.status_code == 502:
response_502 = PostV1DesktopIdBashActionResponse502.from_dict(response.json())
return response_502
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[
Union[
PostV1DesktopIdBashActionResponse200,
PostV1DesktopIdBashActionResponse400,
PostV1DesktopIdBashActionResponse401,
PostV1DesktopIdBashActionResponse403,
PostV1DesktopIdBashActionResponse404,
PostV1DesktopIdBashActionResponse409,
PostV1DesktopIdBashActionResponse429,
PostV1DesktopIdBashActionResponse500,
PostV1DesktopIdBashActionResponse502,
]
]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
id: str,
*,
client: Union[AuthenticatedClient, Client],
body: PostV1DesktopIdBashActionBody,
x_api_key: str,
) -> Response[
Union[
PostV1DesktopIdBashActionResponse200,
PostV1DesktopIdBashActionResponse400,
PostV1DesktopIdBashActionResponse401,
PostV1DesktopIdBashActionResponse403,
PostV1DesktopIdBashActionResponse404,
PostV1DesktopIdBashActionResponse409,
PostV1DesktopIdBashActionResponse429,
PostV1DesktopIdBashActionResponse500,
PostV1DesktopIdBashActionResponse502,
]
]:
"""Execute a bash command on the desktop
Runs a bash command on the desktop and returns the command output
Args:
id (str): Desktop instance ID to run the command on Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
body (PostV1DesktopIdBashActionBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Union[PostV1DesktopIdBashActionResponse200, PostV1DesktopIdBashActionResponse400, PostV1DesktopIdBashActionResponse401, PostV1DesktopIdBashActionResponse403, PostV1DesktopIdBashActionResponse404, PostV1DesktopIdBashActionResponse409, PostV1DesktopIdBashActionResponse429, PostV1DesktopIdBashActionResponse500, PostV1DesktopIdBashActionResponse502]]
"""
kwargs = _get_kwargs(
id=id,
body=body,
x_api_key=x_api_key,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
id: str,
*,
client: Union[AuthenticatedClient, Client],
body: PostV1DesktopIdBashActionBody,
x_api_key: str,
) -> Optional[
Union[
PostV1DesktopIdBashActionResponse200,
PostV1DesktopIdBashActionResponse400,
PostV1DesktopIdBashActionResponse401,
PostV1DesktopIdBashActionResponse403,
PostV1DesktopIdBashActionResponse404,
PostV1DesktopIdBashActionResponse409,
PostV1DesktopIdBashActionResponse429,
PostV1DesktopIdBashActionResponse500,
PostV1DesktopIdBashActionResponse502,
]
]:
"""Execute a bash command on the desktop
Runs a bash command on the desktop and returns the command output
Args:
id (str): Desktop instance ID to run the command on Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
body (PostV1DesktopIdBashActionBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Union[PostV1DesktopIdBashActionResponse200, PostV1DesktopIdBashActionResponse400, PostV1DesktopIdBashActionResponse401, PostV1DesktopIdBashActionResponse403, PostV1DesktopIdBashActionResponse404, PostV1DesktopIdBashActionResponse409, PostV1DesktopIdBashActionResponse429, PostV1DesktopIdBashActionResponse500, PostV1DesktopIdBashActionResponse502]
"""
return sync_detailed(
id=id,
client=client,
body=body,
x_api_key=x_api_key,
).parsed
async def asyncio_detailed(
id: str,
*,
client: Union[AuthenticatedClient, Client],
body: PostV1DesktopIdBashActionBody,
x_api_key: str,
) -> Response[
Union[
PostV1DesktopIdBashActionResponse200,
PostV1DesktopIdBashActionResponse400,
PostV1DesktopIdBashActionResponse401,
PostV1DesktopIdBashActionResponse403,
PostV1DesktopIdBashActionResponse404,
PostV1DesktopIdBashActionResponse409,
PostV1DesktopIdBashActionResponse429,
PostV1DesktopIdBashActionResponse500,
PostV1DesktopIdBashActionResponse502,
]
]:
"""Execute a bash command on the desktop
Runs a bash command on the desktop and returns the command output
Args:
id (str): Desktop instance ID to run the command on Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
body (PostV1DesktopIdBashActionBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Union[PostV1DesktopIdBashActionResponse200, PostV1DesktopIdBashActionResponse400, PostV1DesktopIdBashActionResponse401, PostV1DesktopIdBashActionResponse403, PostV1DesktopIdBashActionResponse404, PostV1DesktopIdBashActionResponse409, PostV1DesktopIdBashActionResponse429, PostV1DesktopIdBashActionResponse500, PostV1DesktopIdBashActionResponse502]]
"""
kwargs = _get_kwargs(
id=id,
body=body,
x_api_key=x_api_key,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
id: str,
*,
client: Union[AuthenticatedClient, Client],
body: PostV1DesktopIdBashActionBody,
x_api_key: str,
) -> Optional[
Union[
PostV1DesktopIdBashActionResponse200,
PostV1DesktopIdBashActionResponse400,
PostV1DesktopIdBashActionResponse401,
PostV1DesktopIdBashActionResponse403,
PostV1DesktopIdBashActionResponse404,
PostV1DesktopIdBashActionResponse409,
PostV1DesktopIdBashActionResponse429,
PostV1DesktopIdBashActionResponse500,
PostV1DesktopIdBashActionResponse502,
]
]:
"""Execute a bash command on the desktop
Runs a bash command on the desktop and returns the command output
Args:
id (str): Desktop instance ID to run the command on Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
body (PostV1DesktopIdBashActionBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Union[PostV1DesktopIdBashActionResponse200, PostV1DesktopIdBashActionResponse400, PostV1DesktopIdBashActionResponse401, PostV1DesktopIdBashActionResponse403, PostV1DesktopIdBashActionResponse404, PostV1DesktopIdBashActionResponse409, PostV1DesktopIdBashActionResponse429, PostV1DesktopIdBashActionResponse500, PostV1DesktopIdBashActionResponse502]
"""
return (
await asyncio_detailed(
id=id,
client=client,
body=body,
x_api_key=x_api_key,
)
).parsed
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/api/desktop/post_v1_desktop_id_computer_action.py
================================================
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.post_v1_desktop_id_computer_action_click_mouse_action import (
PostV1DesktopIdComputerActionClickMouseAction,
)
from ...models.post_v1_desktop_id_computer_action_drag_mouse_action import PostV1DesktopIdComputerActionDragMouseAction
from ...models.post_v1_desktop_id_computer_action_get_cursor_position_action import (
PostV1DesktopIdComputerActionGetCursorPositionAction,
)
from ...models.post_v1_desktop_id_computer_action_move_mouse_action import PostV1DesktopIdComputerActionMoveMouseAction
from ...models.post_v1_desktop_id_computer_action_press_keys_action import PostV1DesktopIdComputerActionPressKeysAction
from ...models.post_v1_desktop_id_computer_action_response_200 import PostV1DesktopIdComputerActionResponse200
from ...models.post_v1_desktop_id_computer_action_response_400 import PostV1DesktopIdComputerActionResponse400
from ...models.post_v1_desktop_id_computer_action_response_401 import PostV1DesktopIdComputerActionResponse401
from ...models.post_v1_desktop_id_computer_action_response_403 import PostV1DesktopIdComputerActionResponse403
from ...models.post_v1_desktop_id_computer_action_response_404 import PostV1DesktopIdComputerActionResponse404
from ...models.post_v1_desktop_id_computer_action_response_409 import PostV1DesktopIdComputerActionResponse409
from ...models.post_v1_desktop_id_computer_action_response_429 import PostV1DesktopIdComputerActionResponse429
from ...models.post_v1_desktop_id_computer_action_response_500 import PostV1DesktopIdComputerActionResponse500
from ...models.post_v1_desktop_id_computer_action_response_502 import PostV1DesktopIdComputerActionResponse502
from ...models.post_v1_desktop_id_computer_action_screenshot_action import PostV1DesktopIdComputerActionScreenshotAction
from ...models.post_v1_desktop_id_computer_action_scroll_action import PostV1DesktopIdComputerActionScrollAction
from ...models.post_v1_desktop_id_computer_action_type_text_action import PostV1DesktopIdComputerActionTypeTextAction
from ...models.post_v1_desktop_id_computer_action_wait_action import PostV1DesktopIdComputerActionWaitAction
from ...types import Response
def _get_kwargs(
id: str,
*,
body: Union[
"PostV1DesktopIdComputerActionClickMouseAction",
"PostV1DesktopIdComputerActionDragMouseAction",
"PostV1DesktopIdComputerActionGetCursorPositionAction",
"PostV1DesktopIdComputerActionMoveMouseAction",
"PostV1DesktopIdComputerActionPressKeysAction",
"PostV1DesktopIdComputerActionScreenshotAction",
"PostV1DesktopIdComputerActionScrollAction",
"PostV1DesktopIdComputerActionTypeTextAction",
"PostV1DesktopIdComputerActionWaitAction",
],
x_api_key: str,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
headers["x-api-key"] = x_api_key
_kwargs: dict[str, Any] = {
"method": "post",
"url": f"/v1/desktop/{id}/computer-action",
}
_body: dict[str, Any]
if isinstance(body, PostV1DesktopIdComputerActionClickMouseAction):
_body = body.to_dict()
elif isinstance(body, PostV1DesktopIdComputerActionScrollAction):
_body = body.to_dict()
elif isinstance(body, PostV1DesktopIdComputerActionMoveMouseAction):
_body = body.to_dict()
elif isinstance(body, PostV1DesktopIdComputerActionDragMouseAction):
_body = body.to_dict()
elif isinstance(body, PostV1DesktopIdComputerActionTypeTextAction):
_body = body.to_dict()
elif isinstance(body, PostV1DesktopIdComputerActionPressKeysAction):
_body = body.to_dict()
elif isinstance(body, PostV1DesktopIdComputerActionWaitAction):
_body = body.to_dict()
elif isinstance(body, PostV1DesktopIdComputerActionScreenshotAction):
_body = body.to_dict()
else:
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[
Union[
PostV1DesktopIdComputerActionResponse200,
PostV1DesktopIdComputerActionResponse400,
PostV1DesktopIdComputerActionResponse401,
PostV1DesktopIdComputerActionResponse403,
PostV1DesktopIdComputerActionResponse404,
PostV1DesktopIdComputerActionResponse409,
PostV1DesktopIdComputerActionResponse429,
PostV1DesktopIdComputerActionResponse500,
PostV1DesktopIdComputerActionResponse502,
]
]:
if response.status_code == 200:
response_200 = PostV1DesktopIdComputerActionResponse200.from_dict(response.json())
return response_200
if response.status_code == 400:
response_400 = PostV1DesktopIdComputerActionResponse400.from_dict(response.json())
return response_400
if response.status_code == 401:
response_401 = PostV1DesktopIdComputerActionResponse401.from_dict(response.json())
return response_401
if response.status_code == 403:
response_403 = PostV1DesktopIdComputerActionResponse403.from_dict(response.json())
return response_403
if response.status_code == 404:
response_404 = PostV1DesktopIdComputerActionResponse404.from_dict(response.json())
return response_404
if response.status_code == 409:
response_409 = PostV1DesktopIdComputerActionResponse409.from_dict(response.json())
return response_409
if response.status_code == 429:
response_429 = PostV1DesktopIdComputerActionResponse429.from_dict(response.json())
return response_429
if response.status_code == 500:
response_500 = PostV1DesktopIdComputerActionResponse500.from_dict(response.json())
return response_500
if response.status_code == 502:
response_502 = PostV1DesktopIdComputerActionResponse502.from_dict(response.json())
return response_502
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[
Union[
PostV1DesktopIdComputerActionResponse200,
PostV1DesktopIdComputerActionResponse400,
PostV1DesktopIdComputerActionResponse401,
PostV1DesktopIdComputerActionResponse403,
PostV1DesktopIdComputerActionResponse404,
PostV1DesktopIdComputerActionResponse409,
PostV1DesktopIdComputerActionResponse429,
PostV1DesktopIdComputerActionResponse500,
PostV1DesktopIdComputerActionResponse502,
]
]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
id: str,
*,
client: Union[AuthenticatedClient, Client],
body: Union[
"PostV1DesktopIdComputerActionClickMouseAction",
"PostV1DesktopIdComputerActionDragMouseAction",
"PostV1DesktopIdComputerActionGetCursorPositionAction",
"PostV1DesktopIdComputerActionMoveMouseAction",
"PostV1DesktopIdComputerActionPressKeysAction",
"PostV1DesktopIdComputerActionScreenshotAction",
"PostV1DesktopIdComputerActionScrollAction",
"PostV1DesktopIdComputerActionTypeTextAction",
"PostV1DesktopIdComputerActionWaitAction",
],
x_api_key: str,
) -> Response[
Union[
PostV1DesktopIdComputerActionResponse200,
PostV1DesktopIdComputerActionResponse400,
PostV1DesktopIdComputerActionResponse401,
PostV1DesktopIdComputerActionResponse403,
PostV1DesktopIdComputerActionResponse404,
PostV1DesktopIdComputerActionResponse409,
PostV1DesktopIdComputerActionResponse429,
PostV1DesktopIdComputerActionResponse500,
PostV1DesktopIdComputerActionResponse502,
]
]:
"""Perform an action on the desktop
Executes a computer action such as mouse clicks, keyboard input, or screenshots on the desktop
Args:
id (str): Desktop instance ID to perform the action on Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
body (Union['PostV1DesktopIdComputerActionClickMouseAction',
'PostV1DesktopIdComputerActionDragMouseAction',
'PostV1DesktopIdComputerActionGetCursorPositionAction',
'PostV1DesktopIdComputerActionMoveMouseAction',
'PostV1DesktopIdComputerActionPressKeysAction',
'PostV1DesktopIdComputerActionScreenshotAction',
'PostV1DesktopIdComputerActionScrollAction',
'PostV1DesktopIdComputerActionTypeTextAction',
'PostV1DesktopIdComputerActionWaitAction']):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Union[PostV1DesktopIdComputerActionResponse200, PostV1DesktopIdComputerActionResponse400, PostV1DesktopIdComputerActionResponse401, PostV1DesktopIdComputerActionResponse403, PostV1DesktopIdComputerActionResponse404, PostV1DesktopIdComputerActionResponse409, PostV1DesktopIdComputerActionResponse429, PostV1DesktopIdComputerActionResponse500, PostV1DesktopIdComputerActionResponse502]]
"""
kwargs = _get_kwargs(
id=id,
body=body,
x_api_key=x_api_key,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
id: str,
*,
client: Union[AuthenticatedClient, Client],
body: Union[
"PostV1DesktopIdComputerActionClickMouseAction",
"PostV1DesktopIdComputerActionDragMouseAction",
"PostV1DesktopIdComputerActionGetCursorPositionAction",
"PostV1DesktopIdComputerActionMoveMouseAction",
"PostV1DesktopIdComputerActionPressKeysAction",
"PostV1DesktopIdComputerActionScreenshotAction",
"PostV1DesktopIdComputerActionScrollAction",
"PostV1DesktopIdComputerActionTypeTextAction",
"PostV1DesktopIdComputerActionWaitAction",
],
x_api_key: str,
) -> Optional[
Union[
PostV1DesktopIdComputerActionResponse200,
PostV1DesktopIdComputerActionResponse400,
PostV1DesktopIdComputerActionResponse401,
PostV1DesktopIdComputerActionResponse403,
PostV1DesktopIdComputerActionResponse404,
PostV1DesktopIdComputerActionResponse409,
PostV1DesktopIdComputerActionResponse429,
PostV1DesktopIdComputerActionResponse500,
PostV1DesktopIdComputerActionResponse502,
]
]:
"""Perform an action on the desktop
Executes a computer action such as mouse clicks, keyboard input, or screenshots on the desktop
Args:
id (str): Desktop instance ID to perform the action on Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
body (Union['PostV1DesktopIdComputerActionClickMouseAction',
'PostV1DesktopIdComputerActionDragMouseAction',
'PostV1DesktopIdComputerActionGetCursorPositionAction',
'PostV1DesktopIdComputerActionMoveMouseAction',
'PostV1DesktopIdComputerActionPressKeysAction',
'PostV1DesktopIdComputerActionScreenshotAction',
'PostV1DesktopIdComputerActionScrollAction',
'PostV1DesktopIdComputerActionTypeTextAction',
'PostV1DesktopIdComputerActionWaitAction']):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Union[PostV1DesktopIdComputerActionResponse200, PostV1DesktopIdComputerActionResponse400, PostV1DesktopIdComputerActionResponse401, PostV1DesktopIdComputerActionResponse403, PostV1DesktopIdComputerActionResponse404, PostV1DesktopIdComputerActionResponse409, PostV1DesktopIdComputerActionResponse429, PostV1DesktopIdComputerActionResponse500, PostV1DesktopIdComputerActionResponse502]
"""
return sync_detailed(
id=id,
client=client,
body=body,
x_api_key=x_api_key,
).parsed
async def asyncio_detailed(
id: str,
*,
client: Union[AuthenticatedClient, Client],
body: Union[
"PostV1DesktopIdComputerActionClickMouseAction",
"PostV1DesktopIdComputerActionDragMouseAction",
"PostV1DesktopIdComputerActionGetCursorPositionAction",
"PostV1DesktopIdComputerActionMoveMouseAction",
"PostV1DesktopIdComputerActionPressKeysAction",
"PostV1DesktopIdComputerActionScreenshotAction",
"PostV1DesktopIdComputerActionScrollAction",
"PostV1DesktopIdComputerActionTypeTextAction",
"PostV1DesktopIdComputerActionWaitAction",
],
x_api_key: str,
) -> Response[
Union[
PostV1DesktopIdComputerActionResponse200,
PostV1DesktopIdComputerActionResponse400,
PostV1DesktopIdComputerActionResponse401,
PostV1DesktopIdComputerActionResponse403,
PostV1DesktopIdComputerActionResponse404,
PostV1DesktopIdComputerActionResponse409,
PostV1DesktopIdComputerActionResponse429,
PostV1DesktopIdComputerActionResponse500,
PostV1DesktopIdComputerActionResponse502,
]
]:
"""Perform an action on the desktop
Executes a computer action such as mouse clicks, keyboard input, or screenshots on the desktop
Args:
id (str): Desktop instance ID to perform the action on Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
body (Union['PostV1DesktopIdComputerActionClickMouseAction',
'PostV1DesktopIdComputerActionDragMouseAction',
'PostV1DesktopIdComputerActionGetCursorPositionAction',
'PostV1DesktopIdComputerActionMoveMouseAction',
'PostV1DesktopIdComputerActionPressKeysAction',
'PostV1DesktopIdComputerActionScreenshotAction',
'PostV1DesktopIdComputerActionScrollAction',
'PostV1DesktopIdComputerActionTypeTextAction',
'PostV1DesktopIdComputerActionWaitAction']):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Union[PostV1DesktopIdComputerActionResponse200, PostV1DesktopIdComputerActionResponse400, PostV1DesktopIdComputerActionResponse401, PostV1DesktopIdComputerActionResponse403, PostV1DesktopIdComputerActionResponse404, PostV1DesktopIdComputerActionResponse409, PostV1DesktopIdComputerActionResponse429, PostV1DesktopIdComputerActionResponse500, PostV1DesktopIdComputerActionResponse502]]
"""
kwargs = _get_kwargs(
id=id,
body=body,
x_api_key=x_api_key,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
id: str,
*,
client: Union[AuthenticatedClient, Client],
body: Union[
"PostV1DesktopIdComputerActionClickMouseAction",
"PostV1DesktopIdComputerActionDragMouseAction",
"PostV1DesktopIdComputerActionGetCursorPositionAction",
"PostV1DesktopIdComputerActionMoveMouseAction",
"PostV1DesktopIdComputerActionPressKeysAction",
"PostV1DesktopIdComputerActionScreenshotAction",
"PostV1DesktopIdComputerActionScrollAction",
"PostV1DesktopIdComputerActionTypeTextAction",
"PostV1DesktopIdComputerActionWaitAction",
],
x_api_key: str,
) -> Optional[
Union[
PostV1DesktopIdComputerActionResponse200,
PostV1DesktopIdComputerActionResponse400,
PostV1DesktopIdComputerActionResponse401,
PostV1DesktopIdComputerActionResponse403,
PostV1DesktopIdComputerActionResponse404,
PostV1DesktopIdComputerActionResponse409,
PostV1DesktopIdComputerActionResponse429,
PostV1DesktopIdComputerActionResponse500,
PostV1DesktopIdComputerActionResponse502,
]
]:
"""Perform an action on the desktop
Executes a computer action such as mouse clicks, keyboard input, or screenshots on the desktop
Args:
id (str): Desktop instance ID to perform the action on Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
body (Union['PostV1DesktopIdComputerActionClickMouseAction',
'PostV1DesktopIdComputerActionDragMouseAction',
'PostV1DesktopIdComputerActionGetCursorPositionAction',
'PostV1DesktopIdComputerActionMoveMouseAction',
'PostV1DesktopIdComputerActionPressKeysAction',
'PostV1DesktopIdComputerActionScreenshotAction',
'PostV1DesktopIdComputerActionScrollAction',
'PostV1DesktopIdComputerActionTypeTextAction',
'PostV1DesktopIdComputerActionWaitAction']):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Union[PostV1DesktopIdComputerActionResponse200, PostV1DesktopIdComputerActionResponse400, PostV1DesktopIdComputerActionResponse401, PostV1DesktopIdComputerActionResponse403, PostV1DesktopIdComputerActionResponse404, PostV1DesktopIdComputerActionResponse409, PostV1DesktopIdComputerActionResponse429, PostV1DesktopIdComputerActionResponse500, PostV1DesktopIdComputerActionResponse502]
"""
return (
await asyncio_detailed(
id=id,
client=client,
body=body,
x_api_key=x_api_key,
)
).parsed
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/api/desktop/post_v1_desktop_id_stop.py
================================================
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.post_v1_desktop_id_stop_response_200 import PostV1DesktopIdStopResponse200
from ...models.post_v1_desktop_id_stop_response_400 import PostV1DesktopIdStopResponse400
from ...models.post_v1_desktop_id_stop_response_401 import PostV1DesktopIdStopResponse401
from ...models.post_v1_desktop_id_stop_response_403 import PostV1DesktopIdStopResponse403
from ...models.post_v1_desktop_id_stop_response_404 import PostV1DesktopIdStopResponse404
from ...models.post_v1_desktop_id_stop_response_409 import PostV1DesktopIdStopResponse409
from ...models.post_v1_desktop_id_stop_response_429 import PostV1DesktopIdStopResponse429
from ...models.post_v1_desktop_id_stop_response_500 import PostV1DesktopIdStopResponse500
from ...models.post_v1_desktop_id_stop_response_502 import PostV1DesktopIdStopResponse502
from ...types import Response
def _get_kwargs(
id: str,
*,
x_api_key: str,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
headers["x-api-key"] = x_api_key
_kwargs: dict[str, Any] = {
"method": "post",
"url": f"/v1/desktop/{id}/stop",
}
_kwargs["headers"] = headers
return _kwargs
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[
Union[
PostV1DesktopIdStopResponse200,
PostV1DesktopIdStopResponse400,
PostV1DesktopIdStopResponse401,
PostV1DesktopIdStopResponse403,
PostV1DesktopIdStopResponse404,
PostV1DesktopIdStopResponse409,
PostV1DesktopIdStopResponse429,
PostV1DesktopIdStopResponse500,
PostV1DesktopIdStopResponse502,
]
]:
if response.status_code == 200:
response_200 = PostV1DesktopIdStopResponse200.from_dict(response.json())
return response_200
if response.status_code == 400:
response_400 = PostV1DesktopIdStopResponse400.from_dict(response.json())
return response_400
if response.status_code == 401:
response_401 = PostV1DesktopIdStopResponse401.from_dict(response.json())
return response_401
if response.status_code == 403:
response_403 = PostV1DesktopIdStopResponse403.from_dict(response.json())
return response_403
if response.status_code == 404:
response_404 = PostV1DesktopIdStopResponse404.from_dict(response.json())
return response_404
if response.status_code == 409:
response_409 = PostV1DesktopIdStopResponse409.from_dict(response.json())
return response_409
if response.status_code == 429:
response_429 = PostV1DesktopIdStopResponse429.from_dict(response.json())
return response_429
if response.status_code == 500:
response_500 = PostV1DesktopIdStopResponse500.from_dict(response.json())
return response_500
if response.status_code == 502:
response_502 = PostV1DesktopIdStopResponse502.from_dict(response.json())
return response_502
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[
Union[
PostV1DesktopIdStopResponse200,
PostV1DesktopIdStopResponse400,
PostV1DesktopIdStopResponse401,
PostV1DesktopIdStopResponse403,
PostV1DesktopIdStopResponse404,
PostV1DesktopIdStopResponse409,
PostV1DesktopIdStopResponse429,
PostV1DesktopIdStopResponse500,
PostV1DesktopIdStopResponse502,
]
]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
id: str,
*,
client: Union[AuthenticatedClient, Client],
x_api_key: str,
) -> Response[
Union[
PostV1DesktopIdStopResponse200,
PostV1DesktopIdStopResponse400,
PostV1DesktopIdStopResponse401,
PostV1DesktopIdStopResponse403,
PostV1DesktopIdStopResponse404,
PostV1DesktopIdStopResponse409,
PostV1DesktopIdStopResponse429,
PostV1DesktopIdStopResponse500,
PostV1DesktopIdStopResponse502,
]
]:
"""Stop a running desktop instance
Stops a running desktop instance and cleans up resources
Args:
id (str): Desktop instance ID to stop Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Union[PostV1DesktopIdStopResponse200, PostV1DesktopIdStopResponse400, PostV1DesktopIdStopResponse401, PostV1DesktopIdStopResponse403, PostV1DesktopIdStopResponse404, PostV1DesktopIdStopResponse409, PostV1DesktopIdStopResponse429, PostV1DesktopIdStopResponse500, PostV1DesktopIdStopResponse502]]
"""
kwargs = _get_kwargs(
id=id,
x_api_key=x_api_key,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
id: str,
*,
client: Union[AuthenticatedClient, Client],
x_api_key: str,
) -> Optional[
Union[
PostV1DesktopIdStopResponse200,
PostV1DesktopIdStopResponse400,
PostV1DesktopIdStopResponse401,
PostV1DesktopIdStopResponse403,
PostV1DesktopIdStopResponse404,
PostV1DesktopIdStopResponse409,
PostV1DesktopIdStopResponse429,
PostV1DesktopIdStopResponse500,
PostV1DesktopIdStopResponse502,
]
]:
"""Stop a running desktop instance
Stops a running desktop instance and cleans up resources
Args:
id (str): Desktop instance ID to stop Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Union[PostV1DesktopIdStopResponse200, PostV1DesktopIdStopResponse400, PostV1DesktopIdStopResponse401, PostV1DesktopIdStopResponse403, PostV1DesktopIdStopResponse404, PostV1DesktopIdStopResponse409, PostV1DesktopIdStopResponse429, PostV1DesktopIdStopResponse500, PostV1DesktopIdStopResponse502]
"""
return sync_detailed(
id=id,
client=client,
x_api_key=x_api_key,
).parsed
async def asyncio_detailed(
id: str,
*,
client: Union[AuthenticatedClient, Client],
x_api_key: str,
) -> Response[
Union[
PostV1DesktopIdStopResponse200,
PostV1DesktopIdStopResponse400,
PostV1DesktopIdStopResponse401,
PostV1DesktopIdStopResponse403,
PostV1DesktopIdStopResponse404,
PostV1DesktopIdStopResponse409,
PostV1DesktopIdStopResponse429,
PostV1DesktopIdStopResponse500,
PostV1DesktopIdStopResponse502,
]
]:
"""Stop a running desktop instance
Stops a running desktop instance and cleans up resources
Args:
id (str): Desktop instance ID to stop Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Union[PostV1DesktopIdStopResponse200, PostV1DesktopIdStopResponse400, PostV1DesktopIdStopResponse401, PostV1DesktopIdStopResponse403, PostV1DesktopIdStopResponse404, PostV1DesktopIdStopResponse409, PostV1DesktopIdStopResponse429, PostV1DesktopIdStopResponse500, PostV1DesktopIdStopResponse502]]
"""
kwargs = _get_kwargs(
id=id,
x_api_key=x_api_key,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
id: str,
*,
client: Union[AuthenticatedClient, Client],
x_api_key: str,
) -> Optional[
Union[
PostV1DesktopIdStopResponse200,
PostV1DesktopIdStopResponse400,
PostV1DesktopIdStopResponse401,
PostV1DesktopIdStopResponse403,
PostV1DesktopIdStopResponse404,
PostV1DesktopIdStopResponse409,
PostV1DesktopIdStopResponse429,
PostV1DesktopIdStopResponse500,
PostV1DesktopIdStopResponse502,
]
]:
"""Stop a running desktop instance
Stops a running desktop instance and cleans up resources
Args:
id (str): Desktop instance ID to stop Example: desktop_12345.
x_api_key (str): API key for authentication Example: api_12345.
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Union[PostV1DesktopIdStopResponse200, PostV1DesktopIdStopResponse400, PostV1DesktopIdStopResponse401, PostV1DesktopIdStopResponse403, PostV1DesktopIdStopResponse404, PostV1DesktopIdStopResponse409, PostV1DesktopIdStopResponse429, PostV1DesktopIdStopResponse500, PostV1DesktopIdStopResponse502]
"""
return (
await asyncio_detailed(
id=id,
client=client,
x_api_key=x_api_key,
)
).parsed
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/client.py
================================================
import ssl
from typing import Any, Optional, Union
import httpx
from attrs import define, evolve, field
@define
class Client:
"""A class for keeping track of data related to the API
The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
``base_url``: The base URL for the API, all requests are made to a relative path to this URL
``cookies``: A dictionary of cookies to be sent with every request
``headers``: A dictionary of headers to be sent with every request
``timeout``: The maximum amount of a time a request can take. API functions will raise
httpx.TimeoutException if this is exceeded.
``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
but can be set to False for testing purposes.
``follow_redirects``: Whether or not to follow redirects. Default value is False.
``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
Attributes:
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
argument to the constructor.
"""
raise_on_unexpected_status: bool = field(default=False, kw_only=True)
_base_url: str = field(alias="base_url")
_cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
_headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
_timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout")
_verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl")
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
_httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
_client: Optional[httpx.Client] = field(default=None, init=False)
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
def with_headers(self, headers: dict[str, str]) -> "Client":
"""Get a new client matching this one with additional headers"""
if self._client is not None:
self._client.headers.update(headers)
if self._async_client is not None:
self._async_client.headers.update(headers)
return evolve(self, headers={**self._headers, **headers})
def with_cookies(self, cookies: dict[str, str]) -> "Client":
"""Get a new client matching this one with additional cookies"""
if self._client is not None:
self._client.cookies.update(cookies)
if self._async_client is not None:
self._async_client.cookies.update(cookies)
return evolve(self, cookies={**self._cookies, **cookies})
def with_timeout(self, timeout: httpx.Timeout) -> "Client":
"""Get a new client matching this one with a new timeout (in seconds)"""
if self._client is not None:
self._client.timeout = timeout
if self._async_client is not None:
self._async_client.timeout = timeout
return evolve(self, timeout=timeout)
def set_httpx_client(self, client: httpx.Client) -> "Client":
"""Manually set the underlying httpx.Client
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._client = client
return self
def get_httpx_client(self) -> httpx.Client:
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
if self._client is None:
self._client = httpx.Client(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
)
return self._client
def __enter__(self) -> "Client":
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
self.get_httpx_client().__enter__()
return self
def __exit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
self.get_httpx_client().__exit__(*args, **kwargs)
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client":
"""Manually the underlying httpx.AsyncClient
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._async_client = async_client
return self
def get_async_httpx_client(self) -> httpx.AsyncClient:
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
if self._async_client is None:
self._async_client = httpx.AsyncClient(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
)
return self._async_client
async def __aenter__(self) -> "Client":
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
await self.get_async_httpx_client().__aenter__()
return self
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
await self.get_async_httpx_client().__aexit__(*args, **kwargs)
@define
class AuthenticatedClient:
"""A Client which has been authenticated for use on secured endpoints
The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
``base_url``: The base URL for the API, all requests are made to a relative path to this URL
``cookies``: A dictionary of cookies to be sent with every request
``headers``: A dictionary of headers to be sent with every request
``timeout``: The maximum amount of a time a request can take. API functions will raise
httpx.TimeoutException if this is exceeded.
``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
but can be set to False for testing purposes.
``follow_redirects``: Whether or not to follow redirects. Default value is False.
``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
Attributes:
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
argument to the constructor.
token: The token to use for authentication
prefix: The prefix to use for the Authorization header
auth_header_name: The name of the Authorization header
"""
raise_on_unexpected_status: bool = field(default=False, kw_only=True)
_base_url: str = field(alias="base_url")
_cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
_headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
_timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout")
_verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl")
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
_httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
_client: Optional[httpx.Client] = field(default=None, init=False)
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
token: str
prefix: str = "Bearer"
auth_header_name: str = "Authorization"
def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient":
"""Get a new client matching this one with additional headers"""
if self._client is not None:
self._client.headers.update(headers)
if self._async_client is not None:
self._async_client.headers.update(headers)
return evolve(self, headers={**self._headers, **headers})
def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient":
"""Get a new client matching this one with additional cookies"""
if self._client is not None:
self._client.cookies.update(cookies)
if self._async_client is not None:
self._async_client.cookies.update(cookies)
return evolve(self, cookies={**self._cookies, **cookies})
def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient":
"""Get a new client matching this one with a new timeout (in seconds)"""
if self._client is not None:
self._client.timeout = timeout
if self._async_client is not None:
self._async_client.timeout = timeout
return evolve(self, timeout=timeout)
def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient":
"""Manually set the underlying httpx.Client
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._client = client
return self
def get_httpx_client(self) -> httpx.Client:
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
if self._client is None:
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
self._client = httpx.Client(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
)
return self._client
def __enter__(self) -> "AuthenticatedClient":
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
self.get_httpx_client().__enter__()
return self
def __exit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
self.get_httpx_client().__exit__(*args, **kwargs)
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient":
"""Manually the underlying httpx.AsyncClient
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._async_client = async_client
return self
def get_async_httpx_client(self) -> httpx.AsyncClient:
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
if self._async_client is None:
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
self._async_client = httpx.AsyncClient(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
)
return self._async_client
async def __aenter__(self) -> "AuthenticatedClient":
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
await self.get_async_httpx_client().__aenter__()
return self
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
await self.get_async_httpx_client().__aexit__(*args, **kwargs)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/errors.py
================================================
"""Contains shared errors types that can be raised from API functions"""
class UnexpectedStatus(Exception):
"""Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True"""
def __init__(self, status_code: int, content: bytes):
self.status_code = status_code
self.content = content
super().__init__(
f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}"
)
__all__ = ["UnexpectedStatus"]
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/__init__.py
================================================
"""Contains all the data models used in inputs/outputs"""
from .get_v1_desktop_id_response_200 import GetV1DesktopIdResponse200
from .get_v1_desktop_id_response_200_status import GetV1DesktopIdResponse200Status
from .get_v1_desktop_id_response_400 import GetV1DesktopIdResponse400
from .get_v1_desktop_id_response_400_status import GetV1DesktopIdResponse400Status
from .get_v1_desktop_id_response_401 import GetV1DesktopIdResponse401
from .get_v1_desktop_id_response_401_status import GetV1DesktopIdResponse401Status
from .get_v1_desktop_id_response_403 import GetV1DesktopIdResponse403
from .get_v1_desktop_id_response_403_status import GetV1DesktopIdResponse403Status
from .get_v1_desktop_id_response_404 import GetV1DesktopIdResponse404
from .get_v1_desktop_id_response_404_status import GetV1DesktopIdResponse404Status
from .get_v1_desktop_id_response_409 import GetV1DesktopIdResponse409
from .get_v1_desktop_id_response_409_status import GetV1DesktopIdResponse409Status
from .get_v1_desktop_id_response_429 import GetV1DesktopIdResponse429
from .get_v1_desktop_id_response_429_status import GetV1DesktopIdResponse429Status
from .get_v1_desktop_id_response_500 import GetV1DesktopIdResponse500
from .get_v1_desktop_id_response_500_status import GetV1DesktopIdResponse500Status
from .get_v1_desktop_id_response_502 import GetV1DesktopIdResponse502
from .get_v1_desktop_id_response_502_status import GetV1DesktopIdResponse502Status
from .post_v1_desktop_body import PostV1DesktopBody
from .post_v1_desktop_id_bash_action_body import PostV1DesktopIdBashActionBody
from .post_v1_desktop_id_bash_action_response_200 import PostV1DesktopIdBashActionResponse200
from .post_v1_desktop_id_bash_action_response_400 import PostV1DesktopIdBashActionResponse400
from .post_v1_desktop_id_bash_action_response_400_status import PostV1DesktopIdBashActionResponse400Status
from .post_v1_desktop_id_bash_action_response_401 import PostV1DesktopIdBashActionResponse401
from .post_v1_desktop_id_bash_action_response_401_status import PostV1DesktopIdBashActionResponse401Status
from .post_v1_desktop_id_bash_action_response_403 import PostV1DesktopIdBashActionResponse403
from .post_v1_desktop_id_bash_action_response_403_status import PostV1DesktopIdBashActionResponse403Status
from .post_v1_desktop_id_bash_action_response_404 import PostV1DesktopIdBashActionResponse404
from .post_v1_desktop_id_bash_action_response_404_status import PostV1DesktopIdBashActionResponse404Status
from .post_v1_desktop_id_bash_action_response_409 import PostV1DesktopIdBashActionResponse409
from .post_v1_desktop_id_bash_action_response_409_status import PostV1DesktopIdBashActionResponse409Status
from .post_v1_desktop_id_bash_action_response_429 import PostV1DesktopIdBashActionResponse429
from .post_v1_desktop_id_bash_action_response_429_status import PostV1DesktopIdBashActionResponse429Status
from .post_v1_desktop_id_bash_action_response_500 import PostV1DesktopIdBashActionResponse500
from .post_v1_desktop_id_bash_action_response_500_status import PostV1DesktopIdBashActionResponse500Status
from .post_v1_desktop_id_bash_action_response_502 import PostV1DesktopIdBashActionResponse502
from .post_v1_desktop_id_bash_action_response_502_status import PostV1DesktopIdBashActionResponse502Status
from .post_v1_desktop_id_computer_action_click_mouse_action import PostV1DesktopIdComputerActionClickMouseAction
from .post_v1_desktop_id_computer_action_click_mouse_action_button import (
PostV1DesktopIdComputerActionClickMouseActionButton,
)
from .post_v1_desktop_id_computer_action_click_mouse_action_click_type import (
PostV1DesktopIdComputerActionClickMouseActionClickType,
)
from .post_v1_desktop_id_computer_action_click_mouse_action_type import (
PostV1DesktopIdComputerActionClickMouseActionType,
)
from .post_v1_desktop_id_computer_action_drag_mouse_action import PostV1DesktopIdComputerActionDragMouseAction
from .post_v1_desktop_id_computer_action_drag_mouse_action_end import PostV1DesktopIdComputerActionDragMouseActionEnd
from .post_v1_desktop_id_computer_action_drag_mouse_action_start import (
PostV1DesktopIdComputerActionDragMouseActionStart,
)
from .post_v1_desktop_id_computer_action_drag_mouse_action_type import PostV1DesktopIdComputerActionDragMouseActionType
from .post_v1_desktop_id_computer_action_get_cursor_position_action import (
PostV1DesktopIdComputerActionGetCursorPositionAction,
)
from .post_v1_desktop_id_computer_action_get_cursor_position_action_type import (
PostV1DesktopIdComputerActionGetCursorPositionActionType,
)
from .post_v1_desktop_id_computer_action_move_mouse_action import PostV1DesktopIdComputerActionMoveMouseAction
from .post_v1_desktop_id_computer_action_move_mouse_action_type import PostV1DesktopIdComputerActionMoveMouseActionType
from .post_v1_desktop_id_computer_action_press_keys_action import PostV1DesktopIdComputerActionPressKeysAction
from .post_v1_desktop_id_computer_action_press_keys_action_key_action_type import (
PostV1DesktopIdComputerActionPressKeysActionKeyActionType,
)
from .post_v1_desktop_id_computer_action_press_keys_action_type import PostV1DesktopIdComputerActionPressKeysActionType
from .post_v1_desktop_id_computer_action_response_200 import PostV1DesktopIdComputerActionResponse200
from .post_v1_desktop_id_computer_action_response_400 import PostV1DesktopIdComputerActionResponse400
from .post_v1_desktop_id_computer_action_response_400_status import PostV1DesktopIdComputerActionResponse400Status
from .post_v1_desktop_id_computer_action_response_401 import PostV1DesktopIdComputerActionResponse401
from .post_v1_desktop_id_computer_action_response_401_status import PostV1DesktopIdComputerActionResponse401Status
from .post_v1_desktop_id_computer_action_response_403 import PostV1DesktopIdComputerActionResponse403
from .post_v1_desktop_id_computer_action_response_403_status import PostV1DesktopIdComputerActionResponse403Status
from .post_v1_desktop_id_computer_action_response_404 import PostV1DesktopIdComputerActionResponse404
from .post_v1_desktop_id_computer_action_response_404_status import PostV1DesktopIdComputerActionResponse404Status
from .post_v1_desktop_id_computer_action_response_409 import PostV1DesktopIdComputerActionResponse409
from .post_v1_desktop_id_computer_action_response_409_status import PostV1DesktopIdComputerActionResponse409Status
from .post_v1_desktop_id_computer_action_response_429 import PostV1DesktopIdComputerActionResponse429
from .post_v1_desktop_id_computer_action_response_429_status import PostV1DesktopIdComputerActionResponse429Status
from .post_v1_desktop_id_computer_action_response_500 import PostV1DesktopIdComputerActionResponse500
from .post_v1_desktop_id_computer_action_response_500_status import PostV1DesktopIdComputerActionResponse500Status
from .post_v1_desktop_id_computer_action_response_502 import PostV1DesktopIdComputerActionResponse502
from .post_v1_desktop_id_computer_action_response_502_status import PostV1DesktopIdComputerActionResponse502Status
from .post_v1_desktop_id_computer_action_screenshot_action import PostV1DesktopIdComputerActionScreenshotAction
from .post_v1_desktop_id_computer_action_screenshot_action_type import PostV1DesktopIdComputerActionScreenshotActionType
from .post_v1_desktop_id_computer_action_scroll_action import PostV1DesktopIdComputerActionScrollAction
from .post_v1_desktop_id_computer_action_scroll_action_direction import (
PostV1DesktopIdComputerActionScrollActionDirection,
)
from .post_v1_desktop_id_computer_action_scroll_action_type import PostV1DesktopIdComputerActionScrollActionType
from .post_v1_desktop_id_computer_action_type_text_action import PostV1DesktopIdComputerActionTypeTextAction
from .post_v1_desktop_id_computer_action_type_text_action_type import PostV1DesktopIdComputerActionTypeTextActionType
from .post_v1_desktop_id_computer_action_wait_action import PostV1DesktopIdComputerActionWaitAction
from .post_v1_desktop_id_computer_action_wait_action_type import PostV1DesktopIdComputerActionWaitActionType
from .post_v1_desktop_id_stop_response_200 import PostV1DesktopIdStopResponse200
from .post_v1_desktop_id_stop_response_200_status import PostV1DesktopIdStopResponse200Status
from .post_v1_desktop_id_stop_response_400 import PostV1DesktopIdStopResponse400
from .post_v1_desktop_id_stop_response_400_status import PostV1DesktopIdStopResponse400Status
from .post_v1_desktop_id_stop_response_401 import PostV1DesktopIdStopResponse401
from .post_v1_desktop_id_stop_response_401_status import PostV1DesktopIdStopResponse401Status
from .post_v1_desktop_id_stop_response_403 import PostV1DesktopIdStopResponse403
from .post_v1_desktop_id_stop_response_403_status import PostV1DesktopIdStopResponse403Status
from .post_v1_desktop_id_stop_response_404 import PostV1DesktopIdStopResponse404
from .post_v1_desktop_id_stop_response_404_status import PostV1DesktopIdStopResponse404Status
from .post_v1_desktop_id_stop_response_409 import PostV1DesktopIdStopResponse409
from .post_v1_desktop_id_stop_response_409_status import PostV1DesktopIdStopResponse409Status
from .post_v1_desktop_id_stop_response_429 import PostV1DesktopIdStopResponse429
from .post_v1_desktop_id_stop_response_429_status import PostV1DesktopIdStopResponse429Status
from .post_v1_desktop_id_stop_response_500 import PostV1DesktopIdStopResponse500
from .post_v1_desktop_id_stop_response_500_status import PostV1DesktopIdStopResponse500Status
from .post_v1_desktop_id_stop_response_502 import PostV1DesktopIdStopResponse502
from .post_v1_desktop_id_stop_response_502_status import PostV1DesktopIdStopResponse502Status
from .post_v1_desktop_response_200 import PostV1DesktopResponse200
from .post_v1_desktop_response_200_status import PostV1DesktopResponse200Status
from .post_v1_desktop_response_400 import PostV1DesktopResponse400
from .post_v1_desktop_response_400_status import PostV1DesktopResponse400Status
from .post_v1_desktop_response_401 import PostV1DesktopResponse401
from .post_v1_desktop_response_401_status import PostV1DesktopResponse401Status
from .post_v1_desktop_response_403 import PostV1DesktopResponse403
from .post_v1_desktop_response_403_status import PostV1DesktopResponse403Status
from .post_v1_desktop_response_404 import PostV1DesktopResponse404
from .post_v1_desktop_response_404_status import PostV1DesktopResponse404Status
from .post_v1_desktop_response_409 import PostV1DesktopResponse409
from .post_v1_desktop_response_409_status import PostV1DesktopResponse409Status
from .post_v1_desktop_response_429 import PostV1DesktopResponse429
from .post_v1_desktop_response_429_status import PostV1DesktopResponse429Status
from .post_v1_desktop_response_500 import PostV1DesktopResponse500
from .post_v1_desktop_response_500_status import PostV1DesktopResponse500Status
from .post_v1_desktop_response_502 import PostV1DesktopResponse502
from .post_v1_desktop_response_502_status import PostV1DesktopResponse502Status
__all__ = (
"GetV1DesktopIdResponse200",
"GetV1DesktopIdResponse200Status",
"GetV1DesktopIdResponse400",
"GetV1DesktopIdResponse400Status",
"GetV1DesktopIdResponse401",
"GetV1DesktopIdResponse401Status",
"GetV1DesktopIdResponse403",
"GetV1DesktopIdResponse403Status",
"GetV1DesktopIdResponse404",
"GetV1DesktopIdResponse404Status",
"GetV1DesktopIdResponse409",
"GetV1DesktopIdResponse409Status",
"GetV1DesktopIdResponse429",
"GetV1DesktopIdResponse429Status",
"GetV1DesktopIdResponse500",
"GetV1DesktopIdResponse500Status",
"GetV1DesktopIdResponse502",
"GetV1DesktopIdResponse502Status",
"PostV1DesktopBody",
"PostV1DesktopIdBashActionBody",
"PostV1DesktopIdBashActionResponse200",
"PostV1DesktopIdBashActionResponse400",
"PostV1DesktopIdBashActionResponse400Status",
"PostV1DesktopIdBashActionResponse401",
"PostV1DesktopIdBashActionResponse401Status",
"PostV1DesktopIdBashActionResponse403",
"PostV1DesktopIdBashActionResponse403Status",
"PostV1DesktopIdBashActionResponse404",
"PostV1DesktopIdBashActionResponse404Status",
"PostV1DesktopIdBashActionResponse409",
"PostV1DesktopIdBashActionResponse409Status",
"PostV1DesktopIdBashActionResponse429",
"PostV1DesktopIdBashActionResponse429Status",
"PostV1DesktopIdBashActionResponse500",
"PostV1DesktopIdBashActionResponse500Status",
"PostV1DesktopIdBashActionResponse502",
"PostV1DesktopIdBashActionResponse502Status",
"PostV1DesktopIdComputerActionClickMouseAction",
"PostV1DesktopIdComputerActionClickMouseActionButton",
"PostV1DesktopIdComputerActionClickMouseActionClickType",
"PostV1DesktopIdComputerActionClickMouseActionType",
"PostV1DesktopIdComputerActionDragMouseAction",
"PostV1DesktopIdComputerActionDragMouseActionEnd",
"PostV1DesktopIdComputerActionDragMouseActionStart",
"PostV1DesktopIdComputerActionDragMouseActionType",
"PostV1DesktopIdComputerActionGetCursorPositionAction",
"PostV1DesktopIdComputerActionGetCursorPositionActionType",
"PostV1DesktopIdComputerActionMoveMouseAction",
"PostV1DesktopIdComputerActionMoveMouseActionType",
"PostV1DesktopIdComputerActionPressKeysAction",
"PostV1DesktopIdComputerActionPressKeysActionKeyActionType",
"PostV1DesktopIdComputerActionPressKeysActionType",
"PostV1DesktopIdComputerActionResponse200",
"PostV1DesktopIdComputerActionResponse400",
"PostV1DesktopIdComputerActionResponse400Status",
"PostV1DesktopIdComputerActionResponse401",
"PostV1DesktopIdComputerActionResponse401Status",
"PostV1DesktopIdComputerActionResponse403",
"PostV1DesktopIdComputerActionResponse403Status",
"PostV1DesktopIdComputerActionResponse404",
"PostV1DesktopIdComputerActionResponse404Status",
"PostV1DesktopIdComputerActionResponse409",
"PostV1DesktopIdComputerActionResponse409Status",
"PostV1DesktopIdComputerActionResponse429",
"PostV1DesktopIdComputerActionResponse429Status",
"PostV1DesktopIdComputerActionResponse500",
"PostV1DesktopIdComputerActionResponse500Status",
"PostV1DesktopIdComputerActionResponse502",
"PostV1DesktopIdComputerActionResponse502Status",
"PostV1DesktopIdComputerActionScreenshotAction",
"PostV1DesktopIdComputerActionScreenshotActionType",
"PostV1DesktopIdComputerActionScrollAction",
"PostV1DesktopIdComputerActionScrollActionDirection",
"PostV1DesktopIdComputerActionScrollActionType",
"PostV1DesktopIdComputerActionTypeTextAction",
"PostV1DesktopIdComputerActionTypeTextActionType",
"PostV1DesktopIdComputerActionWaitAction",
"PostV1DesktopIdComputerActionWaitActionType",
"PostV1DesktopIdStopResponse200",
"PostV1DesktopIdStopResponse200Status",
"PostV1DesktopIdStopResponse400",
"PostV1DesktopIdStopResponse400Status",
"PostV1DesktopIdStopResponse401",
"PostV1DesktopIdStopResponse401Status",
"PostV1DesktopIdStopResponse403",
"PostV1DesktopIdStopResponse403Status",
"PostV1DesktopIdStopResponse404",
"PostV1DesktopIdStopResponse404Status",
"PostV1DesktopIdStopResponse409",
"PostV1DesktopIdStopResponse409Status",
"PostV1DesktopIdStopResponse429",
"PostV1DesktopIdStopResponse429Status",
"PostV1DesktopIdStopResponse500",
"PostV1DesktopIdStopResponse500Status",
"PostV1DesktopIdStopResponse502",
"PostV1DesktopIdStopResponse502Status",
"PostV1DesktopResponse200",
"PostV1DesktopResponse200Status",
"PostV1DesktopResponse400",
"PostV1DesktopResponse400Status",
"PostV1DesktopResponse401",
"PostV1DesktopResponse401Status",
"PostV1DesktopResponse403",
"PostV1DesktopResponse403Status",
"PostV1DesktopResponse404",
"PostV1DesktopResponse404Status",
"PostV1DesktopResponse409",
"PostV1DesktopResponse409Status",
"PostV1DesktopResponse429",
"PostV1DesktopResponse429Status",
"PostV1DesktopResponse500",
"PostV1DesktopResponse500Status",
"PostV1DesktopResponse502",
"PostV1DesktopResponse502Status",
)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_200.py
================================================
import datetime
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast
from uuid import UUID
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse
from ..models.get_v1_desktop_id_response_200_status import GetV1DesktopIdResponse200Status
T = TypeVar("T", bound="GetV1DesktopIdResponse200")
@_attrs_define
class GetV1DesktopIdResponse200:
"""
Attributes:
id (UUID): Unique identifier for the desktop instance Example: a1b2c3d4-e5f6-7890-1234-567890abcdef.
status (GetV1DesktopIdResponse200Status): Current status of the desktop instance Example: running.
stream_url (Union[None, str]): URL for the desktop stream (null if the desktop is not running) Example:
https://cyberdesk.com/vnc/a1b2c3d4-e5f6-7890-1234-567890abcdef.
created_at (datetime.datetime): Timestamp when the instance was created Example: 2023-10-27T10:00:00Z.
timeout_at (datetime.datetime): Timestamp when the instance will automatically time out Example:
2023-10-28T10:00:00Z.
"""
id: UUID
status: GetV1DesktopIdResponse200Status
stream_url: Union[None, str]
created_at: datetime.datetime
timeout_at: datetime.datetime
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
id = str(self.id)
status = self.status.value
stream_url: Union[None, str]
stream_url = self.stream_url
created_at = self.created_at.isoformat()
timeout_at = self.timeout_at.isoformat()
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"id": id,
"status": status,
"stream_url": stream_url,
"created_at": created_at,
"timeout_at": timeout_at,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
id = UUID(d.pop("id"))
status = GetV1DesktopIdResponse200Status(d.pop("status"))
def _parse_stream_url(data: object) -> Union[None, str]:
if data is None:
return data
return cast(Union[None, str], data)
stream_url = _parse_stream_url(d.pop("stream_url"))
created_at = isoparse(d.pop("created_at"))
timeout_at = isoparse(d.pop("timeout_at"))
get_v1_desktop_id_response_200 = cls(
id=id,
status=status,
stream_url=stream_url,
created_at=created_at,
timeout_at=timeout_at,
)
get_v1_desktop_id_response_200.additional_properties = d
return get_v1_desktop_id_response_200
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_200_status.py
================================================
from enum import Enum
class GetV1DesktopIdResponse200Status(str, Enum):
ERROR = "error"
PENDING = "pending"
RUNNING = "running"
TERMINATED = "terminated"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_400.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.get_v1_desktop_id_response_400_status import GetV1DesktopIdResponse400Status
T = TypeVar("T", bound="GetV1DesktopIdResponse400")
@_attrs_define
class GetV1DesktopIdResponse400:
"""
Attributes:
status (GetV1DesktopIdResponse400Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: GetV1DesktopIdResponse400Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = GetV1DesktopIdResponse400Status(d.pop("status"))
error = d.pop("error")
get_v1_desktop_id_response_400 = cls(
status=status,
error=error,
)
get_v1_desktop_id_response_400.additional_properties = d
return get_v1_desktop_id_response_400
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_400_status.py
================================================
from enum import Enum
class GetV1DesktopIdResponse400Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_401.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.get_v1_desktop_id_response_401_status import GetV1DesktopIdResponse401Status
T = TypeVar("T", bound="GetV1DesktopIdResponse401")
@_attrs_define
class GetV1DesktopIdResponse401:
"""
Attributes:
status (GetV1DesktopIdResponse401Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: GetV1DesktopIdResponse401Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = GetV1DesktopIdResponse401Status(d.pop("status"))
error = d.pop("error")
get_v1_desktop_id_response_401 = cls(
status=status,
error=error,
)
get_v1_desktop_id_response_401.additional_properties = d
return get_v1_desktop_id_response_401
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_401_status.py
================================================
from enum import Enum
class GetV1DesktopIdResponse401Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_403.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.get_v1_desktop_id_response_403_status import GetV1DesktopIdResponse403Status
T = TypeVar("T", bound="GetV1DesktopIdResponse403")
@_attrs_define
class GetV1DesktopIdResponse403:
"""
Attributes:
status (GetV1DesktopIdResponse403Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: GetV1DesktopIdResponse403Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = GetV1DesktopIdResponse403Status(d.pop("status"))
error = d.pop("error")
get_v1_desktop_id_response_403 = cls(
status=status,
error=error,
)
get_v1_desktop_id_response_403.additional_properties = d
return get_v1_desktop_id_response_403
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_403_status.py
================================================
from enum import Enum
class GetV1DesktopIdResponse403Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_404.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.get_v1_desktop_id_response_404_status import GetV1DesktopIdResponse404Status
T = TypeVar("T", bound="GetV1DesktopIdResponse404")
@_attrs_define
class GetV1DesktopIdResponse404:
"""
Attributes:
status (GetV1DesktopIdResponse404Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: GetV1DesktopIdResponse404Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = GetV1DesktopIdResponse404Status(d.pop("status"))
error = d.pop("error")
get_v1_desktop_id_response_404 = cls(
status=status,
error=error,
)
get_v1_desktop_id_response_404.additional_properties = d
return get_v1_desktop_id_response_404
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_404_status.py
================================================
from enum import Enum
class GetV1DesktopIdResponse404Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_409.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.get_v1_desktop_id_response_409_status import GetV1DesktopIdResponse409Status
T = TypeVar("T", bound="GetV1DesktopIdResponse409")
@_attrs_define
class GetV1DesktopIdResponse409:
"""
Attributes:
status (GetV1DesktopIdResponse409Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: GetV1DesktopIdResponse409Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = GetV1DesktopIdResponse409Status(d.pop("status"))
error = d.pop("error")
get_v1_desktop_id_response_409 = cls(
status=status,
error=error,
)
get_v1_desktop_id_response_409.additional_properties = d
return get_v1_desktop_id_response_409
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_409_status.py
================================================
from enum import Enum
class GetV1DesktopIdResponse409Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_429.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.get_v1_desktop_id_response_429_status import GetV1DesktopIdResponse429Status
T = TypeVar("T", bound="GetV1DesktopIdResponse429")
@_attrs_define
class GetV1DesktopIdResponse429:
"""
Attributes:
status (GetV1DesktopIdResponse429Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: GetV1DesktopIdResponse429Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = GetV1DesktopIdResponse429Status(d.pop("status"))
error = d.pop("error")
get_v1_desktop_id_response_429 = cls(
status=status,
error=error,
)
get_v1_desktop_id_response_429.additional_properties = d
return get_v1_desktop_id_response_429
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_429_status.py
================================================
from enum import Enum
class GetV1DesktopIdResponse429Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_500.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.get_v1_desktop_id_response_500_status import GetV1DesktopIdResponse500Status
T = TypeVar("T", bound="GetV1DesktopIdResponse500")
@_attrs_define
class GetV1DesktopIdResponse500:
"""
Attributes:
status (GetV1DesktopIdResponse500Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: GetV1DesktopIdResponse500Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = GetV1DesktopIdResponse500Status(d.pop("status"))
error = d.pop("error")
get_v1_desktop_id_response_500 = cls(
status=status,
error=error,
)
get_v1_desktop_id_response_500.additional_properties = d
return get_v1_desktop_id_response_500
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_500_status.py
================================================
from enum import Enum
class GetV1DesktopIdResponse500Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_502.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.get_v1_desktop_id_response_502_status import GetV1DesktopIdResponse502Status
T = TypeVar("T", bound="GetV1DesktopIdResponse502")
@_attrs_define
class GetV1DesktopIdResponse502:
"""
Attributes:
status (GetV1DesktopIdResponse502Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: GetV1DesktopIdResponse502Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = GetV1DesktopIdResponse502Status(d.pop("status"))
error = d.pop("error")
get_v1_desktop_id_response_502 = cls(
status=status,
error=error,
)
get_v1_desktop_id_response_502.additional_properties = d
return get_v1_desktop_id_response_502
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/get_v1_desktop_id_response_502_status.py
================================================
from enum import Enum
class GetV1DesktopIdResponse502Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_body.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..types import UNSET, Unset
T = TypeVar("T", bound="PostV1DesktopBody")
@_attrs_define
class PostV1DesktopBody:
"""
Attributes:
timeout_ms (Union[Unset, int]): Timeout in milliseconds for the desktop session Example: 3600000.
"""
timeout_ms: Union[Unset, int] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
timeout_ms = self.timeout_ms
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update({})
if timeout_ms is not UNSET:
field_dict["timeout_ms"] = timeout_ms
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
timeout_ms = d.pop("timeout_ms", UNSET)
post_v1_desktop_body = cls(
timeout_ms=timeout_ms,
)
post_v1_desktop_body.additional_properties = d
return post_v1_desktop_body
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_body.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
T = TypeVar("T", bound="PostV1DesktopIdBashActionBody")
@_attrs_define
class PostV1DesktopIdBashActionBody:
"""
Attributes:
command (str): Bash command to execute Example: echo 'Hello, World!'.
"""
command: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
command = self.command
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"command": command,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
command = d.pop("command")
post_v1_desktop_id_bash_action_body = cls(
command=command,
)
post_v1_desktop_id_bash_action_body.additional_properties = d
return post_v1_desktop_id_bash_action_body
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_200.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..types import UNSET, Unset
T = TypeVar("T", bound="PostV1DesktopIdBashActionResponse200")
@_attrs_define
class PostV1DesktopIdBashActionResponse200:
"""
Attributes:
output (Union[Unset, str]): Raw string output from the executed command (if any) Example: X=500 Y=300.
error (Union[Unset, str]): Error message if the operation failed (also indicated by non-2xx HTTP status)
Example: Command failed with code 1: xdotool: command not found.
base64_image (Union[Unset, str]): Base64 encoded JPEG image data (only returned for screenshot actions) Example:
/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQ....
"""
output: Union[Unset, str] = UNSET
error: Union[Unset, str] = UNSET
base64_image: Union[Unset, str] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
output = self.output
error = self.error
base64_image = self.base64_image
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update({})
if output is not UNSET:
field_dict["output"] = output
if error is not UNSET:
field_dict["error"] = error
if base64_image is not UNSET:
field_dict["base64_image"] = base64_image
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
output = d.pop("output", UNSET)
error = d.pop("error", UNSET)
base64_image = d.pop("base64_image", UNSET)
post_v1_desktop_id_bash_action_response_200 = cls(
output=output,
error=error,
base64_image=base64_image,
)
post_v1_desktop_id_bash_action_response_200.additional_properties = d
return post_v1_desktop_id_bash_action_response_200
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_400.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_bash_action_response_400_status import PostV1DesktopIdBashActionResponse400Status
T = TypeVar("T", bound="PostV1DesktopIdBashActionResponse400")
@_attrs_define
class PostV1DesktopIdBashActionResponse400:
"""
Attributes:
status (PostV1DesktopIdBashActionResponse400Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdBashActionResponse400Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdBashActionResponse400Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_bash_action_response_400 = cls(
status=status,
error=error,
)
post_v1_desktop_id_bash_action_response_400.additional_properties = d
return post_v1_desktop_id_bash_action_response_400
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_400_status.py
================================================
from enum import Enum
class PostV1DesktopIdBashActionResponse400Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_401.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_bash_action_response_401_status import PostV1DesktopIdBashActionResponse401Status
T = TypeVar("T", bound="PostV1DesktopIdBashActionResponse401")
@_attrs_define
class PostV1DesktopIdBashActionResponse401:
"""
Attributes:
status (PostV1DesktopIdBashActionResponse401Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdBashActionResponse401Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdBashActionResponse401Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_bash_action_response_401 = cls(
status=status,
error=error,
)
post_v1_desktop_id_bash_action_response_401.additional_properties = d
return post_v1_desktop_id_bash_action_response_401
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_401_status.py
================================================
from enum import Enum
class PostV1DesktopIdBashActionResponse401Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_403.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_bash_action_response_403_status import PostV1DesktopIdBashActionResponse403Status
T = TypeVar("T", bound="PostV1DesktopIdBashActionResponse403")
@_attrs_define
class PostV1DesktopIdBashActionResponse403:
"""
Attributes:
status (PostV1DesktopIdBashActionResponse403Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdBashActionResponse403Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdBashActionResponse403Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_bash_action_response_403 = cls(
status=status,
error=error,
)
post_v1_desktop_id_bash_action_response_403.additional_properties = d
return post_v1_desktop_id_bash_action_response_403
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_403_status.py
================================================
from enum import Enum
class PostV1DesktopIdBashActionResponse403Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_404.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_bash_action_response_404_status import PostV1DesktopIdBashActionResponse404Status
T = TypeVar("T", bound="PostV1DesktopIdBashActionResponse404")
@_attrs_define
class PostV1DesktopIdBashActionResponse404:
"""
Attributes:
status (PostV1DesktopIdBashActionResponse404Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdBashActionResponse404Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdBashActionResponse404Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_bash_action_response_404 = cls(
status=status,
error=error,
)
post_v1_desktop_id_bash_action_response_404.additional_properties = d
return post_v1_desktop_id_bash_action_response_404
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_404_status.py
================================================
from enum import Enum
class PostV1DesktopIdBashActionResponse404Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_409.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_bash_action_response_409_status import PostV1DesktopIdBashActionResponse409Status
T = TypeVar("T", bound="PostV1DesktopIdBashActionResponse409")
@_attrs_define
class PostV1DesktopIdBashActionResponse409:
"""
Attributes:
status (PostV1DesktopIdBashActionResponse409Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdBashActionResponse409Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdBashActionResponse409Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_bash_action_response_409 = cls(
status=status,
error=error,
)
post_v1_desktop_id_bash_action_response_409.additional_properties = d
return post_v1_desktop_id_bash_action_response_409
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_409_status.py
================================================
from enum import Enum
class PostV1DesktopIdBashActionResponse409Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_429.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_bash_action_response_429_status import PostV1DesktopIdBashActionResponse429Status
T = TypeVar("T", bound="PostV1DesktopIdBashActionResponse429")
@_attrs_define
class PostV1DesktopIdBashActionResponse429:
"""
Attributes:
status (PostV1DesktopIdBashActionResponse429Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdBashActionResponse429Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdBashActionResponse429Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_bash_action_response_429 = cls(
status=status,
error=error,
)
post_v1_desktop_id_bash_action_response_429.additional_properties = d
return post_v1_desktop_id_bash_action_response_429
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_429_status.py
================================================
from enum import Enum
class PostV1DesktopIdBashActionResponse429Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_500.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_bash_action_response_500_status import PostV1DesktopIdBashActionResponse500Status
T = TypeVar("T", bound="PostV1DesktopIdBashActionResponse500")
@_attrs_define
class PostV1DesktopIdBashActionResponse500:
"""
Attributes:
status (PostV1DesktopIdBashActionResponse500Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdBashActionResponse500Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdBashActionResponse500Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_bash_action_response_500 = cls(
status=status,
error=error,
)
post_v1_desktop_id_bash_action_response_500.additional_properties = d
return post_v1_desktop_id_bash_action_response_500
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_500_status.py
================================================
from enum import Enum
class PostV1DesktopIdBashActionResponse500Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_502.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_bash_action_response_502_status import PostV1DesktopIdBashActionResponse502Status
T = TypeVar("T", bound="PostV1DesktopIdBashActionResponse502")
@_attrs_define
class PostV1DesktopIdBashActionResponse502:
"""
Attributes:
status (PostV1DesktopIdBashActionResponse502Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdBashActionResponse502Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdBashActionResponse502Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_bash_action_response_502 = cls(
status=status,
error=error,
)
post_v1_desktop_id_bash_action_response_502.additional_properties = d
return post_v1_desktop_id_bash_action_response_502
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_bash_action_response_502_status.py
================================================
from enum import Enum
class PostV1DesktopIdBashActionResponse502Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_click_mouse_action.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_click_mouse_action_button import (
PostV1DesktopIdComputerActionClickMouseActionButton,
)
from ..models.post_v1_desktop_id_computer_action_click_mouse_action_click_type import (
PostV1DesktopIdComputerActionClickMouseActionClickType,
)
from ..models.post_v1_desktop_id_computer_action_click_mouse_action_type import (
PostV1DesktopIdComputerActionClickMouseActionType,
)
from ..types import UNSET, Unset
T = TypeVar("T", bound="PostV1DesktopIdComputerActionClickMouseAction")
@_attrs_define
class PostV1DesktopIdComputerActionClickMouseAction:
"""
Attributes:
type_ (PostV1DesktopIdComputerActionClickMouseActionType): Perform a mouse action: click, press (down), or
release (up). Defaults to a single left click at the current position. Example: click_mouse.
x (Union[Unset, int]): X coordinate for the action (optional, uses current position if omitted) Example: 500.
y (Union[Unset, int]): Y coordinate for the action (optional, uses current position if omitted) Example: 300.
button (Union[Unset, PostV1DesktopIdComputerActionClickMouseActionButton]): Mouse button to use (optional,
defaults to 'left') Example: left.
num_of_clicks (Union[Unset, int]): Number of clicks to perform (optional, defaults to 1, only applicable for
'click' type) Example: 1.
click_type (Union[Unset, PostV1DesktopIdComputerActionClickMouseActionClickType]): Type of mouse action
(optional, defaults to 'click') Example: click.
"""
type_: PostV1DesktopIdComputerActionClickMouseActionType
x: Union[Unset, int] = UNSET
y: Union[Unset, int] = UNSET
button: Union[Unset, PostV1DesktopIdComputerActionClickMouseActionButton] = UNSET
num_of_clicks: Union[Unset, int] = UNSET
click_type: Union[Unset, PostV1DesktopIdComputerActionClickMouseActionClickType] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
type_ = self.type_.value
x = self.x
y = self.y
button: Union[Unset, str] = UNSET
if not isinstance(self.button, Unset):
button = self.button.value
num_of_clicks = self.num_of_clicks
click_type: Union[Unset, str] = UNSET
if not isinstance(self.click_type, Unset):
click_type = self.click_type.value
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"type": type_,
}
)
if x is not UNSET:
field_dict["x"] = x
if y is not UNSET:
field_dict["y"] = y
if button is not UNSET:
field_dict["button"] = button
if num_of_clicks is not UNSET:
field_dict["num_of_clicks"] = num_of_clicks
if click_type is not UNSET:
field_dict["click_type"] = click_type
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
type_ = PostV1DesktopIdComputerActionClickMouseActionType(d.pop("type"))
x = d.pop("x", UNSET)
y = d.pop("y", UNSET)
_button = d.pop("button", UNSET)
button: Union[Unset, PostV1DesktopIdComputerActionClickMouseActionButton]
if isinstance(_button, Unset):
button = UNSET
else:
button = PostV1DesktopIdComputerActionClickMouseActionButton(_button)
num_of_clicks = d.pop("num_of_clicks", UNSET)
_click_type = d.pop("click_type", UNSET)
click_type: Union[Unset, PostV1DesktopIdComputerActionClickMouseActionClickType]
if isinstance(_click_type, Unset):
click_type = UNSET
else:
click_type = PostV1DesktopIdComputerActionClickMouseActionClickType(_click_type)
post_v1_desktop_id_computer_action_click_mouse_action = cls(
type_=type_,
x=x,
y=y,
button=button,
num_of_clicks=num_of_clicks,
click_type=click_type,
)
post_v1_desktop_id_computer_action_click_mouse_action.additional_properties = d
return post_v1_desktop_id_computer_action_click_mouse_action
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_click_mouse_action_button.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionClickMouseActionButton(str, Enum):
LEFT = "left"
MIDDLE = "middle"
RIGHT = "right"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_click_mouse_action_click_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionClickMouseActionClickType(str, Enum):
CLICK = "click"
DOWN = "down"
UP = "up"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_click_mouse_action_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionClickMouseActionType(str, Enum):
CLICK_MOUSE = "click_mouse"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_drag_mouse_action.py
================================================
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_drag_mouse_action_type import (
PostV1DesktopIdComputerActionDragMouseActionType,
)
if TYPE_CHECKING:
from ..models.post_v1_desktop_id_computer_action_drag_mouse_action_end import (
PostV1DesktopIdComputerActionDragMouseActionEnd,
)
from ..models.post_v1_desktop_id_computer_action_drag_mouse_action_start import (
PostV1DesktopIdComputerActionDragMouseActionStart,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionDragMouseAction")
@_attrs_define
class PostV1DesktopIdComputerActionDragMouseAction:
"""
Attributes:
type_ (PostV1DesktopIdComputerActionDragMouseActionType): Drag the mouse from start to end coordinates Example:
drag_mouse.
start (PostV1DesktopIdComputerActionDragMouseActionStart): Starting coordinates for the drag operation Example:
{'x': 100, 'y': 100}.
end (PostV1DesktopIdComputerActionDragMouseActionEnd): Ending coordinates for the drag operation Example: {'x':
300, 'y': 300}.
"""
type_: PostV1DesktopIdComputerActionDragMouseActionType
start: "PostV1DesktopIdComputerActionDragMouseActionStart"
end: "PostV1DesktopIdComputerActionDragMouseActionEnd"
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
type_ = self.type_.value
start = self.start.to_dict()
end = self.end.to_dict()
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"type": type_,
"start": start,
"end": end,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
from ..models.post_v1_desktop_id_computer_action_drag_mouse_action_end import (
PostV1DesktopIdComputerActionDragMouseActionEnd,
)
from ..models.post_v1_desktop_id_computer_action_drag_mouse_action_start import (
PostV1DesktopIdComputerActionDragMouseActionStart,
)
d = dict(src_dict)
type_ = PostV1DesktopIdComputerActionDragMouseActionType(d.pop("type"))
start = PostV1DesktopIdComputerActionDragMouseActionStart.from_dict(d.pop("start"))
end = PostV1DesktopIdComputerActionDragMouseActionEnd.from_dict(d.pop("end"))
post_v1_desktop_id_computer_action_drag_mouse_action = cls(
type_=type_,
start=start,
end=end,
)
post_v1_desktop_id_computer_action_drag_mouse_action.additional_properties = d
return post_v1_desktop_id_computer_action_drag_mouse_action
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_drag_mouse_action_end.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
T = TypeVar("T", bound="PostV1DesktopIdComputerActionDragMouseActionEnd")
@_attrs_define
class PostV1DesktopIdComputerActionDragMouseActionEnd:
"""Ending coordinates for the drag operation
Example:
{'x': 300, 'y': 300}
Attributes:
x (int): X coordinate on the screen Example: 500.
y (int): Y coordinate on the screen Example: 300.
"""
x: int
y: int
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
x = self.x
y = self.y
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"x": x,
"y": y,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
x = d.pop("x")
y = d.pop("y")
post_v1_desktop_id_computer_action_drag_mouse_action_end = cls(
x=x,
y=y,
)
post_v1_desktop_id_computer_action_drag_mouse_action_end.additional_properties = d
return post_v1_desktop_id_computer_action_drag_mouse_action_end
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_drag_mouse_action_start.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
T = TypeVar("T", bound="PostV1DesktopIdComputerActionDragMouseActionStart")
@_attrs_define
class PostV1DesktopIdComputerActionDragMouseActionStart:
"""Starting coordinates for the drag operation
Example:
{'x': 100, 'y': 100}
Attributes:
x (int): X coordinate on the screen Example: 500.
y (int): Y coordinate on the screen Example: 300.
"""
x: int
y: int
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
x = self.x
y = self.y
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"x": x,
"y": y,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
x = d.pop("x")
y = d.pop("y")
post_v1_desktop_id_computer_action_drag_mouse_action_start = cls(
x=x,
y=y,
)
post_v1_desktop_id_computer_action_drag_mouse_action_start.additional_properties = d
return post_v1_desktop_id_computer_action_drag_mouse_action_start
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_drag_mouse_action_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionDragMouseActionType(str, Enum):
DRAG_MOUSE = "drag_mouse"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_get_cursor_position_action.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_get_cursor_position_action_type import (
PostV1DesktopIdComputerActionGetCursorPositionActionType,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionGetCursorPositionAction")
@_attrs_define
class PostV1DesktopIdComputerActionGetCursorPositionAction:
"""
Attributes:
type_ (PostV1DesktopIdComputerActionGetCursorPositionActionType): Get the current mouse cursor position Example:
get_cursor_position.
"""
type_: PostV1DesktopIdComputerActionGetCursorPositionActionType
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
type_ = self.type_.value
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"type": type_,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
type_ = PostV1DesktopIdComputerActionGetCursorPositionActionType(d.pop("type"))
post_v1_desktop_id_computer_action_get_cursor_position_action = cls(
type_=type_,
)
post_v1_desktop_id_computer_action_get_cursor_position_action.additional_properties = d
return post_v1_desktop_id_computer_action_get_cursor_position_action
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_get_cursor_position_action_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionGetCursorPositionActionType(str, Enum):
GET_CURSOR_POSITION = "get_cursor_position"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_move_mouse_action.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_move_mouse_action_type import (
PostV1DesktopIdComputerActionMoveMouseActionType,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionMoveMouseAction")
@_attrs_define
class PostV1DesktopIdComputerActionMoveMouseAction:
"""
Attributes:
type_ (PostV1DesktopIdComputerActionMoveMouseActionType): Move the mouse cursor to the specified coordinates
Example: move_mouse.
x (int): X coordinate to move to Example: 500.
y (int): Y coordinate to move to Example: 300.
"""
type_: PostV1DesktopIdComputerActionMoveMouseActionType
x: int
y: int
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
type_ = self.type_.value
x = self.x
y = self.y
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"type": type_,
"x": x,
"y": y,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
type_ = PostV1DesktopIdComputerActionMoveMouseActionType(d.pop("type"))
x = d.pop("x")
y = d.pop("y")
post_v1_desktop_id_computer_action_move_mouse_action = cls(
type_=type_,
x=x,
y=y,
)
post_v1_desktop_id_computer_action_move_mouse_action.additional_properties = d
return post_v1_desktop_id_computer_action_move_mouse_action
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_move_mouse_action_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionMoveMouseActionType(str, Enum):
MOVE_MOUSE = "move_mouse"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_press_keys_action.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_press_keys_action_key_action_type import (
PostV1DesktopIdComputerActionPressKeysActionKeyActionType,
)
from ..models.post_v1_desktop_id_computer_action_press_keys_action_type import (
PostV1DesktopIdComputerActionPressKeysActionType,
)
from ..types import UNSET, Unset
T = TypeVar("T", bound="PostV1DesktopIdComputerActionPressKeysAction")
@_attrs_define
class PostV1DesktopIdComputerActionPressKeysAction:
"""
Attributes:
type_ (PostV1DesktopIdComputerActionPressKeysActionType): Press, hold down, or release one or more keyboard
keys. Defaults to a single press and release. Example: press_keys.
keys (Union[list[str], str]):
key_action_type (Union[Unset, PostV1DesktopIdComputerActionPressKeysActionKeyActionType]): Type of key action
(optional, defaults to 'press' which is a down and up action) Example: press.
"""
type_: PostV1DesktopIdComputerActionPressKeysActionType
keys: Union[list[str], str]
key_action_type: Union[Unset, PostV1DesktopIdComputerActionPressKeysActionKeyActionType] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
type_ = self.type_.value
keys: Union[list[str], str]
if isinstance(self.keys, list):
keys = self.keys
else:
keys = self.keys
key_action_type: Union[Unset, str] = UNSET
if not isinstance(self.key_action_type, Unset):
key_action_type = self.key_action_type.value
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"type": type_,
"keys": keys,
}
)
if key_action_type is not UNSET:
field_dict["key_action_type"] = key_action_type
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
type_ = PostV1DesktopIdComputerActionPressKeysActionType(d.pop("type"))
def _parse_keys(data: object) -> Union[list[str], str]:
try:
if not isinstance(data, list):
raise TypeError()
keys_type_1 = cast(list[str], data)
return keys_type_1
except: # noqa: E722
pass
return cast(Union[list[str], str], data)
keys = _parse_keys(d.pop("keys"))
_key_action_type = d.pop("key_action_type", UNSET)
key_action_type: Union[Unset, PostV1DesktopIdComputerActionPressKeysActionKeyActionType]
if isinstance(_key_action_type, Unset):
key_action_type = UNSET
else:
key_action_type = PostV1DesktopIdComputerActionPressKeysActionKeyActionType(_key_action_type)
post_v1_desktop_id_computer_action_press_keys_action = cls(
type_=type_,
keys=keys,
key_action_type=key_action_type,
)
post_v1_desktop_id_computer_action_press_keys_action.additional_properties = d
return post_v1_desktop_id_computer_action_press_keys_action
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_press_keys_action_key_action_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionPressKeysActionKeyActionType(str, Enum):
DOWN = "down"
PRESS = "press"
UP = "up"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_press_keys_action_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionPressKeysActionType(str, Enum):
PRESS_KEYS = "press_keys"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_200.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..types import UNSET, Unset
T = TypeVar("T", bound="PostV1DesktopIdComputerActionResponse200")
@_attrs_define
class PostV1DesktopIdComputerActionResponse200:
"""
Attributes:
output (Union[Unset, str]): Raw string output from the executed command (if any) Example: X=500 Y=300.
error (Union[Unset, str]): Error message if the operation failed (also indicated by non-2xx HTTP status)
Example: Command failed with code 1: xdotool: command not found.
base64_image (Union[Unset, str]): Base64 encoded JPEG image data (only returned for screenshot actions) Example:
/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQ....
"""
output: Union[Unset, str] = UNSET
error: Union[Unset, str] = UNSET
base64_image: Union[Unset, str] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
output = self.output
error = self.error
base64_image = self.base64_image
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update({})
if output is not UNSET:
field_dict["output"] = output
if error is not UNSET:
field_dict["error"] = error
if base64_image is not UNSET:
field_dict["base64_image"] = base64_image
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
output = d.pop("output", UNSET)
error = d.pop("error", UNSET)
base64_image = d.pop("base64_image", UNSET)
post_v1_desktop_id_computer_action_response_200 = cls(
output=output,
error=error,
base64_image=base64_image,
)
post_v1_desktop_id_computer_action_response_200.additional_properties = d
return post_v1_desktop_id_computer_action_response_200
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_400.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_response_400_status import (
PostV1DesktopIdComputerActionResponse400Status,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionResponse400")
@_attrs_define
class PostV1DesktopIdComputerActionResponse400:
"""
Attributes:
status (PostV1DesktopIdComputerActionResponse400Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdComputerActionResponse400Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdComputerActionResponse400Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_computer_action_response_400 = cls(
status=status,
error=error,
)
post_v1_desktop_id_computer_action_response_400.additional_properties = d
return post_v1_desktop_id_computer_action_response_400
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_400_status.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionResponse400Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_401.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_response_401_status import (
PostV1DesktopIdComputerActionResponse401Status,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionResponse401")
@_attrs_define
class PostV1DesktopIdComputerActionResponse401:
"""
Attributes:
status (PostV1DesktopIdComputerActionResponse401Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdComputerActionResponse401Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdComputerActionResponse401Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_computer_action_response_401 = cls(
status=status,
error=error,
)
post_v1_desktop_id_computer_action_response_401.additional_properties = d
return post_v1_desktop_id_computer_action_response_401
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_401_status.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionResponse401Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_403.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_response_403_status import (
PostV1DesktopIdComputerActionResponse403Status,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionResponse403")
@_attrs_define
class PostV1DesktopIdComputerActionResponse403:
"""
Attributes:
status (PostV1DesktopIdComputerActionResponse403Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdComputerActionResponse403Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdComputerActionResponse403Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_computer_action_response_403 = cls(
status=status,
error=error,
)
post_v1_desktop_id_computer_action_response_403.additional_properties = d
return post_v1_desktop_id_computer_action_response_403
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_403_status.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionResponse403Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_404.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_response_404_status import (
PostV1DesktopIdComputerActionResponse404Status,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionResponse404")
@_attrs_define
class PostV1DesktopIdComputerActionResponse404:
"""
Attributes:
status (PostV1DesktopIdComputerActionResponse404Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdComputerActionResponse404Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdComputerActionResponse404Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_computer_action_response_404 = cls(
status=status,
error=error,
)
post_v1_desktop_id_computer_action_response_404.additional_properties = d
return post_v1_desktop_id_computer_action_response_404
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_404_status.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionResponse404Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_409.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_response_409_status import (
PostV1DesktopIdComputerActionResponse409Status,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionResponse409")
@_attrs_define
class PostV1DesktopIdComputerActionResponse409:
"""
Attributes:
status (PostV1DesktopIdComputerActionResponse409Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdComputerActionResponse409Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdComputerActionResponse409Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_computer_action_response_409 = cls(
status=status,
error=error,
)
post_v1_desktop_id_computer_action_response_409.additional_properties = d
return post_v1_desktop_id_computer_action_response_409
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_409_status.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionResponse409Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_429.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_response_429_status import (
PostV1DesktopIdComputerActionResponse429Status,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionResponse429")
@_attrs_define
class PostV1DesktopIdComputerActionResponse429:
"""
Attributes:
status (PostV1DesktopIdComputerActionResponse429Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdComputerActionResponse429Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdComputerActionResponse429Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_computer_action_response_429 = cls(
status=status,
error=error,
)
post_v1_desktop_id_computer_action_response_429.additional_properties = d
return post_v1_desktop_id_computer_action_response_429
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_429_status.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionResponse429Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_500.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_response_500_status import (
PostV1DesktopIdComputerActionResponse500Status,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionResponse500")
@_attrs_define
class PostV1DesktopIdComputerActionResponse500:
"""
Attributes:
status (PostV1DesktopIdComputerActionResponse500Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdComputerActionResponse500Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdComputerActionResponse500Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_computer_action_response_500 = cls(
status=status,
error=error,
)
post_v1_desktop_id_computer_action_response_500.additional_properties = d
return post_v1_desktop_id_computer_action_response_500
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_500_status.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionResponse500Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_502.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_response_502_status import (
PostV1DesktopIdComputerActionResponse502Status,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionResponse502")
@_attrs_define
class PostV1DesktopIdComputerActionResponse502:
"""
Attributes:
status (PostV1DesktopIdComputerActionResponse502Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdComputerActionResponse502Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdComputerActionResponse502Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_computer_action_response_502 = cls(
status=status,
error=error,
)
post_v1_desktop_id_computer_action_response_502.additional_properties = d
return post_v1_desktop_id_computer_action_response_502
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_response_502_status.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionResponse502Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_screenshot_action.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_screenshot_action_type import (
PostV1DesktopIdComputerActionScreenshotActionType,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionScreenshotAction")
@_attrs_define
class PostV1DesktopIdComputerActionScreenshotAction:
"""
Attributes:
type_ (PostV1DesktopIdComputerActionScreenshotActionType): Take a screenshot of the desktop Example: screenshot.
"""
type_: PostV1DesktopIdComputerActionScreenshotActionType
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
type_ = self.type_.value
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"type": type_,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
type_ = PostV1DesktopIdComputerActionScreenshotActionType(d.pop("type"))
post_v1_desktop_id_computer_action_screenshot_action = cls(
type_=type_,
)
post_v1_desktop_id_computer_action_screenshot_action.additional_properties = d
return post_v1_desktop_id_computer_action_screenshot_action
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_screenshot_action_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionScreenshotActionType(str, Enum):
SCREENSHOT = "screenshot"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_scroll_action.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_scroll_action_direction import (
PostV1DesktopIdComputerActionScrollActionDirection,
)
from ..models.post_v1_desktop_id_computer_action_scroll_action_type import PostV1DesktopIdComputerActionScrollActionType
T = TypeVar("T", bound="PostV1DesktopIdComputerActionScrollAction")
@_attrs_define
class PostV1DesktopIdComputerActionScrollAction:
"""
Attributes:
type_ (PostV1DesktopIdComputerActionScrollActionType): Scroll the mouse wheel in the specified direction
Example: scroll.
direction (PostV1DesktopIdComputerActionScrollActionDirection): Direction to scroll Example: down.
amount (int): Amount to scroll in pixels Example: 100.
"""
type_: PostV1DesktopIdComputerActionScrollActionType
direction: PostV1DesktopIdComputerActionScrollActionDirection
amount: int
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
type_ = self.type_.value
direction = self.direction.value
amount = self.amount
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"type": type_,
"direction": direction,
"amount": amount,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
type_ = PostV1DesktopIdComputerActionScrollActionType(d.pop("type"))
direction = PostV1DesktopIdComputerActionScrollActionDirection(d.pop("direction"))
amount = d.pop("amount")
post_v1_desktop_id_computer_action_scroll_action = cls(
type_=type_,
direction=direction,
amount=amount,
)
post_v1_desktop_id_computer_action_scroll_action.additional_properties = d
return post_v1_desktop_id_computer_action_scroll_action
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_scroll_action_direction.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionScrollActionDirection(str, Enum):
DOWN = "down"
LEFT = "left"
RIGHT = "right"
UP = "up"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_scroll_action_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionScrollActionType(str, Enum):
SCROLL = "scroll"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_type_text_action.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_type_text_action_type import (
PostV1DesktopIdComputerActionTypeTextActionType,
)
T = TypeVar("T", bound="PostV1DesktopIdComputerActionTypeTextAction")
@_attrs_define
class PostV1DesktopIdComputerActionTypeTextAction:
"""
Attributes:
type_ (PostV1DesktopIdComputerActionTypeTextActionType): Type text at the current cursor position Example: type.
text (str): Text to type Example: Hello, World!.
"""
type_: PostV1DesktopIdComputerActionTypeTextActionType
text: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
type_ = self.type_.value
text = self.text
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"type": type_,
"text": text,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
type_ = PostV1DesktopIdComputerActionTypeTextActionType(d.pop("type"))
text = d.pop("text")
post_v1_desktop_id_computer_action_type_text_action = cls(
type_=type_,
text=text,
)
post_v1_desktop_id_computer_action_type_text_action.additional_properties = d
return post_v1_desktop_id_computer_action_type_text_action
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_type_text_action_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionTypeTextActionType(str, Enum):
TYPE = "type"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_wait_action.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_computer_action_wait_action_type import PostV1DesktopIdComputerActionWaitActionType
T = TypeVar("T", bound="PostV1DesktopIdComputerActionWaitAction")
@_attrs_define
class PostV1DesktopIdComputerActionWaitAction:
"""
Attributes:
type_ (PostV1DesktopIdComputerActionWaitActionType): Wait for the specified number of milliseconds Example:
wait.
ms (int): Time to wait in milliseconds Example: 1000.
"""
type_: PostV1DesktopIdComputerActionWaitActionType
ms: int
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
type_ = self.type_.value
ms = self.ms
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"type": type_,
"ms": ms,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
type_ = PostV1DesktopIdComputerActionWaitActionType(d.pop("type"))
ms = d.pop("ms")
post_v1_desktop_id_computer_action_wait_action = cls(
type_=type_,
ms=ms,
)
post_v1_desktop_id_computer_action_wait_action.additional_properties = d
return post_v1_desktop_id_computer_action_wait_action
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_computer_action_wait_action_type.py
================================================
from enum import Enum
class PostV1DesktopIdComputerActionWaitActionType(str, Enum):
WAIT = "wait"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_200.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_stop_response_200_status import PostV1DesktopIdStopResponse200Status
T = TypeVar("T", bound="PostV1DesktopIdStopResponse200")
@_attrs_define
class PostV1DesktopIdStopResponse200:
"""
Attributes:
status (PostV1DesktopIdStopResponse200Status): Status of the desktop instance after stopping Example:
terminated.
"""
status: PostV1DesktopIdStopResponse200Status
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdStopResponse200Status(d.pop("status"))
post_v1_desktop_id_stop_response_200 = cls(
status=status,
)
post_v1_desktop_id_stop_response_200.additional_properties = d
return post_v1_desktop_id_stop_response_200
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_200_status.py
================================================
from enum import Enum
class PostV1DesktopIdStopResponse200Status(str, Enum):
ERROR = "error"
PENDING = "pending"
RUNNING = "running"
TERMINATED = "terminated"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_400.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_stop_response_400_status import PostV1DesktopIdStopResponse400Status
T = TypeVar("T", bound="PostV1DesktopIdStopResponse400")
@_attrs_define
class PostV1DesktopIdStopResponse400:
"""
Attributes:
status (PostV1DesktopIdStopResponse400Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdStopResponse400Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdStopResponse400Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_stop_response_400 = cls(
status=status,
error=error,
)
post_v1_desktop_id_stop_response_400.additional_properties = d
return post_v1_desktop_id_stop_response_400
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_400_status.py
================================================
from enum import Enum
class PostV1DesktopIdStopResponse400Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_401.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_stop_response_401_status import PostV1DesktopIdStopResponse401Status
T = TypeVar("T", bound="PostV1DesktopIdStopResponse401")
@_attrs_define
class PostV1DesktopIdStopResponse401:
"""
Attributes:
status (PostV1DesktopIdStopResponse401Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdStopResponse401Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdStopResponse401Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_stop_response_401 = cls(
status=status,
error=error,
)
post_v1_desktop_id_stop_response_401.additional_properties = d
return post_v1_desktop_id_stop_response_401
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_401_status.py
================================================
from enum import Enum
class PostV1DesktopIdStopResponse401Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_403.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_stop_response_403_status import PostV1DesktopIdStopResponse403Status
T = TypeVar("T", bound="PostV1DesktopIdStopResponse403")
@_attrs_define
class PostV1DesktopIdStopResponse403:
"""
Attributes:
status (PostV1DesktopIdStopResponse403Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdStopResponse403Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdStopResponse403Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_stop_response_403 = cls(
status=status,
error=error,
)
post_v1_desktop_id_stop_response_403.additional_properties = d
return post_v1_desktop_id_stop_response_403
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_403_status.py
================================================
from enum import Enum
class PostV1DesktopIdStopResponse403Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_404.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_stop_response_404_status import PostV1DesktopIdStopResponse404Status
T = TypeVar("T", bound="PostV1DesktopIdStopResponse404")
@_attrs_define
class PostV1DesktopIdStopResponse404:
"""
Attributes:
status (PostV1DesktopIdStopResponse404Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdStopResponse404Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdStopResponse404Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_stop_response_404 = cls(
status=status,
error=error,
)
post_v1_desktop_id_stop_response_404.additional_properties = d
return post_v1_desktop_id_stop_response_404
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_404_status.py
================================================
from enum import Enum
class PostV1DesktopIdStopResponse404Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_409.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_stop_response_409_status import PostV1DesktopIdStopResponse409Status
T = TypeVar("T", bound="PostV1DesktopIdStopResponse409")
@_attrs_define
class PostV1DesktopIdStopResponse409:
"""
Attributes:
status (PostV1DesktopIdStopResponse409Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdStopResponse409Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdStopResponse409Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_stop_response_409 = cls(
status=status,
error=error,
)
post_v1_desktop_id_stop_response_409.additional_properties = d
return post_v1_desktop_id_stop_response_409
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_409_status.py
================================================
from enum import Enum
class PostV1DesktopIdStopResponse409Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_429.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_stop_response_429_status import PostV1DesktopIdStopResponse429Status
T = TypeVar("T", bound="PostV1DesktopIdStopResponse429")
@_attrs_define
class PostV1DesktopIdStopResponse429:
"""
Attributes:
status (PostV1DesktopIdStopResponse429Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdStopResponse429Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdStopResponse429Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_stop_response_429 = cls(
status=status,
error=error,
)
post_v1_desktop_id_stop_response_429.additional_properties = d
return post_v1_desktop_id_stop_response_429
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_429_status.py
================================================
from enum import Enum
class PostV1DesktopIdStopResponse429Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_500.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_stop_response_500_status import PostV1DesktopIdStopResponse500Status
T = TypeVar("T", bound="PostV1DesktopIdStopResponse500")
@_attrs_define
class PostV1DesktopIdStopResponse500:
"""
Attributes:
status (PostV1DesktopIdStopResponse500Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdStopResponse500Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdStopResponse500Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_stop_response_500 = cls(
status=status,
error=error,
)
post_v1_desktop_id_stop_response_500.additional_properties = d
return post_v1_desktop_id_stop_response_500
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_500_status.py
================================================
from enum import Enum
class PostV1DesktopIdStopResponse500Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_502.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_id_stop_response_502_status import PostV1DesktopIdStopResponse502Status
T = TypeVar("T", bound="PostV1DesktopIdStopResponse502")
@_attrs_define
class PostV1DesktopIdStopResponse502:
"""
Attributes:
status (PostV1DesktopIdStopResponse502Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopIdStopResponse502Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopIdStopResponse502Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_id_stop_response_502 = cls(
status=status,
error=error,
)
post_v1_desktop_id_stop_response_502.additional_properties = d
return post_v1_desktop_id_stop_response_502
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_id_stop_response_502_status.py
================================================
from enum import Enum
class PostV1DesktopIdStopResponse502Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_200.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_response_200_status import PostV1DesktopResponse200Status
T = TypeVar("T", bound="PostV1DesktopResponse200")
@_attrs_define
class PostV1DesktopResponse200:
"""
Attributes:
id (str): Unique identifier for the desktop instance Example: desktop_12345.
status (PostV1DesktopResponse200Status): Initial status of the desktop instance after creation request Example:
pending.
"""
id: str
status: PostV1DesktopResponse200Status
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
id = self.id
status = self.status.value
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"id": id,
"status": status,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
id = d.pop("id")
status = PostV1DesktopResponse200Status(d.pop("status"))
post_v1_desktop_response_200 = cls(
id=id,
status=status,
)
post_v1_desktop_response_200.additional_properties = d
return post_v1_desktop_response_200
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_200_status.py
================================================
from enum import Enum
class PostV1DesktopResponse200Status(str, Enum):
ERROR = "error"
PENDING = "pending"
RUNNING = "running"
TERMINATED = "terminated"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_400.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_response_400_status import PostV1DesktopResponse400Status
T = TypeVar("T", bound="PostV1DesktopResponse400")
@_attrs_define
class PostV1DesktopResponse400:
"""
Attributes:
status (PostV1DesktopResponse400Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopResponse400Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopResponse400Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_response_400 = cls(
status=status,
error=error,
)
post_v1_desktop_response_400.additional_properties = d
return post_v1_desktop_response_400
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_400_status.py
================================================
from enum import Enum
class PostV1DesktopResponse400Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_401.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_response_401_status import PostV1DesktopResponse401Status
T = TypeVar("T", bound="PostV1DesktopResponse401")
@_attrs_define
class PostV1DesktopResponse401:
"""
Attributes:
status (PostV1DesktopResponse401Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopResponse401Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopResponse401Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_response_401 = cls(
status=status,
error=error,
)
post_v1_desktop_response_401.additional_properties = d
return post_v1_desktop_response_401
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_401_status.py
================================================
from enum import Enum
class PostV1DesktopResponse401Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_403.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_response_403_status import PostV1DesktopResponse403Status
T = TypeVar("T", bound="PostV1DesktopResponse403")
@_attrs_define
class PostV1DesktopResponse403:
"""
Attributes:
status (PostV1DesktopResponse403Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopResponse403Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopResponse403Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_response_403 = cls(
status=status,
error=error,
)
post_v1_desktop_response_403.additional_properties = d
return post_v1_desktop_response_403
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_403_status.py
================================================
from enum import Enum
class PostV1DesktopResponse403Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_404.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_response_404_status import PostV1DesktopResponse404Status
T = TypeVar("T", bound="PostV1DesktopResponse404")
@_attrs_define
class PostV1DesktopResponse404:
"""
Attributes:
status (PostV1DesktopResponse404Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopResponse404Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopResponse404Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_response_404 = cls(
status=status,
error=error,
)
post_v1_desktop_response_404.additional_properties = d
return post_v1_desktop_response_404
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_404_status.py
================================================
from enum import Enum
class PostV1DesktopResponse404Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_409.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_response_409_status import PostV1DesktopResponse409Status
T = TypeVar("T", bound="PostV1DesktopResponse409")
@_attrs_define
class PostV1DesktopResponse409:
"""
Attributes:
status (PostV1DesktopResponse409Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopResponse409Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopResponse409Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_response_409 = cls(
status=status,
error=error,
)
post_v1_desktop_response_409.additional_properties = d
return post_v1_desktop_response_409
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_409_status.py
================================================
from enum import Enum
class PostV1DesktopResponse409Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_429.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_response_429_status import PostV1DesktopResponse429Status
T = TypeVar("T", bound="PostV1DesktopResponse429")
@_attrs_define
class PostV1DesktopResponse429:
"""
Attributes:
status (PostV1DesktopResponse429Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopResponse429Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopResponse429Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_response_429 = cls(
status=status,
error=error,
)
post_v1_desktop_response_429.additional_properties = d
return post_v1_desktop_response_429
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_429_status.py
================================================
from enum import Enum
class PostV1DesktopResponse429Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_500.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_response_500_status import PostV1DesktopResponse500Status
T = TypeVar("T", bound="PostV1DesktopResponse500")
@_attrs_define
class PostV1DesktopResponse500:
"""
Attributes:
status (PostV1DesktopResponse500Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopResponse500Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopResponse500Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_response_500 = cls(
status=status,
error=error,
)
post_v1_desktop_response_500.additional_properties = d
return post_v1_desktop_response_500
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_500_status.py
================================================
from enum import Enum
class PostV1DesktopResponse500Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_502.py
================================================
from collections.abc import Mapping
from typing import Any, TypeVar
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..models.post_v1_desktop_response_502_status import PostV1DesktopResponse502Status
T = TypeVar("T", bound="PostV1DesktopResponse502")
@_attrs_define
class PostV1DesktopResponse502:
"""
Attributes:
status (PostV1DesktopResponse502Status): Example: error.
error (str): Error message detailing what went wrong Example: Instance not found or unauthorized.
"""
status: PostV1DesktopResponse502Status
error: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
status = self.status.value
error = self.error
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"status": status,
"error": error,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
status = PostV1DesktopResponse502Status(d.pop("status"))
error = d.pop("error")
post_v1_desktop_response_502 = cls(
status=status,
error=error,
)
post_v1_desktop_response_502.additional_properties = d
return post_v1_desktop_response_502
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/models/post_v1_desktop_response_502_status.py
================================================
from enum import Enum
class PostV1DesktopResponse502Status(str, Enum):
ERROR = "error"
def __str__(self) -> str:
return str(self.value)
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/py.typed
================================================
# Marker file for PEP 561
================================================
FILE: sdks/py-sdk/openapi_client/api_reference_client/types.py
================================================
"""Contains some shared types for properties"""
from collections.abc import MutableMapping
from http import HTTPStatus
from typing import BinaryIO, Generic, Literal, Optional, TypeVar
from attrs import define
class Unset:
def __bool__(self) -> Literal[False]:
return False
UNSET: Unset = Unset()
FileJsonType = tuple[Optional[str], BinaryIO, Optional[str]]
@define
class File:
"""Contains information for file uploads"""
payload: BinaryIO
file_name: Optional[str] = None
mime_type: Optional[str] = None
def to_tuple(self) -> FileJsonType:
"""Return a tuple representation that httpx will accept for multipart/form-data"""
return self.file_name, self.payload, self.mime_type
T = TypeVar("T")
@define
class Response(Generic[T]):
"""A response from an endpoint"""
status_code: HTTPStatus
content: bytes
headers: MutableMapping[str, str]
parsed: Optional[T]
__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"]
================================================
FILE: sdks/py-sdk/openapi_client/pyproject.toml
================================================
[tool.poetry]
name = "api-reference-client"
version = "1.2.1"
description = "A client library for accessing API Reference"
authors = []
readme = "README.md"
packages = [
{include = "api_reference_client"},
]
include = ["CHANGELOG.md", "api_reference_client/py.typed"]
[tool.poetry.dependencies]
python = "^3.9"
httpx = ">=0.20.0,<0.29.0"
attrs = ">=22.2.0"
python-dateutil = "^2.8.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
line-length = 120
[tool.ruff.lint]
select = ["F", "I", "UP"]
================================================
FILE: sdks/py-sdk/pyproject.toml
================================================
[project]
name = "cyberdesk"
version = "0.2.7"
description = "The official Python SDK for Cyberdesk"
authors = [{name = "Cyberdesk Team", email = "dev@cyberdesk.io"}]
readme = "README.md"
license = "MIT"
dependencies = [
"httpx",
"attrs"
]
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project.optional-dependencies]
dev = [
"openapi-python-client",
"build",
"twine"
]
[tool.setuptools.packages.find]
include = ["cyberdesk*", "openapi_client*"]
================================================
FILE: sdks/py-sdk/scripts/generate.py
================================================
import subprocess
import sys
from pathlib import Path
# Path to the OpenAPI spec
OPENAPI_PATH = Path(__file__).parent.parent.parent / "openapi.json"
OUTPUT_DIR = Path(__file__).parent.parent / "openapi_client"
cmd = [
sys.executable, "-m", "openapi_python_client", "generate",
"--path", str(OPENAPI_PATH),
"--output-path", str(OUTPUT_DIR),
"--overwrite"
]
print(f"Running: {' '.join(cmd)}")
print(f"OpenAPI path: {OPENAPI_PATH}")
print(f"Output dir: {OUTPUT_DIR}")
subprocess.run(cmd, check=True)
================================================
FILE: sdks/sandbox/py-sdk/.gitignore
================================================
config.py
================================================
FILE: sdks/sandbox/py-sdk/README.md
================================================
# Cyberdesk PyPI Package Sandbox Test
This folder provides a clean environment to test the published `cyberdesk` Python SDK from PyPI. In the future, we'll add proper tests for all of the SDK's, but for now this will have to do in terms of quick manual test scripts.
## Setup Instructions
1. **Create and activate a virtual environment:**
```bash
python -m venv venv
# On Windows:
venv\Scripts\activate
# On Mac/Linux:
source venv/bin/activate
```
2. **Install the published package from PyPI:**
```bash
pip install cyberdesk
```
3. **Set your API key:**
Edit `config.py` and replace `"your-api-key-here"` with your actual Cyberdesk API key.
4. **Run the test script:**
```bash
python test_sdk.py
```
## What This Does
- Launches a desktop via the SDK
- Fetches its details
- Terminates the desktop
If you encounter errors, check your API key and ensure the package is published and available on PyPI.
================================================
FILE: sdks/sandbox/py-sdk/test_sdk.py
================================================
# test_sdk.py
from cyberdesk import CyberdeskClient
from cyberdesk.actions import click_mouse, ClickMouseButton, type_text
from config import API_KEY
import time
def main():
client = CyberdeskClient(api_key=API_KEY)
desktop_id = None
try:
desktop = client.launch_desktop()
print("Launched desktop:", desktop)
desktop_id = desktop.id
# Wait for desktop to be running
print("Waiting for desktop to be running...")
while True:
details = client.get_desktop(desktop_id)
status_str = details.status.value # status is always an Enum
print(f"Current status: {status_str}")
if status_str == "running":
break
time.sleep(2)
print("Desktop is running!")
# Perform a click_mouse action
action = click_mouse(x=100, y=100, button=ClickMouseButton.RIGHT)
action_result = client.execute_computer_action(desktop_id, action)
print("Click mouse result:", action_result)
# Perform a type_text action
action = type_text(text="Hello, World!")
action_result = client.execute_computer_action(desktop_id, action)
print("Type text result:", action_result)
except Exception as e:
print("Error during SDK usage:", e)
finally:
if desktop_id:
try:
result = client.terminate_desktop(desktop_id)
print("Terminated desktop:", result)
if result.status == "terminated":
print("Desktop terminated successfully")
else:
print("Desktop termination failed")
except Exception as term_e:
print("Error during desktop termination:", term_e)
if __name__ == "__main__":
main()
================================================
FILE: sdks/ts-sdk/.gitignore
================================================
# Logs
*.log
# Dependency directories
node_modules/
# Compiled TypeScript output
dist/
# Optional: IDE directories
.vscode/
.idea/
# Optional: OS generated files
.DS_Store
Thumbs.db
# Optional: Environment variables file
.env*
!/.env.example
# Optional: Test coverage
coverage/
================================================
FILE: sdks/ts-sdk/LICENSE
================================================
MIT License
Copyright (c) 2025 Cyberdesk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: sdks/ts-sdk/README.md
================================================
# cyberdesk
[](https://badge.fury.io/js/cyberdesk)
The official TypeScript SDK for Cyberdesk.
## Installation
```bash
npm install cyberdesk
# or
yarn add cyberdesk
# or
pnpm add cyberdesk
```
## Usage
First, create a Cyberdesk client instance with your API key:
```typescript
import { createCyberdeskClient } from 'cyberdesk';
const cyberdesk = createCyberdeskClient({
apiKey: 'YOUR_API_KEY',
// Optionally, you can override the baseUrl or provide a custom fetch implementation
});
```
### Launch a Desktop
```typescript
const launchResult = await cyberdesk.launchDesktop({
body: { timeout_ms: 10000 } // Optional: set a timeout for the desktop session
});
if (launchResult.error) {
throw new Error('Failed to launch desktop: ' + launchResult.error.error);
}
const desktopId = launchResult.id;
console.log('Launched desktop with ID:', desktopId);
```
### Get Desktop Info
```typescript
const info = await cyberdesk.getDesktop({
path: { id: desktopId }
});
if ('error' in info) {
throw new Error('Failed to get desktop info: ' + info.error);
}
console.log('Desktop info:', info);
```
### Perform a Computer Action (e.g., Mouse Click)
```typescript
const actionResult = await cyberdesk.executeComputerAction({
path: { id: desktopId },
body: {
type: 'click_mouse',
x: 100,
y: 150
}
});
if (actionResult.error) {
throw new Error('Action failed: ' + actionResult.error);
}
console.log('Action result:', actionResult);
```
### Run a Bash Command
```typescript
const bashResult = await cyberdesk.executeBashAction({
path: { id: desktopId },
body: {
command: 'echo Hello, world!'
}
});
if (bashResult.error) {
throw new Error('Bash command failed: ' + bashResult.error);
}
console.log('Bash output:', bashResult.output);
```
## TypeScript Support
All request parameter types and the `CyberdeskSDK` type are exported for convenience:
```typescript
import type { LaunchDesktopParams, CyberdeskSDK } from 'cyberdesk';
```
## License
[MIT](LICENSE)
================================================
FILE: sdks/ts-sdk/openapi-ts.config.ts
================================================
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
input: '../openapi.json',
output: 'src/client',
plugins: ['@hey-api/client-fetch'],
});
================================================
FILE: sdks/ts-sdk/package.json
================================================
{
"name": "cyberdesk",
"version": "0.2.1",
"description": "The official TypeScript SDK for Cyberdesk",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"LICENSE",
"README.md"
],
"scripts": {
"generate": "openapi-ts",
"build": "npm run generate && tsc",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/cyberdesk-hq/cyberdesk.git"
},
"license": "MIT",
"author": "Cyberdesk Team ",
"bugs": {
"url": "https://github.com/cyberdesk-hq/cyberdesk/issues"
},
"homepage": "https://github.com/cyberdesk-hq/cyberdesk#readme",
"dependencies": {
"@hey-api/client-fetch": "^0.10.0"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.66.6",
"typescript": "^5.0.0"
}
}
================================================
FILE: sdks/ts-sdk/src/client/client.gen.ts
================================================
// This file is auto-generated by @hey-api/openapi-ts
import type { ClientOptions } from './types.gen';
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig = (override?: Config) => Config & T>;
export const client = createClient(createConfig({
baseUrl: 'https://api.cyberdesk.io'
}));
================================================
FILE: sdks/ts-sdk/src/client/index.ts
================================================
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
export * from './sdk.gen';
================================================
FILE: sdks/ts-sdk/src/client/sdk.gen.ts
================================================
// This file is auto-generated by @hey-api/openapi-ts
import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch';
import type { GetV1DesktopByIdData, GetV1DesktopByIdResponse, GetV1DesktopByIdError, PostV1DesktopData, PostV1DesktopResponse, PostV1DesktopError, PostV1DesktopByIdStopData, PostV1DesktopByIdStopResponse, PostV1DesktopByIdStopError, PostV1DesktopByIdComputerActionData, PostV1DesktopByIdComputerActionResponse, PostV1DesktopByIdComputerActionError, PostV1DesktopByIdBashActionData, PostV1DesktopByIdBashActionResponse, PostV1DesktopByIdBashActionError } from './types.gen';
import { client as _heyApiClient } from './client.gen';
export type Options = ClientOptions & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
/**
* You can pass arbitrary values through the `meta` object. This can be
* used to access values that aren't defined as part of the SDK function.
*/
meta?: Record;
};
/**
* Get details of a specific desktop instance
* Returns the ID, status, creation timestamp, and timeout timestamp for a given desktop instance.
*/
export const getV1DesktopById = (options: Options) => {
return (options.client ?? _heyApiClient).get({
url: '/v1/desktop/{id}',
...options
});
};
/**
* Create a new virtual desktop instance
* Creates a new virtual desktop instance and returns its ID and stream URL
*/
export const postV1Desktop = (options: Options) => {
return (options.client ?? _heyApiClient).post({
url: '/v1/desktop',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
/**
* Stop a running desktop instance
* Stops a running desktop instance and cleans up resources
*/
export const postV1DesktopByIdStop = (options: Options) => {
return (options.client ?? _heyApiClient).post({
url: '/v1/desktop/{id}/stop',
...options
});
};
/**
* Perform an action on the desktop
* Executes a computer action such as mouse clicks, keyboard input, or screenshots on the desktop
*/
export const postV1DesktopByIdComputerAction = (options: Options) => {
return (options.client ?? _heyApiClient).post({
url: '/v1/desktop/{id}/computer-action',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
/**
* Execute a bash command on the desktop
* Runs a bash command on the desktop and returns the command output
*/
export const postV1DesktopByIdBashAction = (options: Options) => {
return (options.client ?? _heyApiClient).post({
url: '/v1/desktop/{id}/bash-action',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};
================================================
FILE: sdks/ts-sdk/src/client/types.gen.ts
================================================
// This file is auto-generated by @hey-api/openapi-ts
export type GetV1DesktopByIdData = {
body?: never;
headers: {
/**
* API key for authentication
*/
'x-api-key': string;
};
path: {
/**
* The UUID of the desktop instance to retrieve
*/
id: string;
};
query?: never;
url: '/v1/desktop/{id}';
};
export type GetV1DesktopByIdErrors = {
/**
* The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
*/
400: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
*/
401: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.
*/
403: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.
*/
404: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* This response is sent when a request conflicts with the current state of the server.
*/
409: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The user has sent too many requests in a given amount of time ("rate limiting")
*/
429: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server has encountered a situation it does not know how to handle.
*/
500: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server, while acting as a gateway or proxy, received an invalid response from the upstream server.
*/
502: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
};
export type GetV1DesktopByIdError = GetV1DesktopByIdErrors[keyof GetV1DesktopByIdErrors];
export type GetV1DesktopByIdResponses = {
/**
* Desktop instance details retrieved successfully
*/
200: {
/**
* Unique identifier for the desktop instance
*/
id: string;
/**
* Current status of the desktop instance
*/
status: 'pending' | 'running' | 'terminated' | 'error';
/**
* URL for the desktop stream (null if the desktop is not running)
*/
stream_url: string;
/**
* Timestamp when the instance was created
*/
created_at: string;
/**
* Timestamp when the instance will automatically time out
*/
timeout_at: string;
};
};
export type GetV1DesktopByIdResponse = GetV1DesktopByIdResponses[keyof GetV1DesktopByIdResponses];
export type PostV1DesktopData = {
body?: {
/**
* Timeout in milliseconds for the desktop session
*/
timeout_ms?: number;
};
headers: {
/**
* API key for authentication
*/
'x-api-key': string;
};
path?: never;
query?: never;
url: '/v1/desktop';
};
export type PostV1DesktopErrors = {
/**
* The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
*/
400: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
*/
401: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.
*/
403: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.
*/
404: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* This response is sent when a request conflicts with the current state of the server.
*/
409: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The user has sent too many requests in a given amount of time ("rate limiting")
*/
429: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server has encountered a situation it does not know how to handle.
*/
500: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server, while acting as a gateway or proxy, received an invalid response from the upstream server.
*/
502: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
};
export type PostV1DesktopError = PostV1DesktopErrors[keyof PostV1DesktopErrors];
export type PostV1DesktopResponses = {
/**
* Desktop creation initiated successfully
*/
200: {
/**
* Unique identifier for the desktop instance
*/
id: string;
/**
* Initial status of the desktop instance after creation request
*/
status: 'pending' | 'running' | 'terminated' | 'error';
};
};
export type PostV1DesktopResponse = PostV1DesktopResponses[keyof PostV1DesktopResponses];
export type PostV1DesktopByIdStopData = {
body?: never;
headers: {
/**
* API key for authentication
*/
'x-api-key': string;
};
path: {
/**
* Desktop instance ID to stop
*/
id: string;
};
query?: never;
url: '/v1/desktop/{id}/stop';
};
export type PostV1DesktopByIdStopErrors = {
/**
* The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
*/
400: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
*/
401: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.
*/
403: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.
*/
404: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* This response is sent when a request conflicts with the current state of the server.
*/
409: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The user has sent too many requests in a given amount of time ("rate limiting")
*/
429: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server has encountered a situation it does not know how to handle.
*/
500: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server, while acting as a gateway or proxy, received an invalid response from the upstream server.
*/
502: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
};
export type PostV1DesktopByIdStopError = PostV1DesktopByIdStopErrors[keyof PostV1DesktopByIdStopErrors];
export type PostV1DesktopByIdStopResponses = {
/**
* Desktop stopped successfully
*/
200: {
/**
* Status of the desktop instance after stopping
*/
status: 'pending' | 'running' | 'terminated' | 'error';
};
};
export type PostV1DesktopByIdStopResponse = PostV1DesktopByIdStopResponses[keyof PostV1DesktopByIdStopResponses];
export type PostV1DesktopByIdComputerActionData = {
body?: {
/**
* Perform a mouse action: click, press (down), or release (up). Defaults to a single left click at the current position.
*/
type: 'click_mouse';
/**
* X coordinate for the action (optional, uses current position if omitted)
*/
x?: number;
/**
* Y coordinate for the action (optional, uses current position if omitted)
*/
y?: number;
/**
* Mouse button to use (optional, defaults to 'left')
*/
button?: 'left' | 'right' | 'middle';
/**
* Number of clicks to perform (optional, defaults to 1, only applicable for 'click' type)
*/
num_of_clicks?: number;
/**
* Type of mouse action (optional, defaults to 'click')
*/
click_type?: 'click' | 'down' | 'up';
} | {
/**
* Scroll the mouse wheel in the specified direction
*/
type: 'scroll';
/**
* Direction to scroll
*/
direction: 'up' | 'down' | 'left' | 'right';
/**
* Amount to scroll in pixels
*/
amount: number;
} | {
/**
* Move the mouse cursor to the specified coordinates
*/
type: 'move_mouse';
/**
* X coordinate to move to
*/
x: number;
/**
* Y coordinate to move to
*/
y: number;
} | {
/**
* Drag the mouse from start to end coordinates
*/
type: 'drag_mouse';
/**
* Starting coordinates for the drag operation
*/
start: {
/**
* X coordinate on the screen
*/
x: number;
/**
* Y coordinate on the screen
*/
y: number;
};
/**
* Ending coordinates for the drag operation
*/
end: {
/**
* X coordinate on the screen
*/
x: number;
/**
* Y coordinate on the screen
*/
y: number;
};
} | {
/**
* Type text at the current cursor position
*/
type: 'type';
/**
* Text to type
*/
text: string;
} | {
/**
* Press, hold down, or release one or more keyboard keys. Defaults to a single press and release.
*/
type: 'press_keys';
keys: string | Array;
/**
* Type of key action (optional, defaults to 'press' which is a down and up action)
*/
key_action_type?: 'press' | 'down' | 'up';
} | {
/**
* Wait for the specified number of milliseconds
*/
type: 'wait';
/**
* Time to wait in milliseconds
*/
ms: number;
} | {
/**
* Take a screenshot of the desktop
*/
type: 'screenshot';
} | {
/**
* Get the current mouse cursor position
*/
type: 'get_cursor_position';
};
headers: {
/**
* API key for authentication
*/
'x-api-key': string;
};
path: {
/**
* Desktop instance ID to perform the action on
*/
id: string;
};
query?: never;
url: '/v1/desktop/{id}/computer-action';
};
export type PostV1DesktopByIdComputerActionErrors = {
/**
* The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
*/
400: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
*/
401: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.
*/
403: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.
*/
404: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* This response is sent when a request conflicts with the current state of the server.
*/
409: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The user has sent too many requests in a given amount of time ("rate limiting")
*/
429: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server has encountered a situation it does not know how to handle.
*/
500: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server, while acting as a gateway or proxy, received an invalid response from the upstream server.
*/
502: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
};
export type PostV1DesktopByIdComputerActionError = PostV1DesktopByIdComputerActionErrors[keyof PostV1DesktopByIdComputerActionErrors];
export type PostV1DesktopByIdComputerActionResponses = {
/**
* Action executed successfully. Response may contain output or image data depending on the action.
*/
200: {
/**
* Raw string output from the executed command (if any)
*/
output?: string;
/**
* Error message if the operation failed (also indicated by non-2xx HTTP status)
*/
error?: string;
/**
* Base64 encoded JPEG image data (only returned for screenshot actions)
*/
base64_image?: string;
};
};
export type PostV1DesktopByIdComputerActionResponse = PostV1DesktopByIdComputerActionResponses[keyof PostV1DesktopByIdComputerActionResponses];
export type PostV1DesktopByIdBashActionData = {
body?: {
/**
* Bash command to execute
*/
command: string;
};
headers: {
/**
* API key for authentication
*/
'x-api-key': string;
};
path: {
/**
* Desktop instance ID to run the command on
*/
id: string;
};
query?: never;
url: '/v1/desktop/{id}/bash-action';
};
export type PostV1DesktopByIdBashActionErrors = {
/**
* The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
*/
400: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
*/
401: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server.
*/
403: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.
*/
404: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* This response is sent when a request conflicts with the current state of the server.
*/
409: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The user has sent too many requests in a given amount of time ("rate limiting")
*/
429: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server has encountered a situation it does not know how to handle.
*/
500: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
/**
* The server, while acting as a gateway or proxy, received an invalid response from the upstream server.
*/
502: {
status: 'error';
/**
* Error message detailing what went wrong
*/
error: string;
};
};
export type PostV1DesktopByIdBashActionError = PostV1DesktopByIdBashActionErrors[keyof PostV1DesktopByIdBashActionErrors];
export type PostV1DesktopByIdBashActionResponses = {
/**
* Command executed successfully. Response contains command output.
*/
200: {
/**
* Raw string output from the executed command (if any)
*/
output?: string;
/**
* Error message if the operation failed (also indicated by non-2xx HTTP status)
*/
error?: string;
/**
* Base64 encoded JPEG image data (only returned for screenshot actions)
*/
base64_image?: string;
};
};
export type PostV1DesktopByIdBashActionResponse = PostV1DesktopByIdBashActionResponses[keyof PostV1DesktopByIdBashActionResponses];
export type ClientOptions = {
baseUrl: 'https://api.cyberdesk.io' | (string & {});
};
================================================
FILE: sdks/ts-sdk/src/index.ts
================================================
///
import { createClient, type Client, type Options as ClientOptions } from '@hey-api/client-fetch';
import * as apiMethods from './client/sdk.gen';
import {
GetV1DesktopByIdData,
PostV1DesktopData,
PostV1DesktopByIdStopData,
PostV1DesktopByIdComputerActionData,
PostV1DesktopByIdBashActionData
} from './client/types.gen';
const DEFAULT_BASE_URL = 'https://api.cyberdesk.io';
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise;
/**
* Configuration options for the Cyberdesk SDK client.
*/
export interface CyberdeskClientOptions {
/** Your Cyberdesk API Key */
apiKey: string;
/** Optional: Override the base URL for the API. Defaults to Cyberdesk production API. */
baseUrl?: string;
/** Optional: Provide a custom fetch implementation. */
fetch?: FetchFn;
/** Optional: Provide additional default options for the underlying client (e.g., timeout, keepalive). */
// Using Partial allows any subset of options but is less strict than Omit.
clientOptions?: Partial;
}
// Named parameter types for SDK methods
export type GetDesktopParams = Omit;
export type LaunchDesktopParams = Omit;
export type TerminateDesktopParams = Omit;
export type ExecuteComputerActionParams = Omit;
export type ExecuteBashActionParams = Omit;
export type CyberdeskSDK = {
getDesktop: (opts: GetDesktopParams) => ReturnType;
launchDesktop: (opts: LaunchDesktopParams) => ReturnType;
terminateDesktop: (opts: TerminateDesktopParams) => ReturnType;
executeComputerAction: (opts: ExecuteComputerActionParams) => ReturnType;
executeBashAction: (opts: ExecuteBashActionParams) => ReturnType;
};
/**
* Creates a Cyberdesk SDK instance configured with your API key.
*
* @param options - Configuration options including the API key.
* @returns An SDK instance with methods ready to be called.
*/
export function createCyberdeskClient(options: CyberdeskClientOptions): CyberdeskSDK {
const { apiKey, baseUrl = DEFAULT_BASE_URL, fetch: customFetch, clientOptions = {} } = options;
if (!apiKey) {
throw new Error('Cyberdesk SDK requires an `apiKey` to be provided.');
}
const finalBaseUrl: string | undefined = baseUrl;
const mergedHeaders = {
'x-api-key': apiKey,
'Content-Type': 'application/json',
...(clientOptions.headers || {}),
};
const finalClientOptions = {
baseUrl: finalBaseUrl,
headers: mergedHeaders,
...(customFetch && { fetch: customFetch })
};
const configuredClient: Client = createClient(finalClientOptions);
// Return an object where each method is pre-configured with the client instance
return {
getDesktop: (opts) => apiMethods.getV1DesktopById({
...(opts as GetV1DesktopByIdData), // Cast opts to allow potential headers
client: configuredClient,
headers: { ...mergedHeaders, ...(opts as GetV1DesktopByIdData).headers }
}),
launchDesktop: (opts) => apiMethods.postV1Desktop({
...(opts as PostV1DesktopData),
client: configuredClient,
headers: { ...mergedHeaders, ...(opts as PostV1DesktopData).headers }
}),
terminateDesktop: (opts) => apiMethods.postV1DesktopByIdStop({
...(opts as PostV1DesktopByIdStopData),
path: { ...(opts as PostV1DesktopByIdStopData).path, id: (opts as PostV1DesktopByIdStopData).path.id },
client: configuredClient,
headers: { ...mergedHeaders, ...(opts as PostV1DesktopByIdStopData).headers }
}),
executeComputerAction: (opts) => apiMethods.postV1DesktopByIdComputerAction({
...(opts as PostV1DesktopByIdComputerActionData),
client: configuredClient,
headers: { ...mergedHeaders, ...(opts as PostV1DesktopByIdComputerActionData).headers }
}),
executeBashAction: (opts) => apiMethods.postV1DesktopByIdBashAction({
...(opts as PostV1DesktopByIdBashActionData),
client: configuredClient,
headers: { ...mergedHeaders, ...(opts as PostV1DesktopByIdBashActionData).headers }
}),
};
}
================================================
FILE: sdks/ts-sdk/tsconfig.json
================================================
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ES2016", // Or a later version like ES2020, ESNext
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitAny": true,
/* If NOT transpiling code: */
// These lines were removed as we need to emit JS for the SDK build.
/* If your code runs in the DOM: */
// "lib": ["es2022", "dom", "dom.iterable"],
/* If your code DOES NOT run in the DOM: */
"lib": ["ES2022"], // Adjust target lib as needed (e.g., ES2016)
/* Publishing Options */
"declaration": true, // Generate .d.ts files
"outDir": "./dist", // Output directory for compiled files
"rootDir": "./src", // Root directory of source files
/* Module Resolution */
"module": "CommonJS", // Or ESNext, NodeNext depending on target environment
"moduleResolution": "node" // How modules get resolved
},
"include": ["src/**/*"], // Which files to include in compilation
"exclude": ["node_modules", "dist", "**/*.test.ts"] // Which files to exclude
}
================================================
FILE: self-host.md
================================================
# Welcome to Self-Hosting Cyberdesk!
We're excited that you're interested in self-hosting Cyberdesk.
- For a detailed, step-by-step guide on installing Cyberdesk's infrastructure components onto an AKS cluster, please see our [deployment guide](infra/README.md).
- We're still fleshing out additional self-hosting steps and documentation. If you'd like personalized help, please [book a call with us here](https://cal.com/aland) and we'll personally guide you through every step of the self-hosting process.
Thank you for being an early adopter!
================================================
FILE: services/cyberdesk-operator/.gitignore
================================================
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.env
.venv
venv/
ENV/
env/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Logs
logs/
*.log
# Default config files (should not be committed)
tests/test-user-data.yaml
tests/test-secret.yaml
# Local Kubernetes configuration
.kube/
kubeconfig
# MacOS
.DS_Store
================================================
FILE: services/cyberdesk-operator/Dockerfile
================================================
FROM python:3.12
WORKDIR /app
# Copy requirements first for layer caching
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY handlers/controller.py /app/
CMD kopf run /app/controller.py --liveness=http://0.0.0.0:8080/healthz --all-namespaces
================================================
FILE: services/cyberdesk-operator/README.md
================================================
Run locally:
- Ensure you are on the developer cluster
- Scale down the cluster operator deployment (notify team): kubectl scale deployment cyberdesk-operator --replicas=0 -n cyberdesk-system
- docker build -t cyberdesk/cyberdesk-operator:local .
- docker run --rm -it -v "${env:USERPROFILE}\.kube:/root/.kube:ro" --env-file ./.env cyberdesk/cyberdesk-operator:local
================================================
FILE: services/cyberdesk-operator/checklist.md
================================================
# Cyberdesk Operator Implementation Checklist
## Overview
The Cyberdesk Operator is a Kubernetes Custom Resource Operator that monitors and manages Cyberdesk resources in a Kubernetes cluster. It's responsible for automatically provisioning and lifecycle management of KubeVirt VirtualMachine resources based on Cyberdesk Custom Resource definitions. The operator handles VM creation, status tracking, timeout enforcement, and cleanup of resources, providing an abstraction layer that simplifies VM provisioning for users while ensuring proper resource management within the cluster.
## Project Setup
- [x] Create basic project structure
- [x] Set up Python virtual environment
- [x] Create requirements.txt with kopf and kubernetes dependencies
- [x] Create a Dockerfile for the operator
- [x] Create a deployment manifest for Kubernetes
## Core Operator Logic
- [x] Create handlers/controller.py for main operator logic
- [x] Implement Kopf watches for Cyberdesk creation
- [x] Implement Kopf watches for VirtualMachineInstance updates
- [x] Implement VM creation logic when a new Cyberdesk is created
- [x] Implement listener for VMI status updates, to perform various actions
- [x] Implement Kopf watches for Cyberdesk deletion, triggering the deletion of the underlaying VirtualMachineInstance
- [x] Implement timeout mechanism based on timeoutMs, shutting down the underlaying VirtualMachineInstance if it's running longer than the timeout
- [x] Ensure event handler idempotency for all handlers by checking the current resource state (using resourceVersion or status fields) to guarantee that duplicate events don't trigger duplicate operations.
## Leader Election and High Availability
- [x] Configure leader election in your operator to designate one active replica for processing state-changing events, while others are available for failover.
- [x] Document the expected behavior and failover process to ensure consistent operation in a multi-replica environment.
## Secrets
- [x] In production, create Kubernetes Secret resources and inject them into your Pods via environment variables or volumes.
- [x] Use python-dotenv only for local development; in production rely entirely on Kubernetes-managed secrets.
## Error Handling and Lifecycle Management
- [x] Ensure proper error handling and retries
- [x] Add graceful cleanup logic for terminated instances
- [x] Implement VM deletion on Cyberdesk deletion
- [x] Add finalizers to ensure clean resource deletion
## Documentation
- [x] Add README.md with operator overview and K8s installation instructions.
- [x] Document troubleshooting steps, including:
- [x] How idempotency is ensured
- [x] How leader election is configured and behaves
- [x] How secrets are managed in different environments
## CI/CD Pipeline
- [x] Add workflow for building and pushing Docker image
## Security
- [x] Ensure secure container settings (non-root user, readonly filesystem where possible)
- [x] Review RBAC policies for accessing secrets and managing resources
## Monitoring & Observability
- [x] Set up logging and metrics (consider integrating with Prometheus and Grafana)
- [x] Document key operational alerts and troubleshooting tips
================================================
FILE: services/cyberdesk-operator/docs/troubleshooting.md
================================================
# Cyberdesk Operator: Troubleshooting Guide
This document provides detailed information about the Cyberdesk Operator implementation, focusing on key aspects that are important for troubleshooting and maintenance.
## Idempotency
The Cyberdesk Operator ensures that all operations are idempotent, meaning that they can be safely retried without causing duplicate or inconsistent states.
### How Idempotency is Implemented
1. **Resource Version Tracking**:
- The operator tracks which resource versions it has already processed using annotations on the Cyberdesk resources.
- Each handler adds its identifier and the resource version it processed to these annotations.
- Before processing a resource, handlers check if they've already processed the current resource version.
2. **Existence Checks**:
- Before creating resources, the operator checks if they already exist.
- For example, before creating a VM, the operator checks if a VM with the same name already exists.
3. **Annotation-Based Tracking**:
- Two key annotations track handler processing:
- `cyberdesk.io/processed-by`: Lists handlers that have processed this resource
- `cyberdesk.io/processed-versions`: Lists handler:resourceVersion pairs that have been processed
4. **Helper Functions**:
- `is_handler_already_processed()`: Checks if a handler has already processed a resource version
- `mark_handler_processed()`: Marks a resource as processed by a handler
### Troubleshooting Idempotency Issues
If you suspect that the operator is not correctly handling idempotency:
1. Check the annotations on the Cyberdesk resource:
```bash
kubectl get cyberdesks -o jsonpath='{.metadata.annotations}'
```
2. Verify the logs to see if handlers are skipping already processed resources:
```bash
kubectl logs -n cyberdesk-system -l app=cyberdesk-operator | grep "already processed"
```
3. If resources are being created multiple times, check if the annotations are being properly updated.
## Leader Election
To ensure that only one instance of the operator processes resources in a multi-replica deployment, the operator uses Kopf's built-in leader election mechanism.
### How Leader Election is Configured
1. **Kopf Peering Resource**:
- The operator uses a Kubernetes custom resource called a "Peering" for leader election.
- The peering resource name is set via the `KOPF_PEERING` environment variable (defaults to "cyberdesk-operator").
2. **Leader ID**:
- Each operator instance has a unique leader ID, by default using the Pod name.
- If POD_NAME is not available, it falls back to using the hostname.
3. **Priority**:
- All instances have the same priority (0) by default.
- The instance with the lowest priority number becomes the leader.
4. **Startup Configuration**:
- Leader election is configured in `main.py` through the `kopf.run()` parameters:
- `peering`: Name of the peering resource
- `id`: Unique ID for this operator instance
- `priority`: Priority in leader election
### Failover Process
1. Kopf periodically updates a heartbeat in the peering resource.
2. If the leader fails to update its heartbeat, Kopf automatically promotes another instance to leader.
3. The new leader will resume processing events.
4. During failover, there might be a brief period (typically less than 30 seconds) where no instance is processing events.
### Troubleshooting Leader Election
1. Check the current leader:
```bash
kubectl get kopfpeering cyberdesk-operator -o yaml
```
2. Check operator logs to see leader election activity:
```bash
kubectl logs -n cyberdesk-system -l app=cyberdesk-operator | grep "leader"
```
3. If no instance becomes the leader, ensure the operator has permissions to create and update the peering resource.
## Secrets Management
The Cyberdesk Operator handles secrets differently in development and production environments.
### Development Environment
In the development environment:
1. **Dotenv**:
- The operator uses `python-dotenv` to load environment variables from a `.env` file.
- This is only intended for local development and testing.
2. **Local Configuration**:
- Secrets such as API keys or credentials should be stored in a `.env` file that is not committed to version control.
- The `.env.example` file provides a template for required variables.
### Production Environment
In the production environment:
1. **Kubernetes Secrets**:
- All secrets are stored as Kubernetes Secret resources.
- Secrets are mounted as either:
- Environment variables using `envFrom` in the Deployment spec
- Volume mounts for sensitive files (like certificates)
2. **No Dotenv Usage**:
- The `python-dotenv` library is still imported but has no effect if `.env` is not present.
- All configuration comes from the container environment or mounted files.
3. **Secret References**:
- The operator's Deployment manifest references Kubernetes Secrets for sensitive data.
- Example:
```yaml
envFrom:
- secretRef:
name: cyberdesk-operator-secrets
```
### Secrets Used by the Operator
The operator may use the following types of secrets:
1. **API Credentials**: For accessing external services (if applicable)
2. **TLS Certificates**: For secure communication (if applicable)
3. **SSH Keys**: For VM access (if applicable)
### Troubleshooting Secrets Issues
1. Check if the required secrets exist:
```bash
kubectl get secrets -n cyberdesk-system
```
2. Verify the operator has permissions to access secrets:
```bash
kubectl auth can-i get secrets -n cyberdesk-system --as=system:serviceaccount:cyberdesk-system:cyberdesk-operator
```
3. Check if secrets are correctly mounted:
```bash
kubectl exec -n cyberdesk-system deploy/cyberdesk-operator -- env | grep SENSITIVE_VAR
```
## Monitoring and Metrics
The operator exposes Prometheus metrics on port 8081:
1. **Available Metrics**:
- `cyberdesk_vm_created_total`: Counter of VMs created
- `cyberdesk_vm_deleted_total`: Counter of VMs deleted
- `cyberdesk_vm_timeout_total`: Counter of VMs that timed out
- `cyberdesk_active_vm_count`: Gauge showing current active VMs
- `cyberdesk_operation_duration_seconds`: Histogram of operation durations
2. **Accessing Metrics**:
```bash
kubectl port-forward -n cyberdesk-system deploy/cyberdesk-operator 8081:8081
curl localhost:8081
```
================================================
FILE: services/cyberdesk-operator/handlers/controller.py
================================================
"""
cyberdesk_operator.py
---------------------
Kopf‑based Kubernetes operator that provisions and manages KubeVirt VMs for a custom
`Cyberdesk` CRD. Supabase is used as an external source of truth for instance state.
Key responsibilities
~~~~~~~~~~~~~~~~~~~~
* Bootstrap the operator (load configuration, check for golden snapshot).
* Handle `Cyberdesk` resource creation:
* Check a `VirtualMachinePool` for an available, running "warm" VM.
* If found, assign the VM from the pool (remove owner refs, add labels), patch its metadata, and notify the gateway.
* If no warm VM is available, initiate a `VirtualMachineClone` from a golden `VirtualMachineSnapshot`.
* Track provisioning state (pool vs. clone) via the `Cyberdesk` status.
* Keep Supabase in sync with KubeVirt `VirtualMachineInstance` phase changes for provisioned VMs.
* Handle `Cyberdesk` resource deletion or expiration:
* Delete the associated `VirtualMachine`.
* Attempt cleanup of any lingering `VirtualMachineClone` operations if provisioning didn't complete.
This single file keeps a clear top‑down structure:
1. Standard‑library / third‑party imports
2. Global configuration & logging
3. Constants & enums
4. Supabase and Kubernetes client bootstrap (sets gateway URL based on environment)
5. Utility helpers (template loading, DB helpers, warm pool lookup, etc.)
6. Kopf event‑handlers (startup, create/update/delete, timers, field watchers)
All helpers are deliberately *side‑effect free* (raise on error, return data), making
unit‑testing straightforward.
"""
from __future__ import annotations
import logging
import os
import socket
import urllib.request
import urllib.error
from datetime import UTC, datetime, timedelta
from enum import Enum
from pathlib import Path
from typing import Dict, Optional
import kopf
import kubernetes
from dotenv import load_dotenv
from kopf import OperatorSettings
from kubernetes.client import ( # noqa: WPS433 — explicit import list for type checking
CoreV1Api,
CustomObjectsApi,
ApiextensionsV1Api,
ApiException,
)
from supabase import Client, create_client
# ---------------------------------------------------------------------------
# Logging & basic config -----------------------------------------------------
# ---------------------------------------------------------------------------
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
logger = logging.getLogger(__name__)
# Ensure ENV is loaded *early* so everything that relies on os.getenv works.
load_dotenv()
# ---------------------------------------------------------------------------
# Constants -----------------------------------------------------------------
# ---------------------------------------------------------------------------
CYBERDESK_GROUP = "cyberdesk.io"
CYBERDESK_VERSION = "v1alpha1"
CYBERDESK_PLURAL = "cyberdesks"
START_OPERATOR_PLURAL = "startcyberdeskoperators"
KUBEVIRT_GROUP = "kubevirt.io"
KUBEVIRT_VERSION = "v1"
KUBEVIRT_NAMESPACE = os.getenv("KUBEVIRT_NAMESPACE", "kubevirt")
KUBEVIRT_VM_PLURAL = "virtualmachines"
KUBEVIRT_VMI_PLURAL = "virtualmachineinstances"
MANAGED_BY = "cyberdesk-operator"
CYBERDESK_NAMESPACE = os.getenv("CYBERDESK_NAMESPACE", "cyberdesk-system")
# ---------------------------------------------------------------------------
# Enums ----------------------------------------------------------------------
# ---------------------------------------------------------------------------
class KubeVirtVMIPhase(str, Enum):
"""Supported phases as emitted by KubeVirt."""
PENDING = "Pending"
SCHEDULING = "Scheduling"
SCHEDULED = "Scheduled"
RUNNING = "Running"
SUCCEEDED = "Succeeded"
FAILED = "Failed"
UNKNOWN = "Unknown"
class SupabaseInstanceStatus(str, Enum):
"""Canonical states stored in Supabase."""
PENDING = "pending"
RUNNING = "running"
TERMINATED = "terminated"
ERROR = "error"
# Static mapping between the two state machines -----------------------------
VMI_PHASE_TO_SUPABASE_STATUS: Dict[KubeVirtVMIPhase, SupabaseInstanceStatus] = {
KubeVirtVMIPhase.PENDING: SupabaseInstanceStatus.PENDING,
KubeVirtVMIPhase.SCHEDULING: SupabaseInstanceStatus.PENDING,
KubeVirtVMIPhase.SCHEDULED: SupabaseInstanceStatus.PENDING,
KubeVirtVMIPhase.RUNNING: SupabaseInstanceStatus.PENDING, # We now only denote running after cloud init is done
KubeVirtVMIPhase.SUCCEEDED: SupabaseInstanceStatus.TERMINATED,
KubeVirtVMIPhase.FAILED: SupabaseInstanceStatus.ERROR,
KubeVirtVMIPhase.UNKNOWN: SupabaseInstanceStatus.ERROR,
}
CLONE_GROUP = "clone.kubevirt.io"
CLONE_VERSION = "v1beta1"
CLONE_PLURAL = "virtualmachineclones"
GOLDEN_SNAPSHOT_NAME = "snapshot-golden-vm" # Name of the golden snapshot source
SNAPSHOT_GROUP = "snapshot.kubevirt.io" # Correct API group for VirtualMachineSnapshot
SNAPSHOT_VERSION = "v1beta1" # Correct version for VirtualMachineSnapshot
SNAPSHOT_PLURAL = "virtualmachinesnapshots"
# ---------------------------------------------------------------------------
# Bootstrap helpers ----------------------------------------------------------
# ---------------------------------------------------------------------------
def _init_supabase() -> Client:
"""Create and return a Supabase client or raise ``kopf.PermanentError``."""
supabase_url = os.getenv("SUPABASE_URL")
supabase_key = os.getenv("SUPABASE_KEY")
if not supabase_url or not supabase_key:
msg = "SUPABASE_URL / SUPABASE_KEY env vars must be set"
logger.critical(msg)
raise kopf.PermanentError(msg)
try:
client = create_client(supabase_url, supabase_key)
logger.info("Supabase client initialised")
return client
except Exception as exc: # noqa: BLE001 — log real error and abort
logger.critical("Failed to initialise Supabase: %s", exc)
raise kopf.PermanentError("Supabase init failed") from exc
def _init_kubernetes_clients() -> tuple[CoreV1Api, CustomObjectsApi, ApiextensionsV1Api]:
"""Return (core_v1, custom_objects, apiext) after loading config and set globals."""
global IS_IN_CLUSTER, GATEWAY_BASE_URL # Declare modification intent
try:
kubernetes.config.load_kube_config()
logger.info("Loaded kube‑config from local file")
IS_IN_CLUSTER = False
# Use a single env var for the full testing URL
testing_url = os.getenv("GATEWAY_TESTING_URL")
if testing_url:
GATEWAY_BASE_URL = testing_url
logger.info(f"Using configured local testing gateway URL: {GATEWAY_BASE_URL}")
# Log a hint if it looks like the user might want host.docker.internal
if "localhost" in testing_url:
logger.warning("GATEWAY_TESTING_URL contains 'localhost'. If operator and gateway run in separate local containers, consider using 'host.docker.internal' instead of 'localhost'.")
else:
logger.warning("Running locally but GATEWAY_TESTING_URL env var not set. Gateway notifications will be skipped.")
GATEWAY_BASE_URL = None
except kubernetes.config.config_exception.ConfigException:
try:
kubernetes.config.load_incluster_config()
logger.info("Loaded in‑cluster kube‑config")
IS_IN_CLUSTER = True
GATEWAY_BASE_URL = "http://gateway.cyberdesk-system.svc.cluster.local:80"
logger.info(f"Using in-cluster gateway URL: {GATEWAY_BASE_URL}")
except kubernetes.config.config_exception.ConfigException as exc:
logger.critical("Failed to load Kubernetes configuration: %s", exc)
raise kopf.PermanentError("Cannot load Kubernetes config") from exc
return CoreV1Api(), CustomObjectsApi(), ApiextensionsV1Api()
# Globals to store environment-dependent configuration set during init
IS_IN_CLUSTER = False # Default, will be updated by _init_kubernetes_clients
GATEWAY_BASE_URL: Optional[str] = None # Default, will be updated by _init_kubernetes_clients
# --- Bootstrap Clients ---
SUPABASE: Client = _init_supabase()
CORE_V1_API, CUSTOM_OBJECTS_API, APIEXT_V1_API = _init_kubernetes_clients()
# ---------------------------------------------------------------------------
# Supabase helpers -----------------------------------------------------------
# ---------------------------------------------------------------------------
def get_instance_status(instance_id: str) -> Optional[str]:
"""Return the current status for *instance_id* or ``None`` if missing/error."""
try:
logger.debug("Supabase query: status for %s", instance_id)
resp = SUPABASE.table("cyberdesk_instances").select("status").eq("id", instance_id).limit(1).execute()
return (resp.data[0]["status"] if resp.data else None)
except Exception as exc: # noqa: BLE001
logger.error("Supabase error: %s", exc)
return None
def update_instance_status(instance_id: str, vmi_phase: str) -> None:
"""Translate *vmi_phase* → Supabase status and update row if needed."""
try:
phase_enum = KubeVirtVMIPhase(vmi_phase)
except ValueError:
logger.error("Unknown VMI phase '%s' → marking ERROR", vmi_phase)
target = SupabaseInstanceStatus.ERROR
else:
target = VMI_PHASE_TO_SUPABASE_STATUS.get(phase_enum, SupabaseInstanceStatus.ERROR)
try:
SUPABASE.table("cyberdesk_instances").update({"status": target.value}).eq("id", instance_id).execute()
logger.info("Supabase status for %s set to %s", instance_id, target.value)
except Exception as exc: # noqa: BLE001
logger.error("Supabase update failed for %s: %s", instance_id, exc)
# ---------------------------------------------------------------------------
# Kubernetes Helpers (including Warm Pool) -----------------------------------
# ---------------------------------------------------------------------------
def get_free_vm_from_pool(namespace: str, logger: kopf.Logger) -> Optional[str]:
"""
Find, claim, and return the name of an available warm VM, or None.
A VM is considered available if it has the 'pool.kubevirt.io/warm=ready'
label, is Running, and does not have 'pool.kubevirt.io/in-use=true'.
If found, the VM is "assigned" from the pool by:
1. Removing ownerReferences.
2. Setting 'pool.kubevirt.io/in-use=true' and 'pool.kubevirt.io/warm=claimed'.
"""
pool_label_selector = "pool.kubevirt.io/warm=ready"
logger.debug(f"Searching for warm VMs in namespace '{namespace}' with label '{pool_label_selector}'")
try:
vms = CUSTOM_OBJECTS_API.list_namespaced_custom_object(
KUBEVIRT_GROUP,
KUBEVIRT_VERSION,
namespace,
KUBEVIRT_VM_PLURAL,
label_selector=pool_label_selector,
)
except ApiException as e:
logger.error(f"Error listing VMs for warm pool: {e.status} {e.reason}")
# Treat as temporary, maybe API server issue
raise kopf.TemporaryError("Failed to list VMs for warm pool.", delay=15) from e
for vm in vms.get("items", []):
meta = vm.get("metadata", {})
status = vm.get("status", {})
labels = meta.get("labels", {})
vm_name = meta.get("name")
if not vm_name:
logger.warning("Found VM in pool list without a name, skipping.")
continue
# Check if already marked as in-use by the pool logic itself
if labels.get("pool.kubevirt.io/in-use") == "true":
logger.debug(f"Warm VM '{vm_name}' found but already marked in-use, skipping.")
continue
# Check if running (important!)
if status.get("printableStatus") != "Running":
logger.debug(f"Warm VM '{vm_name}' found but not Running (status: {status.get('printableStatus')}), skipping.")
continue
# --- Assign the VM from Pool ---
logger.info(f"Found available warm VM: '{vm_name}'. Attempting to assign from pool.")
patch_body = {
"metadata": {
"ownerReferences": None, # Detach from the pool controller
"labels": {
**labels, # Keep existing labels
"pool.kubevirt.io/in-use": "true", # Mark as used
"pool.kubevirt.io/warm": "claimed", # Update pool status label
# Note: We will add cyberdesk-instance label in the main handler
},
}
}
try:
CUSTOM_OBJECTS_API.patch_namespaced_custom_object(
group=KUBEVIRT_GROUP,
version=KUBEVIRT_VERSION,
namespace=namespace,
plural=KUBEVIRT_VM_PLURAL,
name=vm_name,
body=patch_body,
)
logger.info(f"Successfully assigned warm VM '{vm_name}' from pool. Removed ownerReferences and added 'in-use' label.")
return vm_name # Return the name of the assigned VM
except ApiException as e:
logger.error(f"Failed to patch (assign) warm VM '{vm_name}': {e.status} {e.reason}")
# If patching fails, maybe the VM was deleted concurrently? Or permissions issue.
# Log error and continue searching, maybe another VM will work.
# If it's a transient issue, the next reconciliation might succeed.
continue # Try the next VM in the list
logger.info("No available warm VMs found in the pool.")
return None
# ---------------------------------------------------------------------------
# CRD definition -------------------------------------------------------------
# ---------------------------------------------------------------------------
CYBERDESK_CRD_MANIFEST: dict = {
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": {"name": f"{CYBERDESK_PLURAL}.{CYBERDESK_GROUP}"},
"spec": {
"group": CYBERDESK_GROUP,
"scope": "Namespaced",
"names": {
"plural": CYBERDESK_PLURAL,
"singular": "cyberdesk",
"kind": "Cyberdesk",
"shortNames": ["cd", "cds"],
},
"versions": [
{
"name": CYBERDESK_VERSION,
"served": True,
"storage": True,
"schema": {
"openAPIV3Schema": {
"type": "object",
"properties": {
"spec": {
"type": "object",
"properties": {
"timeoutMs": {
"type": "integer",
"minimum": 1000,
"description": "Milliseconds until VM is terminated.",
}
},
"required": ["timeoutMs"],
},
"status": {
"type": "object",
"x-kubernetes-preserve-unknown-fields": True,
},
},
}
},
"subresources": {"status": {}},
}
],
},
}
# ---------------------------------------------------------------------------
# Kopf handlers --------------------------------------------------------------
# ---------------------------------------------------------------------------
def ensure_golden_snapshot_exists():
"""Check if the required golden VirtualMachineSnapshot exists."""
logger.info(f"Checking for golden snapshot: {GOLDEN_SNAPSHOT_NAME} in {KUBEVIRT_NAMESPACE}")
try:
CUSTOM_OBJECTS_API.get_namespaced_custom_object(
group=SNAPSHOT_GROUP,
version=SNAPSHOT_VERSION,
namespace=KUBEVIRT_NAMESPACE,
plural=SNAPSHOT_PLURAL,
name=GOLDEN_SNAPSHOT_NAME,
)
logger.info(f"Golden snapshot '{GOLDEN_SNAPSHOT_NAME}' found.")
except ApiException as e:
if e.status == 404:
msg = f"Required golden snapshot '{GOLDEN_SNAPSHOT_NAME}' not found in namespace '{KUBEVIRT_NAMESPACE}'."
logger.critical(msg)
raise kopf.PermanentError(msg)
else:
msg = f"Error checking for golden snapshot '{GOLDEN_SNAPSHOT_NAME}': {e.status} {e.reason}"
logger.error(msg)
# Treat other errors as temporary to allow retries after potential cluster issues
raise kopf.TemporaryError(msg, delay=30) from e
except Exception as e:
msg = f"Unexpected error checking for golden snapshot '{GOLDEN_SNAPSHOT_NAME}': {e}"
logger.error(msg)
raise kopf.TemporaryError(msg, delay=30) from e
@kopf.on.startup()
def configure_kopf(settings: OperatorSettings, **_: Dict[str, object]) -> None:
"""Tune watch timeouts and ensure golden snapshot exists."""
settings.watching.server_timeout = 210 # seconds
logger.info("Kopf watch server_timeout set to %s", settings.watching.server_timeout)
# Check for snapshot on startup - operator won't function without it.
ensure_golden_snapshot_exists()
@kopf.on.create(CYBERDESK_GROUP, CYBERDESK_VERSION, START_OPERATOR_PLURAL)
def crd_bootstrap(spec: dict, meta: dict, **_: Dict[str, object]) -> None:
"""Ensure the Cyberdesk CRD exists once the *bootstrap* resource is created."""
try:
APIEXT_V1_API.create_custom_resource_definition(body=CYBERDESK_CRD_MANIFEST)
logger.info("Cyberdesk CRD applied")
except kubernetes.client.rest.ApiException as exc:
if exc.status == 409: # already present
logger.debug("Cyberdesk CRD already present")
elif exc.status == 429:
raise kopf.TemporaryError("API busy, retrying", delay=10) from exc
else:
raise kopf.PermanentError(f"CRD creation failed: {exc.status} {exc.reason}") from exc
def _ensure_vm_patched_and_running(vm_name: str, namespace: str, logger: kopf.Logger) -> None:
"""Fetch the VM and apply the required patches (metadata, spec, runStrategy)."""
logger.info(f"Ensuring VM '{vm_name}' is patched and set to run.")
# Labels intended for the VMI must go into spec.template.metadata.labels
# Labels only relevant to the VM object itself can stay at the top level.
patch_body = {
"metadata": {
"labels": {
# Optional: Keep labels specific to the VM object itself here if needed.
# For instance, if you wanted to label the VM resource differently than the VMI.
"managed-by": MANAGED_BY, # Can be useful on the VM too
}
# Add top-level annotations for the VM if needed
},
"spec": {
"runStrategy": "Always", # Ensure VM is set to run
"template": {
"metadata": { # <--- Ensure metadata exists here
"labels": { # <--- Labels for the VMI go here
"app": "cyberdesk",
"cyberdesk-instance": vm_name,
"managed-by": MANAGED_BY, # Also label the VMI for consistency
"kubevirt.io/domain": vm_name, # This is often set here
}
# Add annotations for the VMI if needed
},
"spec": {
"hostname": vm_name
}
}
}
}
try:
CUSTOM_OBJECTS_API.patch_namespaced_custom_object(
group=KUBEVIRT_GROUP,
version=KUBEVIRT_VERSION,
namespace=namespace,
plural=KUBEVIRT_VM_PLURAL,
name=vm_name,
body=patch_body
)
logger.info(f"Successfully patched VM '{vm_name}' metadata, spec, and runStrategy.")
except ApiException as e:
logger.error(f"Error patching VM '{vm_name}': {e.status} {e.reason}")
# If patching fails, it's likely temporary or the VM was deleted.
raise kopf.TemporaryError(f"Failed to patch VM {vm_name}", delay=10) from e
@kopf.on.create(CYBERDESK_GROUP, CYBERDESK_VERSION, CYBERDESK_PLURAL)
def cyberdesk_create(spec: dict, meta: dict, status: dict, logger: kopf.Logger, patch: kopf.Patch, body: dict, retry: int, **_: Dict[str, object]): # noqa: WPS211, WPS231
"""Reconcile a new Cyberdesk CR using status-driven warm pool/clone logic."""
instance_id = meta["name"]
namespace = KUBEVIRT_NAMESPACE
timeout_ms = spec.get("timeoutMs", 3_600_000)
max_clone_wait_retries = 20 # Used later in clone check
clone_wait_delay = 5 # Increased delay slightly
logger.info(f"Reconciling Cyberdesk CR '{instance_id}' (Attempt #{retry})")
# --- Check Status: Determine current state/intent ---
current_status = body.get("status", {}).get("cyberdesk_create", {})
vm_ref = current_status.get("virtualMachineRef")
clone_op_name = current_status.get("cloneOperationName")
last_phase = current_status.get("lastPhase")
# --- Idempotency Check: Already Provisioned? ---
if vm_ref and last_phase in ["AssignedFromPool", "Cloned", "Running"]: # "Running" for older status compatibility
logger.info(f"Cyberdesk '{instance_id}' already has vmRef '{vm_ref}'. Ensuring patch and returning.")
try:
# Make sure the VM (assigned or cloned) is correctly patched
# _ensure_vm_patched_and_running should be idempotent
_ensure_vm_patched_and_running(vm_ref, namespace, logger)
# Ensure status reflects reality (especially startTime/expiryTime if they were missed)
if "startTime" not in current_status or "expiryTime" not in current_status:
logger.warning(f"Status for {instance_id} with vmRef {vm_ref} is incomplete. Re-populating times.")
now = datetime.now(UTC)
expiry = now + timedelta(milliseconds=timeout_ms)
patch.status["cyberdesk_create"] = {
**current_status, # Keep existing fields like vmRef, lastPhase
"startTime": now.isoformat(),
"expiryTime": expiry.isoformat(),
}
else:
# If status is complete, just ensure it's patched back (no-op if unchanged)
patch.status["cyberdesk_create"] = current_status
# Clean up potential old top-level status fields
if "virtualMachineRef" in patch.status: del patch.status["virtualMachineRef"]
if "startTime" in patch.status: del patch.status["startTime"]
if "expiryTime" in patch.status: del patch.status["expiryTime"]
if "cloneOperationName" in patch.status.get("cyberdesk_create", {}):
del patch.status["cyberdesk_create"]["cloneOperationName"] # Should be removed if vmRef exists
except kopf.TemporaryError:
raise # Re-raise patch error
except ApiException as e:
if e.status == 404:
logger.warning(f"vmRef '{vm_ref}' in status for '{instance_id}' not found! Forcing re-provision.")
# Clear status to force reprovisioning
patch.status["cyberdesk_create"] = {}
else:
logger.error(f"Error checking existing vmRef '{vm_ref}' for '{instance_id}': {e.reason}")
raise kopf.TemporaryError(f"Failed to check existing VM {vm_ref}", delay=10) from e
else:
return # AssignedFromPool successful
# --- State Check: Already decided to clone? ---
if clone_op_name:
logger.info(f"Status indicates cloning operation '{clone_op_name}' already initiated for '{instance_id}'. Checking clone status.")
# Skip warm pool check, go directly to checking the clone
pass # Logic continues below in "Check Clone Status" section
else:
# --- State: Try Warm Pool ---
logger.info(f"No active clone operation found in status for '{instance_id}'. Checking warm pool.")
assigned_vm_name = get_free_vm_from_pool(namespace, logger) # Renamed variable for clarity
if assigned_vm_name:
logger.info(f"Using warm VM '{assigned_vm_name}' assigned from pool for Cyberdesk '{instance_id}'.")
try:
# --- Patch Assigned VM ---
vm_patch_body = {
"metadata": {"labels": {"app": "cyberdesk", "cyberdesk-instance": instance_id, "managed-by": MANAGED_BY}},
"spec": {"template": {"metadata": {"labels": {"app": "cyberdesk", "cyberdesk-instance": instance_id, "managed-by": MANAGED_BY, "kubevirt.io/domain": instance_id}}}}
}
# Fetch current VM to merge labels correctly
current_vm = CUSTOM_OBJECTS_API.get_namespaced_custom_object(KUBEVIRT_GROUP, KUBEVIRT_VERSION, namespace, KUBEVIRT_VM_PLURAL, assigned_vm_name)
current_labels = current_vm.get("metadata", {}).get("labels", {})
current_vmi_labels = current_vm.get("spec", {}).get("template", {}).get("metadata", {}).get("labels", {})
vm_patch_body["metadata"]["labels"] = {**current_labels, **vm_patch_body["metadata"]["labels"]}
vm_patch_body["spec"]["template"]["metadata"]["labels"] = {**current_vmi_labels, **vm_patch_body["spec"]["template"]["metadata"]["labels"]}
CUSTOM_OBJECTS_API.patch_namespaced_custom_object(KUBEVIRT_GROUP, KUBEVIRT_VERSION, namespace, KUBEVIRT_VM_PLURAL, assigned_vm_name, body=vm_patch_body)
logger.info(f"Successfully patched VM '{assigned_vm_name}' assigned from pool for '{instance_id}'.")
# --- Verify VMI is running and get IP (Readiness Check) ---
try:
vmi = CUSTOM_OBJECTS_API.get_namespaced_custom_object(
group=KUBEVIRT_GROUP,
version=KUBEVIRT_VERSION,
namespace=namespace,
plural=KUBEVIRT_VMI_PLURAL,
name=assigned_vm_name # VMI name assumed to match assigned VM name
)
interfaces = vmi.get('status', {}).get('interfaces', [])
vmi_ip = interfaces[0].get('ipAddress') if interfaces else None
vmi_phase = vmi.get('status', {}).get('phase')
if not vmi_ip or vmi_phase != 'Running':
logger.warning(f"Pool-assigned VMI '{assigned_vm_name}' is not Running or has no IP yet (Phase: {vmi_phase}, IP: {vmi_ip}). Retrying.")
raise kopf.TemporaryError(f"Pool-assigned VMI {assigned_vm_name} not fully ready.")
logger.info(f"Verified pool-assigned VMI '{assigned_vm_name}' is Running with IP {vmi_ip}.")
except ApiException as vmi_exc:
if vmi_exc.status == 404:
logger.warning(f"VMI '{assigned_vm_name}' not found immediately after patching VM. Retrying.")
raise kopf.TemporaryError("VMI not found yet after patching.") # Retry
else:
logger.error(f"API error getting VMI '{assigned_vm_name}' for readiness check: {vmi_exc.reason}")
raise # Re-raise other API errors
# --- Notify Gateway ---
if not GATEWAY_BASE_URL:
logger.warning(f"Gateway base URL not configured (In cluster? {IS_IN_CLUSTER}). Skipping notification for {instance_id}.")
else:
gateway_url = f"{GATEWAY_BASE_URL}/cyberdesk/{instance_id}/ready"
logger.info(f"Notifying gateway for pool-assigned instance '{instance_id}' at {gateway_url}")
try:
req = urllib.request.Request(gateway_url, method="POST")
with urllib.request.urlopen(req, timeout=5) as response:
logger.info(f"Gateway notified successfully for pool-assigned '{instance_id}', status: {response.status}")
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as e:
logger.error(f"Failed to notify gateway for pool-assigned '{instance_id}': {e}")
except ApiException as e:
logger.error(f"Error patching/notifying for pool-assigned VM '{assigned_vm_name}': {e.reason}")
raise kopf.TemporaryError(f"Failed to finalize pool-assigned VM {assigned_vm_name}", delay=10) from e
# --- Update Status (AssignedFromPool Success) ---
now = datetime.now(UTC)
expiry = now + timedelta(milliseconds=timeout_ms)
logger.info(f"Updating status for '{instance_id}': Assigned VM '{assigned_vm_name}' from pool, expires {expiry.isoformat()}")
patch.status["cyberdesk_create"] = {
"virtualMachineRef": assigned_vm_name,
"startTime": now.isoformat(),
"expiryTime": expiry.isoformat(),
"lastPhase": "AssignedFromPool",
# Explicitly remove cloneOperationName if it somehow existed
"cloneOperationName": None,
}
# Clean up potential old status fields
if "virtualMachineRef" in patch.status: del patch.status["virtualMachineRef"]
if "startTime" in patch.status: del patch.status["startTime"]
if "expiryTime" in patch.status: del patch.status["expiryTime"]
return # Assignment from warm pool successful
else:
# --- State: Initiate Cloning ---
logger.info(f"No warm VM available for '{instance_id}'. Initiating clone.")
clone_op_name = f"clone-for-{instance_id}" # Define the clone op name
# --- Update Status (Pre-Clone) ---
# Set cloneOperationName *before* creating the clone object
logger.info(f"Updating status for '{instance_id}': Setting cloneOperationName to '{clone_op_name}'")
patch.status["cyberdesk_create"] = {
"cloneOperationName": clone_op_name,
"lastPhase": "CloningInitiated",
# Ensure vmRef is not set here
"virtualMachineRef": None,
}
# Return early to allow Kopf to patch the status.
# The next reconciliation will pick up 'cloneOperationName' and proceed.
# This prevents creating the Clone object if the status patch fails.
raise kopf.TemporaryError(f"Clone operation name '{clone_op_name}' set in status. Retrying shortly to initiate/check clone.", delay=clone_wait_delay)
# --- State: Check Clone Status (only reached if clone_op_name is set) ---
if not clone_op_name:
# This should ideally not be reached due to the logic structure, but acts as a safeguard.
logger.error(f"Reached clone checking state for '{instance_id}' but cloneOperationName is not set in status. Retrying.")
raise kopf.TemporaryError("Inconsistent state: clone check without cloneOperationName.", delay=10)
logger.info(f"Checking status of clone operation '{clone_op_name}' for '{instance_id}'.")
try:
# --- Get or Create VirtualMachineClone Object ---
# We try to *get* it first because the status might have been set on a previous attempt
# where the actual clone creation failed afterwards.
try:
clone_obj = CUSTOM_OBJECTS_API.get_namespaced_custom_object(
group=CLONE_GROUP, version=CLONE_VERSION, namespace=namespace, plural=CLONE_PLURAL, name=clone_op_name
)
logger.debug(f"Found existing VirtualMachineClone '{clone_op_name}'.")
except ApiException as e:
if e.status == 404:
logger.info(f"VirtualMachineClone '{clone_op_name}' not found. Creating it now.")
clone_body = {
"apiVersion": f"{CLONE_GROUP}/{CLONE_VERSION}",
"kind": "VirtualMachineClone",
"metadata": {"name": clone_op_name, "namespace": namespace, "labels": {"managed-by": MANAGED_BY, "cyberdesk-instance": instance_id}},
"spec": {
"source": {"apiGroup": SNAPSHOT_GROUP, "kind": "VirtualMachineSnapshot", "name": GOLDEN_SNAPSHOT_NAME},
"target": {
"apiGroup": KUBEVIRT_GROUP,
"kind": "VirtualMachine",
"name": instance_id,
"template": {
"spec": {
"readinessProbe": {
"exec": {
# Use test -f to check for cloud-init completion flag
"command": ["test", "-f", "/var/lib/cloud/instance/boot-finished"]
},
"initialDelaySeconds": 30,
"periodSeconds": 10,
"failureThreshold": 3,
"successThreshold": 1,
}
}
}
},
},
}
clone_obj = CUSTOM_OBJECTS_API.create_namespaced_custom_object(
group=CLONE_GROUP, version=CLONE_VERSION, namespace=namespace, plural=CLONE_PLURAL, body=clone_body
)
logger.info(f"VirtualMachineClone '{clone_op_name}' created.")
# No need to check status immediately, let the next retry handle it
raise kopf.TemporaryError(f"Clone {clone_op_name} just created. Waiting for status.", delay=clone_wait_delay)
else:
# Other API error getting the clone object
logger.error(f"API error getting VirtualMachineClone '{clone_op_name}': {e.reason}")
raise kopf.TemporaryError(f"Failed to get clone object {clone_op_name}", delay=clone_wait_delay) from e
# --- Evaluate Clone Status ---
current_clone_status = clone_obj.get("status", {})
clone_phase = current_clone_status.get("phase")
logger.info(f"Clone '{clone_op_name}' phase: {clone_phase}")
if clone_phase == "Succeeded":
logger.info(f"Clone '{clone_op_name}' succeeded. Finalizing VM '{instance_id}'.")
# --- Ensure the newly created VM is patched and running ---
_ensure_vm_patched_and_running(instance_id, namespace, logger) # Target VM name is instance_id
# --- Update Status (Clone Success) ---
now = datetime.now(UTC)
expiry = now + timedelta(milliseconds=timeout_ms)
logger.info(f"Updating status for '{instance_id}': Cloned VM '{instance_id}', expires {expiry.isoformat()}")
patch.status["cyberdesk_create"] = {
"virtualMachineRef": instance_id, # VM name matches instance_id
"startTime": now.isoformat(),
"expiryTime": expiry.isoformat(),
"lastPhase": "Cloned",
"cloneOperationName": None, # Remove clone name on success
}
# Clean up potential old status fields
if "virtualMachineRef" in patch.status: del patch.status["virtualMachineRef"]
if "startTime" in patch.status: del patch.status["startTime"]
if "expiryTime" in patch.status: del patch.status["expiryTime"]
return # Clone successful
elif clone_phase == "Failed":
logger.error(f"Clone '{clone_op_name}' failed. Check clone object status for details.")
# Update status to reflect failure
patch.status["cyberdesk_create"] = {
**current_status, # Keep existing fields if any
"lastPhase": "CloneFailed",
"cloneOperationName": None, # Remove clone name on failure
"virtualMachineRef": None, # Ensure no vmRef
}
raise kopf.PermanentError(f"Clone {clone_op_name} failed.")
elif clone_phase == "Unknown":
logger.warning(f"Clone '{clone_op_name}' phase is Unknown. Retrying status check...")
raise kopf.TemporaryError(f"Clone {clone_op_name} phase Unknown.", delay=clone_wait_delay)
else: # InProgress phases (SnapshotInProgress, CreatingTargetVM, RestoreInProgress, None)
logger.info(f"Clone '{clone_op_name}' in progress ({clone_phase}). Waiting...")
# Check for timeout only if clone is still in progress
if retry >= max_clone_wait_retries: # Use >= for safety
logger.error(f"Clone '{clone_op_name}' did not succeed within {max_clone_wait_retries} attempts.")
# Attempt to delete the stuck clone object
try:
CUSTOM_OBJECTS_API.delete_namespaced_custom_object(CLONE_GROUP, CLONE_VERSION, namespace, CLONE_PLURAL, clone_op_name)
logger.info(f"Deleted timed-out clone object '{clone_op_name}'.")
except ApiException as del_exc:
if del_exc.status != 404:
logger.warning(f"Failed to delete timed-out clone object '{clone_op_name}': {del_exc.reason}")
# Update status and mark as permanent failure
patch.status["cyberdesk_create"] = {
**current_status,
"lastPhase": "CloneTimeout",
"cloneOperationName": None,
"virtualMachineRef": None,
}
raise kopf.PermanentError(f"Clone {clone_op_name} timed out.")
else:
# Still in progress and within retry limit, raise TemporaryError to retry
raise kopf.TemporaryError(f"Clone {clone_op_name} in progress ({clone_phase}). Waiting...", delay=clone_wait_delay)
except kopf.TemporaryError:
raise # Propagate temporary errors for retry
except kopf.PermanentError:
raise # Propagate permanent errors
except ApiException as e:
# Catch API errors during clone check/creation
logger.error(f"API error during clone processing for '{clone_op_name}': {e.reason}")
raise kopf.TemporaryError(f"API error processing clone {clone_op_name}", delay=clone_wait_delay) from e
except Exception as e:
# Catch unexpected errors
logger.exception(f"Unexpected error during reconciliation of '{instance_id}' at clone check state.") # Use logger.exception to include traceback
raise kopf.TemporaryError(f"Unexpected error for {instance_id}: {e}", delay=30)
@kopf.on.field(KUBEVIRT_GROUP, KUBEVIRT_VERSION, KUBEVIRT_VMI_PLURAL, field="status.phase")
def vmi_phase_change(old: str | None, new: str | None, meta: dict, status: dict, logger: kopf.Logger, **_: Dict[str, object]):
"""Sync Supabase when a VMI phase flips, ignoring expected warm pool VMs."""
if new is None:
return # nothing to do
labels = meta.get("labels", {})
vm_name = meta.get("name", "unknown-vmi")
# Only process VMIs managed by or intended for this operator
if labels.get("app") != "cyberdesk":
logger.debug(f"Ignoring phase change for VMI {vm_name} (missing 'app: cyberdesk' label)")
return
instance_id = labels.get("cyberdesk-instance")
if not instance_id:
# If no instance ID, check if it's expected (part of warm pool)
is_warm_pool_vm = (
labels.get("pool.kubevirt.io/warm") == "ready" or
labels.get("pool.kubevirt.io/in-use") == "true"
)
if is_warm_pool_vm:
# Expected state for a VM in the pool or just assigned
logger.debug(f"Ignoring phase change for warm pool VMI {vm_name} (no instance ID yet)")
else:
# Unexpected: Has 'app: cyberdesk' but no instance ID and no pool labels
logger.warning(f"VMI {vm_name} has 'app: cyberdesk' but is missing 'cyberdesk-instance' label and doesn't appear to be a warm pool VM.")
return # Don't proceed to Supabase update
# --- Proceed with Supabase update only if we have an instance_id ---
logger.info(f"Processing phase change ('{old}' -> '{new}') for VMI {vm_name} linked to instance {instance_id}")
try:
current_db = get_instance_status(instance_id)
# Added check to prevent infinite loops if status already matches
# This check requires get_instance_status to be relatively quick
try:
phase_enum = KubeVirtVMIPhase(new)
desired = VMI_PHASE_TO_SUPABASE_STATUS.get(phase_enum, SupabaseInstanceStatus.ERROR).value
except ValueError:
logger.error(f"Unknown VMI phase '{new}' for {vm_name} -> marking ERROR in Supabase")
desired = SupabaseInstanceStatus.ERROR.value
if current_db != desired:
logger.info(f"Supabase status mismatch for {instance_id} (DB: {current_db}, VMI wants: {desired}). Updating.")
update_instance_status(instance_id, new)
else:
logger.debug(f"Supabase status for {instance_id} already matches desired state ({desired}). No update needed.")
except Exception as e:
# Catch potential errors during DB check/update
logger.exception(f"Error processing VMI phase change for {instance_id} in Supabase: {e}")
@kopf.on.delete(CYBERDESK_GROUP, CYBERDESK_VERSION, CYBERDESK_PLURAL)
def cyberdesk_delete(meta: dict, body: dict, logger: kopf.Logger, **_: Dict[str, object]):
"""Tear down the associated VM when *Cyberdesk* is deleted, if provisioned."""
instance_id = meta["name"]
namespace = KUBEVIRT_NAMESPACE
logger.info(f"Handling deletion for Cyberdesk CR '{instance_id}'.")
vm_name = body.get("status", {}).get("cyberdesk_create", {}).get("virtualMachineRef")
if vm_name:
logger.info(f"Found virtualMachineRef '{vm_name}' in status. Attempting VM deletion.")
try:
CUSTOM_OBJECTS_API.delete_namespaced_custom_object(
group=KUBEVIRT_GROUP,
version=KUBEVIRT_VERSION,
namespace=namespace,
plural=KUBEVIRT_VM_PLURAL,
name=vm_name,
# Optional: Add grace period if needed
# body=kubernetes.client.V1DeleteOptions(grace_period_seconds=0) # Example: Force immediate deletion
)
logger.info(f"Successfully initiated deletion for VM '{vm_name}'.")
# Removed cloud-init secret deletion attempt
except ApiException as exc:
if exc.status not in (404, 410): # Ignore if already deleted
logger.error(f"Failed to delete VM '{vm_name}' during cleanup: {exc.status} {exc.reason}")
raise kopf.TemporaryError(f"VM cleanup failed for {vm_name}, will retry", delay=15) from exc
else:
logger.info(f"VM '{vm_name}' already deleted or not found.")
else:
# --- Handle case where deletion happens before provisioning completes ---
logger.warning(f"No virtualMachineRef found in status for deleted Cyberdesk '{instance_id}'. Provisioning may not have completed.")
clone_op_name = body.get("status", {}).get("cyberdesk_create", {}).get("cloneOperationName")
if clone_op_name:
logger.info(f"Found cloneOperationName '{clone_op_name}'. Attempting to delete potentially lingering clone operation.")
try:
CUSTOM_OBJECTS_API.delete_namespaced_custom_object(
group=CLONE_GROUP,
version=CLONE_VERSION,
namespace=namespace,
plural=CLONE_PLURAL,
name=clone_op_name,
)
logger.info(f"Successfully deleted potentially lingering clone operation '{clone_op_name}'.")
except ApiException as e:
if e.status != 404:
logger.warning(f"Failed to delete clone operation '{clone_op_name}' during cleanup: {e.status} {e.reason}. Manual check might be needed.")
else:
logger.debug(f"Lingering clone operation '{clone_op_name}' not found.")
else:
logger.info(f"No cloneOperationName found either for '{instance_id}'. No KubeVirt resources to clean up based on status.")
@kopf.on.timer(CYBERDESK_GROUP, CYBERDESK_VERSION, CYBERDESK_PLURAL, interval=60)
def cyberdesk_timeout_check(body: dict, logger: kopf.Logger, **_: Dict[str, object]): # Added logger
"""Per‑resource timer: shut down VM once *expiryTime* passes."""
expiry_str = body.get("status", {}).get("cyberdesk_create", {}).get("expiryTime")
instance_id = body["metadata"]["name"]
namespace = body["metadata"]["namespace"]
if not expiry_str:
logger.debug(f"No expiryTime found in status for '{instance_id}', skipping timeout check.")
return
try:
expiry_dt = datetime.fromisoformat(expiry_str)
if datetime.now(UTC) >= expiry_dt:
logger.info(f"Cyberdesk '{instance_id}' expired at {expiry_str} — deleting CR.")
# Deleting the CR will trigger the cyberdesk_delete handler for actual VM cleanup
CUSTOM_OBJECTS_API.delete_namespaced_custom_object(
group=CYBERDESK_GROUP,
version=CYBERDESK_VERSION,
namespace=namespace,
plural=CYBERDESK_PLURAL,
name=instance_id
)
# else: logger.debug(f"Cyberdesk '{instance_id}' not expired yet.")
except ValueError:
logger.error(f"Could not parse expiryTime '{expiry_str}' for '{instance_id}'.")
except ApiException as e:
logger.error(f"API error deleting expired Cyberdesk CR '{instance_id}': {e.reason}")
# Raise temporary error to retry deletion
raise kopf.TemporaryError(f"Failed to delete expired CR {instance_id}", delay=30) from e
# NEW Field Watcher for VMI Readiness (Post Cloud-Init)
@kopf.on.field(KUBEVIRT_GROUP, KUBEVIRT_VERSION, KUBEVIRT_VMI_PLURAL, field='status.conditions')
def vmi_ready_watcher(old, new, status, meta, logger: kopf.Logger, **kwargs):
"""Notify gateway when a VMI's Ready condition becomes True after cloud-init."""
if not new: # Field might be cleared on deletion
return
labels = meta.get("labels", {})
vm_name = meta.get("name", "unknown-vmi")
# Filter for VMIs managed by us AND that have an instance ID
if labels.get("app") != "cyberdesk" or not labels.get("cyberdesk-instance"):
# logger.debug(f"Ignoring condition change for VMI {vm_name} (not a managed cyberdesk instance).")
return
instance_id = labels["cyberdesk-instance"]
# Find the 'Ready' condition in the new status
ready_condition = None
for condition in new:
if condition.get("type") == "Ready":
ready_condition = condition
break
if not ready_condition:
# logger.debug(f"No 'Ready' condition found in status update for VMI {vm_name}.")
return
is_ready = ready_condition.get("status") == "True"
# logger.debug(f"VMI {vm_name} Ready condition status: {ready_condition.get('status')}")
# Check if the old status also had Ready=True to avoid re-notifying
was_ready = False
if old:
for condition in old:
if condition.get("type") == "Ready" and condition.get("status") == "True":
was_ready = True
break
if is_ready and not was_ready:
logger.info(f"VMI '{vm_name}' ({instance_id}) condition changed to Ready=True. Notifying gateway.")
# --- Notify Gateway --- #
if not GATEWAY_BASE_URL:
logger.warning(f"Gateway base URL not configured (In cluster? {IS_IN_CLUSTER}). Skipping notification for ready instance {instance_id}.")
else:
gateway_url = f"{GATEWAY_BASE_URL}/cyberdesk/{instance_id}/ready"
logger.info(f"Notifying gateway for ready instance '{instance_id}' at {gateway_url}")
try:
# Using a simple synchronous request here for simplicity in handler
# Consider making it async if gateway calls become slow/blocking
req = urllib.request.Request(gateway_url, method="POST")
# Add a timeout to prevent blocking indefinitely
with urllib.request.urlopen(req, timeout=10) as response:
logger.info(f"Gateway notified successfully for ready '{instance_id}', status: {response.status}")
except (urllib.error.URLError, urllib.error.HTTPError, socket.timeout, TimeoutError) as e:
# Log error, but don't fail the handler - the VMI *is* ready
logger.error(f"Failed to notify gateway for ready '{instance_id}': {e}")
except Exception as e:
# Catch unexpected errors during notification
logger.exception(f"Unexpected error notifying gateway for ready '{instance_id}': {e}")
# else:
# logger.debug(f"VMI {vm_name} Ready condition did not change to True, or was already True. No action.")
================================================
FILE: services/cyberdesk-operator/requirements.txt
================================================
aiohappyeyeballs==2.6.1
aiohttp==3.11.16
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.9.0
attrs==25.3.0
cachetools==5.5.2
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
colorama==0.4.6
deprecation==2.1.0
durationpy==0.9
frozenlist==1.5.0
google-auth==2.38.0
gotrue==2.12.0
h11==0.14.0
h2==4.2.0
hpack==4.1.0
httpcore==1.0.8
httpx==0.28.1
hyperframe==6.1.0
idna==3.10
iniconfig==2.1.0
iso8601==2.1.0
kopf==1.37.5
kubernetes==32.0.1
multidict==6.4.3
oauthlib==3.2.2
packaging==24.2
pluggy==1.5.0
postgrest==1.0.1
propcache==0.3.1
pyasn1==0.6.1
pyasn1-modules==0.4.2
pydantic==2.11.3
pydantic-core==2.33.1
pyjwt==2.10.1
python-dateutil==2.9.0.post0
python-dotenv==1.1.0
python-json-logger==3.3.0
pyyaml==6.0.2
realtime==2.4.2
requests==2.32.3
requests-oauthlib==2.0.0
rsa==4.9
six==1.17.0
sniffio==1.3.1
storage3==0.11.3
strenum==0.4.15
supabase==2.15.0
supafunc==0.9.4
typing-extensions==4.13.2
typing-inspection==0.4.0
urllib3==2.4.0
websocket-client==1.8.0
websockets==14.2
yarl==1.19.0
python-dotenv
================================================
FILE: services/cyberdesk-operator/tests/README.md
================================================
# Testing the Cyberdesk Operator Locally
This directory contains tests for the Cyberdesk operator that can be run locally against a real Kubernetes cluster, leveraging `kopf.testing.KopfRunner`.
This allows for rapid development and testing without needing to build and push a Docker image for every code change.
## Prerequisites
1. **Running Kubernetes Cluster:** You need access to a Kubernetes cluster (like the AKS cluster deployed via Terraform in the `infra` directory, or Minikube, Kind, etc.).
2. **`kubectl` Configured:** Your local `kubectl` must be configured to communicate with your target cluster (`kubectl config current-context` should show the correct context).
3. **KubeVirt Installed:** KubeVirt must be installed and running on your cluster.
You can typically install it by applying the manifests from the `infra/kubevirt` directory (specifically `kubevirt-operator.yaml` and `kubevirt-cr.yaml`).
4. **StartCyberdeskOperator CRD Applied:** The Custom Resource Definition for `StartCyberdeskOperator` must be applied to the cluster *before* running the tests. Apply the dedicated test CRD file:
```bash
kubectl apply -f tests/test-start-operator-crd.yaml
```
5. **Cyberdesk Namespace:** The `cyberdesk-system` namespace needs to exist, as the test applies resources there. If you haven't created it previously, create it manually:
```bash
kubectl create namespace cyberdesk-system --dry-run=client -o yaml | kubectl apply -f -
```
6. **Python Environment:** You need a Python environment (ideally a virtual environment) with the operator's dependencies installed:
```bash
# Navigate to the operator directory if you aren't already there
# cd /path/to/services/cyberdesk-operator
# Create venv (if you haven't)
# python -m venv .venv
# source .venv/bin/activate # Linux/macOS
# .\.venv\Scripts\Activate.ps1 # Windows PowerShell
# Install dependencies (including kopf and pytest)
pip install -r requirements.txt
pip install pytest
```
7. **Sufficient User Permissions:** The user account associated with your `kubectl` context needs permissions on the cluster to:
* Get/Create/Delete `StartCyberdeskOperator` resources in the `cyberdesk-system` namespace.
* Get/Create/Delete `Cyberdesk` resources in the `cyberdesk-system` namespace.
* Get/Create/Delete `VirtualMachine` resources in the `cyberdesk-system` namespace.
* Get `CustomResourceDefinition` resources (cluster-wide).
*(If you are a cluster admin, you likely have these permissions already).*
## Running the Tests
1. Ensure all prerequisites are met.
2. Navigate to the `services/cyberdesk-operator` directory in your terminal.
3. Run `pytest` targeting the test file:
```bash
pytest tests/test_operator.py -v -s
```
* `-v`: Verbose output.
* `-s`: Show logs (`print` statements and `logging` output) during test execution.
## Understanding the Test (`test_operator.py`)
1. **Setup:**
* It defines paths to necessary CR manifests (`start-cyberdesk-operator-cr.yaml`, `cyberdesk-cr.yaml`).
* It includes a helper function `run_kubectl` to execute `kubectl` commands via `subprocess`.
* It uses `pytest.mark.skipif` to skip the test if `kubectl` isn't configured.
2. **`KopfRunner` Execution:**
* `with kopf.testing.KopfRunner(...) as runner:` starts your operator's `main.py` script in a background thread using your local Python environment.
* The operator code connects to your configured Kubernetes cluster using `load_kube_config()`.
3. **Test Steps (within the `with` block):**
* **Apply `StartCyberdeskOperator` CR:** Uses `run_kubectl` to apply the trigger CR.
* **Wait & Verify CRD:** Waits and repeatedly checks (using `run_kubectl get crd`) if the `Cyberdesk` CRD (`cyberdesks.cyberdesk.io`) has been created by the operator. Asserts that it appears and that the operator logged a success message (`runner.stdout`).
* **Apply `Cyberdesk` CR:** Uses `run_kubectl` to apply the sample `Cyberdesk` instance.
* **Wait & Verify VM:** Waits and repeatedly checks (using `run_kubectl get virtualmachine`) if the corresponding `VirtualMachine` object has been created in the correct namespace. Asserts its existence and checks the operator logs (`runner.stdout`) for creation messages.
* **Cleanup:** Uses `run_kubectl delete` to remove the `Cyberdesk` and `StartCyberdeskOperator` CRs created during the test.
* **Wait & Verify VM Deletion:** Waits and checks if the `VirtualMachine` object is deleted (as a consequence of the `Cyberdesk` CR being deleted).
4. **Post-Run Assertions:**
* After the `with` block finishes (the operator stops), it checks `runner.exit_code` and `runner.exception` to ensure the operator process ran without errors.
## Debugging
* **Operator Logs:** The `-s` flag with `pytest` prints the operator's logs directly to your console, making it easy to see what it's doing or where it failed.
* **`kubectl`:** While the test is running (especially during the `time.sleep` pauses), you can use `kubectl get ...`, `kubectl describe ...`, and `kubectl logs ...` (if the *actual* operator deployment were running, which it isn't here) in a separate terminal to inspect the state of the cluster.
* **Python Debugger:** Since the operator runs as a local Python process, you can use standard Python debugging tools! Add `import pdb; pdb.set_trace()` in your `handlers/controller.py` code where you want to pause, then run the `pytest` command. Execution will stop at the breakpoint, allowing you to inspect variables and step through the code.
================================================
FILE: services/cyberdesk-operator/tests/test-cyberdesk-cr.yaml
================================================
apiVersion: cyberdesk.io/v1alpha1
kind: Cyberdesk
metadata:
name: 6ebf5303-1e81-4203-b870-ccfeb590d02f
namespace: cyberdesk-system
spec:
timeoutMs: 86400000 # 24 hour timeout
================================================
FILE: services/cyberdesk-operator/tests/test-start-operator-cr.yaml
================================================
apiVersion: cyberdesk.io/v1alpha1
kind: StartCyberdeskOperator
metadata:
name: bootstrap-cyberdesk-setup
namespace: cyberdesk-system
spec:
{}
================================================
FILE: services/cyberdesk-operator/tests/test-start-operator-crd.yaml
================================================
# CRD for the trigger resource - needed for local KopfRunner tests
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# Note: The name MUST match the one the operator expects: startcyberdeskoperators.cyberdesk.io
name: startcyberdeskoperators.cyberdesk.io
spec:
group: cyberdesk.io
names:
kind: StartCyberdeskOperator
plural: startcyberdeskoperators
singular: startcyberdeskoperator
shortNames:
- sco
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
description: "Specification for triggering the Cyberdesk operator setup. Currently holds no fields."
# No specific fields needed for now, just the presence triggers the action
================================================
FILE: services/cyberdesk-operator/tests/test.py
================================================
import time
import subprocess
from kopf.testing import KopfRunner
def test_operator():
with KopfRunner(['run', '-A', '--verbose', 'handlers/controller.py']) as runner:
# do something while the operator is running.
subprocess.run("kubectl apply -f examples/obj.yaml", shell=True, check=True)
time.sleep(1) # give it some time to react and to sleep and to retry
subprocess.run("kubectl delete -f examples/obj.yaml", shell=True, check=True)
time.sleep(1) # give it some time to react
assert runner.exit_code == 0
assert runner.exception is None
assert 'And here we are!' in runner.stdout
assert 'Deleted, really deleted' in runner.stdout
================================================
FILE: services/gateway/Dockerfile
================================================
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy app code + noVNC client
COPY main.py .
COPY noVNC/ ./noVNC/
# Expose HTTP port
EXPOSE 80
# Launch with Uvicorn
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
================================================
FILE: services/gateway/README.md
================================================
# Cyberdesk API Gateway
This FastAPI application acts as a micro-gateway for the Cyberdesk system. Its primary responsibilities include:
1. **Serving the noVNC UI:** Provides the static web files for the browser-based VNC client.
2. **Proxying VNC WebSockets:** Securely proxies WebSocket connections from the browser's noVNC client to the KubeVirt VNC sub-resource endpoint for a specific Virtual Machine Instance (VMI).
3. **Managing Cyberdesk CRs:** Handles API requests to create and delete `Cyberdesk` custom resources within the Kubernetes cluster.
4. **(Future)** Proxying terminal commands via execDaemon.
## Running Locally (Development)
These instructions explain how to run the gateway service locally on your machine (e.g., Windows) for development purposes, connecting to a Kubernetes cluster (like AKS, Minikube, k3d, or Docker Desktop's built-in cluster).
### Prerequisites
1. **Docker Desktop:** You need Docker installed and running to build and run the container image. Download and install it from [docker.com](https://www.docker.com/products/docker-desktop/).
2. **Kubernetes Cluster Access:** You need `kubectl` configured to talk to your Kubernetes cluster.
* **AKS Example:** If using Azure Kubernetes Service (AKS) like the main Cyberdesk deployment, you can fetch credentials using the Azure CLI:
```bash
# Login to Azure first if needed: az login
az aks get-credentials --resource-group rg-p-scu-kubevirt --name aks-p-scu-kubevirt
```
*(Replace the resource group and cluster name if you have a different setup).*
* **Other Clusters:** Ensure your `~/.kube/config` (or `${env:USERPROFILE}\.kube\config` on Windows) is correctly set up for `kubectl` to access your cluster.
### Steps
1. **Build the Docker Image:**
Navigate to the `services/gateway` directory in your terminal and run:
```bash
docker build -t cyberdesk/gateway:local .
```
*(You can change the tag `:local` if you prefer).*
2. **Adjust Kubeconfig Server Address (If Necessary):**
The gateway running inside Docker needs to reach your Kubernetes API server. If your `kubeconfig` file (`${env:USERPROFILE}\.kube\config`) has `server:` entries pointing to `https://localhost:...` or `https://127.0.0.1:...`, the container won't be able to connect.
* **Edit the file:** Change `localhost` or `127.0.0.1` to `host.docker.internal`.
* **Example (before):** `server: https://localhost:6443`
* **Example (after):** `server: https://host.docker.internal:6443`
* *(This special DNS name is provided by Docker Desktop).*
3. **Run the Docker Container:**
Open your terminal and run the appropriate command for your shell:
* **PowerShell (Windows):**
```powershell
docker run --rm -it `
-v "${env:USERPROFILE}\.kube:/root/.kube:ro" `
--env-file ./.env `
-p 3001:80 `
cyberdesk/gateway:local
```
*(Note: We use `${env:USERPROFILE}\.kube` to correctly locate your kubeconfig folder on Windows.)*
* **Bash (Linux/macOS/WSL):**
```bash
docker run --rm -it \
-v ~/.kube:/root/.kube:ro \
--env-file ./.env \
-p 3001:80 \
cyberdesk/gateway:local
```
*(Note: We use `~/.kube` for the standard kubeconfig location on Unix-like systems.)*
**Command Breakdown:**
* `--rm`: Automatically removes the container when it exits.
* `-it`: Runs interactively so you can see logs and stop with Ctrl+C.
* `-v ...:/root/.kube:ro`: **Crucial!** Mounts your host's `.kube` directory (containing the `config` file) into the container at `/root/.kube`. We use the appropriate path for PowerShell or Bash. `:ro` makes it read-only inside the container for safety.
* `-p 3001:80`: Maps port 3001 on your host machine to port 80 inside the container (where Uvicorn runs by default). You can change `3001` if needed.
* `cyberdesk/gateway:local`: The image name and tag you built.
4. **Access the Service:**
The gateway should now be running and accessible at `http://localhost:3001` on your host machine. You can test endpoints like `http://localhost:3001/healthz`.
5. **For the noVNC stream, you need to run the following command:**
```bash
kubectl get pods -n kubevirt
# Find the pod with id 'virt-launcher--'
kubectl port-forward 5901:5901 -n kubevirt
```
This will forward port 5901 on your host machine to port 5901 on the pod that runs the Kubevirt VM. This allows you to connect to the VM's desktop environment via the noVNC stream via the URL: `http://localhost:3001/vnc/`.
================================================
FILE: services/gateway/main.py
================================================
"""
cyberdesk_api.py
FastAPI micro-gateway that:
1. Serves the noVNC static front-end.
2. Proxies a browser WebSocket to a websockify instance (port 5901) in the VM pod.
3. Creates / deletes *Cyberdesk* custom resources via the Kubernetes API.
4. Proxies terminal commands to the desired VM, via the execDaemon.
Designed to run either:
* Inside a Kubernetes cluster (uses ServiceAccount & in-cluster DNS), **or**
* Locally, picking up ~/.kube/config for dev workflows. Requires manual port-forwarding for VNC.
"""
from __future__ import annotations
import asyncio
import logging
import os
import ssl
from pathlib import Path
from typing import Callable, Awaitable, Optional, List, Any
import httpx
import websockets
from fastapi import (
FastAPI,
WebSocket,
WebSocketDisconnect,
HTTPException,
status,
)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, RedirectResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from kubernetes import client, config
from kubernetes.client import ApiException, CustomObjectsApi, CoreV1Api
from pydantic import BaseModel, Field
from supabase import create_client, Client
from dotenv import load_dotenv
import json
import socket
# --------------------------------------------------------------------------- #
# Logging
# --------------------------------------------------------------------------- #
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s │ %(levelname)-8s │ %(name)s │ %(message)s",
)
LOG = logging.getLogger("cyberdesk")
# --------------------------------------------------------------------------- #
# Constants & Helpers
# --------------------------------------------------------------------------- #
HOURS = 60 * 60 * 1000 # milliseconds per hour
DEFAULT_TIMEOUT_MS: int = 24 * HOURS
CYBERDESK_GROUP = "cyberdesk.io"
CYBERDESK_VERSION = "v1alpha1"
CYBERDESK_PLURAL = "cyberdesks"
CYBERDESK_NAMESPACE = "cyberdesk-system"
VMI_NAMESPACE = "kubevirt"
GATEWAY_SERVICE_NAME = "gateway"
GATEWAY_NAMESPACE = CYBERDESK_NAMESPACE # Namespace where gateway service runs
# Add KubeVirt constants
KUBEVIRT_GROUP = "kubevirt.io"
KUBEVIRT_VERSION = "v1"
KUBEVIRT_VMI_PLURAL = "virtualmachineinstances"
# --------------------------------------------------------------------------- #
# Kubernetes client bootstrap
# --------------------------------------------------------------------------- #
def init_kube_clients() -> tuple[Optional[CustomObjectsApi], Optional[CoreV1Api]]:
"""Attempt to build a Kubernetes CustomObjectsApi, returning *None* on failure."""
try:
config.load_incluster_config()
LOG.info("Loaded in-cluster Kubernetes config")
return client.CustomObjectsApi(), client.CoreV1Api()
except config.ConfigException:
try:
config.load_kube_config()
LOG.info("Loaded local ~/.kube/config")
return client.CustomObjectsApi(), client.CoreV1Api()
except config.ConfigException as exc:
LOG.warning("No Kubernetes configuration available: %s", exc)
return None, None
K8S_CUSTOM_API, K8S_CORE_V1_API = init_kube_clients()
# --------------------------------------------------------------------------- #
# Supabase Client Setup
# --------------------------------------------------------------------------- #
# Get the directory where main.py is located
script_dir = Path(__file__).resolve().parent
# Construct the path to the .env file in that same directory
dotenv_path = script_dir / ".env"
# Load using that specific path
load_dotenv(dotenv_path=dotenv_path, override=True)
SUPABASE_URL: Optional[str] = os.environ.get("SUPABASE_URL")
SUPABASE_KEY: Optional[str] = os.environ.get("SUPABASE_KEY")
SUPABASE_CLIENT: Optional[Client] = None
if SUPABASE_URL and SUPABASE_KEY:
try:
SUPABASE_CLIENT = create_client(SUPABASE_URL, SUPABASE_KEY)
LOG.info("Successfully initialized Supabase client.")
except Exception as e:
LOG.critical(f"Failed to initialize Supabase client: {e}")
# Depending on requirements, you might want to prevent startup
# raise RuntimeError(f"Failed to initialize Supabase client: {e}")
else:
LOG.warning("SUPABASE_URL or SUPABASE_KEY environment variables not set. Supabase integration disabled.")
# --------------------------------------------------------------------------- #
# Pydantic DTOs
# --------------------------------------------------------------------------- #
class CyberdeskCreateRequest(BaseModel):
"""Payload for POST /cyberdesk/{vm_id}"""
timeout_ms: int = Field(
default=DEFAULT_TIMEOUT_MS, alias="timeoutMs", ge=60_000
)
class CommandRequest(BaseModel):
"""Payload for POST /cyberdesk/{vm_id}/execute-command"""
command: str
# --- Response Models ---
class CyberdeskCreateResponse(BaseModel):
"""Response for POST /cyberdesk/{vm_id}"""
id: str
class StatusMessageResponse(BaseModel):
"""Generic response model for status and message."""
status: str
message: str
class CyberdeskReadyResponse(StatusMessageResponse):
"""Response for POST /cyberdesk/{vm_id}/ready"""
stream_url: str
class VMCommandExecutionResponse(BaseModel):
"""Schema for the response received *from* the VM's /execute-command endpoint."""
args: List[str]
return_code: int
stdout: str
stderr: str
duration_s: float
class GatewayCommandResponse(BaseModel):
"""Response for POST /cyberdesk/{vm_id}/execute-command"""
status: str
vm_status_code: int
vm_response: VMCommandExecutionResponse
class HealthCheckResponse(BaseModel):
"""Response for GET /healthz"""
status: str
class VmHealthCheckResponse(BaseModel):
"""Response for GET /vm/healthcheck/{vmid}"""
status: str # Status reported by the VM's health endpoint
vm_status_code: int # The HTTP status code received from the VM
# --------------------------------------------------------------------------- #
# FastAPI application
# --------------------------------------------------------------------------- #
app = FastAPI(title="Cyberdesk API Gateway", version="1.0")
# Optional: allow the browser UI to be hosted from another domain.
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("CYBERDESK_CORS_ALLOW_ORIGINS", "*").split(","),
allow_methods=["*"],
allow_headers=["*"],
)
# Static noVNC artefacts live relative to this file.
BASE_DIR = Path(__file__).resolve().parent
NOVNC_DIR = BASE_DIR / "noVNC"
app.mount("/static", StaticFiles(directory=NOVNC_DIR), name="static")
# --------------------------------------------------------------------------- #
# Routes – HTML / root
# --------------------------------------------------------------------------- #
@app.get("/vnc/{vm_id}", include_in_schema=False)
async def serve_novnc(vm_id: str) -> FileResponse:
"""
Serve the main noVNC HTML page.
The client-side JavaScript will subsequently establish a WebSocket
back to `/vnc/ws/{vm_id}`.
"""
return FileResponse(NOVNC_DIR / "vnc.html")
# --------------------------------------------------------------------------- #
# WebSocket proxy
# --------------------------------------------------------------------------- #
async def _relay(
recv: Callable[[], Awaitable[bytes]],
send: Callable[[bytes], Awaitable[None]],
) -> None:
"""
Copy bytes from *recv* to *send* until EOF or a normal WebSocket shutdown.
"""
try:
while True:
await send(await recv())
except (WebSocketDisconnect, websockets.exceptions.ConnectionClosed):
# A graceful close on either side ends the task.
return
@app.websocket("/vnc/ws/{vm_id}")
async def proxy_vnc(websocket: WebSocket, vm_id: str) -> None:
"""Proxy WebSocket to the target VMI's VNC port.
Connects via VMI IP if running in-cluster.
Connects via host.docker.internal:5901 if running locally
(requires manual `kubectl port-forward pod/ 5901:5901`).
"""
await websocket.accept()
LOG.info("VNC WebSocket opened for instance ID: %s", vm_id)
target_uri: Optional[str] = None
target_port = 5901 # Standard VNC port
try:
# --- Step 1: Determine Environment ---
token_path = Path("/var/run/secrets/kubernetes.io/serviceaccount/token")
in_cluster = token_path.exists()
LOG.info(f"Running in-cluster: {in_cluster}")
# --- Step 2: Get CR & VMI for validation (and IP if in-cluster) ---
k8s_custom = require_k8s() # Ensure K8S client is available
vm_name: Optional[str] = None
vmi_ip: Optional[str] = None
try:
cr = k8s_custom.get_namespaced_custom_object(
group=CYBERDESK_GROUP,
version=CYBERDESK_VERSION,
namespace=CYBERDESK_NAMESPACE,
plural=CYBERDESK_PLURAL,
name=vm_id,
)
vm_name = cr.get("status", {}).get("cyberdesk_create", {}).get("virtualMachineRef")
if not vm_name:
raise ValueError(f"virtualMachineRef not found in status for Cyberdesk {vm_id}")
LOG.info("Found virtualMachineRef '%s' for instance %s", vm_name, vm_id)
except ApiException as e:
if e.status == 404:
raise ValueError(f"Cyberdesk CR '{vm_id}' not found.") from e
else:
raise ValueError(f"API Error fetching Cyberdesk CR '{vm_id}': {e.reason}") from e
except ValueError as e:
LOG.error(str(e))
await websocket.close(code=1011, reason=str(e))
return
try:
vmi = k8s_custom.get_namespaced_custom_object(
group=KUBEVIRT_GROUP,
version=KUBEVIRT_VERSION,
namespace=VMI_NAMESPACE,
plural=KUBEVIRT_VMI_PLURAL,
name=vm_name,
)
interfaces = vmi.get('status', {}).get('interfaces', [])
vmi_ip = interfaces[0].get('ipAddress') if interfaces else None
vmi_phase = vmi.get('status', {}).get('phase')
if vmi_phase != 'Running':
raise ValueError(f"Target VMI '{vm_name}' is not Running (phase: {vmi_phase}).")
if in_cluster and not vmi_ip:
# Only strictly need IP if in-cluster
raise ValueError(f"Target VMI '{vm_name}' is Running but has no IP address (needed for in-cluster connection)." )
LOG.info("Target VMI '%s' is Running. IP: %s", vm_name, vmi_ip if vmi_ip else "N/A (local)")
except ApiException as e:
if e.status == 404:
raise ValueError(f"VMI '{vm_name}' not found.") from e
else:
raise ValueError(f"API Error fetching VMI '{vm_name}': {e.reason}") from e
except ValueError as e:
LOG.error(str(e))
await websocket.close(code=1011, reason=str(e))
return
# --- Step 3: Determine Target URI based on environment ---
if in_cluster:
if not vmi_ip: # Should have been caught above, but defensive check
raise ValueError("Logic error: In-cluster but VMI IP is missing.")
target_uri = f"ws://{vmi_ip}:{target_port}"
LOG.info("Connecting via VMI IP (in-cluster): %s", target_uri)
else:
# Assume manual port-forward `kubectl port-forward pod/ 5901:5901` is running
target_uri = f"ws://host.docker.internal:{target_port}"
LOG.info("Connecting via Docker host (local): %s (Requires manual port-forward)", target_uri)
# --- Step 4: Establish connection and relay ---
LOG.info("Attempting WebSocket connection to VMI VNC at %s", target_uri)
async with websockets.connect(target_uri, ping_interval=None, open_timeout=10) as vmi_ws:
LOG.info("Successfully connected to VMI VNC at %s", target_uri)
# Start two tasks to relay messages in both directions
consumer_task = asyncio.create_task(
_relay(websocket.receive_bytes, vmi_ws.send)
)
producer_task = asyncio.create_task(
_relay(vmi_ws.recv, websocket.send_bytes) # type: ignore[arg-type] -- websockets.recv() returns Data
)
# Wait for either task to complete (or raise an exception)
done, pending = await asyncio.wait(
[consumer_task, producer_task],
return_when=asyncio.FIRST_COMPLETED,
)
# Cancel pending tasks to clean up resources
for task in pending:
task.cancel()
# Raise exceptions if any task failed
for task in done:
if task.exception():
raise task.exception()
except (websockets.exceptions.ConnectionClosedError, websockets.exceptions.ConnectionClosedOK) as e:
LOG.info("VNC WebSocket connection closed cleanly: %s", e)
except WebSocketDisconnect as e:
LOG.info("Browser WebSocket disconnected: %s", e.code)
except Exception as e:
LOG.error("VNC proxy error: %s", e, exc_info=True)
# Attempt to close the browser WebSocket with an error code
try:
await websocket.close(code=1011, reason=f"Proxy error: {e}")
except RuntimeError: # Handle cases where socket might already be closed
pass
finally:
LOG.info("VNC WebSocket closed for instance ID: %s", vm_id)
# --------------------------------------------------------------------------- #
# Cyberdesk custom-resource endpoints
# --------------------------------------------------------------------------- #
def require_k8s() -> CustomObjectsApi:
"""Return a live CustomObjectsApi or raise 503 HTTPException."""
if K8S_CUSTOM_API is None:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Kubernetes client not configured.",
)
return K8S_CUSTOM_API
def require_k8s_core() -> CoreV1Api:
"""Return a live CoreV1Api or raise 503 HTTPException."""
if K8S_CORE_V1_API is None:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Kubernetes CoreV1Api client not configured.",
)
return K8S_CORE_V1_API
def require_supabase() -> Client:
"""Return a live Supabase client or raise 503 HTTPException."""
if SUPABASE_CLIENT is None:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Supabase client not configured (missing SUPABASE_URL/KEY env vars?).",
)
return SUPABASE_CLIENT
# --- Helper Function to Update Supabase ---
async def update_supabase_instance(vm_id: str, stream_url: str):
"""Updates the Supabase instance entry with stream URL and status."""
supabase_client = require_supabase()
try:
# Run blocking Supabase call in a separate thread
# Assign the entire response object
response = await asyncio.to_thread(
lambda: supabase_client.table("cyberdesk_instances")
.update({"status": "running", "stream_url": stream_url})
.eq("id", vm_id)
.execute()
)
# Log the actual response structure for clarity (can be removed later)
LOG.info(f"Supabase raw response object for {vm_id}: {response}")
# Access the data list via response.data
updated_data = response.data
# Access count via response.count (though not strictly needed for the check here)
# updated_count = response.count
LOG.info(f"Supabase update response for {vm_id}: data={updated_data}, count={getattr(response, 'count', 'N/A')}")
# Check if the update was successful using the actual data list
if isinstance(updated_data, list) and len(updated_data) > 0 and updated_data[0]:
LOG.info(f"Successfully updated Supabase for instance {vm_id}")
return True
else:
# This might happen if the row doesn't exist or based on Supabase return preferences
LOG.warning(f"Supabase update for instance {vm_id} completed, but response data indicates no rows updated or an unexpected format: {updated_data}")
return False
except Exception as e:
LOG.exception(f"Error updating Supabase for instance {vm_id}: {e}")
return False
@app.post(
"/cyberdesk/{vm_id}",
status_code=status.HTTP_201_CREATED,
response_model=CyberdeskCreateResponse
)
async def create_cyberdesk(vm_id: str, payload: CyberdeskCreateRequest):
"""Create a Cyberdesk CR in the cluster."""
api = require_k8s()
body = {
"apiVersion": f"{CYBERDESK_GROUP}/{CYBERDESK_VERSION}",
"kind": "Cyberdesk",
"metadata": {"name": vm_id, "namespace": CYBERDESK_NAMESPACE},
"spec": {"timeoutMs": payload.timeout_ms},
}
try:
resp = await asyncio.to_thread(
api.create_namespaced_custom_object,
group=CYBERDESK_GROUP,
version=CYBERDESK_VERSION,
namespace=CYBERDESK_NAMESPACE,
plural=CYBERDESK_PLURAL,
body=body,
)
return {"id": resp["metadata"]["name"]}
except ApiException as exc:
LOG.error("Kubernetes API error: %s", exc, exc_info=False)
if exc.status == 409:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Already exists") from exc
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=exc.reason) from exc
@app.post(
"/cyberdesk/{vm_id}/stop",
status_code=status.HTTP_200_OK,
response_model=StatusMessageResponse
)
async def stop_cyberdesk(vm_id: str):
"""Delete a Cyberdesk CR from the cluster."""
api = require_k8s()
try:
await asyncio.to_thread(
api.delete_namespaced_custom_object,
group=CYBERDESK_GROUP,
version=CYBERDESK_VERSION,
namespace=CYBERDESK_NAMESPACE,
plural=CYBERDESK_PLURAL,
name=vm_id,
body=client.V1DeleteOptions(),
)
# Return a dictionary matching the response model
return {"status": "success", "message": f"Deletion of '{vm_id}' initiated."}
except ApiException as exc:
LOG.error("Kubernetes API error: %s", exc, exc_info=False)
if exc.status == 404:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found") from exc
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=exc.reason) from exc
# --- NEW Endpoint ---
@app.post(
"/cyberdesk/{vm_id}/ready",
status_code=status.HTTP_200_OK,
response_model=CyberdeskReadyResponse
)
async def cyberdesk_ready(vm_id: str):
"""
Signal that a VM is ready. Updates Supabase with the stream URL.
"""
LOG.info(f"Received ready signal for VM: {vm_id}")
# 2. Construct Stream URL
# Assuming default HTTP port 80 for the gateway service
stream_url = f"https://gateway.cyberdesk.io/vnc/{vm_id}"
LOG.info(f"Constructed stream URL for {vm_id}: {stream_url}")
# 3. Update Supabase
success = await update_supabase_instance(vm_id, stream_url)
if success:
LOG.info(f"Successfully processed ready signal for {vm_id}")
# Return a dictionary matching the response model
return {"status": "success", "message": f"Instance {vm_id} marked as running.", "stream_url": stream_url}
else:
LOG.error(f"Failed to update Supabase for {vm_id} after getting IP.")
# Indicate failure - maybe the instance ID was wrong or DB issue
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update instance status in database for {vm_id}."
)
@app.post(
"/cyberdesk/{vm_id}/execute-command",
response_model=GatewayCommandResponse
)
async def execute_vm_command(vm_id: str, payload: CommandRequest):
"""
Sends a command string to the execute-command endpoint of the specified VM
using the proxy helper.
"""
command_port = 8000
command_path = "execute-command" # Path on the VM service
command_timeout = 30.0 # Longer timeout for potential command execution
command_to_execute = payload.command
# IMPORTANT: Ensure the receiving service expects this JSON structure
request_payload = {"cmd": command_to_execute}
LOG.info(f"Attempting command execution for VM {vm_id}: '{command_to_execute[:80]}...'")
try:
status_code, response_json = await _proxy_request_to_vm(
vmid=vm_id,
port=command_port,
path=command_path,
method="POST",
json_payload=request_payload,
timeout=command_timeout
)
# Check if the VM's command endpoint returned a success status
if 200 <= status_code < 300:
LOG.info(f"Command execution for {vm_id} successful: {status_code}, Response: { response_json}")
return {
"status": "success",
"vm_status_code": status_code,
"vm_response": {
"args": response_json["args"],
"return_code": response_json["return_code"],
"stdout": response_json["stdout"],
"stderr": response_json["stderr"],
"duration_s": response_json["duration_s"]
}
}
else:
# VM is reachable, but the command endpoint returned an error
LOG.error(f"VM {vm_id} command execution failed with status {status_code}.")
raise HTTPException(
status_code=502, # Bad Gateway, as the upstream VM endpoint failed
detail=f"VM {vm_id} command execution failed: {status_code}",
headers={"X-VM-Status-Code": str(status_code)}
)
except HTTPException as e:
# Re-raise known HTTP exceptions from the proxy helper
LOG.error(f"Command execution failed for {vm_id} due to proxy error: {e.status_code} - {e.detail}")
raise e
except Exception as e:
# Catch any other unexpected errors during the process
LOG.exception(f"Unexpected error during command execution processing for {vm_id}: {e}")
raise HTTPException(status_code=500, detail=f"An unexpected error occurred during command execution for VM {vm_id}")
# --------------------------------------------------------------------------- #
# Liveness / readiness
# --------------------------------------------------------------------------- #
@app.get("/healthz", response_model=HealthCheckResponse)
async def health_check():
"""Kubernetes livenessProbe target."""
return {"status": "ok"}
@app.get("/vm/healthcheck/{vmid}", response_model=VmHealthCheckResponse)
async def vm_health_check(vmid: str):
"""
Performs a health check on the specified VM instance using the proxy helper.
"""
health_port = 8000
health_path = "health"
try:
status_code, response_json = await _proxy_request_to_vm(
vmid=vmid,
port=health_port,
path=health_path,
method="GET",
timeout=10.0
)
# Check if the VM's health endpoint returned a success status
if 200 <= status_code < 300:
LOG.info(f"Health check for {vmid} successful: {status_code}")
return {
"status": "ok",
"vm_status_code": status_code
}
else:
# VM is reachable, but reported unhealthy
LOG.warning(f"VM {vmid} health check failed with status {status_code}")
raise HTTPException(
status_code=502, # Bad Gateway, as the upstream VM is unhealthy
detail=f"VM {vmid} health check reported failure: {status_code}",
headers={"X-VM-Status-Code": str(status_code)}
)
except HTTPException as e:
# Re-raise known HTTP exceptions from the proxy helper
# (e.g., 404 if pod not found, 503 if connection failed, 504 timeout)
LOG.error(f"Health check failed for {vmid} due to proxy error: {e.status_code} - {e.detail}")
raise e
except Exception as e:
# Catch any other unexpected errors during the process
LOG.exception(f"Unexpected error during health check processing for {vmid}: {e}")
raise HTTPException(status_code=500, detail=f"An unexpected error occurred during health check for VM {vmid}")
# --- Generic VM Pod Communication Helper ---
async def _proxy_request_to_vm(
vmid: str,
port: int,
path: str,
method: str = "GET",
json_payload: Optional[dict] = None,
timeout: float = 10.0
) -> tuple[int, Any]:
"""
Sends an HTTP request to a specific port/path on the VM pod.
Handles routing via internal DNS (in-cluster) or K8s API proxy (local).
Finds the correct virt-launcher pod name when running locally.
Args:
vmid: The target Virtual Machine ID.
port: The target port on the VM pod.
path: The target URL path on the VM pod (e.g., "health", "execute-command").
method: HTTP method ("GET", "POST", etc.).
json_payload: Optional dictionary to send as JSON body (for POST/PUT).
timeout: Request timeout in seconds.
Returns:
A tuple containing (status_code, response_body). The response_body
will be a parsed JSON object (dict/list) if possible, otherwise raw text.
Raises:
HTTPException: If the request fails due to connection errors, timeouts,
API errors, non-2xx VM responses, or pod lookup issues.
"""
vm_namespace = "kubevirt"
path = path.lstrip('/') # Ensure path doesn't start with /
# Check if running in-cluster
token_path = Path("/var/run/secrets/kubernetes.io/serviceaccount/token")
in_cluster = token_path.exists()
if in_cluster:
# --- In-Cluster Logic (Get VMI IP) ---
LOG.info(f"Proxying {method} to VM {vmid} (in-cluster) via IP lookup -> :{port}/{path}")
k8s_custom = require_k8s()
vm_name: Optional[str] = None
vmi_ip: Optional[str] = None
target_url: Optional[str] = None
try:
# 1. Get CR to find VM name
try:
cr = k8s_custom.get_namespaced_custom_object(
group=CYBERDESK_GROUP,
version=CYBERDESK_VERSION,
namespace=CYBERDESK_NAMESPACE,
plural=CYBERDESK_PLURAL,
name=vmid, # vmid is the instance ID here
)
vm_name = cr.get("status", {}).get("cyberdesk_create", {}).get("virtualMachineRef")
if not vm_name:
raise ValueError(f"virtualMachineRef not found in status for Cyberdesk {vmid}")
LOG.debug(f"Found virtualMachineRef '{vm_name}' for instance {vmid}")
except ApiException as e:
if e.status == 404:
raise ValueError(f"Cyberdesk CR '{vmid}' not found.") from e
else:
raise ValueError(f"API Error fetching Cyberdesk CR '{vmid}': {e.reason}") from e
# 2. Get VMI to find IP address
try:
vmi = k8s_custom.get_namespaced_custom_object(
group=KUBEVIRT_GROUP,
version=KUBEVIRT_VERSION,
namespace=vm_namespace, # Defined earlier in function
plural=KUBEVIRT_VMI_PLURAL,
name=vm_name,
)
interfaces = vmi.get('status', {}).get('interfaces', [])
vmi_ip = interfaces[0].get('ipAddress') if interfaces else None
vmi_phase = vmi.get('status', {}).get('phase')
if vmi_phase != 'Running':
raise ValueError(f"Target VMI '{vm_name}' is not Running (phase: {vmi_phase}).")
if not vmi_ip:
raise ValueError(f"Target VMI '{vm_name}' is Running but has no IP address.")
LOG.debug(f"Found target VMI IP '{vmi_ip}' for VM '{vm_name}'")
except ApiException as e:
if e.status == 404:
raise ValueError(f"VMI '{vm_name}' not found.") from e
else:
raise ValueError(f"API Error fetching VMI '{vm_name}': {e.reason}") from e
# 3. Construct Target URL
target_url = f"http://{vmi_ip}:{port}/{path}"
LOG.debug(f"Target URL (in-cluster, via IP): {target_url}")
except ValueError as e:
# Handle all lookup errors gracefully
LOG.error(f"Failed lookup for VM {vmid} proxy target: {e}")
raise HTTPException(status_code=404, detail=f"Target VM or its resources not found/ready: {e}")
# --- Make Request using IP-based URL ---
async with httpx.AsyncClient(timeout=timeout) as client:
try:
response = await client.request(
method,
target_url,
json=json_payload # httpx handles None payload correctly
)
response.raise_for_status() # Raise exception for 4xx/5xx responses
# Attempt to parse response as JSON
try:
json_response = response.json()
LOG.debug(f"Successfully parsed JSON response from {target_url}")
return response.status_code, json_response
except Exception as json_exc: # Catches JSONDecodeError and others
LOG.warning(f"Failed to parse response from {target_url} as JSON: {json_exc}. Returning raw text.")
return response.status_code, response.text
except httpx.TimeoutException:
LOG.error(f"Timeout connecting to VM {vmid} (in-cluster) at {target_url}")
raise HTTPException(status_code=504, detail=f"Request timed out connecting to VM {vmid}")
except httpx.ConnectError as e:
LOG.error(f"Connection error to VM {vmid} (in-cluster) at {target_url}: {e}")
raise HTTPException(status_code=503, detail=f"Could not connect to VM {vmid} (DNS issue?): {e}")
except Exception as e:
LOG.exception(f"Unexpected error during httpx request to VM {vmid} (in-cluster): {e}")
raise HTTPException(status_code=500, detail=f"Unexpected error connecting to VM {vmid}")
else:
# --- Local Logic (Kubernetes API Proxy) ---
LOG.info(f"Proxying {method} to VM {vmid} (local) via K8s API -> :{port}/{path}")
core_api = require_k8s_core() # Ensures K8s client is loaded
k8s_custom = require_k8s() # Need custom objects API as well
api_client = core_api.api_client # Get the underlying ApiClient
# 1. Find the VM Name from CR
vm_name: Optional[str] = None
try:
cr = k8s_custom.get_namespaced_custom_object(
group=CYBERDESK_GROUP,
version=CYBERDESK_VERSION,
namespace=CYBERDESK_NAMESPACE,
plural=CYBERDESK_PLURAL,
name=vmid,
)
vm_name = cr.get("status", {}).get("cyberdesk_create", {}).get("virtualMachineRef")
if not vm_name:
raise ValueError(f"virtualMachineRef not found in status for Cyberdesk {vmid}")
LOG.debug(f"Found virtualMachineRef '{vm_name}' for instance {vmid}")
except ApiException as e:
if e.status == 404:
raise HTTPException(status_code=404, detail=f"Cyberdesk CR '{vmid}' not found.") from e
else:
raise HTTPException(status_code=500, detail=f"API Error fetching Cyberdesk CR '{vmid}': {e.reason}") from e
except ValueError as e:
LOG.error(str(e))
raise HTTPException(status_code=404, detail=str(e))
# 2. Find the Pod Name using the VM Name by checking annotations
pod_name: Optional[str] = None
running_pod_found = False
try:
LOG.debug(f"Listing pods in namespace '{vm_namespace}' to find one for VM '{vm_name}'.")
# List all pods in the namespace - might need adjustment if too many pods
pod_list_response = await asyncio.to_thread(
core_api.list_namespaced_pod,
namespace=vm_namespace,
_request_timeout=10 # Increase timeout slightly for list operation
)
pods = pod_list_response.items
LOG.debug(f"Found {len(pods)} pods in namespace. Iterating to find match.")
for pod in pods:
annotations = pod.metadata.annotations
pod_domain = annotations.get("kubevirt.io/domain")
# Check if annotation matches the target VM name
if pod_domain == vm_name:
pod_name = pod.metadata.name
pod_phase = pod.status.phase
LOG.debug(f"Found candidate pod '{pod_name}' with matching domain annotation. Phase: {pod_phase}")
if pod_phase == "Running":
LOG.info(f"Found running virt-launcher pod '{pod_name}' for VM '{vm_name}'.")
running_pod_found = True
break # Found the running pod we need
else:
# Found a pod, but it's not running. Keep looking in case
# there's an older non-running one and a newer running one somehow.
LOG.warning(f"Found pod {pod_name} for VM {vm_name}, but phase is {pod_phase}. Continuing search.")
pod_name = None # Reset pod_name if not running
if not running_pod_found:
# If loop finishes and we didn't find a running pod
if pod_name:
# We found a pod but it wasn't running
raise HTTPException(status_code=503, detail=f"VM pod {pod_name} for {vm_name} found but not in Running phase.")
else:
# We didn't find any pod with the matching annotation
LOG.warning(f"No virt-launcher pod found with annotation kubevirt.io/domain={vm_name}")
raise HTTPException(status_code=404, detail=f"VM pod for {vm_name} not found.")
except ApiException as e:
LOG.error(f"K8s API error listing pods for {vm_name}: {e.status} {e.reason}")
raise HTTPException(status_code=500, detail=f"API error listing pods for VM {vm_name}: {e.reason}")
if not pod_name:
# Should be caught above, but defensive check
raise HTTPException(status_code=500, detail="Could not determine pod name")
# 3. Make the Proxied Request (using the found pod_name)
api_proxy_path = f"/api/v1/namespaces/{vm_namespace}/pods/{pod_name}:{port}/proxy/{path}"
LOG.debug(f"Attempting {method} via K8s API proxy path: {api_proxy_path}")
try:
# Prepare arguments for call_api
call_api_args = {
'resource_path': api_proxy_path,
'method': method,
'auth_settings': ['BearerToken'],
'response_type': 'str', # Expect text back
'_request_timeout': timeout
}
header_params = {}
# Set body and Content-Type for methods that have payloads
if json_payload is not None and method in ["POST", "PUT", "PATCH"]:
call_api_args['body'] = json_payload
header_params['Content-Type'] = api_client.select_header_content_type(['application/json'])
call_api_args['header_params'] = header_params
# Run synchronous call_api in thread
response_data = await asyncio.to_thread(
api_client.call_api,
**call_api_args
)
# response_data = (data, status_code, headers)
status_code = response_data[1]
response_text = response_data[0]
LOG.debug(f"K8s API proxy request to VM {vmid} completed with status: {status_code}")
LOG.debug(f"Response data: {response_data}...")
# Attempt to parse response as JSON, fall back to text
try:
corrected_json_string = response_text.replace("'", '"') # Basic, might break
json_response = json.loads(corrected_json_string)
LOG.debug(f"Successfully parsed JSON response from K8s proxy for {vmid}")
return status_code, json_response
except json.JSONDecodeError:
LOG.warning(f"Failed to parse response from K8s proxy for {vmid} as JSON. Returning raw text. {response_text[:100]}...")
return status_code, response_text
except ApiException as e:
LOG.error(f"K8s API error during proxy request to {vmid}: {e.status} {e.reason} - Body: {e.body}")
# Map common K8s API errors during proxying
if e.status == 404:
detail = f"Proxy path not found on pod '{pod_name}' (or pod disappeared)."
http_status = 502 # Treat as bad gateway, pod endpoint issue
elif e.status == 503 or e.status == 504:
detail = f"Could not connect to Pod '{pod_name}' via K8s API proxy: {e.reason}"
http_status = 503
elif e.status == 401 or e.status == 403:
detail = f"Permission denied accessing K8s pod proxy: {e.reason}"
http_status = 500
else:
detail = f"Kubernetes API error during proxy: {e.reason}"
http_status = 500
raise HTTPException(status_code=http_status, detail=detail)
except asyncio.TimeoutError:
LOG.error(f"Timeout during K8s API proxy request to {vmid}")
raise HTTPException(status_code=504, detail=f"Request via K8s API timed out for VM {vmid}")
except Exception as e:
LOG.exception(f"Unexpected error during K8s API proxy request to {vmid}: {e}")
raise HTTPException(status_code=500, detail=f"Unexpected error during proxy request to VM {vmid}")
================================================
FILE: services/gateway/noVNC/.github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Client (please complete the following information):**
- OS: [e.g. iOS]
- Browser: [e.g. chrome, safari]
- Browser version: [e.g. 22]
**Server (please complete the following information):**
- noVNC version: [e.g. 1.0.0 or git commit id]
- VNC server: [e.g. QEMU, TigerVNC]
- WebSocket proxy: [e.g. websockify]
**Additional context**
Add any other context about the problem here.
================================================
FILE: services/gateway/noVNC/.github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Question or discussion
url: https://groups.google.com/forum/?fromgroups#!forum/novnc
about: Ask a question or start a discussion
================================================
FILE: services/gateway/noVNC/.github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: services/gateway/noVNC/.github/workflows/deploy.yml
================================================
name: Publish
on:
push:
pull_request:
release:
types: [published]
jobs:
npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
GITREV=$(git rev-parse --short HEAD)
echo $GITREV
sed -i "s/^\(.*\"version\".*\)\"\([^\"]\+\)\"\(.*\)\$/\1\"\2-g$GITREV\"\3/" package.json
if: github.event_name != 'release'
- uses: actions/setup-node@v4
with:
# Needs to be explicitly specified for auth to work
registry-url: 'https://registry.npmjs.org'
- run: npm install
- uses: actions/upload-artifact@v4
with:
name: npm
path: lib
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
!github.event.release.prerelease
- run: npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
github.event.release.prerelease
- run: npm publish --access public --tag dev
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'push' &&
github.event.ref == 'refs/heads/master'
snap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
GITREV=$(git rev-parse --short HEAD)
echo $GITREV
sed -i "s/^\(.*\"version\".*\)\"\([^\"]\+\)\"\(.*\)\$/\1\"\2-g$GITREV\"\3/" package.json
if: github.event_name != 'release'
- run: |
VERSION=$(grep '"version"' package.json | cut -d '"' -f 4)
echo $VERSION
sed -i "s/^version:.*/version: '$VERSION'/" snap/snapcraft.yaml
- uses: snapcore/action-build@v1
id: snapcraft
- uses: actions/upload-artifact@v4
with:
name: snap
path: ${{ steps.snapcraft.outputs.snap }}
- uses: snapcore/action-publish@v1
with:
snap: ${{ steps.snapcraft.outputs.snap }}
release: stable
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_LOGIN }}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
!github.event.release.prerelease
- uses: snapcore/action-publish@v1
with:
snap: ${{ steps.snapcraft.outputs.snap }}
release: beta
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_LOGIN }}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
github.event.release.prerelease
- uses: snapcore/action-publish@v1
with:
snap: ${{ steps.snapcraft.outputs.snap }}
release: edge
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_LOGIN }}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'push' &&
github.event.ref == 'refs/heads/master'
================================================
FILE: services/gateway/noVNC/.github/workflows/lint.yml
================================================
name: Lint
on: [push, pull_request]
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: npm run lint
html:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate
================================================
FILE: services/gateway/noVNC/.github/workflows/test.yml
================================================
name: Test
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
os:
- ubuntu-latest
- windows-latest
browser:
- ChromeHeadless
- FirefoxHeadless
include:
- os: macos-latest
browser: Safari
- os: windows-latest
browser: EdgeHeadless
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: npm run test
env:
TEST_BROWSER_NAME: ${{ matrix.browser }}
================================================
FILE: services/gateway/noVNC/.github/workflows/translate.yml
================================================
name: Translate
on: [push, pull_request]
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: sudo apt-get install gettext
- run: make -C po update-pot
- run: make -C po update-po
- run: make -C po update-js
================================================
FILE: services/gateway/noVNC/.gitignore
================================================
# *.pyc
# *.o
# tests/data_*.js
# utils/rebind.so
# utils/websockify
# /node_modules
# /build
# /lib
# recordings
# *.swp
# *~
# noVNC-*.tgz
================================================
FILE: services/gateway/noVNC/.gitmodules
================================================
================================================
FILE: services/gateway/noVNC/AUTHORS
================================================
maintainers:
- Samuel Mannehed for Cendio AB (@samhed)
- Pierre Ossman for Cendio AB (@CendioOssman)
maintainersEmeritus:
- Joel Martin (@kanaka)
- Solly Ross (@directxman12)
- @astrand
contributors:
# There are a bunch of people that should be here.
# If you want to be on this list, feel free send a PR
# to add yourself.
- jalf
- NTT corp.
================================================
FILE: services/gateway/noVNC/LICENSE.txt
================================================
noVNC is Copyright (C) 2022 The noVNC authors
(./AUTHORS)
The noVNC core library files are licensed under the MPL 2.0 (Mozilla
Public License 2.0). The noVNC core library is composed of the
Javascript code necessary for full noVNC operation. This includes (but
is not limited to):
core/**/*.js
app/*.js
test/playback.js
The HTML, CSS, font and images files that included with the noVNC
source distibution (or repository) are not considered part of the
noVNC core library and are licensed under more permissive licenses.
The intent is to allow easy integration of noVNC into existing web
sites and web applications.
The HTML, CSS, font and image files are licensed as follows:
*.html : 2-Clause BSD license
app/styles/*.css : 2-Clause BSD license
app/styles/Orbitron* : SIL Open Font License 1.1
(Copyright 2009 Matt McInerney)
app/images/ : Creative Commons Attribution-ShareAlike
http://creativecommons.org/licenses/by-sa/3.0/
Some portions of noVNC are copyright to their individual authors.
Please refer to the individual source files and/or to the noVNC commit
history: https://github.com/novnc/noVNC/commits/master
The are several files and projects that have been incorporated into
the noVNC core library. Here is a list of those files and the original
licenses (all MPL 2.0 compatible):
core/base64.js : MPL 2.0
core/des.js : Various BSD style licenses
vendor/pako/ : MIT
Any other files not mentioned above are typically marked with
a copyright/license header at the top of the file. The default noVNC
license is MPL-2.0.
The following license texts are included:
docs/LICENSE.MPL-2.0
docs/LICENSE.OFL-1.1
docs/LICENSE.BSD-3-Clause (New BSD)
docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD)
vendor/pako/LICENSE (MIT)
Or alternatively the license texts may be found here:
http://www.mozilla.org/MPL/2.0/
http://scripts.sil.org/OFL
http://en.wikipedia.org/wiki/BSD_licenses
https://opensource.org/licenses/MIT
================================================
FILE: services/gateway/noVNC/README.md
================================================
## noVNC: HTML VNC client library and application
[](https://github.com/novnc/noVNC/actions?query=workflow%3ATest)
[](https://github.com/novnc/noVNC/actions?query=workflow%3ALint)
### Description
noVNC is both a HTML VNC client JavaScript library and an application built on
top of that library. noVNC runs well in any modern browser including mobile
browsers (iOS and Android).
*CYBERDESK NOTE: This is a fork of the noVNC project with the following changes:*
- The base href is set to /static/ so the noVNC client assets are served from the /static/ directory.
- The vnc.html and vnc_lite.html files are served from the /static/ directory.
- The WebSocket path is dynamically constructed from the URL path, and autoconnect is enabled by default.
See the ui.js file for relevant changes.
Many companies, projects and products have integrated noVNC including
[OpenStack](http://www.openstack.org),
[OpenNebula](http://opennebula.org/),
[LibVNCServer](http://libvncserver.sourceforge.net), and
[ThinLinc](https://cendio.com/thinlinc). See
[the Projects and companies wiki page](https://github.com/novnc/noVNC/wiki/Projects-and-companies-using-noVNC)
for a more complete list with additional info and links.
### Table of contents
- [News/help/contact](#newshelpcontact)
- [Features](#features)
- [Screenshots](#screenshots)
- [Browser requirements](#browser-requirements)
- [Server requirements](#server-requirements)
- [Quick start](#quick-start)
- [Installation from snap package](#installation-from-snap-package)
- [Integration and deployment](#integration-and-deployment)
- [Authors/Contributors](#authorscontributors)
### News/help/contact
The project website is found at [novnc.com](http://novnc.com).
If you are a noVNC developer/integrator/user (or want to be) please join the
[noVNC discussion group](https://groups.google.com/forum/?fromgroups#!forum/novnc).
Bugs and feature requests can be submitted via
[github issues](https://github.com/novnc/noVNC/issues). If you have questions
about using noVNC then please first use the
[discussion group](https://groups.google.com/forum/?fromgroups#!forum/novnc).
We also have a [wiki](https://github.com/novnc/noVNC/wiki/) with lots of
helpful information.
If you are looking for a place to start contributing to noVNC, a good place to
start would be the issues that are marked as
["patchwelcome"](https://github.com/novnc/noVNC/issues?labels=patchwelcome).
Please check our
[contribution guide](https://github.com/novnc/noVNC/wiki/Contributing) though.
If you want to show appreciation for noVNC you could donate to a great non-
profits such as:
[Compassion International](http://www.compassion.com/),
[SIL](http://www.sil.org),
[Habitat for Humanity](http://www.habitat.org),
[Electronic Frontier Foundation](https://www.eff.org/),
[Against Malaria Foundation](http://www.againstmalaria.com/),
[Nothing But Nets](http://www.nothingbutnets.net/), etc.
### Features
* Supports all modern browsers including mobile (iOS, Android)
* Supported authentication methods: none, classical VNC, RealVNC's
RSA-AES, Tight, VeNCrypt Plain, XVP, Apple's Diffie-Hellman,
UltraVNC's MSLogonII
* Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG,
ZRLE, JPEG, Zlib, H.264
* Supports scaling, clipping and resizing the desktop
* Supports back & forward mouse buttons
* Local cursor rendering
* Clipboard copy/paste with full Unicode support
* Translations
* Touch gestures for emulating common mouse actions
* Licensed mainly under the [MPL 2.0](http://www.mozilla.org/MPL/2.0/), see
[the license document](LICENSE.txt) for details
### Screenshots
Running in Firefox before and after connecting:
See more screenshots
[here](http://novnc.com/screenshots.html).
### Browser requirements
noVNC uses many modern web technologies so a formal requirement list is
not available. However these are the minimum versions we are currently
aware of:
* Chrome 89, Firefox 89, Safari 15, Opera 75, Edge 89
### Server requirements
noVNC follows the standard VNC protocol, but unlike other VNC clients it does
require WebSockets support. Many servers include support (e.g.
[x11vnc/libvncserver](http://libvncserver.sourceforge.net/),
[QEMU](http://www.qemu.org/), and
[MobileVNC](http://www.smartlab.at/mobilevnc/)), but for the others you need to
use a WebSockets to TCP socket proxy. noVNC has a sister project
[websockify](https://github.com/novnc/websockify) that provides a simple such
proxy.
### Quick start
* Use the `novnc_proxy` script to automatically download and start websockify, which
includes a mini-webserver and the WebSockets proxy. The `--vnc` option is
used to specify the location of a running VNC server:
`./utils/novnc_proxy --vnc localhost:5901`
* If you don't need to expose the web server to public internet, you can
bind to localhost:
`./utils/novnc_proxy --vnc localhost:5901 --listen localhost:6081`
* Point your browser to the cut-and-paste URL that is output by the `novnc_proxy`
script. Hit the Connect button, enter a password if the VNC server has one
configured, and enjoy!
### Installation from snap package
Running the command below will install the latest release of noVNC from snap:
`sudo snap install novnc`
#### Running noVNC from snap directly
You can run the snap package installed novnc directly with, for example:
`novnc --listen 6081 --vnc localhost:5901 # /snap/bin/novnc if /snap/bin is not in your PATH`
If you want to use certificate files, due to standard snap confinement restrictions you need to have them in the /home/\/snap/novnc/current/ directory. If your username is jsmith an example command would be:
`novnc --listen 8443 --cert ~jsmith/snap/novnc/current/self.crt --key ~jsmith/snap/novnc/current/self.key --vnc ubuntu.example.com:5901`
#### Running noVNC from snap as a service (daemon)
The snap package also has the capability to run a 'novnc' service which can be
configured to listen on multiple ports connecting to multiple VNC servers
(effectively a service running multiple instances of novnc).
Instructions (with example values):
List current services (out-of-box this will be blank):
```
sudo snap get novnc services
Key Value
services.n6080 {...}
services.n6081 {...}
```
Create a new service that listens on port 6082 and connects to the VNC server
running on port 5902 on localhost:
`sudo snap set novnc services.n6082.listen=6082 services.n6082.vnc=localhost:5902`
(Any services you define with 'snap set' will be automatically started)
Note that the name of the service, 'n6082' in this example, can be anything
as long as it doesn't start with a number or contain spaces/special characters.
View the configuration of the service just created:
```
sudo snap get novnc services.n6082
Key Value
services.n6082.listen 6082
services.n6082.vnc localhost:5902
```
Disable a service (note that because of a limitation in snap it's currently not
possible to unset config variables, setting them to blank values is the way
to disable a service):
`sudo snap set novnc services.n6082.listen='' services.n6082.vnc=''`
(Any services you set to blank with 'snap set' like this will be automatically stopped)
Verify that the service is disabled (blank values):
```
sudo snap get novnc services.n6082
Key Value
services.n6082.listen
services.n6082.vnc
```
### Integration and deployment
Please see our other documents for how to integrate noVNC in your own software,
or deploying the noVNC application in production environments:
* [Embedding](docs/EMBEDDING.md) - For the noVNC application
* [Library](docs/LIBRARY.md) - For the noVNC JavaScript library
### Authors/Contributors
See [AUTHORS](AUTHORS) for a (full-ish) list of authors. If you're not on
that list and you think you should be, feel free to send a PR to fix that.
* Core team:
* [Samuel Mannehed](https://github.com/samhed) (Cendio)
* [Pierre Ossman](https://github.com/CendioOssman) (Cendio)
* Previous core contributors:
* [Joel Martin](https://github.com/kanaka) (Project founder)
* [Solly Ross](https://github.com/DirectXMan12) (Red Hat / OpenStack)
* Notable contributions:
* UI and icons : Pierre Ossman, Chris Gordon
* Original logo : Michael Sersen
* tight encoding : Michael Tinglof (Mercuri.ca)
* RealVNC RSA AES authentication : USTC Vlab Team
* Included libraries:
* base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net)
* DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs)
* Pako : Vitaly Puzrin (https://github.com/nodeca/pako)
Do you want to be on this list? Check out our
[contribution guide](https://github.com/novnc/noVNC/wiki/Contributing) and
start hacking!
================================================
FILE: services/gateway/noVNC/app/error-handler.js
================================================
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
// Fallback for all uncaught errors
function handleError(event, err) {
try {
const msg = document.getElementById('noVNC_fallback_errormsg');
// Work around Firefox bug:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1685038
if (event.message === "ResizeObserver loop completed with undelivered notifications.") {
return false;
}
// Only show the initial error
if (msg.hasChildNodes()) {
return false;
}
let div = document.createElement("div");
div.classList.add('noVNC_message');
div.appendChild(document.createTextNode(event.message));
msg.appendChild(div);
if (event.filename) {
div = document.createElement("div");
div.className = 'noVNC_location';
let text = event.filename;
if (event.lineno !== undefined) {
text += ":" + event.lineno;
if (event.colno !== undefined) {
text += ":" + event.colno;
}
}
div.appendChild(document.createTextNode(text));
msg.appendChild(div);
}
if (err && err.stack) {
div = document.createElement("div");
div.className = 'noVNC_stack';
div.appendChild(document.createTextNode(err.stack));
msg.appendChild(div);
}
document.getElementById('noVNC_fallback_error')
.classList.add("noVNC_open");
} catch (exc) {
document.write("noVNC encountered an error.");
}
// Try to disable keyboard interaction, best effort
try {
// Remove focus from the currently focused element in order to
// prevent keyboard interaction from continuing
if (document.activeElement) { document.activeElement.blur(); }
// Don't let any element be focusable when showing the error
let keyboardFocusable = 'a[href], button, input, textarea, select, details, [tabindex]';
document.querySelectorAll(keyboardFocusable).forEach((elem) => {
elem.setAttribute("tabindex", "-1");
});
} catch (exc) {
// Do nothing
}
// Don't return true since this would prevent the error
// from being printed to the browser console.
return false;
}
window.addEventListener('error', evt => handleError(evt, evt.error));
window.addEventListener('unhandledrejection', evt => handleError(evt.reason, evt.reason));
================================================
FILE: services/gateway/noVNC/app/images/icons/Makefile
================================================
BROWSER_SIZES := 16 24 32 48 64
#ANDROID_SIZES := 72 96 144 192
# FIXME: The ICO is limited to 8 icons due to a Chrome bug:
# https://bugs.chromium.org/p/chromium/issues/detail?id=1381393
ANDROID_SIZES := 96 144 192
WEB_ICON_SIZES := $(BROWSER_SIZES) $(ANDROID_SIZES)
#IOS_1X_SIZES := 20 29 40 76 # No such devices exist anymore
IOS_2X_SIZES := 40 58 80 120 152 167
IOS_3X_SIZES := 60 87 120 180
ALL_IOS_SIZES := $(IOS_1X_SIZES) $(IOS_2X_SIZES) $(IOS_3X_SIZES)
ALL_ICONS := \
$(ALL_IOS_SIZES:%=novnc-ios-%.png) \
novnc.ico
all: $(ALL_ICONS)
# Our testing shows that the ICO file need to be sorted in largest to
# smallest to get the apporpriate behviour
WEB_ICON_SIZES_REVERSE := $(shell echo $(WEB_ICON_SIZES) | tr ' ' '\n' | sort -nr | tr '\n' ' ')
WEB_BASE_ICONS := $(WEB_ICON_SIZES_REVERSE:%=novnc-%.png)
.INTERMEDIATE: $(WEB_BASE_ICONS)
novnc.ico: $(WEB_BASE_ICONS)
convert $(WEB_BASE_ICONS) "$@"
# General conversion
novnc-%.png: novnc-icon.svg
convert -depth 8 -background transparent \
-size $*x$* "$(lastword $^)" "$@"
# iOS icons use their own SVG
novnc-ios-%.png: novnc-ios-icon.svg
convert -depth 8 -background transparent \
-size $*x$* "$(lastword $^)" "$@"
# The smallest sizes are generated using a different SVG
novnc-16.png novnc-24.png novnc-32.png: novnc-icon-sm.svg
clean:
rm -f *.png
================================================
FILE: services/gateway/noVNC/app/locale/README
================================================
DO NOT MODIFY THE FILES IN THIS FOLDER, THEY ARE AUTOMATICALLY GENERATED FROM THE PO-FILES.
================================================
FILE: services/gateway/noVNC/app/locale/cs.json
================================================
{
"Connecting...": "Připojení...",
"Disconnecting...": "Odpojení...",
"Reconnecting...": "Obnova připojení...",
"Internal error": "Vnitřní chyba",
"Must set host": "Hostitel musí být nastavení",
"Connected (encrypted) to ": "Připojení (šifrované) k ",
"Connected (unencrypted) to ": "Připojení (nešifrované) k ",
"Something went wrong, connection is closed": "Něco se pokazilo, odpojeno",
"Failed to connect to server": "Chyba připojení k serveru",
"Disconnected": "Odpojeno",
"New connection has been rejected with reason: ": "Nové připojení bylo odmítnuto s odůvodněním: ",
"New connection has been rejected": "Nové připojení bylo odmítnuto",
"Password is required": "Je vyžadováno heslo",
"noVNC encountered an error:": "noVNC narazilo na chybu:",
"Hide/Show the control bar": "Skrýt/zobrazit ovládací panel",
"Move/Drag viewport": "Přesunout/přetáhnout výřez",
"viewport drag": "přesun výřezu",
"Active Mouse Button": "Aktivní tlačítka myši",
"No mousebutton": "Žádné",
"Left mousebutton": "Levé tlačítko myši",
"Middle mousebutton": "Prostřední tlačítko myši",
"Right mousebutton": "Pravé tlačítko myši",
"Keyboard": "Klávesnice",
"Show keyboard": "Zobrazit klávesnici",
"Extra keys": "Extra klávesy",
"Show extra keys": "Zobrazit extra klávesy",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Přepnout Ctrl",
"Alt": "Alt",
"Toggle Alt": "Přepnout Alt",
"Send Tab": "Odeslat tabulátor",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Odeslat Esc",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Poslat Ctrl-Alt-Del",
"Shutdown/Reboot": "Vypnutí/Restart",
"Shutdown/Reboot...": "Vypnutí/Restart...",
"Power": "Napájení",
"Shutdown": "Vypnout",
"Reboot": "Restart",
"Reset": "Reset",
"Clipboard": "Schránka",
"Clear": "Vymazat",
"Fullscreen": "Celá obrazovka",
"Settings": "Nastavení",
"Shared mode": "Sdílený režim",
"View only": "Pouze prohlížení",
"Clip to window": "Přizpůsobit oknu",
"Scaling mode:": "Přizpůsobení velikosti",
"None": "Žádné",
"Local scaling": "Místní",
"Remote resizing": "Vzdálené",
"Advanced": "Pokročilé",
"Repeater ID:": "ID opakovače",
"WebSocket": "WebSocket",
"Encrypt": "Šifrování:",
"Host:": "Hostitel:",
"Port:": "Port:",
"Path:": "Cesta",
"Automatic reconnect": "Automatická obnova připojení",
"Reconnect delay (ms):": "Zpoždění připojení (ms)",
"Show dot when no cursor": "Tečka místo chybějícího kurzoru myši",
"Logging:": "Logování:",
"Disconnect": "Odpojit",
"Connect": "Připojit",
"Password:": "Heslo",
"Send Password": "Odeslat heslo",
"Cancel": "Zrušit"
}
================================================
FILE: services/gateway/noVNC/app/locale/de.json
================================================
{
"Connecting...": "Verbinden...",
"Disconnecting...": "Verbindung trennen...",
"Reconnecting...": "Verbindung wiederherstellen...",
"Internal error": "Interner Fehler",
"Must set host": "Richten Sie den Server ein",
"Connected (encrypted) to ": "Verbunden mit (verschlüsselt) ",
"Connected (unencrypted) to ": "Verbunden mit (unverschlüsselt) ",
"Something went wrong, connection is closed": "Etwas lief schief, Verbindung wurde getrennt",
"Disconnected": "Verbindung zum Server getrennt",
"New connection has been rejected with reason: ": "Verbindung wurde aus folgendem Grund abgelehnt: ",
"New connection has been rejected": "Verbindung wurde abgelehnt",
"Password is required": "Passwort ist erforderlich",
"noVNC encountered an error:": "Ein Fehler ist aufgetreten:",
"Hide/Show the control bar": "Kontrollleiste verstecken/anzeigen",
"Move/Drag viewport": "Ansichtsfenster verschieben/ziehen",
"viewport drag": "Ansichtsfenster ziehen",
"Active Mouse Button": "Aktive Maustaste",
"No mousebutton": "Keine Maustaste",
"Left mousebutton": "Linke Maustaste",
"Middle mousebutton": "Mittlere Maustaste",
"Right mousebutton": "Rechte Maustaste",
"Keyboard": "Tastatur",
"Show keyboard": "Tastatur anzeigen",
"Extra keys": "Zusatztasten",
"Show extra keys": "Zusatztasten anzeigen",
"Ctrl": "Strg",
"Toggle Ctrl": "Strg umschalten",
"Alt": "Alt",
"Toggle Alt": "Alt umschalten",
"Send Tab": "Tab senden",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Escape senden",
"Ctrl+Alt+Del": "Strg+Alt+Entf",
"Send Ctrl-Alt-Del": "Strg+Alt+Entf senden",
"Shutdown/Reboot": "Herunterfahren/Neustarten",
"Shutdown/Reboot...": "Herunterfahren/Neustarten...",
"Power": "Energie",
"Shutdown": "Herunterfahren",
"Reboot": "Neustarten",
"Reset": "Zurücksetzen",
"Clipboard": "Zwischenablage",
"Clear": "Löschen",
"Fullscreen": "Vollbild",
"Settings": "Einstellungen",
"Shared mode": "Geteilter Modus",
"View only": "Nur betrachten",
"Clip to window": "Auf Fenster begrenzen",
"Scaling mode:": "Skalierungsmodus:",
"None": "Keiner",
"Local scaling": "Lokales skalieren",
"Remote resizing": "Serverseitiges skalieren",
"Advanced": "Erweitert",
"Repeater ID:": "Repeater ID:",
"WebSocket": "WebSocket",
"Encrypt": "Verschlüsselt",
"Host:": "Server:",
"Port:": "Port:",
"Path:": "Pfad:",
"Automatic reconnect": "Automatisch wiederverbinden",
"Reconnect delay (ms):": "Wiederverbindungsverzögerung (ms):",
"Logging:": "Protokollierung:",
"Disconnect": "Verbindung trennen",
"Connect": "Verbinden",
"Password:": "Passwort:",
"Cancel": "Abbrechen",
"Canvas not supported.": "Canvas nicht unterstützt.",
"Disconnect timeout": "Zeitüberschreitung beim Trennen",
"Local Downscaling": "Lokales herunterskalieren",
"Local Cursor": "Lokaler Mauszeiger",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstützt",
"True Color": "True Color"
}
================================================
FILE: services/gateway/noVNC/app/locale/el.json
================================================
{
"HTTPS is required for full functionality": "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα",
"Connecting...": "Συνδέεται...",
"Disconnecting...": "Aποσυνδέεται...",
"Reconnecting...": "Επανασυνδέεται...",
"Internal error": "Εσωτερικό σφάλμα",
"Must set host": "Πρέπει να οριστεί ο διακομιστής",
"Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ",
"Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ",
"Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε",
"Failed to connect to server": "Αποτυχία στη σύνδεση με το διακομιστή",
"Disconnected": "Αποσυνδέθηκε",
"New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ",
"New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ",
"Credentials are required": "Απαιτούνται διαπιστευτήρια",
"noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:",
"Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου",
"Drag": "Σύρσιμο",
"Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου",
"Keyboard": "Πληκτρολόγιο",
"Show Keyboard": "Εμφάνιση Πληκτρολογίου",
"Extra keys": "Επιπλέον πλήκτρα",
"Show Extra Keys": "Εμφάνιση Επιπλέον Πλήκτρων",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Εναλλαγή Ctrl",
"Alt": "Alt",
"Toggle Alt": "Εναλλαγή Alt",
"Toggle Windows": "Εναλλαγή Παράθυρων",
"Windows": "Παράθυρα",
"Send Tab": "Αποστολή Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Αποστολή Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Αποστολή Ctrl-Alt-Del",
"Shutdown/Reboot": "Κλείσιμο/Επανεκκίνηση",
"Shutdown/Reboot...": "Κλείσιμο/Επανεκκίνηση...",
"Power": "Απενεργοποίηση",
"Shutdown": "Κλείσιμο",
"Reboot": "Επανεκκίνηση",
"Reset": "Επαναφορά",
"Clipboard": "Πρόχειρο",
"Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.",
"Full Screen": "Πλήρης Οθόνη",
"Settings": "Ρυθμίσεις",
"Shared Mode": "Κοινόχρηστη Λειτουργία",
"View Only": "Μόνο Θέαση",
"Clip to Window": "Αποκοπή στο όριο του Παράθυρου",
"Scaling Mode:": "Λειτουργία Κλιμάκωσης:",
"None": "Καμία",
"Local Scaling": "Τοπική Κλιμάκωση",
"Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους",
"Advanced": "Για προχωρημένους",
"Quality:": "Ποιότητα:",
"Compression level:": "Επίπεδο συμπίεσης:",
"Repeater ID:": "Repeater ID:",
"WebSocket": "WebSocket",
"Encrypt": "Κρυπτογράφηση",
"Host:": "Όνομα διακομιστή:",
"Port:": "Πόρτα διακομιστή:",
"Path:": "Διαδρομή:",
"Automatic Reconnect": "Αυτόματη επανασύνδεση",
"Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):",
"Show Dot when No Cursor": "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας",
"Logging:": "Καταγραφή:",
"Version:": "Έκδοση:",
"Disconnect": "Αποσύνδεση",
"Connect": "Σύνδεση",
"Server identity": "Ταυτότητα Διακομιστή",
"The server has provided the following identifying information:": "Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:",
"Fingerprint:": "Δακτυλικό αποτύπωμα:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \"Αποδοχή\". Αλλιώς πιέστε \"Απόρριψη\".",
"Approve": "Αποδοχή",
"Reject": "Απόρριψη",
"Credentials": "Διαπιστευτήρια",
"Username:": "Κωδικός Χρήστη:",
"Password:": "Κωδικός Πρόσβασης:",
"Send Credentials": "Αποστολή Διαπιστευτηρίων",
"Cancel": "Ακύρωση",
"Password is required": "Απαιτείται ο κωδικός πρόσβασης",
"viewport drag": "σύρσιμο θεατού πεδίου",
"Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού",
"No mousebutton": "Χωρίς Πλήκτρο Ποντικιού",
"Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού",
"Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού",
"Right mousebutton": "Δεξί Πλήκτρο Ποντικιού",
"Clear": "Καθάρισμα",
"Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas",
"Disconnect timeout": "Παρέλευση χρονικού ορίου αποσύνδεσης",
"Local Downscaling": "Τοπική Συρρίκνωση",
"Local Cursor": "Τοπικός Δρομέας",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Εφαρμογή λειτουργίας αποκοπής αφού δεν υποστηρίζονται οι λωρίδες κύλισης σε πλήρη οθόνη στον IE",
"True Color": "Πραγματικά Χρώματα",
"Style:": "Στυλ:",
"default": "προεπιλεγμένο",
"Apply": "Εφαρμογή",
"Connection": "Σύνδεση",
"Token:": "Διακριτικό:",
"Send Password": "Αποστολή Κωδικού Πρόσβασης"
}
================================================
FILE: services/gateway/noVNC/app/locale/es.json
================================================
{
"Connecting...": "Conectando...",
"Connected (encrypted) to ": "Conectado (con encriptación) a",
"Connected (unencrypted) to ": "Conectado (sin encriptación) a",
"Disconnecting...": "Desconectando...",
"Disconnected": "Desconectado",
"Must set host": "Se debe configurar el host",
"Reconnecting...": "Reconectando...",
"Password is required": "La contraseña es obligatoria",
"Disconnect timeout": "Tiempo de desconexión agotado",
"noVNC encountered an error:": "noVNC ha encontrado un error:",
"Hide/Show the control bar": "Ocultar/Mostrar la barra de control",
"Move/Drag viewport": "Mover/Arrastrar la ventana",
"viewport drag": "Arrastrar la ventana",
"Active Mouse Button": "Botón activo del ratón",
"No mousebutton": "Ningún botón del ratón",
"Left mousebutton": "Botón izquierdo del ratón",
"Middle mousebutton": "Botón central del ratón",
"Right mousebutton": "Botón derecho del ratón",
"Keyboard": "Teclado",
"Show keyboard": "Mostrar teclado",
"Extra keys": "Teclas adicionales",
"Show Extra Keys": "Mostrar Teclas Adicionales",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Pulsar/Soltar Ctrl",
"Alt": "Alt",
"Toggle Alt": "Pulsar/Soltar Alt",
"Send Tab": "Enviar Tabulación",
"Tab": "Tabulación",
"Esc": "Esc",
"Send Escape": "Enviar Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Enviar Ctrl+Alt+Del",
"Shutdown/Reboot": "Apagar/Reiniciar",
"Shutdown/Reboot...": "Apagar/Reiniciar...",
"Power": "Encender",
"Shutdown": "Apagar",
"Reboot": "Reiniciar",
"Reset": "Restablecer",
"Clipboard": "Portapapeles",
"Clear": "Vaciar",
"Fullscreen": "Pantalla Completa",
"Settings": "Configuraciones",
"Encrypt": "Encriptar",
"Shared Mode": "Modo Compartido",
"View only": "Solo visualización",
"Clip to window": "Recortar al tamaño de la ventana",
"Scaling mode:": "Modo de escalado:",
"None": "Ninguno",
"Local Scaling": "Escalado Local",
"Local Downscaling": "Reducción de escala local",
"Remote resizing": "Cambio de tamaño remoto",
"Advanced": "Avanzado",
"Local Cursor": "Cursor Local",
"Repeater ID:": "ID del Repetidor:",
"WebSocket": "WebSocket",
"Host:": "Host:",
"Port:": "Puerto:",
"Path:": "Ruta:",
"Automatic reconnect": "Reconexión automática",
"Reconnect delay (ms):": "Retraso en la reconexión (ms):",
"Logging:": "Registrando:",
"Disconnect": "Desconectar",
"Connect": "Conectar",
"Password:": "Contraseña:",
"Cancel": "Cancelar",
"Canvas not supported.": "Canvas no soportado."
}
================================================
FILE: services/gateway/noVNC/app/locale/fr.json
================================================
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Lancer sans HTTPS n'est pas recommandé, crashs ou autres problèmes en vue.",
"Connecting...": "En cours de connexion...",
"Disconnecting...": "Déconnexion en cours...",
"Reconnecting...": "Reconnexion en cours...",
"Internal error": "Erreur interne",
"Failed to connect to server: ": "Échec de connexion au serveur ",
"Connected (encrypted) to ": "Connecté (chiffré) à ",
"Connected (unencrypted) to ": "Connecté (non chiffré) à ",
"Something went wrong, connection is closed": "Quelque chose s'est mal passé, la connexion a été fermée",
"Failed to connect to server": "Échec de connexion au serveur",
"Disconnected": "Déconnecté",
"New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec motif : ",
"New connection has been rejected": "Une nouvelle connexion a été rejetée",
"Credentials are required": "Les identifiants sont requis",
"noVNC encountered an error:": "noVNC a rencontré une erreur :",
"Hide/Show the control bar": "Masquer/Afficher la barre de contrôle",
"Drag": "Faire glisser",
"Move/Drag viewport": "Déplacer la fenêtre de visualisation",
"Keyboard": "Clavier",
"Show keyboard": "Afficher le clavier",
"Extra keys": "Touches supplémentaires",
"Show extra keys": "Afficher les touches supplémentaires",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Basculer Ctrl",
"Alt": "Alt",
"Toggle Alt": "Basculer Alt",
"Toggle Windows": "Basculer Windows",
"Windows": "Fenêtre",
"Send Tab": "Envoyer Tab",
"Tab": "Tabulation",
"Esc": "Esc",
"Send Escape": "Envoyer Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Envoyer Ctrl-Alt-Del",
"Shutdown/Reboot": "Arrêter/Redémarrer",
"Shutdown/Reboot...": "Arrêter/Redémarrer...",
"Power": "Alimentation",
"Shutdown": "Arrêter",
"Reboot": "Redémarrer",
"Reset": "Réinitialiser",
"Clipboard": "Presse-papiers",
"Edit clipboard content in the textarea below.": "Editer le contenu du presse-papier dans la zone ci-dessous.",
"Full screen": "Plein écran",
"Settings": "Paramètres",
"Shared mode": "Mode partagé",
"View only": "Afficher uniquement",
"Clip to window": "Ajuster à la fenêtre",
"Scaling mode:": "Mode mise à l'échelle :",
"None": "Aucun",
"Local scaling": "Mise à l'échelle locale",
"Remote resizing": "Redimensionnement à distance",
"Advanced": "Avancé",
"Quality:": "Qualité :",
"Compression level:": "Niveau de compression :",
"Repeater ID:": "ID Répéteur :",
"WebSocket": "WebSocket",
"Encrypt": "Chiffrer",
"Host:": "Hôte :",
"Port:": "Port :",
"Path:": "Chemin :",
"Automatic reconnect": "Reconnecter automatiquement",
"Reconnect delay (ms):": "Délai de reconnexion (ms) :",
"Show dot when no cursor": "Afficher le point lorsqu'il n'y a pas de curseur",
"Logging:": "Se connecter :",
"Version:": "Version :",
"Disconnect": "Déconnecter",
"Connect": "Connecter",
"Server identity": "Identité du serveur",
"The server has provided the following identifying information:": "Le serveur a fourni l'identification suivante :",
"Fingerprint:": "Empreinte digitale :",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "SVP, verifiez que l'information est correcte et pressez \"Accepter\". Sinon pressez \"Refuser\".",
"Approve": "Accepter",
"Reject": "Refuser",
"Credentials": "Envoyer les identifiants",
"Username:": "Nom d'utilisateur :",
"Password:": "Mot de passe :",
"Send credentials": "Envoyer les identifiants",
"Cancel": "Annuler",
"Must set host": "Doit définir l'hôte",
"Clear": "Effacer"
}
================================================
FILE: services/gateway/noVNC/app/locale/it.json
================================================
{
"Connecting...": "Connessione in corso...",
"Disconnecting...": "Disconnessione...",
"Reconnecting...": "Riconnessione...",
"Internal error": "Errore interno",
"Must set host": "Devi impostare l'host",
"Connected (encrypted) to ": "Connesso (crittografato) a ",
"Connected (unencrypted) to ": "Connesso (non crittografato) a",
"Something went wrong, connection is closed": "Qualcosa è andato storto, la connessione è stata chiusa",
"Failed to connect to server": "Impossibile connettersi al server",
"Disconnected": "Disconnesso",
"New connection has been rejected with reason: ": "La nuova connessione è stata rifiutata con motivo: ",
"New connection has been rejected": "La nuova connessione è stata rifiutata",
"Credentials are required": "Le credenziali sono obbligatorie",
"noVNC encountered an error:": "noVNC ha riscontrato un errore:",
"Hide/Show the control bar": "Nascondi/Mostra la barra di controllo",
"Keyboard": "Tastiera",
"Show keyboard": "Mostra tastiera",
"Extra keys": "Tasti Aggiuntivi",
"Show Extra Keys": "Mostra Tasti Aggiuntivi",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Tieni premuto Ctrl",
"Alt": "Alt",
"Toggle Alt": "Tieni premuto Alt",
"Toggle Windows": "Tieni premuto Windows",
"Windows": "Windows",
"Send Tab": "Invia Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Invia Esc",
"Ctrl+Alt+Del": "Ctrl+Alt+Canc",
"Send Ctrl-Alt-Del": "Invia Ctrl-Alt-Canc",
"Shutdown/Reboot": "Spegnimento/Riavvio",
"Shutdown/Reboot...": "Spegnimento/Riavvio...",
"Power": "Alimentazione",
"Shutdown": "Spegnimento",
"Reboot": "Riavvio",
"Reset": "Reset",
"Clipboard": "Clipboard",
"Clear": "Pulisci",
"Fullscreen": "Schermo intero",
"Settings": "Impostazioni",
"Shared mode": "Modalità condivisa",
"View Only": "Sola Visualizzazione",
"Scaling mode:": "Modalità di ridimensionamento:",
"None": "Nessuna",
"Local Scaling": "Ridimensionamento Locale",
"Remote Resizing": "Ridimensionamento Remoto",
"Advanced": "Avanzate",
"Quality:": "Qualità:",
"Compression level:": "Livello Compressione:",
"Repeater ID:": "ID Ripetitore:",
"WebSocket": "WebSocket",
"Encrypt": "Crittografa",
"Host:": "Host:",
"Port:": "Porta:",
"Path:": "Percorso:",
"Automatic Reconnect": "Riconnessione Automatica",
"Reconnect Delay (ms):": "Ritardo Riconnessione (ms):",
"Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore",
"Version:": "Versione:",
"Disconnect": "Disconnetti",
"Connect": "Connetti",
"Username:": "Utente:",
"Password:": "Password:",
"Send Credentials": "Invia Credenziale",
"Cancel": "Annulla"
}
================================================
FILE: services/gateway/noVNC/app/locale/ja.json
================================================
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "HTTPS接続なしで実行することは推奨されません。クラッシュしたりその他の問題が発生したりする可能性があります。",
"Connecting...": "接続しています...",
"Disconnecting...": "切断しています...",
"Reconnecting...": "再接続しています...",
"Internal error": "内部エラー",
"Must set host": "ホストを設定する必要があります",
"Failed to connect to server: ": "サーバーへの接続に失敗しました: ",
"Connected (encrypted) to ": "接続しました (暗号化済み): ",
"Connected (unencrypted) to ": "接続しました (暗号化されていません): ",
"Something went wrong, connection is closed": "問題が発生したため、接続が閉じられました",
"Failed to connect to server": "サーバーへの接続に失敗しました",
"Disconnected": "切断しました",
"New connection has been rejected with reason: ": "新規接続は次の理由で拒否されました: ",
"New connection has been rejected": "新規接続は拒否されました",
"Credentials are required": "資格情報が必要です",
"noVNC encountered an error:": "noVNC でエラーが発生しました:",
"Hide/Show the control bar": "コントロールバーを隠す/表示する",
"Drag": "ドラッグ",
"Move/Drag viewport": "ビューポートを移動/ドラッグ",
"Keyboard": "キーボード",
"Show keyboard": "キーボードを表示",
"Extra keys": "追加キー",
"Show extra keys": "追加キーを表示",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl キーをトグル",
"Alt": "Alt",
"Toggle Alt": "Alt キーをトグル",
"Toggle Windows": "Windows キーをトグル",
"Windows": "Windows",
"Send Tab": "Tab キーを送信",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Escape キーを送信",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del を送信",
"Shutdown/Reboot": "シャットダウン/再起動",
"Shutdown/Reboot...": "シャットダウン/再起動...",
"Power": "電源",
"Shutdown": "シャットダウン",
"Reboot": "再起動",
"Reset": "リセット",
"Clipboard": "クリップボード",
"Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。",
"Full screen": "全画面表示",
"Settings": "設定",
"Shared mode": "共有モード",
"View only": "表示専用",
"Clip to window": "ウィンドウにクリップ",
"Scaling mode:": "スケーリングモード:",
"None": "なし",
"Local scaling": "ローカルでスケーリング",
"Remote resizing": "リモートでリサイズ",
"Advanced": "高度",
"Quality:": "品質:",
"Compression level:": "圧縮レベル:",
"Repeater ID:": "リピーター ID:",
"WebSocket": "WebSocket",
"Encrypt": "暗号化",
"Host:": "ホスト:",
"Port:": "ポート:",
"Path:": "パス:",
"Automatic reconnect": "自動再接続",
"Reconnect delay (ms):": "再接続する遅延 (ミリ秒):",
"Show dot when no cursor": "カーソルがないときにドットを表示する",
"Logging:": "ロギング:",
"Version:": "バージョン:",
"Disconnect": "切断",
"Connect": "接続",
"Server identity": "サーバーの識別情報",
"The server has provided the following identifying information:": "サーバーは以下の識別情報を提供しています:",
"Fingerprint:": "フィンガープリント:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。",
"Approve": "承認",
"Reject": "拒否",
"Credentials": "資格情報",
"Username:": "ユーザー名:",
"Password:": "パスワード:",
"Send credentials": "資格情報を送信",
"Cancel": "キャンセル"
}
================================================
FILE: services/gateway/noVNC/app/locale/ko.json
================================================
{
"Connecting...": "연결중...",
"Disconnecting...": "연결 해제중...",
"Reconnecting...": "재연결중...",
"Internal error": "내부 오류",
"Must set host": "호스트는 설정되어야 합니다.",
"Connected (encrypted) to ": "다음과 (암호화되어) 연결되었습니다:",
"Connected (unencrypted) to ": "다음과 (암호화 없이) 연결되었습니다:",
"Something went wrong, connection is closed": "무언가 잘못되었습니다, 연결이 닫혔습니다.",
"Failed to connect to server": "서버에 연결하지 못했습니다.",
"Disconnected": "연결이 해제되었습니다.",
"New connection has been rejected with reason: ": "새 연결이 다음 이유로 거부되었습니다:",
"New connection has been rejected": "새 연결이 거부되었습니다.",
"Password is required": "비밀번호가 필요합니다.",
"noVNC encountered an error:": "noVNC에 오류가 발생했습니다:",
"Hide/Show the control bar": "컨트롤 바 숨기기/보이기",
"Move/Drag viewport": "움직이기/드래그 뷰포트",
"viewport drag": "뷰포트 드래그",
"Active Mouse Button": "마우스 버튼 활성화",
"No mousebutton": "마우스 버튼 없음",
"Left mousebutton": "왼쪽 마우스 버튼",
"Middle mousebutton": "중간 마우스 버튼",
"Right mousebutton": "오른쪽 마우스 버튼",
"Keyboard": "키보드",
"Show keyboard": "키보드 보이기",
"Extra keys": "기타 키들",
"Show extra keys": "기타 키들 보이기",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl 켜기/끄기",
"Alt": "Alt",
"Toggle Alt": "Alt 켜기/끄기",
"Send Tab": "Tab 보내기",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Esc 보내기",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Ctrl+Alt+Del 보내기",
"Shutdown/Reboot": "셧다운/리붓",
"Shutdown/Reboot...": "셧다운/리붓...",
"Power": "전원",
"Shutdown": "셧다운",
"Reboot": "리붓",
"Reset": "리셋",
"Clipboard": "클립보드",
"Clear": "지우기",
"Fullscreen": "전체화면",
"Settings": "설정",
"Shared mode": "공유 모드",
"View only": "보기 전용",
"Clip to window": "창에 클립",
"Scaling mode:": "스케일링 모드:",
"None": "없음",
"Local scaling": "로컬 스케일링",
"Remote resizing": "원격 크기 조절",
"Advanced": "고급",
"Repeater ID:": "중계 ID",
"WebSocket": "웹소켓",
"Encrypt": "암호화",
"Host:": "호스트:",
"Port:": "포트:",
"Path:": "위치:",
"Automatic reconnect": "자동 재연결",
"Reconnect delay (ms):": "재연결 지연 시간 (ms)",
"Logging:": "로깅",
"Disconnect": "연결 해제",
"Connect": "연결",
"Password:": "비밀번호:",
"Send Password": "비밀번호 전송",
"Cancel": "취소"
}
================================================
FILE: services/gateway/noVNC/app/locale/nl.json
================================================
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Het is niet aan te raden om zonder HTTPS te werken, crashes of andere problemen zijn dan waarschijnlijk.",
"Connecting...": "Aan het verbinden…",
"Disconnecting...": "Bezig om verbinding te verbreken...",
"Reconnecting...": "Opnieuw verbinding maken...",
"Internal error": "Interne fout",
"Failed to connect to server: ": "Verbinding maken met server is mislukt",
"Connected (encrypted) to ": "Verbonden (versleuteld) met ",
"Connected (unencrypted) to ": "Verbonden (onversleuteld) met ",
"Something went wrong, connection is closed": "Er iets fout gelopen, verbinding werd verbroken",
"Failed to connect to server": "Verbinding maken met server is mislukt",
"Disconnected": "Verbinding verbroken",
"New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd met de volgende reden: ",
"New connection has been rejected": "Nieuwe verbinding is geweigerd",
"Credentials are required": "Inloggegevens zijn nodig",
"noVNC encountered an error:": "noVNC heeft een fout bemerkt:",
"Hide/Show the control bar": "Verberg/Toon de bedieningsbalk",
"Drag": "Sleep",
"Move/Drag viewport": "Verplaats/Versleep Kijkvenster",
"Keyboard": "Toetsenbord",
"Show keyboard": "Toon Toetsenbord",
"Extra keys": "Extra toetsen",
"Show extra keys": "Toon Extra Toetsen",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl omschakelen",
"Alt": "Alt",
"Toggle Alt": "Alt omschakelen",
"Toggle Windows": "Vensters omschakelen",
"Windows": "Vensters",
"Send Tab": "Tab Sturen",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Escape Sturen",
"Ctrl+Alt+Del": "Ctrl-Alt-Del",
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del Sturen",
"Shutdown/Reboot": "Uitschakelen/Herstarten",
"Shutdown/Reboot...": "Uitschakelen/Herstarten...",
"Power": "Systeem",
"Shutdown": "Uitschakelen",
"Reboot": "Herstarten",
"Reset": "Resetten",
"Clipboard": "Klembord",
"Edit clipboard content in the textarea below.": "Edit de inhoud van het klembord in het tekstveld hieronder",
"Full screen": "Volledig Scherm",
"Settings": "Instellingen",
"Shared mode": "Gedeelde Modus",
"View only": "Alleen Kijken",
"Clip to window": "Randen buiten venster afsnijden",
"Scaling mode:": "Schaalmodus:",
"None": "Geen",
"Local scaling": "Lokaal Schalen",
"Remote resizing": "Op Afstand Formaat Wijzigen",
"Advanced": "Geavanceerd",
"Quality:": "Kwaliteit:",
"Compression level:": "Compressieniveau:",
"Repeater ID:": "Repeater ID:",
"WebSocket": "WebSocket",
"Encrypt": "Versleutelen",
"Host:": "Host:",
"Port:": "Poort:",
"Path:": "Pad:",
"Automatic reconnect": "Automatisch Opnieuw Verbinden",
"Reconnect delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):",
"Show dot when no cursor": "Geef stip weer indien geen cursor",
"Logging:": "Logmeldingen:",
"Version:": "Versie:",
"Disconnect": "Verbinding verbreken",
"Connect": "Verbinden",
"Server identity": "Serveridentiteit",
"The server has provided the following identifying information:": "De server geeft de volgende identificerende informatie:",
"Fingerprint:": "Vingerafdruk:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Verifieer dat de informatie is correct en druk “OK”. Druk anders op “Afwijzen”.",
"Approve": "OK",
"Reject": "Afwijzen",
"Credentials": "Inloggegevens",
"Username:": "Gebruikersnaam:",
"Password:": "Wachtwoord:",
"Send credentials": "Stuur inloggegevens",
"Cancel": "Annuleren",
"Must set host": "Host moeten worden ingesteld",
"Password is required": "Wachtwoord is vereist",
"viewport drag": "kijkvenster slepen",
"Active Mouse Button": "Actieve Muisknop",
"No mousebutton": "Geen muisknop",
"Left mousebutton": "Linker muisknop",
"Middle mousebutton": "Middelste muisknop",
"Right mousebutton": "Rechter muisknop",
"Clear": "Wissen",
"Send Password": "Verzend Wachtwoord:",
"Disconnect timeout": "Timeout tijdens verbreken van verbinding",
"Local Downscaling": "Lokaal Neerschalen",
"Local Cursor": "Lokale Cursor",
"Canvas not supported.": "Canvas wordt niet ondersteund.",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-modus in IE niet worden ondersteund"
}
================================================
FILE: services/gateway/noVNC/app/locale/pl.json
================================================
{
"Connecting...": "Łączenie...",
"Disconnecting...": "Rozłączanie...",
"Reconnecting...": "Łączenie...",
"Internal error": "Błąd wewnętrzny",
"Must set host": "Host i port są wymagane",
"Connected (encrypted) to ": "Połączenie (szyfrowane) z ",
"Connected (unencrypted) to ": "Połączenie (nieszyfrowane) z ",
"Something went wrong, connection is closed": "Coś poszło źle, połączenie zostało zamknięte",
"Disconnected": "Rozłączony",
"New connection has been rejected with reason: ": "Nowe połączenie zostało odrzucone z powodu: ",
"New connection has been rejected": "Nowe połączenie zostało odrzucone",
"Password is required": "Hasło jest wymagane",
"noVNC encountered an error:": "noVNC napotkało błąd:",
"Hide/Show the control bar": "Pokaż/Ukryj pasek ustawień",
"Move/Drag Viewport": "Ruszaj/Przeciągaj Viewport",
"viewport drag": "przeciągnij viewport",
"Active Mouse Button": "Aktywny Przycisk Myszy",
"No mousebutton": "Brak przycisku myszy",
"Left mousebutton": "Lewy przycisk myszy",
"Middle mousebutton": "Środkowy przycisk myszy",
"Right mousebutton": "Prawy przycisk myszy",
"Keyboard": "Klawiatura",
"Show keyboard": "Pokaż klawiaturę",
"Extra keys": "Przyciski dodatkowe",
"Show extra keys": "Pokaż przyciski dodatkowe",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Przełącz Ctrl",
"Alt": "Alt",
"Toggle Alt": "Przełącz Alt",
"Send Tab": "Wyślij Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Wyślij Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Wyślij Ctrl-Alt-Del",
"Shutdown/Reboot": "Wyłącz/Uruchom ponownie",
"Shutdown/Reboot...": "Wyłącz/Uruchom ponownie...",
"Power": "Włączony",
"Shutdown": "Wyłącz",
"Reboot": "Uruchom ponownie",
"Reset": "Resetuj",
"Clipboard": "Schowek",
"Clear": "Wyczyść",
"Fullscreen": "Pełny ekran",
"Settings": "Ustawienia",
"Shared Mode": "Tryb Współdzielenia",
"View Only": "Tylko Podgląd",
"Clip to Window": "Przytnij do Okna",
"Scaling Mode:": "Tryb Skalowania:",
"None": "Brak",
"Local scaling": "Skalowanie lokalne",
"Remote resizing": "Skalowanie zdalne",
"Advanced": "Zaawansowane",
"Repeater ID:": "ID Repeatera:",
"WebSocket": "WebSocket",
"Encrypt": "Szyfrowanie",
"Host:": "Host:",
"Port:": "Port:",
"Path:": "Ścieżka:",
"Automatic reconnect": "Automatycznie wznawiaj połączenie",
"Reconnect delay (ms):": "Opóźnienie wznawiania (ms):",
"Logging:": "Poziom logowania:",
"Disconnect": "Rozłącz",
"Connect": "Połącz",
"Password:": "Hasło:",
"Cancel": "Anuluj",
"Canvas not supported.": "Element Canvas nie jest wspierany.",
"Disconnect timeout": "Timeout rozłączenia",
"Local Downscaling": "Downscaling lokalny",
"Local Cursor": "Lokalny kursor",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Wymuszam clipping mode ponieważ paski przewijania nie są wspierane przez IE w trybie pełnoekranowym",
"True Color": "True Color",
"Style:": "Styl:",
"default": "domyślny",
"Apply": "Zapisz",
"Connection": "Połączenie",
"Token:": "Token:",
"Send Password": "Wyślij Hasło"
}
================================================
FILE: services/gateway/noVNC/app/locale/pt_BR.json
================================================
{
"Connecting...": "Conectando...",
"Disconnecting...": "Desconectando...",
"Reconnecting...": "Reconectando...",
"Internal error": "Erro interno",
"Must set host": "É necessário definir o host",
"Connected (encrypted) to ": "Conectado (com criptografia) a ",
"Connected (unencrypted) to ": "Conectado (sem criptografia) a ",
"Something went wrong, connection is closed": "Algo deu errado. A conexão foi encerrada.",
"Failed to connect to server": "Falha ao conectar-se ao servidor",
"Disconnected": "Desconectado",
"New connection has been rejected with reason: ": "A nova conexão foi rejeitada pelo motivo: ",
"New connection has been rejected": "A nova conexão foi rejeitada",
"Credentials are required": "Credenciais são obrigatórias",
"noVNC encountered an error:": "O noVNC encontrou um erro:",
"Hide/Show the control bar": "Esconder/mostrar a barra de controles",
"Drag": "Arrastar",
"Move/Drag viewport": "Mover/arrastar a janela",
"Keyboard": "Teclado",
"Show keyboard": "Mostrar teclado",
"Extra keys": "Teclas adicionais",
"Show extra keys": "Mostrar teclas adicionais",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Pressionar/soltar Ctrl",
"Alt": "Alt",
"Toggle Alt": "Pressionar/soltar Alt",
"Toggle Windows": "Pressionar/soltar Windows",
"Windows": "Windows",
"Send Tab": "Enviar Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Enviar Esc",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Enviar Ctrl-Alt-Del",
"Shutdown/Reboot": "Desligar/reiniciar",
"Shutdown/Reboot...": "Desligar/reiniciar...",
"Power": "Ligar",
"Shutdown": "Desligar",
"Reboot": "Reiniciar",
"Reset": "Reiniciar (forçado)",
"Clipboard": "Área de transferência",
"Clear": "Limpar",
"Fullscreen": "Tela cheia",
"Settings": "Configurações",
"Shared mode": "Modo compartilhado",
"View only": "Apenas visualizar",
"Clip to window": "Recortar à janela",
"Scaling mode:": "Modo de dimensionamento:",
"None": "Nenhum",
"Local scaling": "Local",
"Remote resizing": "Remoto",
"Advanced": "Avançado",
"Quality:": "Qualidade:",
"Compression level:": "Nível de compressão:",
"Repeater ID:": "ID do repetidor:",
"WebSocket": "WebSocket",
"Encrypt": "Criptografar",
"Host:": "Host:",
"Port:": "Porta:",
"Path:": "Caminho:",
"Automatic reconnect": "Reconexão automática",
"Reconnect delay (ms):": "Atraso da reconexão (ms)",
"Show dot when no cursor": "Mostrar ponto quando não há cursor",
"Logging:": "Registros:",
"Version:": "Versão:",
"Disconnect": "Desconectar",
"Connect": "Conectar",
"Username:": "Nome de usuário:",
"Password:": "Senha:",
"Send credentials": "Enviar credenciais",
"Cancel": "Cancelar"
}
================================================
FILE: services/gateway/noVNC/app/locale/ru.json
================================================
{
"Connecting...": "Подключение...",
"Disconnecting...": "Отключение...",
"Reconnecting...": "Переподключение...",
"Internal error": "Внутренняя ошибка",
"Must set host": "Задайте имя сервера или IP",
"Connected (encrypted) to ": "Подключено (с шифрованием) к ",
"Connected (unencrypted) to ": "Подключено (без шифрования) к ",
"Something went wrong, connection is closed": "Что-то пошло не так, подключение разорвано",
"Failed to connect to server": "Ошибка подключения к серверу",
"Disconnected": "Отключено",
"New connection has been rejected with reason: ": "Новое соединение отклонено по причине: ",
"New connection has been rejected": "Новое соединение отклонено",
"Credentials are required": "Требуются учетные данные",
"noVNC encountered an error:": "Ошибка noVNC: ",
"Hide/Show the control bar": "Скрыть/Показать контрольную панель",
"Drag": "Переместить",
"Move/Drag viewport": "Переместить окно",
"Keyboard": "Клавиатура",
"Show keyboard": "Показать клавиатуру",
"Extra keys": "Дополнительные Кнопки",
"Show Extra Keys": "Показать Дополнительные Кнопки",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Зажать Ctrl",
"Alt": "Alt",
"Toggle Alt": "Зажать Alt",
"Toggle Windows": "Зажать Windows",
"Windows": "Вкладка",
"Send Tab": "Передать нажатие Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Передать нажатие Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Передать нажатие Ctrl-Alt-Del",
"Shutdown/Reboot": "Выключить/Перезагрузить",
"Shutdown/Reboot...": "Выключить/Перезагрузить...",
"Power": "Питание",
"Shutdown": "Выключить",
"Reboot": "Перезагрузить",
"Reset": "Сброс",
"Clipboard": "Буфер обмена",
"Clear": "Очистить",
"Fullscreen": "Во весь экран",
"Settings": "Настройки",
"Shared mode": "Общий режим",
"View Only": "Только Просмотр",
"Clip to window": "В окно",
"Scaling mode:": "Масштаб:",
"None": "Нет",
"Local scaling": "Локальный масштаб",
"Remote resizing": "Удаленная перенастройка размера",
"Advanced": "Дополнительно",
"Quality:": "Качество",
"Compression level:": "Уровень Сжатия",
"Repeater ID:": "Идентификатор ID:",
"WebSocket": "WebSocket",
"Encrypt": "Шифрование",
"Host:": "Сервер:",
"Port:": "Порт:",
"Path:": "Путь:",
"Automatic reconnect": "Автоматическое переподключение",
"Reconnect delay (ms):": "Задержка переподключения (мс):",
"Show dot when no cursor": "Показать точку вместо курсора",
"Logging:": "Лог:",
"Version:": "Версия",
"Disconnect": "Отключение",
"Connect": "Подключение",
"Username:": "Имя Пользователя",
"Password:": "Пароль:",
"Send Credentials": "Передача Учетных Данных",
"Cancel": "Выход"
}
================================================
FILE: services/gateway/noVNC/app/locale/sv.json
================================================
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är troliga.",
"Connecting...": "Ansluter...",
"Disconnecting...": "Kopplar ner...",
"Reconnecting...": "Återansluter...",
"Internal error": "Internt fel",
"Failed to connect to server: ": "Misslyckades att ansluta till servern: ",
"Connected (encrypted) to ": "Ansluten (krypterat) till ",
"Connected (unencrypted) to ": "Ansluten (okrypterat) till ",
"Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades",
"Failed to connect to server": "Misslyckades att ansluta till servern",
"Disconnected": "Frånkopplad",
"New connection has been rejected with reason: ": "Ny anslutning har blivit nekad med följande skäl: ",
"New connection has been rejected": "Ny anslutning har blivit nekad",
"Credentials are required": "Användaruppgifter krävs",
"noVNC encountered an error:": "noVNC stötte på ett problem:",
"Hide/Show the control bar": "Göm/Visa kontrollbaren",
"Drag": "Dra",
"Move/Drag viewport": "Flytta/Dra vyn",
"Keyboard": "Tangentbord",
"Show keyboard": "Visa tangentbord",
"Extra keys": "Extraknappar",
"Show extra keys": "Visa extraknappar",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Växla Ctrl",
"Alt": "Alt",
"Toggle Alt": "Växla Alt",
"Toggle Windows": "Växla Windows",
"Windows": "Windows",
"Send Tab": "Skicka Tab",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "Skicka Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "Skicka Ctrl-Alt-Del",
"Shutdown/Reboot": "Stäng av/Boota om",
"Shutdown/Reboot...": "Stäng av/Boota om...",
"Power": "Ström",
"Shutdown": "Stäng av",
"Reboot": "Boota om",
"Reset": "Återställ",
"Clipboard": "Urklipp",
"Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.",
"Full screen": "Fullskärm",
"Settings": "Inställningar",
"Shared mode": "Delat läge",
"View only": "Endast visning",
"Clip to window": "Begränsa till fönster",
"Scaling mode:": "Skalningsläge:",
"None": "Ingen",
"Local scaling": "Lokal skalning",
"Remote resizing": "Ändra storlek",
"Advanced": "Avancerat",
"Quality:": "Kvalitet:",
"Compression level:": "Kompressionsnivå:",
"Repeater ID:": "Repeater-ID:",
"WebSocket": "WebSocket",
"Encrypt": "Kryptera",
"Host:": "Värd:",
"Port:": "Port:",
"Path:": "Sökväg:",
"Automatic reconnect": "Automatisk återanslutning",
"Reconnect delay (ms):": "Fördröjning (ms):",
"Show dot when no cursor": "Visa prick när ingen muspekare finns",
"Logging:": "Loggning:",
"Version:": "Version:",
"Disconnect": "Koppla från",
"Connect": "Anslut",
"Server identity": "Server-identitet",
"The server has provided the following identifying information:": "Servern har gett följande identifierande information:",
"Fingerprint:": "Fingeravtryck:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck annars \"Neka\".",
"Approve": "Godkänn",
"Reject": "Neka",
"Credentials": "Användaruppgifter",
"Username:": "Användarnamn:",
"Password:": "Lösenord:",
"Send credentials": "Skicka användaruppgifter",
"Cancel": "Avbryt",
"Must set host": "Du måste specifiera en värd",
"HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet",
"Clear": "Rensa"
}
================================================
FILE: services/gateway/noVNC/app/locale/tr.json
================================================
{
"Connecting...": "Bağlanıyor...",
"Disconnecting...": "Bağlantı kesiliyor...",
"Reconnecting...": "Yeniden bağlantı kuruluyor...",
"Internal error": "İç hata",
"Must set host": "Sunucuyu kur",
"Connected (encrypted) to ": "Bağlı (şifrelenmiş)",
"Connected (unencrypted) to ": "Bağlandı (şifrelenmemiş)",
"Something went wrong, connection is closed": "Bir şeyler ters gitti, bağlantı kesildi",
"Disconnected": "Bağlantı kesildi",
"New connection has been rejected with reason: ": "Bağlantı aşağıdaki nedenlerden dolayı reddedildi: ",
"New connection has been rejected": "Bağlantı reddedildi",
"Password is required": "Şifre gerekli",
"noVNC encountered an error:": "Bir hata oluştu:",
"Hide/Show the control bar": "Denetim masasını Gizle/Göster",
"Move/Drag Viewport": "Görünümü Taşı/Sürükle",
"viewport drag": "Görüntü penceresini sürükle",
"Active Mouse Button": "Aktif Fare Düğmesi",
"No mousebutton": "Fare düğmesi yok",
"Left mousebutton": "Farenin sol düğmesi",
"Middle mousebutton": "Farenin orta düğmesi",
"Right mousebutton": "Farenin sağ düğmesi",
"Keyboard": "Klavye",
"Show Keyboard": "Klavye Düzenini Göster",
"Extra keys": "Ekstra tuşlar",
"Show extra keys": "Ekstra tuşları göster",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl Değiştir ",
"Alt": "Alt",
"Toggle Alt": "Alt Değiştir",
"Send Tab": "Sekme Gönder",
"Tab": "Sekme",
"Esc": "Esc",
"Send Escape": "Boşluk Gönder",
"Ctrl+Alt+Del": "Ctrl + Alt + Del",
"Send Ctrl-Alt-Del": "Ctrl-Alt-Del Gönder",
"Shutdown/Reboot": "Kapat/Yeniden Başlat",
"Shutdown/Reboot...": "Kapat/Yeniden Başlat...",
"Power": "Güç",
"Shutdown": "Kapat",
"Reboot": "Yeniden Başlat",
"Reset": "Sıfırla",
"Clipboard": "Pano",
"Clear": "Temizle",
"Fullscreen": "Tam Ekran",
"Settings": "Ayarlar",
"Shared Mode": "Paylaşım Modu",
"View Only": "Sadece Görüntüle",
"Clip to Window": "Pencereye Tıkla",
"Scaling Mode:": "Ölçekleme Modu:",
"None": "Bilinmeyen",
"Local Scaling": "Yerel Ölçeklendirme",
"Remote Resizing": "Uzaktan Yeniden Boyutlandırma",
"Advanced": "Gelişmiş",
"Repeater ID:": "Tekralayıcı ID:",
"WebSocket": "WebSocket",
"Encrypt": "Şifrele",
"Host:": "Ana makine:",
"Port:": "Port:",
"Path:": "Yol:",
"Automatic Reconnect": "Otomatik Yeniden Bağlan",
"Reconnect Delay (ms):": "Yeniden Bağlanma Süreci (ms):",
"Logging:": "Giriş yapılıyor:",
"Disconnect": "Bağlantıyı Kes",
"Connect": "Bağlan",
"Password:": "Parola:",
"Cancel": "Vazgeç",
"Canvas not supported.": "Tuval desteklenmiyor."
}
================================================
FILE: services/gateway/noVNC/app/locale/zh_CN.json
================================================
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "不建议在没有 HTTPS 的情况下运行,可能会出现崩溃或出现其他问题。",
"Connecting...": "连接中...",
"Disconnecting...": "正在断开连接...",
"Reconnecting...": "重新连接中...",
"Internal error": "内部错误",
"Must set host": "必须设置主机",
"Failed to connect to server: ": "无法连接到服务器:",
"Connected (encrypted) to ": "已连接(已加密)到",
"Connected (unencrypted) to ": "已连接(未加密)到",
"Something went wrong, connection is closed": "出了点问题,连接已关闭",
"Failed to connect to server": "无法连接到服务器",
"Disconnected": "已断开连接",
"New connection has been rejected with reason: ": "新连接被拒绝,原因如下:",
"New connection has been rejected": "新连接已被拒绝",
"Credentials are required": "需要凭证",
"noVNC encountered an error:": "noVNC 遇到一个错误:",
"Hide/Show the control bar": "显示/隐藏控制栏",
"Drag": "拖动",
"Move/Drag viewport": "移动/拖动窗口",
"Keyboard": "键盘",
"Show keyboard": "显示键盘",
"Extra keys": "额外按键",
"Show extra keys": "显示额外按键",
"Ctrl": "Ctrl",
"Toggle Ctrl": "切换 Ctrl",
"Alt": "Alt",
"Toggle Alt": "切换 Alt",
"Toggle Windows": "切换窗口",
"Windows": "窗口",
"Send Tab": "发送 Tab 键",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "发送 Escape 键",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
"Send Ctrl-Alt-Del": "发送 Ctrl+Alt+Del 键",
"Shutdown/Reboot": "关机/重启",
"Shutdown/Reboot...": "关机/重启...",
"Power": "电源",
"Shutdown": "关机",
"Reboot": "重启",
"Reset": "重置",
"Clipboard": "剪贴板",
"Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。",
"Full screen": "全屏",
"Settings": "设置",
"Shared mode": "分享模式",
"View only": "仅查看",
"Clip to window": "限制/裁切窗口大小",
"Scaling mode:": "缩放模式:",
"None": "无",
"Local scaling": "本地缩放",
"Remote resizing": "远程调整大小",
"Advanced": "高级",
"Quality:": "品质:",
"Compression level:": "压缩级别:",
"Repeater ID:": "中继站 ID",
"WebSocket": "WebSocket",
"Encrypt": "加密",
"Host:": "主机:",
"Port:": "端口:",
"Path:": "路径:",
"Automatic reconnect": "自动重新连接",
"Reconnect delay (ms):": "重新连接间隔 (ms):",
"Show dot when no cursor": "无光标时显示点",
"Logging:": "日志级别:",
"Version:": "版本:",
"Disconnect": "断开连接",
"Connect": "连接",
"Server identity": "服务器身份",
"The server has provided the following identifying information:": "服务器提供了以下识别信息:",
"Fingerprint:": "指纹:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "请核实信息是否正确,并按 “同意”,否则按 “拒绝”。",
"Approve": "同意",
"Reject": "拒绝",
"Credentials": "凭证",
"Username:": "用户名:",
"Password:": "密码:",
"Send credentials": "发送凭证",
"Cancel": "取消",
"Password is required": "请提供密码",
"Disconnect timeout": "超时断开",
"viewport drag": "窗口拖动",
"Active Mouse Button": "启动鼠标按键",
"No mousebutton": "禁用鼠标按键",
"Left mousebutton": "鼠标左键",
"Middle mousebutton": "鼠标中键",
"Right mousebutton": "鼠标右键",
"Clear": "清除",
"Local Downscaling": "降低本地尺寸",
"Local Cursor": "本地光标",
"Canvas not supported.": "不支持 Canvas。"
}
================================================
FILE: services/gateway/noVNC/app/locale/zh_TW.json
================================================
{
"Connecting...": "連線中...",
"Disconnecting...": "正在中斷連線...",
"Reconnecting...": "重新連線中...",
"Internal error": "內部錯誤",
"Must set host": "請提供主機資訊",
"Connected (encrypted) to ": "已加密連線到",
"Connected (unencrypted) to ": "未加密連線到",
"Something went wrong, connection is closed": "發生錯誤,連線已關閉",
"Failed to connect to server": "無法連線到伺服器",
"Disconnected": "連線已中斷",
"New connection has been rejected with reason: ": "連線被拒絕,原因:",
"New connection has been rejected": "連線被拒絕",
"Password is required": "請提供密碼",
"noVNC encountered an error:": "noVNC 遇到一個錯誤:",
"Hide/Show the control bar": "顯示/隱藏控制列",
"Move/Drag viewport": "拖放顯示範圍",
"viewport drag": "顯示範圍拖放",
"Active Mouse Button": "啟用滑鼠按鍵",
"No mousebutton": "無滑鼠按鍵",
"Left mousebutton": "滑鼠左鍵",
"Middle mousebutton": "滑鼠中鍵",
"Right mousebutton": "滑鼠右鍵",
"Keyboard": "鍵盤",
"Show keyboard": "顯示鍵盤",
"Extra keys": "額外按鍵",
"Show extra keys": "顯示額外按鍵",
"Ctrl": "Ctrl",
"Toggle Ctrl": "切換 Ctrl",
"Alt": "Alt",
"Toggle Alt": "切換 Alt",
"Send Tab": "送出 Tab 鍵",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "送出 Escape 鍵",
"Ctrl+Alt+Del": "Ctrl-Alt-Del",
"Send Ctrl-Alt-Del": "送出 Ctrl-Alt-Del 快捷鍵",
"Shutdown/Reboot": "關機/重新啟動",
"Shutdown/Reboot...": "關機/重新啟動...",
"Power": "電源",
"Shutdown": "關機",
"Reboot": "重新啟動",
"Reset": "重設",
"Clipboard": "剪貼簿",
"Clear": "清除",
"Fullscreen": "全螢幕",
"Settings": "設定",
"Shared mode": "分享模式",
"View only": "僅檢視",
"Clip to window": "限制/裁切視窗大小",
"Scaling mode:": "縮放模式:",
"None": "無",
"Local scaling": "本機縮放",
"Remote resizing": "遠端調整大小",
"Advanced": "進階",
"Repeater ID:": "中繼站 ID",
"WebSocket": "WebSocket",
"Encrypt": "加密",
"Host:": "主機:",
"Port:": "連接埠:",
"Path:": "路徑:",
"Automatic reconnect": "自動重新連線",
"Reconnect delay (ms):": "重新連線間隔 (ms):",
"Logging:": "日誌級別:",
"Disconnect": "中斷連線",
"Connect": "連線",
"Password:": "密碼:",
"Cancel": "取消"
}
================================================
FILE: services/gateway/noVNC/app/localization.js
================================================
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
/*
* Localization utilities
*/
export class Localizer {
constructor() {
// Currently configured language
this.language = 'en';
// Current dictionary of translations
this._dictionary = undefined;
}
// Configure suitable language based on user preferences
async setup(supportedLanguages, baseURL) {
this.language = 'en'; // Default: US English
this._dictionary = undefined;
this._setupLanguage(supportedLanguages);
await this._setupDictionary(baseURL);
}
_setupLanguage(supportedLanguages) {
/*
* Navigator.languages only available in Chrome (32+) and FireFox (32+)
* Fall back to navigator.language for other browsers
*/
let userLanguages;
if (typeof window.navigator.languages == 'object') {
userLanguages = window.navigator.languages;
} else {
userLanguages = [navigator.language || navigator.userLanguage];
}
for (let i = 0;i < userLanguages.length;i++) {
const userLang = userLanguages[i]
.toLowerCase()
.replace("_", "-")
.split("-");
// First pass: perfect match
for (let j = 0; j < supportedLanguages.length; j++) {
const supLang = supportedLanguages[j]
.toLowerCase()
.replace("_", "-")
.split("-");
if (userLang[0] !== supLang[0]) {
continue;
}
if (userLang[1] !== supLang[1]) {
continue;
}
this.language = supportedLanguages[j];
return;
}
// Second pass: English fallback
if (userLang[0] === 'en') {
return;
}
// Third pass pass: other fallback
for (let j = 0;j < supportedLanguages.length;j++) {
const supLang = supportedLanguages[j]
.toLowerCase()
.replace("_", "-")
.split("-");
if (userLang[0] !== supLang[0]) {
continue;
}
if (supLang[1] !== undefined) {
continue;
}
this.language = supportedLanguages[j];
return;
}
}
}
async _setupDictionary(baseURL) {
if (baseURL) {
if (!baseURL.endsWith("/")) {
baseURL = baseURL + "/";
}
} else {
baseURL = "";
}
if (this.language === "en") {
return;
}
let response = await fetch(baseURL + this.language + ".json");
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
this._dictionary = await response.json();
}
// Retrieve localised text
get(id) {
if (typeof this._dictionary !== 'undefined' &&
this._dictionary[id]) {
return this._dictionary[id];
} else {
return id;
}
}
// Traverses the DOM and translates relevant fields
// See https://html.spec.whatwg.org/multipage/dom.html#attr-translate
translateDOM() {
const self = this;
function process(elem, enabled) {
function isAnyOf(searchElement, items) {
return items.indexOf(searchElement) !== -1;
}
function translateString(str) {
// We assume surrounding whitespace, and whitespace around line
// breaks is just for source formatting
str = str.split("\n").map(s => s.trim()).join(" ").trim();
return self.get(str);
}
function translateAttribute(elem, attr) {
const str = translateString(elem.getAttribute(attr));
elem.setAttribute(attr, str);
}
function translateTextNode(node) {
const str = translateString(node.data);
node.data = str;
}
if (elem.hasAttribute("translate")) {
if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) {
enabled = true;
} else if (isAnyOf(elem.getAttribute("translate"), ["no"])) {
enabled = false;
}
}
if (enabled) {
if (elem.hasAttribute("abbr") &&
elem.tagName === "TH") {
translateAttribute(elem, "abbr");
}
if (elem.hasAttribute("alt") &&
isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) {
translateAttribute(elem, "alt");
}
if (elem.hasAttribute("download") &&
isAnyOf(elem.tagName, ["A", "AREA"])) {
translateAttribute(elem, "download");
}
if (elem.hasAttribute("label") &&
isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP",
"OPTION", "TRACK"])) {
translateAttribute(elem, "label");
}
// FIXME: Should update "lang"
if (elem.hasAttribute("placeholder") &&
isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) {
translateAttribute(elem, "placeholder");
}
if (elem.hasAttribute("title")) {
translateAttribute(elem, "title");
}
if (elem.hasAttribute("value") &&
elem.tagName === "INPUT" &&
isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) {
translateAttribute(elem, "value");
}
}
for (let i = 0; i < elem.childNodes.length; i++) {
const node = elem.childNodes[i];
if (node.nodeType === node.ELEMENT_NODE) {
process(node, enabled);
} else if (node.nodeType === node.TEXT_NODE && enabled) {
translateTextNode(node);
}
}
}
process(document.body, true);
}
}
export const l10n = new Localizer();
export default l10n.get.bind(l10n);
================================================
FILE: services/gateway/noVNC/app/sounds/CREDITS
================================================
bell
Copyright: Dr. Richard Boulanger et al
URL: http://www.archive.org/details/Berklee44v12
License: CC-BY Attribution 3.0 Unported
================================================
FILE: services/gateway/noVNC/app/styles/base.css
================================================
/*
* noVNC base CSS
* Copyright (C) 2019 The noVNC authors
* noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
* This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
*/
/*
* Z index layers:
*
* 0: Main screen
* 10: Control bar
* 50: Transition blocker
* 60: Connection popups
* 100: Status bar
* ...
* 1000: Javascript crash
* ...
* 10000: Max (used for polyfills)
*/
/*
* State variables (set on :root):
*
* noVNC_loading: Page is still loading
* noVNC_connecting: Connecting to server
* noVNC_reconnecting: Re-establishing a connection
* noVNC_connected: Connected to server (most common state)
* noVNC_disconnecting: Disconnecting from server
*/
:root {
font-family: sans-serif;
line-height: 1.6;
}
body {
margin:0;
padding:0;
/*Background image with light grey curve.*/
background-color:#494949;
background-repeat:no-repeat;
background-position:right bottom;
height:100%;
touch-action: none;
}
html {
height:100%;
}
.noVNC_only_touch.noVNC_hidden {
display: none;
}
.noVNC_disabled {
color: var(--novnc-grey);
}
/* ----------------------------------------
* Spinner
* ----------------------------------------
*/
.noVNC_spinner {
position: relative;
}
.noVNC_spinner, .noVNC_spinner::before, .noVNC_spinner::after {
width: 10px;
height: 10px;
border-radius: 2px;
box-shadow: -60px 10px 0 rgba(255, 255, 255, 0);
animation: noVNC_spinner 1.0s linear infinite;
}
.noVNC_spinner::before {
content: "";
position: absolute;
left: 0px;
top: 0px;
animation-delay: -0.1s;
}
.noVNC_spinner::after {
content: "";
position: absolute;
top: 0px;
left: 0px;
animation-delay: 0.1s;
}
@keyframes noVNC_spinner {
0% { box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); width: 20px; }
25% { box-shadow: 20px 10px 0 rgba(255, 255, 255, 1); width: 10px; }
50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; }
}
/* ----------------------------------------
* WebKit centering hacks
* ----------------------------------------
*/
.noVNC_center {
/*
* This is a workaround because webkit misrenders transforms and
* uses non-integer coordinates, resulting in blurry content.
* Ideally we'd use "top: 50%; transform: translateY(-50%);" on
* the objects instead.
*/
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.noVNC_center > * {
pointer-events: auto;
}
.noVNC_vcenter {
display: flex !important;
flex-direction: column;
justify-content: center;
position: fixed;
top: 0;
left: 0;
height: 100%;
margin: 0 !important;
padding: 0 !important;
pointer-events: none;
}
.noVNC_vcenter > * {
pointer-events: auto;
}
/* ----------------------------------------
* Layering
* ----------------------------------------
*/
.noVNC_connect_layer {
z-index: 60;
}
/* ----------------------------------------
* Fallback error
* ----------------------------------------
*/
#noVNC_fallback_error {
z-index: 1000;
visibility: hidden;
/* Put a dark background in front of everything but the error,
and don't let mouse events pass through */
background: rgba(0, 0, 0, 0.8);
pointer-events: all;
}
#noVNC_fallback_error.noVNC_open {
visibility: visible;
}
#noVNC_fallback_error > div {
max-width: calc(100vw - 30px - 30px);
max-height: calc(100vh - 30px - 30px);
overflow: auto;
padding: 15px;
transition: 0.5s ease-in-out;
transform: translateY(-50px);
opacity: 0;
text-align: center;
font-weight: bold;
color: #fff;
border-radius: 12px;
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
background: rgba(200,55,55,0.8);
}
#noVNC_fallback_error.noVNC_open > div {
transform: translateY(0);
opacity: 1;
}
#noVNC_fallback_errormsg {
font-weight: normal;
}
#noVNC_fallback_errormsg .noVNC_message {
display: inline-block;
text-align: left;
font-family: monospace;
white-space: pre-wrap;
}
#noVNC_fallback_error .noVNC_location {
font-style: italic;
font-size: 0.8em;
color: rgba(255, 255, 255, 0.8);
}
#noVNC_fallback_error .noVNC_stack {
padding: 10px;
margin: 10px;
font-size: 0.8em;
text-align: left;
font-family: monospace;
white-space: pre;
border: 1px solid rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.2);
overflow: auto;
}
/* ----------------------------------------
* Control bar
* ----------------------------------------
*/
#noVNC_control_bar_anchor {
/* The anchor is needed to get z-stacking to work */
position: fixed;
z-index: 10;
transition: 0.5s ease-in-out;
/* Edge misrenders animations wihthout this */
transform: translateX(0);
}
:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle {
opacity: 0.8;
}
#noVNC_control_bar_anchor.noVNC_right {
left: auto;
right: 0;
}
#noVNC_control_bar {
position: relative;
left: -100%;
transition: 0.5s ease-in-out;
background-color: var(--novnc-blue);
border-radius: 0 12px 12px 0;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none; /* Disable iOS image long-press popup */
}
#noVNC_control_bar.noVNC_open {
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
left: 0;
}
#noVNC_control_bar::before {
/* This extra element is to get a proper shadow */
content: "";
position: absolute;
z-index: -1;
height: 100%;
width: 30px;
left: -30px;
transition: box-shadow 0.5s ease-in-out;
}
#noVNC_control_bar.noVNC_open::before {
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
}
.noVNC_right #noVNC_control_bar {
left: 100%;
border-radius: 12px 0 0 12px;
}
.noVNC_right #noVNC_control_bar.noVNC_open {
left: 0;
}
.noVNC_right #noVNC_control_bar::before {
visibility: hidden;
}
#noVNC_control_bar_handle {
position: absolute;
left: -15px;
top: 0;
transform: translateY(35px);
width: calc(100% + 30px);
height: 50px;
z-index: -1;
cursor: pointer;
border-radius: 6px;
background-color: var(--novnc-darkblue);
background-image: url("../images/handle_bg.svg");
background-repeat: no-repeat;
background-position: right;
box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5);
}
#noVNC_control_bar_handle:after {
content: "";
transition: transform 0.5s ease-in-out;
background: url("../images/handle.svg");
position: absolute;
top: 22px; /* (50px-6px)/2 */
right: 5px;
width: 5px;
height: 6px;
}
#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: translateX(1px) rotate(180deg);
}
:root:not(.noVNC_connected) #noVNC_control_bar_handle {
display: none;
}
.noVNC_right #noVNC_control_bar_handle {
background-position: left;
}
.noVNC_right #noVNC_control_bar_handle:after {
left: 5px;
right: 0;
transform: translateX(1px) rotate(180deg);
}
.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: none;
}
/* Larger touch area for the handle, used when a touch screen is available */
#noVNC_control_bar_handle div {
position: absolute;
right: -35px;
top: 0;
width: 50px;
height: 100%;
display: none;
}
@media (any-pointer: coarse) {
#noVNC_control_bar_handle div {
display: initial;
}
}
.noVNC_right #noVNC_control_bar_handle div {
left: -35px;
right: auto;
}
#noVNC_control_bar > .noVNC_scroll {
max-height: 100vh; /* Chrome is buggy with 100% */
overflow-x: hidden;
overflow-y: auto;
padding: 0 10px;
}
#noVNC_control_bar > .noVNC_scroll > * {
display: block;
margin: 10px auto;
}
/* Control bar hint */
#noVNC_hint_anchor {
position: fixed;
right: -50px;
left: auto;
}
#noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor {
left: -50px;
right: auto;
}
#noVNC_control_bar_hint {
position: relative;
transform: scale(0);
width: 100px;
height: 50%;
max-height: 600px;
visibility: hidden;
opacity: 0;
transition: 0.2s ease-in-out;
background: transparent;
box-shadow: 0 0 10px black, inset 0 0 10px 10px var(--novnc-darkblue);
border-radius: 12px;
transition-delay: 0s;
}
#noVNC_control_bar_hint.noVNC_active {
visibility: visible;
opacity: 1;
transition-delay: 0.2s;
transform: scale(1);
}
#noVNC_control_bar_hint.noVNC_notransition {
transition: none !important;
}
/* Control bar buttons */
#noVNC_control_bar .noVNC_button {
min-width: unset;
padding: 4px 4px;
vertical-align: middle;
border:1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background-color: transparent;
}
#noVNC_control_bar .noVNC_button.noVNC_selected {
border-color: rgba(0, 0, 0, 0.8);
background-color: rgba(0, 0, 0, 0.5);
}
#noVNC_control_bar .noVNC_button.noVNC_hidden {
display: none !important;
}
/* Panels */
.noVNC_panel {
transform: translateX(25px);
transition: 0.5s ease-in-out;
box-sizing: border-box; /* so max-width don't have to care about padding */
max-width: calc(100vw - 75px - 25px); /* minus left and right margins */
max-height: 100vh; /* Chrome is buggy with 100% */
overflow-x: hidden;
overflow-y: auto;
visibility: hidden;
opacity: 0;
padding: 15px;
background: #fff;
border-radius: 12px;
color: #000;
border: 2px solid #E0E0E0;
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
}
.noVNC_panel.noVNC_open {
visibility: visible;
opacity: 1;
transform: translateX(75px);
}
.noVNC_right .noVNC_vcenter {
left: auto;
right: 0;
}
.noVNC_right .noVNC_panel {
transform: translateX(-25px);
}
.noVNC_right .noVNC_panel.noVNC_open {
transform: translateX(-75px);
}
.noVNC_panel > * {
display: block;
margin: 10px auto;
}
.noVNC_panel > *:first-child {
margin-top: 0 !important;
}
.noVNC_panel > *:last-child {
margin-bottom: 0 !important;
}
.noVNC_panel hr {
border: none;
border-top: 1px solid var(--novnc-lightgrey);
width: 100%; /* inside a flexbox will otherwise be 0px wide */
}
.noVNC_panel label {
display: block;
white-space: nowrap;
margin: 5px;
}
@media (max-width: 540px) {
/* Allow wrapping on small screens */
.noVNC_panel label {
white-space: unset;
}
}
.noVNC_panel li {
margin: 5px;
}
.noVNC_panel .noVNC_heading {
background-color: var(--novnc-blue);
border-radius: 6px;
padding: 5px 8px;
/* Compensate for padding in image */
padding-right: 11px;
display: flex;
align-items: center;
gap: 6px;
color: white;
font-size: 20px;
font-weight: bold;
white-space: nowrap;
}
.noVNC_panel .noVNC_heading img {
vertical-align: bottom;
}
.noVNC_panel form {
display: flex;
flex-direction: column;
gap: 12px
}
.noVNC_panel .button_row {
margin-top: 10px;
display: flex;
gap: 10px;
justify-content: space-between;
}
.noVNC_panel .button_row *:only-child {
margin-left: auto; /* Align single buttons to the right */
}
/* Expanders */
.noVNC_expander {
cursor: pointer;
}
.noVNC_expander::before {
content: url("../images/expander.svg");
display: inline-block;
margin-right: 5px;
transition: 0.2s ease-in-out;
}
.noVNC_expander.noVNC_open::before {
transform: rotateZ(90deg);
}
.noVNC_expander ~ * {
margin: 5px;
margin-left: 10px;
padding: 5px;
background: rgba(0, 0, 0, 0.04);
border-radius: 6px;
}
.noVNC_expander:not(.noVNC_open) ~ * {
display: none;
}
/* Control bar content */
#noVNC_control_bar .noVNC_logo {
font-size: 13px;
}
.noVNC_logo + hr {
/* Remove all but top border */
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
:root:not(.noVNC_connected) #noVNC_view_drag_button {
display: none;
}
/* noVNC Touch Device only buttons */
:root:not(.noVNC_connected) #noVNC_mobile_buttons {
display: none;
}
@media not all and (any-pointer: coarse) {
/* FIXME: The button for the virtual keyboard is the only button in this
group of "mobile buttons". It is bad to assume that no touch
devices have physical keyboards available. Hopefully we can get
a media query for this:
https://github.com/w3c/csswg-drafts/issues/3871 */
:root.noVNC_connected #noVNC_mobile_buttons {
display: none;
}
}
/* Extra manual keys */
:root:not(.noVNC_connected) #noVNC_toggle_extra_keys_button {
display: none;
}
#noVNC_modifiers {
background-color: var(--novnc-darkgrey);
border: none;
padding: 10px;
}
/* Shutdown/Reboot */
:root:not(.noVNC_connected) #noVNC_power_button {
display: none;
}
#noVNC_power {
}
#noVNC_power_buttons {
display: none;
}
#noVNC_power input[type=button] {
width: 100%;
}
/* Clipboard */
:root:not(.noVNC_connected) #noVNC_clipboard_button {
display: none;
}
#noVNC_clipboard_text {
width: 360px;
min-width: 150px;
height: 160px;
min-height: 70px;
box-sizing: border-box;
max-width: 100%;
/* minus approximate height of title, height of subtitle, and margin */
max-height: calc(100vh - 10em - 25px);
}
/* Settings */
#noVNC_settings {
}
#noVNC_settings ul {
list-style: none;
padding: 0px;
}
#noVNC_settings button,
#noVNC_settings select,
#noVNC_settings textarea,
#noVNC_settings input:not([type=checkbox]):not([type=radio]) {
margin-left: 6px;
/* Prevent inputs in settings from being too wide */
max-width: calc(100% - 6px - var(--input-xpadding) * 2);
}
#noVNC_setting_port {
width: 80px;
}
#noVNC_setting_path {
width: 100px;
}
/* Version */
.noVNC_version_wrapper {
font-size: small;
}
.noVNC_version {
margin-left: 1rem;
}
/* Connection controls */
:root:not(.noVNC_connected) #noVNC_disconnect_button {
display: none;
}
/* ----------------------------------------
* Status dialog
* ----------------------------------------
*/
#noVNC_status {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
transform: translateY(-100%);
cursor: pointer;
transition: 0.5s ease-in-out;
visibility: hidden;
opacity: 0;
padding: 5px;
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
line-height: 1.6;
word-wrap: break-word;
color: #fff;
border-bottom: 1px solid rgba(0, 0, 0, 0.9);
}
#noVNC_status.noVNC_open {
transform: translateY(0);
visibility: visible;
opacity: 1;
}
#noVNC_status::before {
content: "";
display: inline-block;
width: 25px;
height: 25px;
margin-right: 5px;
}
#noVNC_status.noVNC_status_normal {
background: rgba(128,128,128,0.9);
}
#noVNC_status.noVNC_status_normal::before {
content: url("../images/info.svg") " ";
}
#noVNC_status.noVNC_status_error {
background: rgba(200,55,55,0.9);
}
#noVNC_status.noVNC_status_error::before {
content: url("../images/error.svg") " ";
}
#noVNC_status.noVNC_status_warn {
background: rgba(180,180,30,0.9);
}
#noVNC_status.noVNC_status_warn::before {
content: url("../images/warning.svg") " ";
}
/* ----------------------------------------
* Connect dialog
* ----------------------------------------
*/
#noVNC_connect_dlg {
transition: 0.5s ease-in-out;
transform: scale(0, 0);
visibility: hidden;
opacity: 0;
}
#noVNC_connect_dlg.noVNC_open {
transform: scale(1, 1);
visibility: visible;
opacity: 1;
}
#noVNC_connect_dlg .noVNC_logo {
transition: 0.5s ease-in-out;
padding: 10px;
margin-bottom: 10px;
font-size: 80px;
text-align: center;
border-radius: 6px;
}
@media (max-width: 440px) {
#noVNC_connect_dlg {
max-width: calc(100vw - 100px);
}
#noVNC_connect_dlg .noVNC_logo {
font-size: calc(25vw - 30px);
}
}
#noVNC_connect_dlg div {
padding: 18px;
background-color: var(--novnc-darkgrey);
border-radius: 12px;
text-align: center;
font-size: 20px;
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
}
#noVNC_connect_button {
width: 100%;
padding: 6px 30px;
cursor: pointer;
border-color: transparent;
border-radius: 12px;
background-color: var(--novnc-blue);
color: white;
display: flex;
justify-content: center;
place-items: center;
gap: 4px;
}
#noVNC_connect_button img {
vertical-align: bottom;
height: 1.3em;
}
/* ----------------------------------------
* Server verification dialog
* ----------------------------------------
*/
#noVNC_verify_server_dlg {
position: relative;
transform: translateY(-50px);
}
#noVNC_verify_server_dlg.noVNC_open {
transform: translateY(0);
}
#noVNC_fingerprint_block {
margin: 10px;
}
/* ----------------------------------------
* Password dialog
* ----------------------------------------
*/
#noVNC_credentials_dlg {
position: relative;
transform: translateY(-50px);
}
#noVNC_credentials_dlg.noVNC_open {
transform: translateY(0);
}
#noVNC_username_block.noVNC_hidden,
#noVNC_password_block.noVNC_hidden {
display: none;
}
/* ----------------------------------------
* Main area
* ----------------------------------------
*/
/* Transition screen */
#noVNC_transition {
transition: 0.5s ease-in-out;
display: flex;
opacity: 0;
visibility: hidden;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
color: white;
background: rgba(0, 0, 0, 0.5);
z-index: 50;
/*display: flex;*/
align-items: center;
justify-content: center;
flex-direction: column;
}
:root.noVNC_loading #noVNC_transition,
:root.noVNC_connecting #noVNC_transition,
:root.noVNC_disconnecting #noVNC_transition,
:root.noVNC_reconnecting #noVNC_transition {
opacity: 1;
visibility: visible;
}
:root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button {
display: none;
}
#noVNC_transition_text {
font-size: 1.5em;
}
/* Main container */
#noVNC_container {
width: 100%;
height: 100%;
background-color: #313131;
border-bottom-right-radius: 800px 600px;
/*border-top-left-radius: 800px 600px;*/
/* If selection isn't disabled, long-pressing stuff in the sidebar
can accidentally select the container or the canvas. This can
happen when attempting to move the handle. */
user-select: none;
-webkit-user-select: none;
}
#noVNC_keyboardinput {
width: 1px;
height: 1px;
background-color: #fff;
color: #fff;
border: 0;
position: absolute;
left: -40px;
z-index: -1;
ime-mode: disabled;
}
/*Default noVNC logo.*/
/* From: http://fonts.googleapis.com/css?family=Orbitron:700 */
@font-face {
font-family: 'Orbitron';
font-style: normal;
font-weight: 700;
src: local('?'), url('Orbitron700.woff') format('woff'),
url('Orbitron700.ttf') format('truetype');
}
.noVNC_logo {
color: var(--novnc-yellow);
font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
line-height: 0.9;
text-shadow: 0.1em 0.1em 0 black;
}
.noVNC_logo span{
color: var(--novnc-green);
}
#noVNC_bell {
display: none;
}
/* ----------------------------------------
* Media sizing
* ----------------------------------------
*/
@media screen and (max-width: 640px){
#noVNC_logo {
font-size: 150px;
}
}
@media screen and (min-width: 321px) and (max-width: 480px) {
#noVNC_logo {
font-size: 110px;
}
}
@media screen and (max-width: 320px) {
#noVNC_logo {
font-size: 90px;
}
}
================================================
FILE: services/gateway/noVNC/app/styles/constants.css
================================================
/*
* noVNC general CSS constant variables
* Copyright (C) 2025 The noVNC authors
* noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
* This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
*/
/* ---------- COLORS ----------- */
:root {
--novnc-grey: rgb(128, 128, 128);
--novnc-lightgrey: rgb(192, 192, 192);
--novnc-darkgrey: rgb(92, 92, 92);
/* Transparent to make button colors adapt to the background */
--novnc-buttongrey: rgba(192, 192, 192, 0.5);
--novnc-blue: rgb(110, 132, 163);
--novnc-lightblue: rgb(74, 144, 217);
--novnc-darkblue: rgb(83, 99, 122);
--novnc-green: rgb(0, 128, 0);
--novnc-yellow: rgb(255, 255, 0);
}
/* ------ MISC PROPERTIES ------ */
:root {
--input-xpadding: 1em;
}
================================================
FILE: services/gateway/noVNC/app/styles/input.css
================================================
/*
* noVNC general input element CSS
* Copyright (C) 2025 The noVNC authors
* noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
* This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
*/
/* ------- SHARED BETWEEN INPUT ELEMENTS -------- */
input,
textarea,
button,
select,
input::file-selector-button {
padding: 0.5em var(--input-xpadding);
border-radius: 6px;
appearance: none;
text-overflow: ellipsis;
/* Respect standard font settings */
font: inherit;
line-height: 1.6;
}
input:disabled,
textarea:disabled,
button:disabled,
select:disabled,
label[disabled] {
opacity: 0.4;
}
input:focus-visible,
textarea:focus-visible,
button:focus-visible,
select:focus-visible,
input:focus-visible::file-selector-button {
outline: 2px solid var(--novnc-lightblue);
outline-offset: 1px;
}
/* ------- TEXT INPUT -------- */
input:not([type]),
input[type=date],
input[type=datetime-local],
input[type=email],
input[type=month],
input[type=number],
input[type=password],
input[type=search],
input[type=tel],
input[type=text],
input[type=time],
input[type=url],
input[type=week],
textarea {
border: 1px solid var(--novnc-lightgrey);
/* Account for borders on text inputs, buttons dont have borders */
padding: calc(0.5em - 1px) var(--input-xpadding);
}
input:not([type]):focus-visible,
input[type=date]:focus-visible,
input[type=datetime-local]:focus-visible,
input[type=email]:focus-visible,
input[type=month]:focus-visible,
input[type=number]:focus-visible,
input[type=password]:focus-visible,
input[type=search]:focus-visible,
input[type=tel]:focus-visible,
input[type=text]:focus-visible,
input[type=time]:focus-visible,
input[type=url]:focus-visible,
input[type=week]:focus-visible,
textarea:focus-visible {
outline-offset: -1px;
}
textarea {
margin: unset; /* Remove Firefox's built in margin */
/* Prevent layout from shifting when scrollbars show */
scrollbar-gutter: stable;
/* Make textareas show at minimum one line. This does not work when
using box-sizing border-box, in which case, vertical padding and
border width needs to be taken into account. */
min-height: 1lh;
vertical-align: baseline; /* Firefox gives "text-bottom" by default */
}
/* ------- NUMBER PICKERS ------- */
/* We can't style the number spinner buttons:
https://github.com/w3c/csswg-drafts/issues/8777 */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
/* Get rid of increase/decrease buttons in WebKit */
appearance: none;
}
input[type=number] {
/* Get rid of increase/decrease buttons in Firefox */
appearance: textfield;
}
/* ------- BUTTON ACTIVATIONS -------- */
/* A color overlay that depends on the activation level. The level can then be
set for different states on an element, for example hover and click on a