From 54cdc999829bda55c1c2cd65d288428c1eeb5ca7 Mon Sep 17 00:00:00 2001 From: Damian Sieradzki Date: Mon, 9 Oct 2023 09:03:40 +0200 Subject: [PATCH] feat: Refactoring, added e2e tests, change os image to lts, added newer microk8s versions --- .gitignore | 3 +- Cargo.toml | 9 +- Dockerfile | 17 +- Jenkinsfile | 23 + README.md | 2 +- core/Cargo.toml | 21 + .../src}/dispatcher/dispatcher.rs | 14 +- .../operator => core/src}/dispatcher/mod.rs | 6 +- .../dispatcher/usecase/add_node_to_cluster.rs | 143 +++++ .../dispatcher/usecase/change_resources.rs | 8 +- core/src/dispatcher/usecase/common.rs | 524 ++++++++++++++++++ core/src/dispatcher/usecase/create_cluster.rs | 334 +++++++++++ core/src/dispatcher/usecase/delete_cluster.rs | 82 +++ .../usecase/delete_node_from_cluster.rs | 186 +++++++ .../src}/dispatcher/usecase/mod.rs | 1 - .../operator => core/src}/dispatcher/utils.rs | 26 +- {makoon/src/operator => core/src}/error.rs | 29 +- {makoon/src/operator => core/src}/event.rs | 2 +- .../src/operator => core/src}/generator.rs | 103 ++-- makoon/src/operator/mod.rs => core/src/lib.rs | 20 +- {makoon/src/operator => core/src}/model.rs | 17 +- {makoon/src/operator => core/src}/operator.rs | 465 ++++++++++------ .../src/operator => core/src}/repository.rs | 18 +- .../operator => core/src}/repository_json.rs | 78 +-- core/src/supported.rs | 20 + helm-client/Cargo.toml | 8 + .../src}/command_builder.rs | 38 +- .../src/helm/mod.rs => helm-client/src/lib.rs | 2 +- makoon/Cargo.toml | 24 - makoon/src/handlers/actix.rs | 69 --- makoon/src/handlers/cluster.rs | 186 ------- makoon/src/handlers/network.rs | 32 -- makoon/src/handlers/nodes.rs | 26 - .../dispatcher/usecase/add_node_to_cluster.rs | 86 --- .../src/operator/dispatcher/usecase/common.rs | 381 ------------- .../dispatcher/usecase/create_cluster.rs | 236 -------- .../dispatcher/usecase/delete_cluster.rs | 51 -- .../usecase/delete_node_from_cluster.rs | 102 ---- makoon/src/operator/ssh/mod.rs | 3 - {proxmox => proxmox-client}/Cargo.toml | 2 +- {proxmox => proxmox-client}/src/client.rs | 0 .../src/client_operations.rs | 0 {proxmox => proxmox-client}/src/error.rs | 0 {proxmox => proxmox-client}/src/http.rs | 49 +- {proxmox => proxmox-client}/src/lib.rs | 0 {proxmox => proxmox-client}/src/model.rs | 0 ssh-client/Cargo.toml | 12 + ssh-client/src/lib.rs | 5 + .../operator/ssh => ssh-client/src}/ssh.rs | 61 +- web/Cargo.toml | 31 ++ {makoon => web}/Makefile | 0 {makoon => web}/src-web/.gitignore | 0 {makoon => web}/src-web/index.html | 0 {makoon => web}/src-web/package.json | 0 {makoon => web}/src-web/pnpm-lock.yaml | 8 +- {makoon => web}/src-web/postcss.config.js | 0 {makoon => web}/src-web/public/favicon.ico | Bin {makoon => web}/src-web/src/App.css | 0 {makoon => web}/src-web/src/App.tsx | 0 {makoon => web}/src-web/src/Router.tsx | 0 {makoon => web}/src-web/src/api/api.ts | 4 +- {makoon => web}/src-web/src/api/apps.ts | 0 {makoon => web}/src-web/src/api/auth.ts | 0 .../src-web/src/api/cluster_resources.ts | 0 {makoon => web}/src-web/src/api/clusters.ts | 0 {makoon => web}/src-web/src/api/model.ts | 9 + {makoon => web}/src-web/src/api/networks.ts | 0 {makoon => web}/src-web/src/api/nodes.ts | 0 web/src-web/src/api/settings.ts | 11 + {makoon => web}/src-web/src/api/storage.ts | 0 .../assets/fonts/Poppins/Poppins-Black.ttf | Bin .../fonts/Poppins/Poppins-BlackItalic.ttf | Bin .../src/assets/fonts/Poppins/Poppins-Bold.ttf | Bin .../fonts/Poppins/Poppins-BoldItalic.ttf | Bin .../fonts/Poppins/Poppins-ExtraBold.ttf | Bin .../fonts/Poppins/Poppins-ExtraBoldItalic.ttf | Bin .../fonts/Poppins/Poppins-ExtraLight.ttf | Bin .../Poppins/Poppins-ExtraLightItalic.ttf | Bin .../assets/fonts/Poppins/Poppins-Italic.ttf | Bin .../assets/fonts/Poppins/Poppins-Light.ttf | Bin .../fonts/Poppins/Poppins-LightItalic.ttf | Bin .../assets/fonts/Poppins/Poppins-Medium.ttf | Bin .../fonts/Poppins/Poppins-MediumItalic.ttf | Bin .../assets/fonts/Poppins/Poppins-Regular.ttf | Bin .../assets/fonts/Poppins/Poppins-SemiBold.ttf | Bin .../fonts/Poppins/Poppins-SemiBoldItalic.ttf | Bin .../src/assets/fonts/Poppins/Poppins-Thin.ttf | Bin .../fonts/Poppins/Poppins-ThinItalic.ttf | Bin .../src-web/src/assets/fonts/fonts.css | 0 .../src-web/src/assets/images/makonn_logo.svg | 0 .../src/assets/images/makonn_logo_wh.svg | 0 .../src-web/src/components/Content.tsx | 0 .../src/components/ErrorPanel/ErrorPanel.css | 0 .../src/components/ErrorPanel/ErrorPanel.tsx | 0 .../src-web/src/components/FormError.tsx | 0 .../src-web/src/components/Header.tsx | 0 .../src-web/src/components/HiddenPassword.tsx | 0 .../src-web/src/components/LogoContainer.tsx | 0 .../src-web/src/components/MainContainer.tsx | 0 .../src-web/src/components/MainMenu.tsx | 0 .../src-web/src/components/Panel.tsx | 0 .../src-web/src/components/Section.tsx | 0 .../src/components/StorageDropdownOption.tsx | 0 {makoon => web}/src-web/src/constants.ts | 0 {makoon => web}/src-web/src/main.tsx | 0 .../src-web/src/store/application-store.ts | 0 .../src/store/cluster-creator-store.ts | 0 .../src/store/cluster-management-store.ts | 0 .../src-web/src/store/clusters-list-store.ts | 0 .../src/store/processing-indicator-store.ts | 0 {makoon => web}/src-web/src/style.css | 0 .../src-web/src/theme/theme-base/_colors.scss | 0 .../src-web/src/theme/theme-base/_common.scss | 0 .../src/theme/theme-base/_components.scss | 0 .../src-web/src/theme/theme-base/_mixins.scss | 0 .../theme-base/components/button/_button.scss | 0 .../components/button/_speeddial.scss | 0 .../components/button/_splitbutton.scss | 0 .../theme-base/components/data/_carousel.scss | 0 .../components/data/_datascroller.scss | 0 .../components/data/_datatable.scss | 0 .../theme-base/components/data/_dataview.scss | 0 .../theme-base/components/data/_filter.scss | 0 .../components/data/_fullcalendar.scss | 0 .../components/data/_orderlist.scss | 0 .../components/data/_organizationchart.scss | 0 .../components/data/_paginator.scss | 0 .../theme-base/components/data/_picklist.scss | 0 .../theme-base/components/data/_timeline.scss | 0 .../theme-base/components/data/_tree.scss | 0 .../components/data/_treetable.scss | 0 .../components/file/_fileupload.scss | 0 .../components/input/_autocomplete.scss | 0 .../components/input/_calendar.scss | 0 .../components/input/_cascadeselect.scss | 0 .../components/input/_checkbox.scss | 0 .../theme-base/components/input/_chips.scss | 0 .../components/input/_colorpicker.scss | 0 .../components/input/_dropdown.scss | 0 .../theme-base/components/input/_editor.scss | 0 .../components/input/_inputgroup.scss | 0 .../components/input/_inputnumber.scss | 0 .../components/input/_inputswitch.scss | 0 .../components/input/_inputtext.scss | 0 .../theme-base/components/input/_listbox.scss | 0 .../theme-base/components/input/_mention.scss | 0 .../components/input/_multiselect.scss | 0 .../components/input/_password.scss | 0 .../components/input/_radiobutton.scss | 0 .../theme-base/components/input/_rating.scss | 0 .../components/input/_selectbutton.scss | 0 .../theme-base/components/input/_slider.scss | 0 .../components/input/_togglebutton.scss | 0 .../components/input/_treeselect.scss | 0 .../components/menu/_breadcrumb.scss | 0 .../components/menu/_contextmenu.scss | 0 .../theme-base/components/menu/_dock.scss | 0 .../theme-base/components/menu/_megamenu.scss | 0 .../theme-base/components/menu/_menu.scss | 0 .../theme-base/components/menu/_menubar.scss | 0 .../components/menu/_panelmenu.scss | 0 .../components/menu/_slidemenu.scss | 0 .../theme-base/components/menu/_steps.scss | 0 .../theme-base/components/menu/_tabmenu.scss | 0 .../components/menu/_tieredmenu.scss | 0 .../components/messages/_inlinemessage.scss | 0 .../components/messages/_message.scss | 0 .../components/messages/_toast.scss | 0 .../theme-base/components/misc/_avatar.scss | 0 .../theme-base/components/misc/_badge.scss | 0 .../theme-base/components/misc/_blockui.scss | 0 .../theme-base/components/misc/_chip.scss | 0 .../theme-base/components/misc/_inplace.scss | 0 .../components/misc/_progressbar.scss | 0 .../components/misc/_scrolltop.scss | 0 .../theme-base/components/misc/_skeleton.scss | 0 .../theme-base/components/misc/_tag.scss | 0 .../theme-base/components/misc/_terminal.scss | 0 .../components/multimedia/_galleria.scss | 0 .../components/multimedia/_image.scss | 0 .../components/overlay/_confirmpopup.scss | 0 .../components/overlay/_dialog.scss | 0 .../components/overlay/_overlaypanel.scss | 0 .../components/overlay/_sidebar.scss | 0 .../components/overlay/_tooltip.scss | 0 .../components/panel/_accordion.scss | 0 .../theme-base/components/panel/_card.scss | 0 .../theme-base/components/panel/_divider.scss | 0 .../components/panel/_fieldset.scss | 0 .../theme-base/components/panel/_panel.scss | 0 .../components/panel/_scrollpanel.scss | 0 .../components/panel/_splitter.scss | 0 .../theme-base/components/panel/_tabview.scss | 0 .../theme-base/components/panel/_toolbar.scss | 0 .../fluent/fluent-light/_extensions.scss | 0 .../themes/fluent/fluent-light/_fonts.scss | 0 .../fluent/fluent-light/_variables.scss | 0 .../themes/fluent/fluent-light/theme.scss | 0 {makoon => web}/src-web/src/utils/api.ts | 0 {makoon => web}/src-web/src/utils/hooks.ts | 0 {makoon => web}/src-web/src/utils/nodes.ts | 0 {makoon => web}/src-web/src/utils/patterns.ts | 0 {makoon => web}/src-web/src/utils/size.ts | 0 .../cluster-creator/CreatorNavigator.tsx | 0 .../src/views/cluster-creator/context.ts | 0 .../src/views/cluster-creator/index.tsx | 0 .../steps/apps/CreatorHelmAppDialog.tsx | 0 .../steps/apps/HelmAppsSection.tsx | 0 .../cluster-creator/steps/apps/index.tsx | 0 .../cluster-creator/steps/cluster/index.tsx | 0 .../steps/nodes/CreatorNodeDialog.tsx | 0 .../steps/nodes/NodesSection.tsx | 0 .../steps/nodes/TableNodes.tsx | 0 .../cluster-creator/steps/nodes/index.tsx | 0 .../cluster-creator/steps/settings/index.tsx | 34 +- .../workloads/CreatorWorkloadsDialog.tsx | 0 .../steps/workloads/WorkloadsSection.tsx | 0 .../cluster-creator/steps/workloads/index.tsx | 0 .../components/ClusterListPanel.tsx | 0 .../cluster-list/components/ClusterStatus.tsx | 0 .../components/UsedResourcesPanel.tsx | 0 .../src-web/src/views/cluster-list/index.tsx | 0 .../components/apps/HelmAppDialog.tsx | 0 .../components/apps/HelmAppStatus.tsx | 0 .../components/apps/index.tsx | 0 .../components/cluster-logs/LogLevel.tsx | 0 .../components/cluster-logs/index.tsx | 0 .../components/nodes/AddNodeDialog.tsx | 0 .../components/nodes/EditNodeDialog.tsx | 0 .../components/nodes/KubeStatusInfo.tsx | 0 .../components/nodes/NodesTable.tsx | 0 .../components/nodes/VmStatusInfo.tsx | 0 .../components/nodes/index.tsx | 0 .../components/workloads/WorkloadsDialog.tsx | 0 .../components/workloads/index.tsx | 0 .../src/views/clusters-management/index.tsx | 3 +- .../src-web/src/views/login/index.tsx | 0 .../src-web/src/views/settings/index.tsx | 0 {makoon => web}/src-web/src/vite-env.d.ts | 0 {makoon => web}/src-web/tailwind.config.js | 0 {makoon => web}/src-web/tsconfig.json | 0 {makoon => web}/src-web/tsconfig.node.json | 0 {makoon => web}/src-web/vite.config.ts | 0 web/src/handlers/actix.rs | 63 +++ {makoon => web}/src/handlers/apps.rs | 50 +- {makoon => web}/src/handlers/auth.rs | 28 +- web/src/handlers/cluster.rs | 233 ++++++++ .../src/handlers/cluster_resources.rs | 41 +- {makoon => web}/src/handlers/error.rs | 42 +- {makoon => web}/src/handlers/export.rs | 17 +- {makoon => web}/src/handlers/mod.rs | 13 +- {makoon => web}/src/handlers/model.rs | 35 +- web/src/handlers/network.rs | 35 ++ web/src/handlers/nodes.rs | 26 + web/src/handlers/settings.rs | 22 + {makoon => web}/src/handlers/storage.rs | 19 +- web/src/lib.rs | 17 + {makoon => web}/src/main.rs | 57 +- web/tests/common/makoon_client.rs | 98 ++++ web/tests/common/mod.rs | 2 + web/tests/e2e_create_cluster.rs | 90 +++ {makoon => web}/typeshare.toml | 0 262 files changed, 2823 insertions(+), 1689 deletions(-) create mode 100644 Jenkinsfile create mode 100644 core/Cargo.toml rename {makoon/src/operator => core/src}/dispatcher/dispatcher.rs (96%) rename {makoon/src/operator => core/src}/dispatcher/mod.rs (100%) create mode 100644 core/src/dispatcher/usecase/add_node_to_cluster.rs rename {makoon/src/operator => core/src}/dispatcher/usecase/change_resources.rs (91%) create mode 100644 core/src/dispatcher/usecase/common.rs create mode 100644 core/src/dispatcher/usecase/create_cluster.rs create mode 100644 core/src/dispatcher/usecase/delete_cluster.rs create mode 100644 core/src/dispatcher/usecase/delete_node_from_cluster.rs rename {makoon/src/operator => core/src}/dispatcher/usecase/mod.rs (99%) rename {makoon/src/operator => core/src}/dispatcher/utils.rs (58%) rename {makoon/src/operator => core/src}/error.rs (58%) rename {makoon/src/operator => core/src}/event.rs (93%) rename {makoon/src/operator => core/src}/generator.rs (58%) rename makoon/src/operator/mod.rs => core/src/lib.rs (79%) rename {makoon/src/operator => core/src}/model.rs (96%) rename {makoon/src/operator => core/src}/operator.rs (52%) rename {makoon/src/operator => core/src}/repository.rs (83%) rename {makoon/src/operator => core/src}/repository_json.rs (56%) create mode 100644 core/src/supported.rs create mode 100644 helm-client/Cargo.toml rename {makoon/src/helm => helm-client/src}/command_builder.rs (90%) rename makoon/src/helm/mod.rs => helm-client/src/lib.rs (98%) delete mode 100644 makoon/Cargo.toml delete mode 100644 makoon/src/handlers/actix.rs delete mode 100644 makoon/src/handlers/cluster.rs delete mode 100644 makoon/src/handlers/network.rs delete mode 100644 makoon/src/handlers/nodes.rs delete mode 100644 makoon/src/operator/dispatcher/usecase/add_node_to_cluster.rs delete mode 100644 makoon/src/operator/dispatcher/usecase/common.rs delete mode 100644 makoon/src/operator/dispatcher/usecase/create_cluster.rs delete mode 100644 makoon/src/operator/dispatcher/usecase/delete_cluster.rs delete mode 100644 makoon/src/operator/dispatcher/usecase/delete_node_from_cluster.rs delete mode 100644 makoon/src/operator/ssh/mod.rs rename {proxmox => proxmox-client}/Cargo.toml (93%) rename {proxmox => proxmox-client}/src/client.rs (100%) rename {proxmox => proxmox-client}/src/client_operations.rs (100%) rename {proxmox => proxmox-client}/src/error.rs (100%) rename {proxmox => proxmox-client}/src/http.rs (78%) rename {proxmox => proxmox-client}/src/lib.rs (100%) rename {proxmox => proxmox-client}/src/model.rs (100%) create mode 100644 ssh-client/Cargo.toml create mode 100644 ssh-client/src/lib.rs rename {makoon/src/operator/ssh => ssh-client/src}/ssh.rs (69%) create mode 100644 web/Cargo.toml rename {makoon => web}/Makefile (100%) rename {makoon => web}/src-web/.gitignore (100%) rename {makoon => web}/src-web/index.html (100%) rename {makoon => web}/src-web/package.json (100%) rename {makoon => web}/src-web/pnpm-lock.yaml (100%) rename {makoon => web}/src-web/postcss.config.js (100%) rename {makoon => web}/src-web/public/favicon.ico (100%) rename {makoon => web}/src-web/src/App.css (100%) rename {makoon => web}/src-web/src/App.tsx (100%) rename {makoon => web}/src-web/src/Router.tsx (100%) rename {makoon => web}/src-web/src/api/api.ts (83%) rename {makoon => web}/src-web/src/api/apps.ts (100%) rename {makoon => web}/src-web/src/api/auth.ts (100%) rename {makoon => web}/src-web/src/api/cluster_resources.ts (100%) rename {makoon => web}/src-web/src/api/clusters.ts (100%) rename {makoon => web}/src-web/src/api/model.ts (96%) rename {makoon => web}/src-web/src/api/networks.ts (100%) rename {makoon => web}/src-web/src/api/nodes.ts (100%) create mode 100644 web/src-web/src/api/settings.ts rename {makoon => web}/src-web/src/api/storage.ts (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-Black.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-BlackItalic.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-Bold.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-BoldItalic.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-ExtraBold.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-ExtraBoldItalic.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-ExtraLight.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-ExtraLightItalic.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-Italic.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-Light.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-LightItalic.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-Medium.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-MediumItalic.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-Regular.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-SemiBold.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-SemiBoldItalic.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-Thin.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/Poppins/Poppins-ThinItalic.ttf (100%) rename {makoon => web}/src-web/src/assets/fonts/fonts.css (100%) rename {makoon => web}/src-web/src/assets/images/makonn_logo.svg (100%) rename {makoon => web}/src-web/src/assets/images/makonn_logo_wh.svg (100%) rename {makoon => web}/src-web/src/components/Content.tsx (100%) rename {makoon => web}/src-web/src/components/ErrorPanel/ErrorPanel.css (100%) rename {makoon => web}/src-web/src/components/ErrorPanel/ErrorPanel.tsx (100%) rename {makoon => web}/src-web/src/components/FormError.tsx (100%) rename {makoon => web}/src-web/src/components/Header.tsx (100%) rename {makoon => web}/src-web/src/components/HiddenPassword.tsx (100%) rename {makoon => web}/src-web/src/components/LogoContainer.tsx (100%) rename {makoon => web}/src-web/src/components/MainContainer.tsx (100%) rename {makoon => web}/src-web/src/components/MainMenu.tsx (100%) rename {makoon => web}/src-web/src/components/Panel.tsx (100%) rename {makoon => web}/src-web/src/components/Section.tsx (100%) rename {makoon => web}/src-web/src/components/StorageDropdownOption.tsx (100%) rename {makoon => web}/src-web/src/constants.ts (100%) rename {makoon => web}/src-web/src/main.tsx (100%) rename {makoon => web}/src-web/src/store/application-store.ts (100%) rename {makoon => web}/src-web/src/store/cluster-creator-store.ts (100%) rename {makoon => web}/src-web/src/store/cluster-management-store.ts (100%) rename {makoon => web}/src-web/src/store/clusters-list-store.ts (100%) rename {makoon => web}/src-web/src/store/processing-indicator-store.ts (100%) rename {makoon => web}/src-web/src/style.css (100%) rename {makoon => web}/src-web/src/theme/theme-base/_colors.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/_common.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/_components.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/_mixins.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/button/_button.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/button/_speeddial.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/button/_splitbutton.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_carousel.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_datascroller.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_datatable.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_dataview.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_filter.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_fullcalendar.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_orderlist.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_organizationchart.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_paginator.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_picklist.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_timeline.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_tree.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/data/_treetable.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/file/_fileupload.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_autocomplete.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_calendar.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_cascadeselect.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_checkbox.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_chips.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_colorpicker.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_dropdown.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_editor.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_inputgroup.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_inputnumber.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_inputswitch.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_inputtext.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_listbox.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_mention.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_multiselect.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_password.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_radiobutton.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_rating.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_selectbutton.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_slider.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_togglebutton.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/input/_treeselect.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_breadcrumb.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_contextmenu.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_dock.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_megamenu.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_menu.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_menubar.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_panelmenu.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_slidemenu.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_steps.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_tabmenu.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/menu/_tieredmenu.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/messages/_inlinemessage.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/messages/_message.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/messages/_toast.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/misc/_avatar.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/misc/_badge.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/misc/_blockui.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/misc/_chip.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/misc/_inplace.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/misc/_progressbar.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/misc/_scrolltop.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/misc/_skeleton.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/misc/_tag.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/misc/_terminal.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/multimedia/_galleria.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/multimedia/_image.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/overlay/_confirmpopup.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/overlay/_dialog.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/overlay/_overlaypanel.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/overlay/_sidebar.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/overlay/_tooltip.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/panel/_accordion.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/panel/_card.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/panel/_divider.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/panel/_fieldset.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/panel/_panel.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/panel/_scrollpanel.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/panel/_splitter.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/panel/_tabview.scss (100%) rename {makoon => web}/src-web/src/theme/theme-base/components/panel/_toolbar.scss (100%) rename {makoon => web}/src-web/src/theme/themes/fluent/fluent-light/_extensions.scss (100%) rename {makoon => web}/src-web/src/theme/themes/fluent/fluent-light/_fonts.scss (100%) rename {makoon => web}/src-web/src/theme/themes/fluent/fluent-light/_variables.scss (100%) rename {makoon => web}/src-web/src/theme/themes/fluent/fluent-light/theme.scss (100%) rename {makoon => web}/src-web/src/utils/api.ts (100%) rename {makoon => web}/src-web/src/utils/hooks.ts (100%) rename {makoon => web}/src-web/src/utils/nodes.ts (100%) rename {makoon => web}/src-web/src/utils/patterns.ts (100%) rename {makoon => web}/src-web/src/utils/size.ts (100%) rename {makoon => web}/src-web/src/views/cluster-creator/CreatorNavigator.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/context.ts (100%) rename {makoon => web}/src-web/src/views/cluster-creator/index.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/apps/CreatorHelmAppDialog.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/apps/HelmAppsSection.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/apps/index.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/cluster/index.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/nodes/CreatorNodeDialog.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/nodes/NodesSection.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/nodes/TableNodes.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/nodes/index.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/settings/index.tsx (90%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/workloads/CreatorWorkloadsDialog.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/workloads/WorkloadsSection.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-creator/steps/workloads/index.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-list/components/ClusterListPanel.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-list/components/ClusterStatus.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-list/components/UsedResourcesPanel.tsx (100%) rename {makoon => web}/src-web/src/views/cluster-list/index.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/apps/HelmAppDialog.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/apps/HelmAppStatus.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/apps/index.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/cluster-logs/LogLevel.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/cluster-logs/index.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/nodes/AddNodeDialog.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/nodes/EditNodeDialog.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/nodes/KubeStatusInfo.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/nodes/NodesTable.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/nodes/VmStatusInfo.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/nodes/index.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/workloads/WorkloadsDialog.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/components/workloads/index.tsx (100%) rename {makoon => web}/src-web/src/views/clusters-management/index.tsx (98%) rename {makoon => web}/src-web/src/views/login/index.tsx (100%) rename {makoon => web}/src-web/src/views/settings/index.tsx (100%) rename {makoon => web}/src-web/src/vite-env.d.ts (100%) rename {makoon => web}/src-web/tailwind.config.js (100%) rename {makoon => web}/src-web/tsconfig.json (100%) rename {makoon => web}/src-web/tsconfig.node.json (100%) rename {makoon => web}/src-web/vite.config.ts (100%) create mode 100644 web/src/handlers/actix.rs rename {makoon => web}/src/handlers/apps.rs (53%) rename {makoon => web}/src/handlers/auth.rs (68%) create mode 100644 web/src/handlers/cluster.rs rename {makoon => web}/src/handlers/cluster_resources.rs (53%) rename {makoon => web}/src/handlers/error.rs (57%) rename {makoon => web}/src/handlers/export.rs (70%) rename {makoon => web}/src/handlers/mod.rs (91%) rename {makoon => web}/src/handlers/model.rs (67%) create mode 100644 web/src/handlers/network.rs create mode 100644 web/src/handlers/nodes.rs create mode 100644 web/src/handlers/settings.rs rename {makoon => web}/src/handlers/storage.rs (63%) create mode 100644 web/src/lib.rs rename {makoon => web}/src/main.rs (76%) create mode 100644 web/tests/common/makoon_client.rs create mode 100644 web/tests/common/mod.rs create mode 100644 web/tests/e2e_create_cluster.rs rename {makoon => web}/typeshare.toml (100%) diff --git a/.gitignore b/.gitignore index e2a324a..69a8c3e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ Cargo.lock **/*.rs.bk **/*.iml - +.vscode +makoon.json diff --git a/Cargo.toml b/Cargo.toml index de4cded..68c86d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,10 @@ [workspace] +resolver = "2" + members = [ - "proxmox", - "makoon" + "core", + "web", + "proxmox-client", + "helm-client", + "ssh-client" ] \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e6ae050..dc7d7a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM docker.io/rust:1.71.1 as build +FROM docker.io/rust:1.73.0 as build # Install and configure NODE using NVM RUN mkdir /usr/local/nvm ENV NVM_DIR /usr/local/nvm -ENV NODE_VERSION 18.17.1 +ENV NODE_VERSION 18.18.0 RUN curl https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash \ && . $NVM_DIR/nvm.sh \ && nvm install $NODE_VERSION \ @@ -16,12 +16,19 @@ RUN npm install -g pnpm RUN mkdir /build WORKDIR /build COPY . . -RUN cd makoon/src-web; \ +RUN cd web/src-web; \ pnpm install; \ pnpm build; + +RUN cargo test RUN cargo build --release -FROM debian:bullseye-slim +FROM debian:bookworm-slim +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install libssl3 -y && \ + apt-get clean + RUN mkdir /app WORKDIR /app COPY --from=build /build/target/release/makoon . @@ -29,4 +36,4 @@ ENV RUST_LOG="info" ENV MAKOON_DB_PATH="/app/data/makoon.db" ENV MAKOON_SERVER_PORT=8080 VOLUME /app/data -CMD ["/app/makoon"] \ No newline at end of file +CMD ["/app/makoon"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..74a7d5f --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,23 @@ +pipeline { + agent { + docker { image 'rust:1.73.0-bullseye' } + } + stages { + stage('Checkout') { + steps { + sh 'rm -rf makoon' + sh 'git clone -b integration_tests --depth 1 https://github.com/dsieradzki/makoon' + } + } + stage('Test') { + steps { + dir('makoon') { + withCredentials([usernamePassword(credentialsId: 'proxmox', usernameVariable: 'PROXMOX_USER', passwordVariable: 'PROXMOX_PASSWORD')]) { + sh 'mkdir web/src-web/dist/ -p && touch web/src-web/dist/dummy' + sh 'cargo test --features e2e -- --nocapture' + } + } + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index fb35df3..ec83c29 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

## Table of contents diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..f9116af --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proxmox-client = { path = "../proxmox-client" } +helm-client = { path = "../helm-client" } +ssh-client = { path = "../ssh-client" } +openssl = "0.10" +pem = "3.0" +ssh-keys = "0.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +typeshare = "1.0" +chrono = { version = "0.4", features = ["serde"] } +log = "0.4" +env_logger = "0.10" +uuid = { version = "1.4", features = ["v4", "fast-rng"] } diff --git a/makoon/src/operator/dispatcher/dispatcher.rs b/core/src/dispatcher/dispatcher.rs similarity index 96% rename from makoon/src/operator/dispatcher/dispatcher.rs rename to core/src/dispatcher/dispatcher.rs index 2246889..158af48 100644 --- a/makoon/src/operator/dispatcher/dispatcher.rs +++ b/core/src/dispatcher/dispatcher.rs @@ -1,19 +1,20 @@ use std::sync::Arc; +use log::info; +use crate::dispatcher::usecase; +use crate::event::Event; +use crate::model::{ClusterStatus, LogEntry}; +use crate::Repository; -use crate::operator::dispatcher::usecase; -use crate::operator::event::Event; -use crate::operator::model::{ClusterStatus, LogEntry}; -use crate::operator::repository::Repository; use super::usecase::change_resources; pub struct Dispatcher { - proxmox_client: Arc, + proxmox_client: Arc, repo: Arc, } impl Dispatcher { - pub fn new(proxmox_client: Arc, repo: Arc) -> Self { + pub fn new(proxmox_client: Arc, repo: Arc) -> Self { Dispatcher { proxmox_client, repo, @@ -200,7 +201,6 @@ impl Dispatcher { ClusterStatus::Sync, )?; - let mut cluster = self .repo .get_cluster(&cluster_name)? diff --git a/makoon/src/operator/dispatcher/mod.rs b/core/src/dispatcher/mod.rs similarity index 100% rename from makoon/src/operator/dispatcher/mod.rs rename to core/src/dispatcher/mod.rs index 6f101c3..8e2dd93 100644 --- a/makoon/src/operator/dispatcher/mod.rs +++ b/core/src/dispatcher/mod.rs @@ -1,8 +1,8 @@ pub use dispatcher::*; mod dispatcher; -mod utils; mod usecase; -pub use usecase::HELM_CMD; -pub use usecase::install_helm_app; +mod utils; pub use usecase::install_cluster_resource; +pub use usecase::install_helm_app; +pub use usecase::HELM_CMD; diff --git a/core/src/dispatcher/usecase/add_node_to_cluster.rs b/core/src/dispatcher/usecase/add_node_to_cluster.rs new file mode 100644 index 0000000..bcfe0d9 --- /dev/null +++ b/core/src/dispatcher/usecase/add_node_to_cluster.rs @@ -0,0 +1,143 @@ +use std::collections::HashMap; +use std::sync::Arc; +use log::info; + +use proxmox_client::model::AccessData; +use proxmox_client::Client; +use crate::dispatcher::usecase::common; +use crate::model::{Cluster, ClusterNode, ClusterNodeType, LogEntry}; +use crate::Repository; + + +pub(crate) fn execute( + proxmox_client: Arc, + repo: Arc, + access: AccessData, + cluster_name: String, + node_name: String, +) -> Result<(), String> { + info!("Request to add node to the cluster has been received"); + let proxmox_client = proxmox_client.operations(access); + + repo.save_log(LogEntry::info( + &cluster_name, + "Start creating node".to_string(), + ))?; + + let cluster = repo + .get_cluster(&cluster_name)? + .ok_or("Cannot find cluster")?; + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master && i.name != node_name) + .cloned() + .ok_or("Cannot find any master node".to_string())?; + + let exising_cluster_hosts = cluster + .nodes + .iter() + .filter(|i| i.name != node_name) + .map(|i| { + ( + format!("{}-{}", cluster.cluster_name, i.name), + i.ip_address.clone(), + ) + }) + .collect::>(); + + let existing_nodes = common::vm::get_existing_vms(&proxmox_client, &cluster)? + .into_iter() + .filter(|i| i.name != node_name) + .collect(); + + let node_to_add = cluster + .nodes + .iter() + .find(|i| i.name == node_name) + .ok_or("Cannot find node to create")?; + + common::vm::create(&proxmox_client, repo.clone(), &cluster, node_to_add)?; + + proxmox_client + .start_vm(&cluster.node, node_to_add.vm_id) + .map_err(|e| format!("Cannot start VM [{}]: {}", node_to_add.vm_id, e))?; + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Starting VM [{}]", node_to_add.vm_id), + ))?; + + common::vm::wait_for_start(&proxmox_client, &cluster, &node_to_add) + .map_err(|e| format!("Cannot start VM [{}]: {}", node_to_add.vm_id, e))?; + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("VM [{}] has been started", node_to_add.vm_id), + ))?; + + common::vm::restart_vm_if_necessary(&proxmox_client, repo.clone(), &cluster, node_to_add)?; + + setup_vm(repo.clone(), &cluster, node_to_add, exising_cluster_hosts)?; + + add_new_node_host_to_existing_cluster(repo.clone(), &cluster, existing_nodes)?; + + common::cluster::install_kubernetes(repo.clone(), &cluster, node_to_add)?; + + common::cluster::wait_for_ready_kubernetes(repo.clone(), &cluster, node_to_add)?; + + common::cluster::join_node_to_cluster(repo.clone(), &cluster, &master_node, node_to_add)?; + + Ok(()) +} + +fn add_new_node_host_to_existing_cluster( + repo: Arc, + cluster: &Cluster, + existing_nodes: Vec, +) -> Result<(), String> { + let hosts = cluster + .nodes + .iter() + .map(|i| { + ( + format!("{}-{}", cluster.cluster_name, i.name), + i.ip_address.clone(), + ) + }) + .collect::>(); + + for node in existing_nodes.iter() { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Add new node hostname to exising VM [{}]", node.vm_id), + ))?; + common::vm::setup_vm(cluster, node, &hosts)?; + } + Ok(()) +} + +fn setup_vm( + repo: Arc, + cluster: &Cluster, + node: &ClusterNode, + current_cluster_hosts: HashMap, +) -> Result<(), String> { + let mut hosts = cluster + .nodes + .iter() + .map(|i| { + ( + format!("{}-{}", cluster.cluster_name, i.name), + i.ip_address.clone(), + ) + }) + .collect::>(); + + hosts.extend(current_cluster_hosts); + + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Configure VM [{}]", node.vm_id), + ))?; + common::vm::setup_vm(cluster, node, &hosts)?; + Ok(()) +} diff --git a/makoon/src/operator/dispatcher/usecase/change_resources.rs b/core/src/dispatcher/usecase/change_resources.rs similarity index 91% rename from makoon/src/operator/dispatcher/usecase/change_resources.rs rename to core/src/dispatcher/usecase/change_resources.rs index 0f4d4dd..fcc7281 100644 --- a/makoon/src/operator/dispatcher/usecase/change_resources.rs +++ b/core/src/dispatcher/usecase/change_resources.rs @@ -1,12 +1,14 @@ use std::sync::Arc; +use log::info; -use proxmox::{ +use proxmox_client::{ model::{AccessData, VmConfig}, Client, }; +use crate::dispatcher::usecase::common; +use crate::model::LogEntry; +use crate::Repository; -use crate::operator::{model::LogEntry, Repository}; -use crate::operator::dispatcher::usecase::common; pub(crate) fn execute( proxmox_client: Arc, diff --git a/core/src/dispatcher/usecase/common.rs b/core/src/dispatcher/usecase/common.rs new file mode 100644 index 0000000..111e287 --- /dev/null +++ b/core/src/dispatcher/usecase/common.rs @@ -0,0 +1,524 @@ +pub(crate) mod vm { + use std::collections::HashMap; + use std::path::Path; + use std::sync::Arc; + use log::{error, info}; + + use proxmox_client::model::{ + CreateVirtualMachine, DownloadImage, DownloadImageContentType, OsType, ParamBuilder, + ResizeDisk, ScsiHw, StorageContent, VmStatus, + }; + use proxmox_client::{to_url_encoded, ClientOperations}; + use crate::dispatcher::utils::retry; + use crate::model::{Cluster, ClusterNode, LogEntry}; + use crate::Repository; + + + pub(crate) fn create( + proxmox_client: &ClientOperations, + repo: Arc, + cluster: &Cluster, + node: &ClusterNode, + ) -> Result<(), String> { + let os_image_path = download_os_image(proxmox_client, repo, cluster)?; + + proxmox_client.create_virtual_machine(CreateVirtualMachine { + vm_id: node.vm_id, + node: cluster.node.clone(), + name: format!("{}-{}", cluster.cluster_name, node.name), + cores: node.cores, + memory: node.memory, + os_type: OsType::L26, + net: HashMap::from([( + "net0".to_owned(), + ParamBuilder::default() + .add_param("model", "virtio") + .add_param("bridge", &cluster.network.bridge) + .build(), + )]), + scsihw: Some(ScsiHw::VirtioScsiPci), + scsi: HashMap::from([( + "scsi0".to_owned(), + ParamBuilder::default() + .add_param_with_separator(&node.storage_pool, "0", ":") + .add_param("import-from", &os_image_path) + .build(), + )]), + ide: HashMap::from([( + "ide2".to_owned(), + ParamBuilder::default() + .add_param_with_separator(&node.storage_pool, "cloudinit", ":") + .build(), + )]), + boot: Some(ParamBuilder::default().add_param("order", "scsi0").build()), + vga: Some("serial0".to_string()), + serial: HashMap::from([("serial0".to_owned(), "socket".to_owned())]), + ipconfig: HashMap::from([( + "ipconfig0".to_owned(), + ParamBuilder::default() + .add_param( + "ip", + format!("{}/{}", node.ip_address, cluster.network.subnet_mask).as_str(), + ) + .add_param("gw", cluster.network.gateway.as_str()) + .build(), + )]), + nameserver: Some(cluster.network.dns.clone()), + ci_user: Some(cluster.node_username.clone()), + ci_password: Some(cluster.node_password.clone()), + ssh_keys: Some(to_url_encoded(&cluster.ssh_key.public_key)), + })?; + + retry(|| { + let locked = proxmox_client + .virtual_machines(&cluster.node, Some(true))? + .iter() + .find(|i| i.vm_id == node.vm_id) + .map(|i| i.lock.is_some()) + .unwrap_or(false); + if locked { + Err(format!("VM [{}] is locked", node.vm_id)) + } else { + Ok(()) + } + })?; + + proxmox_client.resize_disk(ResizeDisk { + vm_id: node.vm_id, + node: cluster.node.clone(), + disk: "scsi0".to_string(), + size: format!("{}G", cluster.disk_size), + })?; + Ok(()) + } + + fn download_os_image( + proxmox_client: &ClientOperations, + repo: Arc, + cluster: &Cluster, + ) -> Result { + let os_image = cluster.os_image.clone().unwrap_or( + "https://cloud-images.ubuntu.com/kinetic/current/kinetic-server-cloudimg-amd64.img" + .to_string(), + ); + let os_image_storage = cluster + .os_image_storage + .clone() + .unwrap_or("local".to_string()); + + let file_name = Path::new(&os_image) + .file_name() + .map(|i| i.to_string_lossy().to_string()) + .ok_or("Cannot extract file name for path".to_string())?; + + let get_storage_content = || -> Result, String> { + Ok(proxmox_client + .storage_content(&cluster.node, &os_image_storage)? + .iter() + .find(|i| i.volid.ends_with(&file_name)) + .cloned()) + }; + + let existing_image = get_storage_content() + .map_err(|e| format!("Cannot check image availability [{}]", e.to_string()))?; + + let existing_image = match existing_image { + Some(v) => v, + None => { + proxmox_client.download_image(DownloadImage { + content: DownloadImageContentType::Iso, + filename: file_name.clone(), + node: cluster.node.clone(), + storage: os_image_storage.clone(), + url: os_image, + checksum: None, + checksum_algorithm: None, + verify_certificates: None, + })?; + + let image = retry(|| { + get_storage_content() + .map_err(|e| { + format!("Cannot check image availability [{}]", e.to_string()) + })? + .ok_or("Cannot download os image".to_string()) + })?; + + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("OS image has been downloaded [{}]", file_name), + ))?; + image + } + }; + + proxmox_client + .storage_content_details(&cluster.node, &os_image_storage, &existing_image.volid) + .map(|i| i.path) + .map_err(|e| e.to_string()) + } + + pub fn wait_for_shutdown( + proxmox_client: &proxmox_client::ClientOperations, + node: &str, + vm_id: u32, + ) -> Result { + let is_shutdown = retry(|| { + let status = proxmox_client + .status_vm(node, vm_id) + .map(|i| i.status) + .map_err(|e| format!("Status VM {}, error: {}", vm_id, e))?; + + match status { + VmStatus::Running => { + info!( + "VM [{}] is running, wait 10 sec for gracefully shutdown", + vm_id + ); + Err("Running".to_string()) + } + VmStatus::Stopped => Ok(()), + } + }); + + match is_shutdown { + Ok(_) => Ok(true), + Err(e) => match e.as_str() { + "Running" => Ok(false), + _ => Err(e), + }, + } + } + + pub fn wait_for_start( + proxmox_client: &proxmox_client::ClientOperations, + cluster: &Cluster, + cluster_node: &ClusterNode, + ) -> Result<(), String> { + info!("Wait for VM start"); + retry(|| { + let status = proxmox_client + .status_vm(&cluster.node, cluster_node.vm_id) + .map(|i| i.status) + .map_err(|e| format!("Status VM [{}], error: {}", cluster_node.vm_id, e))?; + + match status { + VmStatus::Running => { + info!("VM [{}] is running", cluster_node.vm_id); + Ok(()) + } + VmStatus::Stopped => Err(format!("VM [{}] is stopped", cluster_node.vm_id)), + } + })?; + + info!("Check cloud-init status for VM [{}]", cluster_node.vm_id); + retry(|| { + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &cluster_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + ssh_client.execute("cloud-init status --wait") + })?; + Ok(()) + } + + pub(crate) fn get_existing_vms( + proxmox_client: &ClientOperations, + cluster: &Cluster, + ) -> Result, String> { + let vms = proxmox_client + .virtual_machines(&cluster.node, None)? + .into_iter() + .map(|i| (i.vm_id.clone(), i.name.unwrap_or_default())) + .collect::>(); + + let existing_nodes = cluster + .nodes + .clone() + .into_iter() + .filter(|e| { + let name = format!("{}-{}", cluster.cluster_name, e.name); + vms.contains(&(e.vm_id.clone(), name)) + }) + .collect::>(); + Ok(existing_nodes) + } + + pub(crate) fn stop_vm( + proxmox_client: &ClientOperations, + node: &str, + vm_id: u32, + ) -> Result<(), String> { + proxmox_client.shutdown_vm(node, vm_id)?; + let is_shutdown = wait_for_shutdown(proxmox_client, node, vm_id)?; + if !is_shutdown { + info!("Shutdown VM [{}] is timeout, stop VM immediately", vm_id); + proxmox_client.stop_vm(node, vm_id)?; + let is_shutdown = wait_for_shutdown(proxmox_client, node, vm_id)?; + if !is_shutdown { + error!("Cannot shutdown VM [{}]", vm_id); + return Err(format!("Cannot shutdown VM [{}]", vm_id)); + } + } + Ok(()) + } + + pub(crate) fn restart_vm_if_necessary( + proxmox_client: &ClientOperations, + repo: Arc, + cluster: &Cluster, + node: &ClusterNode, + ) -> Result<(), String> { + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + let is_restart_required = ssh_client.is_file_exists("/var/run/reboot-required")?; + if !is_restart_required { + return Ok(()); + } + + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Reboot is required, shutdown VM [{}]", node.vm_id), + ))?; + + stop_vm(proxmox_client, &cluster.node, node.vm_id)?; + + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Starting VM [{}]", node.vm_id), + ))?; + proxmox_client.start_vm(&cluster.node, node.vm_id)?; + wait_for_start(proxmox_client, cluster, node) + .map_err(|e| format!("Cannot start VM [{}]: {}", node.vm_id, e))?; + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("VM [{}] has been started", node.vm_id), + ))?; + Ok(()) + } + + pub(crate) fn setup_vm( + cluster: &Cluster, + node: &ClusterNode, + hosts: &HashMap, + ) -> Result<(), String> { + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + ssh_client.execute("sudo systemctl enable iscsid")?; + for (host, ip) in hosts.iter() { + ssh_client.execute( + format!( + "echo '{} {}' | sudo tee -a /etc/cloud/templates/hosts.debian.tmpl", + ip, host + ) + .as_str(), + )?; + ssh_client + .execute(format!("echo '{} {}' | sudo tee -a /etc/hosts", ip, host).as_str())?; + } + Ok(()) + } +} + +pub(crate) mod cluster { + use serde::{Deserialize, Serialize}; + use std::sync::Arc; + use crate::model::{Cluster, ClusterNode, ClusterNodeType, LogEntry}; + use crate::Repository; + + pub(crate) fn install_kubernetes( + repo: Arc, + cluster: &Cluster, + node: &ClusterNode, + ) -> Result<(), String> { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Install Kubernetes on VM [{}]", node.vm_id), + ))?; + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + ssh_client.execute( + format!( + "sudo snap install microk8s --channel={} --classic", + cluster + .kube_version + .clone() + .unwrap_or("1.24/stable".to_owned()) + ) + .as_str(), + )?; + Ok(()) + } + + pub(crate) fn wait_for_ready_kubernetes( + repo: Arc, + cluster: &Cluster, + node: &ClusterNode, + ) -> Result<(), String> { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Wait for Kubernetes on VM [{}]", node.vm_id), + ))?; + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + ssh_client.execute("sudo microk8s status --wait-ready")?; + Ok(()) + } + + #[derive(Serialize, Deserialize)] + pub(crate) struct JoinNode { + pub(crate) token: String, + pub(crate) urls: Vec, + } + + pub(crate) fn join_node_to_cluster( + repo: Arc, + cluster: &Cluster, + master_node: &ClusterNode, + node_to_join: &ClusterNode, + ) -> Result<(), String> { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Generate join token on VM [{}] ", master_node.vm_id), + ))?; + + let mut master_ssh_client = ssh_client::Client::new(); + master_ssh_client.connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + let token_content = master_ssh_client.execute("sudo microk8s add-node --format json")?; + let join: JoinNode = serde_json::from_str(&token_content).map_err(|e| e.to_string())?; + if join.urls.is_empty() { + return Err("Join token doesn't have urls to join node".to_string()); + } + let join = join + .urls + .first() + .ok_or("Cannot get url to join".to_string())?; + let mut worker_ssh_client = ssh_client::Client::new(); + worker_ssh_client.connect( + &node_to_join.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + let command = format!("sudo microk8s join {}", join); + let command = match node_to_join.node_type { + ClusterNodeType::Master => command, + ClusterNodeType::Worker => format!("{} --worker", command), + }; + + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!( + "Join node [{}] with role [{}] to cluster", + node_to_join.vm_id, node_to_join.node_type + ), + ))?; + worker_ssh_client.execute(command.as_str())?; + // master_ssh_client.execute( + // format!("sudo microk8s.kubectl label node {}-{} node-role.kubernetes.io/{}={}", + // cluster.cluster_name, + // node_to_join.name, + // node_to_join.node_type, + // node_to_join.node_type).as_str())?; + Ok(()) + } +} + +pub(crate) mod apps { + use crate::model::{ClusterResource, HelmApp}; + + pub const HELM_CMD: &str = "microk8s.helm3"; + + + pub fn install_helm_app(ssh_client: &ssh_client::Client, app: &HelmApp) -> Result<(), String> { + let values_file_name = app.release_name.trim().replace(" ", "_"); + + ssh_client.execute( + &helm_client::new(HELM_CMD) + .sudo() + .repo() + .add(&app.chart_name, &app.repository) + .build(), + )?; + + ssh_client.execute(&helm_client::new(HELM_CMD).sudo().repo().update().build())?; + + if !app.values.is_empty() { + ssh_client.upload_file( + format!("/tmp/{}.yaml", values_file_name).as_str(), + &app.values, + )?; + } + + let mut command_builder = helm_client::new(HELM_CMD) + .sudo() + .upgrade_or_install() + .create_namespace() + .namespace(&app.namespace) + .name(&app.release_name) + .chart(&app.chart_name, &app.chart_name); + + if !app.values.is_empty() { + command_builder = + command_builder.with_values_file(&format!("/tmp/{}.yaml", values_file_name)); + } + if !app.chart_version.is_empty() { + command_builder = command_builder.version(&app.chart_version); + } + if app.wait { + command_builder = command_builder.wait(); + } + ssh_client.execute(&command_builder.build())?; + + if !app.values.is_empty() { + ssh_client.execute(format!("sudo rm /tmp/{}.yaml", values_file_name).as_str())?; + } + Ok(()) + } + + pub fn install_cluster_resource( + ssh_client: &ssh_client::Client, + resource: &ClusterResource, + ) -> Result<(), String> { + let file_name = format!( + "{}_cluster_resource", + resource.name.trim().replace(" ", "_") + ); + ssh_client.upload_file( + format!("/tmp/{}.yaml", file_name).as_str(), + resource.content.as_str(), + )?; + ssh_client + .execute(format!("sudo microk8s.kubectl apply -f /tmp/{}.yaml", file_name).as_str())?; + ssh_client.execute(format!("sudo rm /tmp/{}.yaml", file_name).as_str())?; + Ok(()) + } +} diff --git a/core/src/dispatcher/usecase/create_cluster.rs b/core/src/dispatcher/usecase/create_cluster.rs new file mode 100644 index 0000000..4a97607 --- /dev/null +++ b/core/src/dispatcher/usecase/create_cluster.rs @@ -0,0 +1,334 @@ +use std::collections::HashMap; +use std::sync::Arc; +use log::info; + +use openssl::rsa::Rsa; +use pem::{encode, Pem}; + +use proxmox_client::model::AccessData; +use proxmox_client::{Client, ClientOperations}; +use crate::dispatcher::usecase::common; +use crate::model::{Cluster, ClusterNode, ClusterNodeType, KeyPair, LogEntry}; +use crate::Repository; + + +pub(crate) fn execute( + proxmox_client: Arc, + repo: Arc, + access: AccessData, + cluster_name: String, +) -> Result<(), String> { + info!("Cluster creation request has been received"); + let proxmox_client = proxmox_client.operations(access); + + repo.save_log(LogEntry::info( + &cluster_name, + "Start creating cluster".to_string(), + ))?; + + let mut cluster = repo + .get_cluster(&cluster_name)? + .ok_or("Cannot find cluster")?; + let keys = generate_ssh_keys()?; + cluster.ssh_key = keys; + repo.save_cluster(cluster.clone())?; + + create_vms(&proxmox_client, &cluster, repo.clone())?; + start_vms(&proxmox_client, &cluster, repo.clone())?; + wait_for_vms_start(&proxmox_client, &cluster, repo.clone())?; + restart_vms_if_necessary(&proxmox_client, &cluster, repo.clone())?; + setup_vms(repo.clone(), &cluster)?; + install_kubernetes(repo.clone(), &cluster)?; + wait_for_ready_kubernetes(repo.clone(), &cluster)?; + join_nodes_to_cluster(repo.clone(), &cluster)?; + add_kubeconfig_to_project(repo.clone(), &mut cluster)?; + enable_microk8s_addons(repo.clone(), &cluster)?; + install_helm_apps(repo.clone(), &cluster)?; + install_cluster_resources(repo.clone(), &cluster)?; + Ok(()) +} + +fn install_cluster_resources(repo: Arc, cluster: &Cluster) -> Result<(), String> { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + "Install Cluster resources".to_string(), + ))?; + + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master) + .map(|i| i.clone()) + .ok_or("Cannot find any master node".to_string())?; + + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + + for resource in cluster.cluster_resources.iter() { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Apply cluster resource: [{}]", resource.name), + ))?; + common::apps::install_cluster_resource(&ssh_client, resource)?; + } + Ok(()) +} + +fn install_helm_apps(repo: Arc, cluster: &Cluster) -> Result<(), String> { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + "Install Helm apps".to_string(), + ))?; + + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master) + .cloned() + .ok_or("Cannot find any master node".to_string())?; + + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + + for app in cluster.helm_apps.iter() { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Install Helm app: [{}]", app.release_name), + ))?; + common::apps::install_helm_app(&ssh_client, app)?; + } + Ok(()) +} + +fn enable_microk8s_addons(repo: Arc, cluster: &Cluster) -> Result<(), String> { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + "Enable MicroK8s addons: [dns, helm3]".to_string(), + ))?; + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master) + .cloned() + .ok_or("Cannot find any master node".to_string())?; + + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + ssh_client.execute("sudo microk8s enable dns")?; + ssh_client.execute("sudo microk8s enable helm3")?; + Ok(()) +} + +fn add_kubeconfig_to_project(repo: Arc, cluster: &mut Cluster) -> Result<(), String> { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Add kube config to project"), + ))?; + let mut master_nodes = cluster + .nodes + .iter() + .filter(|i| i.node_type == ClusterNodeType::Master) + .cloned() + .collect::>(); + master_nodes.sort_by(|a, b| a.vm_id.cmp(&b.vm_id)); + let first_master_node = master_nodes + .first() + .ok_or("Cannot get first master node".to_string())?; + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &first_master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + let kube_config_content = ssh_client.execute("sudo microk8s config")?; + cluster.cluster_config = kube_config_content.clone(); + let mut cluster_to_update = repo + .get_cluster(&cluster.cluster_name)? + .ok_or("Cannot read cluster from repository".to_string())?; + cluster_to_update.cluster_config = kube_config_content; + repo.save_cluster(cluster_to_update)?; + Ok(()) +} + +fn join_nodes_to_cluster(repo: Arc, cluster: &Cluster) -> Result<(), String> { + if cluster.nodes.len() == 1 { + return Ok(()); + } + + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master) + .cloned() + .ok_or("Cannot find any master node".to_string())?; + + let nodes_to_join = cluster + .nodes + .clone() + .into_iter() + .filter(|i| i.vm_id != master_node.vm_id) + .collect::>(); + + for node_to_join in nodes_to_join.iter() { + common::cluster::join_node_to_cluster(repo.clone(), cluster, &master_node, node_to_join)?; + } + Ok(()) +} + +pub(crate) fn install_kubernetes(repo: Arc, cluster: &Cluster) -> Result<(), String> { + info!("Install Kubernetes"); + for node in cluster.nodes.iter() { + common::cluster::install_kubernetes(repo.clone(), cluster, node)?; + } + Ok(()) +} + +pub(crate) fn wait_for_ready_kubernetes( + repo: Arc, + cluster: &Cluster, +) -> Result<(), String> { + for node in cluster.nodes.iter() { + common::cluster::wait_for_ready_kubernetes(repo.clone(), cluster, node)?; + } + Ok(()) +} + +pub(crate) fn restart_vms_if_necessary( + proxmox_client: &ClientOperations, + cluster: &Cluster, + repo: Arc, +) -> Result<(), String> { + info!("Restart VM's if necessary"); + for node in cluster.nodes.iter() { + common::vm::restart_vm_if_necessary(proxmox_client, repo.clone(), cluster, node)?; + } + Ok(()) +} + +pub(crate) fn wait_for_vms_start( + proxmox_client: &ClientOperations, + cluster: &Cluster, + repo: Arc, +) -> Result<(), String> { + info!("Waiting for VM's start"); + for node in cluster.nodes.iter() { + common::vm::wait_for_start(proxmox_client, cluster, node) + .map_err(|e| format!("Cannot start VM [{}]: {}", node.vm_id, e))?; + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("VM [{}] has been started", node.vm_id), + ))?; + } + Ok(()) +} + +fn setup_vms(repo: Arc, cluster: &Cluster) -> Result<(), String> { + info!("Setup VM's"); + let hosts = cluster + .nodes + .iter() + .map(|i| { + ( + format!("{}-{}", cluster.cluster_name, i.name), + i.ip_address.clone(), + ) + }) + .collect::>(); + + for node in cluster.nodes.iter() { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Configure VM [{}]", node.vm_id), + ))?; + common::vm::setup_vm(cluster, node, &hosts)?; + } + Ok(()) +} + +fn generate_ssh_keys() -> Result { + info!("Generate SSH keys"); + let rsa = Rsa::generate(4096).map_err(|e| e.to_string())?; + let private_key = rsa.private_key_to_der().map_err(|e| e.to_string())?; + let private_key = Pem::new(String::from("RSA PRIVATE KEY"), private_key); + let private_key: String = encode(&private_key); + + let ssh_private_key = + ssh_keys::openssh::parse_private_key(private_key.as_str()).map_err(|e| e.to_string())?; + let ssh_private_key = ssh_private_key.get(0).ok_or("Cannot parse private key")?; + let ssh_public_key = ssh_private_key.public_key(); + info!("SSH keys has been generated"); + Ok(KeyPair { + public_key: ssh_public_key.to_string(), + private_key, + }) +} + +pub(crate) fn create_vms( + proxmox_client: &ClientOperations, + cluster: &Cluster, + repo: Arc, +) -> Result<(), String> { + info!("Create VM's"); + let mut used_vm_ids: Vec = proxmox_client + .virtual_machines(&cluster.node, None)? + .iter() + .map(|i| i.vm_id) + .collect(); + + used_vm_ids.extend( + proxmox_client + .lxc_containers(&cluster.node)? + .iter() + .map(|i| &i.vm_id) + .map(|i| i.parse::().unwrap_or_default()) + .collect::>(), + ); + + for node in cluster.nodes.iter() { + if used_vm_ids.contains(&node.vm_id) { + return Err(format!("VM with id [{}] already exists", node.vm_id)); + } + common::vm::create(proxmox_client, repo.clone(), cluster, node) + .map_err(|e| format!("Cannot create VM [{}]: {}", node.vm_id, e))?; + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("VM [{}] has been created", node.vm_id), + ))?; + } + info!("VM's has been created"); + Ok(()) +} + +pub(crate) fn start_vms( + proxmox_client: &ClientOperations, + cluster: &Cluster, + repo: Arc, +) -> Result<(), String> { + info!("Start VM's"); + for node in cluster.nodes.iter() { + proxmox_client + .start_vm(&cluster.node, node.vm_id) + .map_err(|e| format!("Cannot start VM [{}]: {}", node.vm_id, e))?; + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Starting VM [{}]", node.vm_id), + ))?; + } + Ok(()) +} diff --git a/core/src/dispatcher/usecase/delete_cluster.rs b/core/src/dispatcher/usecase/delete_cluster.rs new file mode 100644 index 0000000..886d7c8 --- /dev/null +++ b/core/src/dispatcher/usecase/delete_cluster.rs @@ -0,0 +1,82 @@ +use std::sync::Arc; + +use proxmox_client::model::AccessData; +use proxmox_client::ClientOperations; +use crate::dispatcher::usecase::common; +use crate::model::{Cluster, ClusterNode, LogEntry}; +use crate::Repository; + + +pub(crate) fn execute( + proxmox_client: Arc, + repo: Arc, + access: AccessData, + cluster_name: String, +) -> Result<(), String> { + let proxmox_client = proxmox_client.operations(access); + let cluster = repo + .get_cluster(&cluster_name)? + .ok_or("Cannot find cluster")?; + + let existing_nodes = common::vm::get_existing_vms(&proxmox_client, &cluster)?; + stop_vms(&repo, &proxmox_client, &cluster, &existing_nodes)?; + delete_vms(repo.clone(), &proxmox_client, &cluster, &existing_nodes)?; + + repo.delete_cluster(&cluster_name)?; + Ok(()) +} + +pub(crate) fn stop_vms( + repo: &Arc, + proxmox_client: &ClientOperations, + cluster: &Cluster, + existing_nodes: &[ClusterNode], +) -> Result<(), String> { + for node in existing_nodes.iter() { + proxmox_client + .shutdown_vm(&cluster.node, node.vm_id) + .map_err(|e| format!("Shutdown VM [{}], error: [{}]", node.vm_id, e))?; + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Requested VM [{}] to shutdown", node.vm_id), + ))?; + } + + for node in existing_nodes.iter() { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Wait for VM [{}] shutdown", node.vm_id), + ))?; + let is_shutdown = common::vm::wait_for_shutdown(proxmox_client, &cluster.node, node.vm_id)?; + if !is_shutdown { + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!( + "VM [{}] cannot be shouted down gracefully, stop VM imminently", + node.vm_id + ), + ))?; + proxmox_client.stop_vm(&cluster.node, node.vm_id)?; + common::vm::wait_for_shutdown(proxmox_client, &cluster.node, node.vm_id)?; + } + } + Ok(()) +} + +pub(crate) fn delete_vms( + repo: Arc, + proxmox_client: &ClientOperations, + cluster: &Cluster, + existing_nodes: &[ClusterNode], +) -> Result<(), String> { + for node in existing_nodes.iter() { + proxmox_client + .delete_vm(&cluster.node, node.vm_id) + .map_err(|e| format!("Delete VM [{}], error: [{}]", node.vm_id, e))?; + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("VM [{}] has been deleted", node.vm_id), + ))?; + } + Ok(()) +} diff --git a/core/src/dispatcher/usecase/delete_node_from_cluster.rs b/core/src/dispatcher/usecase/delete_node_from_cluster.rs new file mode 100644 index 0000000..4f2aeb9 --- /dev/null +++ b/core/src/dispatcher/usecase/delete_node_from_cluster.rs @@ -0,0 +1,186 @@ +use std::sync::Arc; +use std::time::Duration; + +use proxmox_client::model::AccessData; +use proxmox_client::{Client, ClientOperations}; +use crate::dispatcher::usecase::common; +use crate::model::{ClusterNodeType, LogEntry}; +use crate::Repository; + + +pub(crate) fn execute( + proxmox_client: Arc, + repo: Arc, + access: AccessData, + cluster_name: String, + node_name: String, +) -> Result<(), String> { + let proxmox_client = proxmox_client.operations(access); + repo.save_log(LogEntry::info( + &cluster_name, + format!("Start deleting node [{}-{}]", cluster_name, node_name), + ))?; + + let mut cluster = repo + .get_cluster(&cluster_name)? + .ok_or("Cannot find cluster")?; + if cluster.nodes.len() == 1 { + repo.save_log(LogEntry::error( + &cluster_name, + format!("Cannot delete last node, delete whole cluster instead"), + ))?; + return Ok(()); + } + + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master && i.name != node_name) + .map(|i| i.clone()) + .ok_or("Cannot find any master node".to_string())?; + + let node_to_delete = cluster + .nodes + .iter() + .find(|i| i.name == node_name) + .map(|i| i.clone()) + .ok_or("Cannot find node to delete".to_string())?; + + if common::vm::get_existing_vms(&proxmox_client, &cluster)? + .iter() + .find(|i| i.vm_id == node_to_delete.vm_id) + .is_none() + { + remove_node_from_project(repo.clone(), &cluster_name, &node_name)?; + remove_hosts_from_rest_of_nodes(repo.clone(), &proxmox_client, &cluster_name, &node_name)?; + return Ok(()); + } + + let mut master_ssh_client = ssh_client::Client::new(); + master_ssh_client.connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + + repo.save_log(LogEntry::info( + &cluster_name, + format!("Drain a node [{}-{}]", cluster_name, node_name), + ))?; + master_ssh_client.execute( + format!( + "sudo microk8s.kubectl drain {}-{} --ignore-daemonsets --grace-period=30 --timeout=60s", + cluster_name, node_name + ) + .as_str(), + )?; + repo.save_log(LogEntry::info( + &cluster_name, + "Wait 30s to gracefully shutdown pods".to_string(), + ))?; + std::thread::sleep(Duration::from_secs(30)); + + repo.save_log(LogEntry::info( + &cluster_name, + format!( + "Detach a node [{}-{}] from the cluster", + cluster_name, node_name + ), + ))?; + let mut node_to_delete_ssh_client = ssh_client::Client::new(); + node_to_delete_ssh_client.connect( + &node_to_delete.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + node_to_delete_ssh_client.execute("sudo microk8s leave")?; + + master_ssh_client + .execute(format!("sudo microk8s remove-node {}-{}", cluster_name, node_name).as_str())?; + + cluster.nodes.retain_mut(|i| i.name == node_name); + let vm_exists = common::vm::get_existing_vms(&proxmox_client, &cluster)? + .iter() + .any(|i| i.vm_id == node_to_delete.vm_id); + + if vm_exists { + repo.save_log(LogEntry::info( + &cluster_name, + format!("Removing VM [{}]", node_to_delete.vm_id), + ))?; + + proxmox_client + .shutdown_vm(&cluster.node, node_to_delete.vm_id) + .map_err(|e| format!("Shutdown VM [{}], error: [{}]", node_to_delete.vm_id, e))?; + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("Requested VM [{}] to shutdown", node_to_delete.vm_id), + ))?; + let is_shutdown = + common::vm::wait_for_shutdown(&proxmox_client, &cluster.node, node_to_delete.vm_id)?; + if !is_shutdown { + proxmox_client.stop_vm(&cluster.node, node_to_delete.vm_id)?; + common::vm::wait_for_shutdown(&proxmox_client, &cluster.node, node_to_delete.vm_id)?; + } + + proxmox_client + .delete_vm(&cluster.node, node_to_delete.vm_id) + .map_err(|e| format!("Delete VM [{}], error: [{}]", node_to_delete.vm_id, e))?; + repo.save_log(LogEntry::info( + &cluster.cluster_name, + format!("VM [{}] has been deleted", node_to_delete.vm_id), + ))?; + } + + remove_node_from_project(repo.clone(), &cluster_name, &node_name)?; + remove_hosts_from_rest_of_nodes(repo.clone(), &proxmox_client, &cluster_name, &node_name)?; + + Ok(()) +} + +fn remove_node_from_project( + repo: Arc, + cluster_name: &str, + node_name: &str, +) -> Result<(), String> { + let mut cluster = repo + .get_cluster(cluster_name)? + .ok_or("Cannot find cluster")?; + cluster.nodes.retain_mut(|i| i.name != node_name); + repo.save_cluster(cluster)?; + Ok(()) +} + +fn remove_hosts_from_rest_of_nodes( + repo: Arc, + proxmox_client: &ClientOperations, + cluster_name: &str, + node_name: &str, +) -> Result<(), String> { + let cluster = repo + .get_cluster(cluster_name)? + .ok_or("Cannot find cluster")?; + let existing_nodes = common::vm::get_existing_vms(&proxmox_client, &cluster)?; + for node in existing_nodes.iter() { + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + ssh_client.execute( + format!( + "sudo sed -i '/{}-{}/d' /etc/cloud/templates/hosts.debian.tmpl", + cluster_name, node_name + ) + .as_str(), + )?; + ssh_client.execute( + format!("sudo sed -i '/{}-{}/d' /etc/hosts", cluster_name, node_name).as_str(), + )?; + } + Ok(()) +} diff --git a/makoon/src/operator/dispatcher/usecase/mod.rs b/core/src/dispatcher/usecase/mod.rs similarity index 99% rename from makoon/src/operator/dispatcher/usecase/mod.rs rename to core/src/dispatcher/usecase/mod.rs index 2755718..2bb55bf 100644 --- a/makoon/src/operator/dispatcher/usecase/mod.rs +++ b/core/src/dispatcher/usecase/mod.rs @@ -7,4 +7,3 @@ pub mod delete_node_from_cluster; pub use common::apps::install_cluster_resource; pub use common::apps::install_helm_app; pub use common::apps::HELM_CMD; - diff --git a/makoon/src/operator/dispatcher/utils.rs b/core/src/dispatcher/utils.rs similarity index 58% rename from makoon/src/operator/dispatcher/utils.rs rename to core/src/dispatcher/utils.rs index 08f5eb4..98cc788 100644 --- a/makoon/src/operator/dispatcher/utils.rs +++ b/core/src/dispatcher/utils.rs @@ -1,17 +1,19 @@ use std::time::Duration; +use log::info; pub fn retry(f: F) -> Result - where - E: ToString, - F: Fn() -> Result { +where + E: ToString, + F: Fn() -> Result, +{ retry_with_opts::(f, 10, 30) } - pub fn retry_with_opts(f: F, delay: u64, attempts: u64) -> Result - where - E: ToString, - F: Fn() -> Result { +where + E: ToString, + F: Fn() -> Result, +{ let mut attempts_left = attempts; loop { match f() { @@ -19,7 +21,13 @@ pub fn retry_with_opts(f: F, delay: u64, attempts: u64) -> Result Err(e) => { if attempts_left > 0 { attempts_left -= 1; - info!("Operation probe: [{}/{}], wait [{}] seconds: {}", (attempts-attempts_left), attempts, delay, e.to_string()); + info!( + "Operation probe: [{}/{}], wait [{}] seconds: {}", + (attempts - attempts_left), + attempts, + delay, + e.to_string() + ); std::thread::sleep(Duration::from_secs(delay)) } else { return Err(e); @@ -27,4 +35,4 @@ pub fn retry_with_opts(f: F, delay: u64, attempts: u64) -> Result } } } -} \ No newline at end of file +} diff --git a/makoon/src/operator/error.rs b/core/src/error.rs similarity index 58% rename from makoon/src/operator/error.rs rename to core/src/error.rs index 3c534e7..fbd6975 100644 --- a/makoon/src/operator/error.rs +++ b/core/src/error.rs @@ -1,7 +1,7 @@ use std::fmt::{Display, Formatter}; use std::sync::mpsc::SendError; +use crate::repository; -use crate::operator::repository; #[derive(Debug)] pub enum Error { @@ -14,19 +14,26 @@ impl From for Error { fn from(value: repository::Error) -> Self { match value { repository::Error::IO(e) => Error::Generic(e), - repository::Error::DB(e) => Error::Generic(e) + repository::Error::DB(e) => Error::Generic(e), } } } -impl From for Error { - fn from(value: proxmox::Error) -> Self { +impl From for Error { + fn from(value: proxmox_client::Error) -> Self { match value { - proxmox::Error::Generic(e) => Error::Generic(e), - proxmox::Error::CannotConnectToProxmox(e) => Error::Generic(e), - proxmox::Error::CredentialsInvalid => Error::Generic(String::new()), - proxmox::Error::HttpError { status, response, reason } => Error::Generic(format!("Http status: [{}], reason: [{}], response: [{}]", status, reason, response)), - proxmox::Error::BodyMalformed(e) => Error::Generic(e) + proxmox_client::Error::Generic(e) => Error::Generic(e), + proxmox_client::Error::CannotConnectToProxmox(e) => Error::Generic(e), + proxmox_client::Error::CredentialsInvalid => Error::Generic(String::new()), + proxmox_client::Error::HttpError { + status, + response, + reason, + } => Error::Generic(format!( + "Http status: [{}], reason: [{}], response: [{}]", + status, reason, response + )), + proxmox_client::Error::BodyMalformed(e) => Error::Generic(e), } } } @@ -54,7 +61,7 @@ impl Display for Error { match self { Error::ResourceAlreadyExists => write!(f, "Cluster already exists"), Error::Generic(e) => write!(f, "Unknown error: {}", e), - Error::ResourceNotFound => write!(f, "Cluster not found") + Error::ResourceNotFound => write!(f, "Cluster not found"), } } -} \ No newline at end of file +} diff --git a/makoon/src/operator/event.rs b/core/src/event.rs similarity index 93% rename from makoon/src/operator/event.rs rename to core/src/event.rs index 9c3db51..a119a06 100644 --- a/makoon/src/operator/event.rs +++ b/core/src/event.rs @@ -1,4 +1,4 @@ -use proxmox::model::AccessData; +use proxmox_client::model::AccessData; #[derive(Debug)] pub enum Event { diff --git a/makoon/src/operator/generator.rs b/core/src/generator.rs similarity index 58% rename from makoon/src/operator/generator.rs rename to core/src/generator.rs index a0fa174..deba9d1 100644 --- a/makoon/src/operator/generator.rs +++ b/core/src/generator.rs @@ -1,10 +1,11 @@ use std::string::ToString; +use log::info; -use proxmox::ClientOperations; -use proxmox::model::{NetworkType, StorageContentType}; +use proxmox_client::model::{NetworkType, StorageContentType}; +use proxmox_client::ClientOperations; +use crate::Error; +use crate::model::{ClusterNode, ClusterNodeType, ClusterRequest, KeyPair, Network}; -use crate::operator::{Error, Result}; -use crate::operator::model::{ClusterNode, ClusterNodeType, ClusterRequest, KeyPair, Network}; const EMPTY: String = String::new(); @@ -14,22 +15,23 @@ pub struct DefaultClusterConfigurationGenerator { impl DefaultClusterConfigurationGenerator { pub fn new(proxmox_client: ClientOperations) -> Self { - DefaultClusterConfigurationGenerator { - proxmox_client - } + DefaultClusterConfigurationGenerator { proxmox_client } } - pub fn generate(&self) -> Result { + pub fn generate(&self) -> crate::Result { info!("Generate default cluster"); let default_proxmox_node = get_default_proxmox_node(&self.proxmox_client)?; - let default_iso_storage = get_default_iso_storage(&self.proxmox_client, &default_proxmox_node)?; - let default_disk_storage = get_default_disk_storage(&self.proxmox_client, &default_proxmox_node)?; + let default_iso_storage = + get_default_iso_storage(&self.proxmox_client, &default_proxmox_node)?; + let default_disk_storage = + get_default_disk_storage(&self.proxmox_client, &default_proxmox_node)?; let default_network = get_default_network(&self.proxmox_client, &default_proxmox_node)?; - let default_start_vm_id = get_default_start_vm_id(&self.proxmox_client, &default_proxmox_node)?; + let default_start_vm_id = + get_default_start_vm_id(&self.proxmox_client, &default_proxmox_node)?; Ok(ClusterRequest { - os_image: "https://cloud-images.ubuntu.com/kinetic/current/kinetic-server-cloudimg-amd64.img".to_string(), + os_image: get_default_os_image(), os_image_storage: default_iso_storage, - kube_version: "1.24/stable".to_string(), + kube_version: get_default_kube_version(), node: default_proxmox_node, cluster_name: EMPTY, ssh_key: KeyPair { @@ -53,7 +55,11 @@ impl DefaultClusterConfigurationGenerator { }], network: Network { gateway: default_network.gateway.clone().unwrap_or_default(), - subnet_mask: default_network.netmask.unwrap_or("24".to_string()).parse::().unwrap_or(24), + subnet_mask: default_network + .netmask + .unwrap_or("24".to_string()) + .parse::() + .unwrap_or(24), dns: default_network.gateway.unwrap_or_default(), bridge: default_network.iface, }, @@ -61,24 +67,41 @@ impl DefaultClusterConfigurationGenerator { } } -fn get_default_start_vm_id(proxmox_client: &ClientOperations, node: &str) -> Result { +fn get_default_os_image() -> String { + crate::supported::os_images().values().next().unwrap().to_string() +} + +fn get_default_kube_version() -> String { + crate::supported::kube_versions().first().unwrap().to_string() +} + +fn get_default_start_vm_id(proxmox_client: &ClientOperations, node: &str) -> crate::Result { let mut used_vm_ids: Vec = proxmox_client - .virtual_machines(node.clone(), None)?.iter() + .virtual_machines(node, None)? + .iter() .map(|i| i.vm_id) .collect(); - used_vm_ids.extend(proxmox_client.lxc_containers(node.clone())?.iter() - .map(|i| &i.vm_id) - .map(|i| i.parse::().unwrap_or_default()) - .collect::>()); + used_vm_ids.extend( + proxmox_client + .lxc_containers(node)? + .iter() + .map(|i| &i.vm_id) + .map(|i| i.parse::().unwrap_or_default()) + .collect::>(), + ); used_vm_ids.sort(); find_empty_slot_for_vm_ids(&used_vm_ids) } -fn find_empty_slot_for_vm_ids(used_vm_ids: &[u32]) -> Result { +fn find_empty_slot_for_vm_ids(used_vm_ids: &[u32]) -> crate::Result { for x in (100..999_999_999).step_by(10) { - let range_already_used = used_vm_ids.iter().filter(|i| **i >= x && **i <= x + 10).count() > 0; + let range_already_used = used_vm_ids + .iter() + .filter(|i| **i >= x && **i <= x + 10) + .count() + > 0; if range_already_used { continue; } else { @@ -88,10 +111,13 @@ fn find_empty_slot_for_vm_ids(used_vm_ids: &[u32]) -> Result { Err(Error::Generic("Cannot find free slot".to_string())) } - -fn get_default_network(proxmox_client: &ClientOperations, node: &str) -> Result { - let bridges = proxmox_client.networks(&node, Some(NetworkType::Bridge))?; - Ok(bridges.into_iter() +fn get_default_network( + proxmox_client: &ClientOperations, + node: &str, +) -> crate::Result { + let bridges = proxmox_client.networks(node, Some(NetworkType::Bridge))?; + Ok(bridges + .into_iter() .find(|i| { let address = i.address.clone().unwrap_or_default(); address == proxmox_client.host() @@ -99,31 +125,33 @@ fn get_default_network(proxmox_client: &ClientOperations, node: &str) -> Result< .unwrap_or_default()) } -fn get_default_iso_storage(proxmox_client: &ClientOperations, node: &str) -> Result { - let result = proxmox_client.storage(&node, Some(StorageContentType::Iso))?; - result.get(0) +fn get_default_iso_storage(proxmox_client: &ClientOperations, node: &str) -> crate::Result { + let result = proxmox_client.storage(node, Some(StorageContentType::Iso))?; + result + .get(0) .map(|i| i.storage.clone()) .ok_or(Error::ResourceNotFound) } - -fn get_default_disk_storage(proxmox_client: &ClientOperations, node: &str) -> Result { - let result = proxmox_client.storage(&node, Some(StorageContentType::Images))?; - result.get(0) +fn get_default_disk_storage(proxmox_client: &ClientOperations, node: &str) -> crate::Result { + let result = proxmox_client.storage(node, Some(StorageContentType::Images))?; + result + .get(0) .map(|i| i.storage.clone()) .ok_or(Error::ResourceNotFound) } -fn get_default_proxmox_node(proxmox_client: &ClientOperations) -> Result { +fn get_default_proxmox_node(proxmox_client: &ClientOperations) -> crate::Result { let nodes = proxmox_client.nodes()?; - nodes.get(0) + nodes + .get(0) .map(|i| i.node.clone()) .ok_or(Error::ResourceNotFound) } #[cfg(test)] mod test { - use crate::operator::generator::find_empty_slot_for_vm_ids; + use crate::generator::find_empty_slot_for_vm_ids; #[test] fn find_slot_with_first_taken() { @@ -143,10 +171,9 @@ mod test { assert_eq!(result, 120); } - #[test] fn find_slot_with_taken_in_three_slots() { let result = find_empty_slot_for_vm_ids(&[105, 112, 129]).unwrap(); assert_eq!(result, 130); } -} \ No newline at end of file +} diff --git a/makoon/src/operator/mod.rs b/core/src/lib.rs similarity index 79% rename from makoon/src/operator/mod.rs rename to core/src/lib.rs index 85e1f11..ba0daf7 100644 --- a/makoon/src/operator/mod.rs +++ b/core/src/lib.rs @@ -1,18 +1,18 @@ - -pub mod model; -mod operator; +mod dispatcher; +mod error; mod event; +mod generator; +mod operator; mod repository; mod repository_json; -mod generator; -mod error; -mod dispatcher; -mod ssh; -pub use operator::{Config, Operator}; +pub mod model; +pub mod supported; + pub use dispatcher::Dispatcher; -pub use repository::Repository; pub use error::Error; +pub use operator::{Config, Operator}; +pub use repository::Repository; pub type Result = std::result::Result; -pub use generator::DefaultClusterConfigurationGenerator; \ No newline at end of file +pub use generator::DefaultClusterConfigurationGenerator; diff --git a/makoon/src/operator/model.rs b/core/src/model.rs similarity index 96% rename from makoon/src/operator/model.rs rename to core/src/model.rs index 0e1c8b6..3c332b5 100644 --- a/makoon/src/operator/model.rs +++ b/core/src/model.rs @@ -44,7 +44,7 @@ impl Display for ClusterNodeType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ClusterNodeType::Master => write!(f, "master"), - ClusterNodeType::Worker => write!(f, "worker") + ClusterNodeType::Worker => write!(f, "worker"), } } } @@ -91,7 +91,7 @@ pub struct KeyPair { } #[typeshare] -#[derive(Clone, Serialize, Deserialize, Debug, Default)] +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub enum ClusterStatus { #[default] @@ -103,7 +103,6 @@ pub enum ClusterStatus { Error, } - #[typeshare] #[derive(PartialEq, Eq, Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -124,7 +123,9 @@ pub struct LogEntry { impl LogEntry { pub fn info(cluster_name: &str, message: T) -> Self - where T: ToString { + where + T: ToString, + { LogEntry { date: Utc::now().naive_local(), cluster_name: cluster_name.to_string(), @@ -133,7 +134,9 @@ impl LogEntry { } } pub fn error(cluster_name: &str, message: T) -> Self - where T: ToString { + where + T: ToString, + { LogEntry { date: Utc::now().naive_local(), cluster_name: cluster_name.to_string(), @@ -259,8 +262,8 @@ impl FromStr for KubeStatus { pub mod helm { use serde::{Deserialize, Serialize}; + use crate::model::AppStatusType; - use crate::operator::model::AppStatusType; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct InstalledRelease { @@ -304,4 +307,4 @@ pub mod kube { pub struct Nodes { pub items: Vec, } -} \ No newline at end of file +} diff --git a/makoon/src/operator/operator.rs b/core/src/operator.rs similarity index 52% rename from makoon/src/operator/operator.rs rename to core/src/operator.rs index b6e892a..6e5afb9 100644 --- a/makoon/src/operator/operator.rs +++ b/core/src/operator.rs @@ -1,19 +1,17 @@ use std::str::FromStr; -use std::sync::{Arc, mpsc}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{Receiver, SyncSender}; +use std::sync::{mpsc, Arc}; use std::time::Duration; +use log::{error, info}; -use proxmox::model::AccessData; +use proxmox_client::model::AccessData; +use crate::event::Event; +use crate::{Dispatcher, Error, Repository}; +use crate::dispatcher::HELM_CMD; +use crate::model::{AppStatus, AppStatusType, Cluster, ClusterHeader, ClusterNode, ClusterNodeLock, ClusterNodeStatus, ClusterNodeType, ClusterRequest, ClusterResource, ClusterStatus, HelmApp, kube, KubeStatus, LogEntry}; +use crate::model::helm::InstalledRelease; -use crate::helm; -use crate::operator::{Error, Result, ssh}; -use crate::operator::Dispatcher; -use crate::operator::dispatcher::HELM_CMD; -use crate::operator::event::Event; -use crate::operator::model::{LogEntry, AppStatus, AppStatusType, Cluster, ClusterHeader, ClusterNode, ClusterNodeLock, ClusterNodeStatus, ClusterNodeType, ClusterRequest, ClusterResource, ClusterStatus, HelmApp, kube, KubeStatus}; -use crate::operator::model::helm::InstalledRelease; -use crate::operator::repository::Repository; pub struct Config { pub worker_thread_probe_duration: u64, @@ -27,7 +25,6 @@ impl Default for Config { } } - pub struct Operator { executor: Option>, tx: SyncSender, @@ -41,7 +38,7 @@ impl Drop for Operator { self.shutdown.store(true, Ordering::SeqCst); match self.executor.take() { Some(v) => v.join().expect("cannot join thread"), - None => info!("Executor not exists") + None => info!("Executor not exists"), } } } @@ -71,7 +68,9 @@ impl Operator { }; } Err(_) => { - std::thread::sleep(Duration::from_millis(config.worker_thread_probe_duration)); + std::thread::sleep(Duration::from_millis( + config.worker_thread_probe_duration, + )); if shutdown.load(Ordering::SeqCst) { info!("Operator working thread has been requested to shut down"); return; @@ -83,7 +82,11 @@ impl Operator { } } - pub fn create_cluster(&self, access: AccessData, cluster_request: ClusterRequest) -> Result<()> { + pub fn create_cluster( + &self, + access: AccessData, + cluster_request: ClusterRequest, + ) -> crate::Result<()> { info!("Start creating cluster"); let cluster_name = cluster_request.cluster_name.clone(); @@ -101,14 +104,22 @@ impl Operator { ssh_key: cluster_request.ssh_key, node_username: cluster_request.node_username, node_password: cluster_request.node_password, - helm_apps: cluster_request.helm_apps.into_iter().map(|mut i| { - i.id = uuid::Uuid::new_v4().to_string(); - i - }).collect(), - cluster_resources: cluster_request.cluster_resources.into_iter().map(|mut i| { - i.id = uuid::Uuid::new_v4().to_string(); - i - }).collect(), + helm_apps: cluster_request + .helm_apps + .into_iter() + .map(|mut i| { + i.id = uuid::Uuid::new_v4().to_string(); + i + }) + .collect(), + cluster_resources: cluster_request + .cluster_resources + .into_iter() + .map(|mut i| { + i.id = uuid::Uuid::new_v4().to_string(); + i + }) + .collect(), disk_size: cluster_request.disk_size, nodes: cluster_request.nodes, network: cluster_request.network, @@ -124,9 +135,17 @@ impl Operator { Ok(()) } - pub fn change_node_resources(&self, access: AccessData, cluster_name: String, node_name: String, cores: u16, memory: u64) -> Result<()> { + pub fn change_node_resources( + &self, + access: AccessData, + cluster_name: String, + node_name: String, + cores: u16, + memory: u64, + ) -> crate::Result<()> { info!("Start changing node resources"); - let mut cluster = self.repository + let mut cluster = self + .repository .get_cluster(&cluster_name)? .ok_or(Error::Generic("Cannot get cluster".to_string()))?; @@ -148,9 +167,15 @@ impl Operator { Ok(()) } - pub fn add_node_cluster(&self, access: AccessData, cluster_name: String, node_request: ClusterNode) -> Result { + pub fn add_node_cluster( + &self, + access: AccessData, + cluster_name: String, + node_request: ClusterNode, + ) -> crate::Result { info!("Start adding node to the cluster"); - let mut cluster = self.repository + let mut cluster = self + .repository .get_cluster(&cluster_name)? .ok_or(Error::Generic("Cannot get cluster".to_string()))?; @@ -159,7 +184,13 @@ impl Operator { cluster.nodes.push(node_request.clone()); self.repository.save_cluster(cluster)?; - self.repository.save_log(LogEntry::info(&cluster_name, format!("Adding node [{}] to cluster [{}] has been started", node_request.name, cluster_name)))?; + self.repository.save_log(LogEntry::info( + &cluster_name, + format!( + "Adding node [{}] to cluster [{}] has been started", + node_request.name, cluster_name + ), + ))?; self.tx.send(Event::AddNodeToCluster { access, @@ -170,14 +201,18 @@ impl Operator { Ok(node_request) } - pub fn delete_cluster(&self, access: AccessData, cluster_name: String) -> Result<()> { + pub fn delete_cluster(&self, access: AccessData, cluster_name: String) -> crate::Result<()> { info!("Start deleting cluster"); - let mut cluster = self.repository + let mut cluster = self + .repository .get_cluster(&cluster_name)? .ok_or(Error::Generic("Cannot get cluster".to_string()))?; cluster.status = ClusterStatus::Destroying; self.repository.save_cluster(cluster)?; - self.repository.save_log(LogEntry::info(&cluster_name, "Cluster deletion started".to_string()))?; + self.repository.save_log(LogEntry::info( + &cluster_name, + "Cluster deletion started".to_string(), + ))?; self.tx.send(Event::DeleteCluster { access, @@ -187,13 +222,30 @@ impl Operator { Ok(()) } - pub fn delete_node_from_cluster(&self, access: AccessData, cluster_name: String, node_name: String) -> Result { + pub fn delete_node_from_cluster( + &self, + access: AccessData, + cluster_name: String, + node_name: String, + ) -> crate::Result { info!("Start deleting node from the cluster"); - self.repository.save_log(LogEntry::info(&cluster_name, format!("Deleting node [{}] from cluster has been started", node_name)))?; - - let mut cluster = self.repository.get_cluster(&cluster_name)?.ok_or(Error::ResourceNotFound)?; - let node_to_delete = cluster.nodes.iter_mut() - .find(|i| i.name == node_name).ok_or(Error::ResourceNotFound)?; + self.repository.save_log(LogEntry::info( + &cluster_name, + format!( + "Deleting node [{}] from cluster has been started", + node_name + ), + ))?; + + let mut cluster = self + .repository + .get_cluster(&cluster_name)? + .ok_or(Error::ResourceNotFound)?; + let node_to_delete = cluster + .nodes + .iter_mut() + .find(|i| i.name == node_name) + .ok_or(Error::ResourceNotFound)?; node_to_delete.lock = Some(ClusterNodeLock::Delete); let result = node_to_delete.clone(); @@ -208,23 +260,16 @@ impl Operator { Ok(result) } - pub fn get_clusters(&self) -> Result> { + pub fn get_clusters(&self) -> crate::Result> { info!("Get clusters"); let repo = self.repository.clone(); - let result = repo. - get_clusters(). - map(|r| r.into_iter() + let result = repo.get_clusters().map(|r| { + r.into_iter() .map(|i| -> ClusterHeader { - let cores = i.nodes. - iter(). - map(|i| i.cores). - reduce(|a, b| a + b); + let cores = i.nodes.iter().map(|i| i.cores).reduce(|a, b| a + b); - let memory = i.nodes. - iter(). - map(|i| i.memory). - reduce(|a, b| a + b); + let memory = i.nodes.iter().map(|i| i.memory).reduce(|a, b| a + b); let node_count = u64::try_from(i.nodes.len()).unwrap_or(0); @@ -237,38 +282,58 @@ impl Operator { status: i.status, } }) - .collect())?; + .collect() + })?; Ok(result) } - pub fn cluster_status(&self, cluster_name: &str) -> Result> { + pub fn cluster_status(&self, cluster_name: &str) -> crate::Result> { info!("Get cluster status"); - let cluster = self.get_cluster(cluster_name)? + let cluster = self + .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master).cloned().ok_or(Error::ResourceNotFound)?; - - - let mut result: Vec = cluster.nodes.iter().map(|i| ClusterNodeStatus { - name: format!("{}-{}", cluster.cluster_name, i.name), - status: KubeStatus::Unknown, - }).collect(); - + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master) + .cloned() + .ok_or(Error::ResourceNotFound)?; - let mut ssh_client = ssh::Client::new(); - if ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key).is_err() { + let mut result: Vec = cluster + .nodes + .iter() + .map(|i| ClusterNodeStatus { + name: format!("{}-{}", cluster.cluster_name, i.name), + status: KubeStatus::Unknown, + }) + .collect(); + + let mut ssh_client = ssh_client::Client::new(); + if ssh_client + .connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + ) + .is_err() + { return Ok(result); - } else {}; - let nodes_status = ssh_client.execute("sudo microk8s kubectl get nodes -o json --request-timeout='5s'"); + } else { + }; + let nodes_status = + ssh_client.execute("sudo microk8s kubectl get nodes -o json --request-timeout='5s'"); let nodes_status = match nodes_status { Ok(v) => v, - Err(_) => return Ok(result) + Err(_) => return Ok(result), }; let kube_nodes: kube::Nodes = serde_json::from_str(&nodes_status)?; for status in result.iter_mut() { - let node_status = kube_nodes.items.iter() + let node_status = kube_nodes + .items + .iter() .find(|i| i.metadata.name == status.name) .map(|i| i.status.conditions.clone()) .unwrap_or_default() @@ -282,71 +347,94 @@ impl Operator { Ok(result) } - pub fn apps_status(&self, cluster_name: &str) -> Result> { + pub fn apps_status(&self, cluster_name: &str) -> crate::Result> { info!("Get apps status"); - let cluster = self.get_cluster(cluster_name)? + let cluster = self + .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master).cloned().ok_or(Error::ResourceNotFound)?; - - let result = cluster.helm_apps.iter().map(|i| AppStatus { - id: i.id.clone(), - status: AppStatusType::Unknown, - }).collect::>(); + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master) + .cloned() + .ok_or(Error::ResourceNotFound)?; - let mut ssh_client = ssh::Client::new(); - if ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key).is_err() { + let result = cluster + .helm_apps + .iter() + .map(|i| AppStatus { + id: i.id.clone(), + status: AppStatusType::Unknown, + }) + .collect::>(); + + let mut ssh_client = ssh_client::Client::new(); + if ssh_client + .connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + ) + .is_err() + { return Ok(result); } - let installed_releases = ssh_client.execute_to(&helm::new(HELM_CMD) - .sudo() - .list() - .all() - .json() - .build()); + let installed_releases = + ssh_client.execute_to(&helm_client::new(HELM_CMD).sudo().list().all().json().build()); let installed_releases: Vec = match installed_releases { Ok(v) => v, - Err(_) => return Ok(result) + Err(_) => return Ok(result), }; - Ok(cluster.helm_apps.iter().map(|i| { - let status = installed_releases.iter() - .find(|ir| ir.name == i.release_name && ir.namespace == i.namespace) - .map(|ir| ir.status.clone()) - .unwrap_or(AppStatusType::NotInstalled); - - AppStatus { - id: i.id.clone(), - status, - } - }).collect()) + Ok(cluster + .helm_apps + .iter() + .map(|i| { + let status = installed_releases + .iter() + .find(|ir| ir.name == i.release_name && ir.namespace == i.namespace) + .map(|ir| ir.status.clone()) + .unwrap_or(AppStatusType::NotInstalled); + + AppStatus { + id: i.id.clone(), + status, + } + }) + .collect()) } - pub fn logs_for_cluster(&self, name: &str) -> Result> { + pub fn logs_for_cluster(&self, name: &str) -> crate::Result> { info!("Get logs for the cluster"); Ok(self.repository.logs(name)?) } - pub fn clear_logs_for_cluster(&self, name: &str) -> Result<()> { + pub fn clear_logs_for_cluster(&self, name: &str) -> crate::Result<()> { info!("Clear logs for the cluster"); self.repository.delete_logs(name)?; Ok(()) } - pub fn get_cluster(&self, cluster_name: &str) -> Result> { + pub fn get_cluster(&self, cluster_name: &str) -> crate::Result> { info!("Get cluster"); Ok(self.repository.get_cluster(cluster_name)?) } - pub fn get_nodes(&self, cluster_name: &str) -> Result> { + pub fn get_nodes(&self, cluster_name: &str) -> crate::Result> { info!("Get nodes for the cluster"); - Ok(self.repository.get_cluster(cluster_name)?.ok_or(Error::ResourceNotFound)?.nodes) + Ok(self + .repository + .get_cluster(cluster_name)? + .ok_or(Error::ResourceNotFound)? + .nodes) } - pub fn save_helm_app(&self, cluster_name: &str, app: HelmApp) -> Result { + pub fn save_helm_app(&self, cluster_name: &str, app: HelmApp) -> crate::Result { info!("Save Helm application"); - let mut cluster = self.repository + let mut cluster = self + .repository .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; @@ -357,13 +445,16 @@ impl Operator { self.repository.save_cluster(cluster)?; Ok(app_id) } - pub fn update_helm_app(&self, cluster_name: &str, app: HelmApp) -> Result<()> { + pub fn update_helm_app(&self, cluster_name: &str, app: HelmApp) -> crate::Result<()> { info!("Update Helm application"); - let mut cluster = self.repository + let mut cluster = self + .repository .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; - let app_to_update = cluster.helm_apps.iter_mut() + let app_to_update = cluster + .helm_apps + .iter_mut() .find(|i| i.id == app.id) .ok_or(Error::ResourceNotFound)?; @@ -378,9 +469,10 @@ impl Operator { self.repository.save_cluster(cluster)?; Ok(()) } - pub fn delete_helm_app(&self, cluster_name: &str, app_id: &str) -> Result<()> { + pub fn delete_helm_app(&self, cluster_name: &str, app_id: &str) -> crate::Result<()> { info!("Delete Helm application"); - let mut cluster = self.repository + let mut cluster = self + .repository .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; @@ -389,55 +481,84 @@ impl Operator { self.repository.save_cluster(cluster)?; Ok(()) } - pub fn install_helm_app(&self, cluster_name: &str, app_id: &str) -> Result<()> { + pub fn install_helm_app(&self, cluster_name: &str, app_id: &str) -> crate::Result<()> { info!("Install Helm application"); - let cluster = self.repository + let cluster = self + .repository .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; - let app = cluster.helm_apps.iter() + let app = cluster + .helm_apps + .iter() .find(|i| i.id == app_id) .ok_or(Error::ResourceNotFound)?; - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master).cloned().ok_or(Error::ResourceNotFound)?; - - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master) + .cloned() + .ok_or(Error::ResourceNotFound)?; + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; - crate::operator::dispatcher::install_helm_app(&ssh_client, app)?; + crate::dispatcher::install_helm_app(&ssh_client, app)?; Ok(()) } - pub fn uninstall_helm_app(&self, cluster_name: &str, app_id: &str) -> Result<()> { + pub fn uninstall_helm_app(&self, cluster_name: &str, app_id: &str) -> crate::Result<()> { info!("Uninstall Helm application"); - let cluster = self.repository + let cluster = self + .repository .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; - let app = cluster.helm_apps.iter() + let app = cluster + .helm_apps + .iter() .find(|i| i.id == app_id) .ok_or(Error::ResourceNotFound)?; - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master).cloned().ok_or(Error::ResourceNotFound)?; - - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master) + .cloned() + .ok_or(Error::ResourceNotFound)?; - ssh_client.execute(&helm::new(HELM_CMD) - .sudo() - .uninstall(&app.release_name) - .namespace(&app.namespace) - .build())?; + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; + + ssh_client.execute( + &helm_client::new(HELM_CMD) + .sudo() + .uninstall(&app.release_name) + .namespace(&app.namespace) + .build(), + )?; Ok(()) } - - pub fn save_cluster_workload(&self, cluster_name: &str, res: ClusterResource) -> Result { + pub fn save_cluster_workload( + &self, + cluster_name: &str, + res: ClusterResource, + ) -> crate::Result { info!("Save cluster workload"); - let mut cluster = self.repository + let mut cluster = self + .repository .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; @@ -449,13 +570,16 @@ impl Operator { Ok(res_id) } - pub fn update_cluster_workload(&self, cluster_name: &str, res: ClusterResource) -> Result<()> { + pub fn update_cluster_workload(&self, cluster_name: &str, res: ClusterResource) -> crate::Result<()> { info!("Update cluster workload"); - let mut cluster = self.repository + let mut cluster = self + .repository .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; - let res_to_update = cluster.cluster_resources.iter_mut() + let res_to_update = cluster + .cluster_resources + .iter_mut() .find(|i| i.id == res.id) .ok_or(Error::ResourceNotFound)?; @@ -466,53 +590,80 @@ impl Operator { Ok(()) } - pub fn install_cluster_workload(&self, cluster_name: &str, res_id: &str) -> Result<()> { + pub fn install_cluster_workload(&self, cluster_name: &str, res_id: &str) -> crate::Result<()> { info!("Install cluster workload"); - let cluster = self.repository + let cluster = self + .repository .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; - let res = cluster.cluster_resources.iter() + let res = cluster + .cluster_resources + .iter() .find(|i| i.id == res_id) .ok_or(Error::ResourceNotFound)?; - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master).cloned().ok_or(Error::ResourceNotFound)?; - - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master) + .cloned() + .ok_or(Error::ResourceNotFound)?; + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; - crate::operator::dispatcher::install_cluster_resource(&ssh_client, res)?; + crate::dispatcher::install_cluster_resource(&ssh_client, res)?; Ok(()) } - pub fn uninstall_cluster_workload(&self, cluster_name: &str, res_id: &str) -> Result<()> { + pub fn uninstall_cluster_workload(&self, cluster_name: &str, res_id: &str) -> crate::Result<()> { info!("Uninstall cluster workload"); - let cluster = self.repository + let cluster = self + .repository .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; - let res = cluster.cluster_resources.iter() + let res = cluster + .cluster_resources + .iter() .find(|i| i.id == res_id) .ok_or(Error::ResourceNotFound)?; - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master).cloned().ok_or(Error::ResourceNotFound)?; - - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; + let master_node = cluster + .nodes + .iter() + .find(|i| i.node_type == ClusterNodeType::Master) + .cloned() + .ok_or(Error::ResourceNotFound)?; + let mut ssh_client = ssh_client::Client::new(); + ssh_client.connect( + &master_node.ip_address, + &cluster.node_username, + &cluster.ssh_key.private_key, + &cluster.ssh_key.public_key, + )?; let file_name = format!("{}_cluster_resource", res.name.trim().replace(' ', "_")); - ssh_client.upload_file(format!("/tmp/{}.yaml", file_name).as_str(), res.content.as_str())?; - ssh_client.execute(format!("sudo microk8s.kubectl delete -f /tmp/{}.yaml", file_name).as_str())?; + ssh_client.upload_file( + format!("/tmp/{}.yaml", file_name).as_str(), + res.content.as_str(), + )?; + ssh_client + .execute(format!("sudo microk8s.kubectl delete -f /tmp/{}.yaml", file_name).as_str())?; ssh_client.execute(format!("sudo rm /tmp/{}.yaml", file_name).as_str())?; Ok(()) } - pub fn delete_cluster_workload(&self, cluster_name: &str, res_id: &str) -> Result<()> { + pub fn delete_cluster_workload(&self, cluster_name: &str, res_id: &str) -> crate::Result<()> { info!("Delete cluster workload"); - let mut cluster = self.repository + let mut cluster = self + .repository .get_cluster(cluster_name)? .ok_or(Error::ResourceNotFound)?; @@ -521,4 +672,4 @@ impl Operator { self.repository.save_cluster(cluster)?; Ok(()) } -} \ No newline at end of file +} diff --git a/makoon/src/operator/repository.rs b/core/src/repository.rs similarity index 83% rename from makoon/src/operator/repository.rs rename to core/src/repository.rs index 6e50b6d..5b13bda 100644 --- a/makoon/src/operator/repository.rs +++ b/core/src/repository.rs @@ -1,7 +1,7 @@ use std::fmt::{Debug, Display, Formatter}; +use crate::model::{Cluster, LogEntry}; +use crate::repository_json::JsonRepository; -use crate::operator::model::{Cluster, LogEntry}; -use crate::operator::repository_json::JsonRepository; #[derive(Clone, Debug)] pub enum Error { @@ -13,7 +13,7 @@ impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Error::DB(e) => write!(f, "{}", e), - Error::IO(e) => write!(f, "{}", e) + Error::IO(e) => write!(f, "{}", e), } } } @@ -26,19 +26,15 @@ impl From for String { pub type Result = std::result::Result; - pub struct Repository { inner: JsonRepository, } - impl Repository { pub fn new(path: &str) -> Result { - Ok( - Repository { - inner: JsonRepository::new(path)? - } - ) + Ok(Repository { + inner: JsonRepository::new(path)?, + }) } pub fn get_clusters(&self) -> Result> { @@ -69,5 +65,3 @@ impl Repository { Ok(()) } } - - diff --git a/makoon/src/operator/repository_json.rs b/core/src/repository_json.rs similarity index 56% rename from makoon/src/operator/repository_json.rs rename to core/src/repository_json.rs index d7ed4b3..aa6dd20 100644 --- a/makoon/src/operator/repository_json.rs +++ b/core/src/repository_json.rs @@ -1,18 +1,18 @@ use std::fs::{File, OpenOptions}; use std::io::{BufReader, Write}; use std::sync::{Arc, Mutex}; +use log::{error, warn}; use serde::{Deserialize, Serialize}; +use crate::model::{Cluster, LogEntry}; +use crate::repository::Error; -use crate::operator::model::{Cluster, LogEntry}; -use crate::operator::repository::{Error, Result}; pub struct JsonRepository { path: String, mutex: Arc>, } - #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct DbData { @@ -21,33 +21,36 @@ pub(crate) struct DbData { } impl JsonRepository { - pub fn new(path: &str) -> Result { + pub fn new(path: &str) -> crate::repository::Result { let repo = JsonRepository { path: format!("{}.json", path), mutex: Arc::new(Mutex::new(0)), }; match repo.load() { Ok(_) => (), - Err(e) => { - match e.clone() { - Error::DB(es) => { - error!("Cannot load database, error: [{}]", es); - return Err(e); - } - Error::IO(e) => { - warn!("Cannot load database file, error: [{}], create new database file", e); - repo.save( - DbData { - clusters: vec![], - action_log: vec![], - }).unwrap_or_else(|_| panic!("cannot save database to [{}], error: [{:?}]", path, e)); - } + Err(e) => match e.clone() { + Error::DB(es) => { + error!("Cannot load database, error: [{}]", es); + return Err(e); + } + Error::IO(e) => { + warn!( + "Cannot load database file, error: [{}], create new database file", + e + ); + repo.save(DbData { + clusters: vec![], + action_log: vec![], + }) + .unwrap_or_else(|_| { + panic!("cannot save database to [{}], error: [{:?}]", path, e) + }); } - } + }, }; Ok(repo) } - fn save(&self, data: DbData) -> Result<()> { + fn save(&self, data: DbData) -> crate::repository::Result<()> { let mutex = self.mutex.clone(); let mutex = mutex.lock().unwrap(); @@ -60,11 +63,13 @@ impl JsonRepository { .map_err(|e| Error::IO(e.to_string()))?; let result = serde_json::to_string_pretty(&data).map_err(|e| Error::DB(e.to_string()))?; - let result = file.write_all(result.as_bytes()).map_err(|e| Error::DB(e.to_string())); + let result = file + .write_all(result.as_bytes()) + .map_err(|e| Error::DB(e.to_string())); drop(mutex); return result; } - fn load(&self) -> Result { + fn load(&self) -> crate::repository::Result { let mutex = self.mutex.clone(); let mutex = mutex.lock().unwrap(); @@ -75,17 +80,16 @@ impl JsonRepository { Ok(result) } - pub fn get_clusters(&self) -> Result> { + pub fn get_clusters(&self) -> crate::repository::Result> { self.load().map(|v| v.clusters) } - pub fn get_cluster(&self, name: &str) -> Result> { + pub fn get_cluster(&self, name: &str) -> crate::repository::Result> { let clusters = self.get_clusters()?; - Ok(clusters.into_iter() - .find(|i| i.cluster_name == name)) + Ok(clusters.into_iter().find(|i| i.cluster_name == name)) } - pub fn delete_cluster(&self, name: &str) -> Result<()> { + pub fn delete_cluster(&self, name: &str) -> crate::repository::Result<()> { let mut data = self.load()?; data.clusters.retain(|e| e.cluster_name != name); @@ -94,12 +98,17 @@ impl JsonRepository { self.save(data) } - pub fn save_cluster(&self, cluster_to_save: Cluster) -> Result<()> { + pub fn save_cluster(&self, cluster_to_save: Cluster) -> crate::repository::Result<()> { let mut data = self.load()?; - let to_update = data.clusters.iter().any(|i| i.cluster_name == cluster_to_save.cluster_name); + let to_update = data + .clusters + .iter() + .any(|i| i.cluster_name == cluster_to_save.cluster_name); if to_update { - let clusters = data.clusters.into_iter() + let clusters = data + .clusters + .into_iter() .map(move |i| { return if i.cluster_name == (&cluster_to_save).cluster_name { (&cluster_to_save).clone() @@ -116,9 +125,10 @@ impl JsonRepository { self.save(data) } - pub fn logs(&self, cluster_name: &str) -> Result> { + pub fn logs(&self, cluster_name: &str) -> crate::repository::Result> { let data = self.load()?; - let mut result: Vec = data.action_log + let mut result: Vec = data + .action_log .into_iter() .filter(|i| i.cluster_name == cluster_name) .collect(); @@ -126,12 +136,12 @@ impl JsonRepository { Ok(result) } - pub fn save_log(&self, entry: LogEntry) -> Result<()> { + pub fn save_log(&self, entry: LogEntry) -> crate::repository::Result<()> { let mut data = self.load()?; data.action_log.push(entry); self.save(data) } - pub fn delete_logs(&self, cluster_name: &str) -> Result<()> { + pub fn delete_logs(&self, cluster_name: &str) -> crate::repository::Result<()> { let mut data = self.load()?; data.action_log.retain(|i| i.cluster_name != cluster_name); self.save(data)?; diff --git a/core/src/supported.rs b/core/src/supported.rs new file mode 100644 index 0000000..e8489ad --- /dev/null +++ b/core/src/supported.rs @@ -0,0 +1,20 @@ +use std::collections::HashMap; + +pub fn kube_versions() -> Vec { + vec![ + "1.28/stable".to_string(), + "1.27/stable".to_string(), + "1.26/stable".to_string(), + "1.25/stable".to_string(), + "1.24/stable".to_string(), + ] +} + +pub fn os_images() -> HashMap { + HashMap::from([ + ( + "Ubuntu Server 22.04 LTS - jammy-server-cloudimg-amd64.img".to_string(), + "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img".to_string() + ) + ]) +} \ No newline at end of file diff --git a/helm-client/Cargo.toml b/helm-client/Cargo.toml new file mode 100644 index 0000000..d54cd8f --- /dev/null +++ b/helm-client/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "helm-client" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/makoon/src/helm/command_builder.rs b/helm-client/src/command_builder.rs similarity index 90% rename from makoon/src/helm/command_builder.rs rename to helm-client/src/command_builder.rs index 414030b..5155e68 100644 --- a/makoon/src/helm/command_builder.rs +++ b/helm-client/src/command_builder.rs @@ -45,45 +45,34 @@ impl CommandBuilder { self } - #[allow(dead_code)] pub fn install(&self) -> InstallCommand { let mut parts = self.build(); parts.push("install".to_string()); - InstallCommand { - parts - } + InstallCommand { parts } } pub fn upgrade_or_install(&self) -> InstallCommand { let mut parts = self.build(); parts.push("upgrade".to_string()); parts.push("--install".to_string()); - InstallCommand { - parts - } + InstallCommand { parts } } pub fn uninstall(&self, name: &str) -> UninstallCommand { let mut parts = self.build(); parts.push("uninstall".to_string()); parts.push(name.to_string()); - UninstallCommand { - parts - } + UninstallCommand { parts } } pub fn list(&self) -> ListCommand { let mut parts = self.build(); parts.push("list".to_string()); - ListCommand { - parts - } + ListCommand { parts } } pub fn repo(&self) -> RepoCommand { let mut parts = self.build(); parts.push("repo".to_string()); - RepoCommand { - parts - } + RepoCommand { parts } } } @@ -187,10 +176,9 @@ impl RepoCommand { } } - #[cfg(test)] mod test { - use crate::helm::command_builder::CommandBuilder; + use crate::CommandBuilder; #[test] fn install_chart() { @@ -222,11 +210,7 @@ mod test { #[test] fn list_releases() { - let result = CommandBuilder::default() - .list() - .all() - .json() - .build(); + let result = CommandBuilder::default().list().all().json().build(); assert_eq!("helm list -A -o json", result) } @@ -250,14 +234,10 @@ mod test { assert_eq!("helm repo add sample https://aaa.com", result); } - #[test] fn repo_update() { - let result = CommandBuilder::default() - .repo() - .update() - .build(); + let result = CommandBuilder::default().repo().update().build(); assert_eq!("helm repo update", result); } -} \ No newline at end of file +} diff --git a/makoon/src/helm/mod.rs b/helm-client/src/lib.rs similarity index 98% rename from makoon/src/helm/mod.rs rename to helm-client/src/lib.rs index 6726f9c..bc3233b 100644 --- a/makoon/src/helm/mod.rs +++ b/helm-client/src/lib.rs @@ -4,4 +4,4 @@ pub use command_builder::*; pub fn new(binary: &str) -> CommandBuilder { CommandBuilder::new(binary) -} \ No newline at end of file +} diff --git a/makoon/Cargo.toml b/makoon/Cargo.toml deleted file mode 100644 index 231ab8a..0000000 --- a/makoon/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "makoon" -version = "0.1.0" -edition = "2021" - -[dependencies] -tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } -actix-web = "4.3.1" -actix-session = { version = "0.7.2", features = ["cookie-session"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -proxmox = { path = "../proxmox" } -rust-embed = "6.8.1" -mime_guess = "2.0.4" -typeshare = "1.0.1" -chrono = { version = "0.4", features = ["serde"] } -log = "0.4.19" -env_logger = "0.10.0" -openssl = "0.10.55" -pem = "3.0.1" -ssh-keys = "0.1.4" -ssh2 = "0.9.4" -rayon = "1.7.0" -uuid = { version = "1.4.1", features = ["v4", "fast-rng"] } \ No newline at end of file diff --git a/makoon/src/handlers/actix.rs b/makoon/src/handlers/actix.rs deleted file mode 100644 index c329660..0000000 --- a/makoon/src/handlers/actix.rs +++ /dev/null @@ -1,69 +0,0 @@ -use actix_session::Session; -use serde::{Deserialize, Serialize}; - -#[macro_export] -macro_rules! logged_in { - ($session: expr, $proxmox_client: expr) => { - { - - let access = crate::handlers::actix::get_session(&$session); - let access = match access { - Some(v) => v.access, - None => return Err(crate::handlers::error::HandlerError::UnAuthorized) - }; - - let access_to_get_permissions = access.clone(); - let proxmox_to_get_permissions = $proxmox_client.clone(); - let permissions_handle = tokio::task::spawn_blocking( - move || proxmox_to_get_permissions - .operations(access_to_get_permissions) - .permissions()); - - match permissions_handle.await.unwrap() { - Ok(_) => access, - Err(_) => return Err(crate::handlers::error::HandlerError::UnAuthorized) - - } - } - } -} - - -pub mod inject { - use actix_web::web; - use crate::operator; - pub type ProxmoxClient = web::Data; - pub type Operator = web::Data; -} - - -const SESSION_DATA_KEY: &str = "data"; - -#[derive(Serialize, Deserialize, Debug)] -pub struct WebSession { - pub access: proxmox::model::AccessData, -} - -pub fn store_session(session: &Session, data: WebSession) { - let session_data = serde_json::to_string(&data).unwrap(); - session.insert(SESSION_DATA_KEY, session_data).unwrap(); -} - -pub fn get_session(session: &Session) -> Option { - let result = session.get::(SESSION_DATA_KEY); - let result = match result { - Ok(v) => v, - Err(_) => return None - }; - - let result = match result { - Some(v) => v, - None => return None - }; - - let result = serde_json::from_str(&result); - match result { - Ok(v) => Some(v), - Err(_) => None - } -} \ No newline at end of file diff --git a/makoon/src/handlers/cluster.rs b/makoon/src/handlers/cluster.rs deleted file mode 100644 index 815e996..0000000 --- a/makoon/src/handlers/cluster.rs +++ /dev/null @@ -1,186 +0,0 @@ -use actix_session::Session; -use actix_web::{delete, get, HttpResponse, post, put, Responder, web}; - -use proxmox::model::VirtualMachine; - -use crate::{logged_in, operator}; -use crate::handlers::actix::inject; -use crate::handlers::error::HandlerError; -use crate::handlers::model::{ChangeNodeResourcesRequest, ClusterNodeVmStatus}; -use crate::operator::model::{Cluster, ClusterNode, ClusterRequest, LogEntry}; - -#[get("/api/v1/clusters/{cluster_name}/nodes")] -pub async fn get_nodes(path: web::Path, session: Session, operator: inject::Operator, proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let _ = logged_in!(session, proxmox_client); - - let cluster_name = path.into_inner(); - - let result = operator - .get_nodes(&cluster_name)?; - - Ok(HttpResponse::Ok().json(result)) -} - -#[post("/api/v1/clusters/{cluster_name}/nodes")] -pub async fn add_node_to_cluster(body: web::Json, - path: web::Path, - session: Session, - operator: inject::Operator, - proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let access = logged_in!(session, proxmox_client); - let cluster_name = path.into_inner(); - - let added_node = operator.add_node_cluster(access, cluster_name, body.0)?; - Ok(HttpResponse::Created().json(added_node)) -} - -#[put("/api/v1/clusters/{cluster_name}/nodes/{node_name}/resources")] -pub async fn change_node_resources(body: web::Json, - path: web::Path<(String, String)>, - session: Session, - operator: inject::Operator, - proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let access = logged_in!(session, proxmox_client); - let (cluster_name, node_name) = path.into_inner(); - operator.change_node_resources(access, cluster_name, node_name, body.cores, body.memory)?; - Ok(HttpResponse::Accepted()) -} - -#[post("/api/v1/clusters")] -pub async fn create_cluster(body: web::Json, - session: Session, - operator: inject::Operator, - proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let access = logged_in!(session, proxmox_client); - - operator.create_cluster(access, body.0)?; - Ok(HttpResponse::Created().finish()) -} - -#[get("/api/v1/clusters")] -pub async fn get_clusters(session: Session, operator: inject::Operator, proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let _ = logged_in!(session, proxmox_client); - let result = operator.get_clusters()?; - Ok(HttpResponse::Ok().json(result)) -} - -#[get("/api/v1/clusters/{name}")] -pub async fn get_cluster(path: web::Path, session: Session, operator: inject::Operator, proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let _ = logged_in!(session, proxmox_client); - - let name = path.into_inner(); - - let result = operator - .get_cluster(&name)? - .ok_or(HandlerError::NotFound("Cluster not found".to_string()))?; - - Ok(HttpResponse::Ok().json(result)) -} - - -#[delete("/api/v1/clusters/{name}")] -pub async fn delete_cluster(path: web::Path, session: Session, operator: inject::Operator, proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let access = logged_in!(session, proxmox_client); - let name = path.into_inner(); - - operator.delete_cluster(access, name)?; - Ok(HttpResponse::Ok().finish()) -} - -#[delete("/api/v1/clusters/{cluster_name}/nodes/{node_name}")] -pub async fn delete_node_from_cluster(path: web::Path<(String, String)>, session: Session, operator: inject::Operator, proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let access = logged_in!(session, proxmox_client); - let (cluster_name, node_name) = path.into_inner(); - - let deleted_node = operator.delete_node_from_cluster(access, cluster_name, node_name)?; - Ok(HttpResponse::Ok().json(deleted_node)) -} - -#[get("/api/v1/clusters/generate")] -pub async fn generate_default_cluster_configuration(session: Session, proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let access = logged_in!(session, proxmox_client); - - let result = web::block( - move || { - let generator = operator::DefaultClusterConfigurationGenerator::new(proxmox_client.operations(access)); - Ok::(generator.generate()?) - }).await??; - - Ok(HttpResponse::Ok().json(result)) -} - -#[get("/api/v1/clusters/{name}/logs")] -pub async fn logs_for_cluster(path: web::Path, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { - let _ = logged_in!(session, proxmox_client); - - let name = path.into_inner(); - - let result = web::block( - move || { - Ok::, HandlerError>(operator.logs_for_cluster(&name)?) - }).await??; - - Ok(HttpResponse::Ok().json(result)) -} - -#[delete("/api/v1/clusters/{name}/logs")] -pub async fn clear_logs_for_cluster(path: web::Path, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { - let _ = logged_in!(session, proxmox_client); - - let name = path.into_inner(); - - web::block( - move || { - operator.clear_logs_for_cluster(&name)?; - Ok::<(), HandlerError>(()) - }).await??; - - Ok(HttpResponse::Ok()) -} - -#[get("/api/v1/clusters/{name}/status/vms")] -pub async fn cluster_vm_status(path: web::Path, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { - let access = logged_in!(session, proxmox_client); - let name = path.into_inner(); - - let cluster = web::block( - move || { - let result = operator.get_cluster(&name)?; - Ok::, HandlerError>(result) - }) - .await?? - .ok_or(HandlerError::NotFound("Cluster not found".to_string()))?; - - - let vms = web::block(move || { - let result = proxmox_client - .operations(access) - .virtual_machines(&cluster.node, None)?; - Ok::, HandlerError>(result) - }).await??; - - let cluster_vm_ids = cluster.nodes.iter() - .map(|i| i.vm_id) - .collect::>(); - - let result = vms.iter() - .filter(|i| cluster_vm_ids.contains(&i.vm_id)) - .map(|i| ClusterNodeVmStatus { - vm_id: i.vm_id, - status: i.status.clone(), - }) - .collect::>(); - - Ok(HttpResponse::Ok().json(result)) -} - -#[get("/api/v1/clusters/{name}/status/kube")] -pub async fn cluster_kube_status(path: web::Path, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { - let _ = logged_in!(session, proxmox_client); - let name = path.into_inner(); - - - let result = web::block(move || operator.cluster_status(&name)).await??; - - Ok(HttpResponse::Ok().json(result)) -} \ No newline at end of file diff --git a/makoon/src/handlers/network.rs b/makoon/src/handlers/network.rs deleted file mode 100644 index a8e3fb4..0000000 --- a/makoon/src/handlers/network.rs +++ /dev/null @@ -1,32 +0,0 @@ -use actix_session::Session; -use actix_web::{get, HttpResponse, Responder, web}; - - -use crate::handlers::actix::inject; -use crate::handlers::error::HandlerError; -use crate::handlers::model::AvailableNetwork; -use crate::logged_in; - -#[get("/api/v1/nodes/{node}/networks/bridges")] -pub async fn networks_bridges(path: web::Path, session: Session, proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let node = path.into_inner(); - let access = logged_in!(session, proxmox_client); - - - let result = web::block( - move || { - let result = proxmox_client - .operations(access) - .networks(&node, Some(proxmox::model::NetworkType::Bridge))?; - Ok::, HandlerError>(result) - }).await??; - - let result = result.into_iter() - .map(|i| AvailableNetwork { - iface: i.iface, - address: i.address, - }) - .collect::>(); - - Ok(HttpResponse::Ok().json(result)) -} \ No newline at end of file diff --git a/makoon/src/handlers/nodes.rs b/makoon/src/handlers/nodes.rs deleted file mode 100644 index b814cb7..0000000 --- a/makoon/src/handlers/nodes.rs +++ /dev/null @@ -1,26 +0,0 @@ -use actix_session::Session; -use actix_web::{get, HttpResponse, Responder, web}; - -use proxmox::model::Node; - -use crate::handlers::actix::inject; -use crate::handlers::error::HandlerError; -use crate::logged_in; - -#[get("/api/v1/nodes")] -pub async fn nodes(session: Session, proxmox_client: inject::ProxmoxClient) -> actix_web::Result { - let access = logged_in!(session, proxmox_client); - - let result = web::block(move || { - let result = proxmox_client - .operations(access) - .nodes()?; - Ok::, HandlerError>(result) - }).await??; - - let result: Vec = result.into_iter() - .map(|i| i.node) - .collect(); - - Ok(HttpResponse::Ok().json(result)) -} diff --git a/makoon/src/operator/dispatcher/usecase/add_node_to_cluster.rs b/makoon/src/operator/dispatcher/usecase/add_node_to_cluster.rs deleted file mode 100644 index 639273d..0000000 --- a/makoon/src/operator/dispatcher/usecase/add_node_to_cluster.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use proxmox::Client; -use proxmox::model::AccessData; - -use crate::operator::Repository; -use crate::operator::dispatcher::usecase::common; -use crate::operator::model::{Cluster, ClusterNode, ClusterNodeType, LogEntry}; - -pub(crate) fn execute( - proxmox_client: Arc, - repo: Arc, - access: AccessData, - cluster_name: String, - node_name: String) -> Result<(), String> { - info!("Request to add node to the cluster has been received"); - let proxmox_client = proxmox_client.operations(access); - - repo.save_log(LogEntry::info(&cluster_name, "Start creating node".to_string()))?; - - - let cluster = repo.get_cluster(&cluster_name)?.ok_or("Cannot find cluster")?; - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master && i.name != node_name).cloned().ok_or("Cannot find any master node".to_string())?; - - let exising_cluster_hosts = cluster.nodes.iter() - .filter(|i| i.name != node_name) - .map(|i| (format!("{}-{}", cluster.cluster_name, i.name), i.ip_address.clone())) - .collect::>(); - - let existing_nodes = common::vm::get_existing_vms(&proxmox_client, &cluster)?.into_iter() - .filter(|i| i.name != node_name) - .collect(); - - let node_to_add = cluster.nodes.iter().find(|i| i.name == node_name).ok_or("Cannot find node to create")?; - - - common::vm::create(&proxmox_client, repo.clone(), &cluster, node_to_add)?; - - proxmox_client.start_vm(&cluster.node, node_to_add.vm_id).map_err(|e| format!("Cannot start VM [{}]: {}", node_to_add.vm_id, e))?; - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Starting VM [{}]", node_to_add.vm_id)))?; - - - common::vm::wait_for_start(&proxmox_client, &cluster, &node_to_add).map_err(|e| format!("Cannot start VM [{}]: {}", node_to_add.vm_id, e))?; - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("VM [{}] has been started", node_to_add.vm_id)))?; - - - common::vm::restart_vm_if_necessary(&proxmox_client, repo.clone(), &cluster, node_to_add)?; - - setup_vm(repo.clone(), &cluster, node_to_add, exising_cluster_hosts)?; - - add_new_node_host_to_existing_cluster(repo.clone(), &cluster, existing_nodes)?; - - common::cluster::install_kubernetes(repo.clone(), &cluster, node_to_add)?; - - common::cluster::wait_for_ready_kubernetes(repo.clone(), &cluster, node_to_add)?; - - common::cluster::join_node_to_cluster(repo.clone(), &cluster, &master_node, node_to_add)?; - - Ok(()) -} - -fn add_new_node_host_to_existing_cluster(repo: Arc, cluster: &Cluster, existing_nodes: Vec) -> Result<(), String> { - let hosts = cluster.nodes.iter() - .map(|i| (format!("{}-{}", cluster.cluster_name, i.name), i.ip_address.clone())) - .collect::>(); - - for node in existing_nodes.iter() { - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Add new node hostname to exising VM [{}]", node.vm_id)))?; - common::vm::setup_vm(cluster, node, &hosts)?; - } - Ok(()) -} - -fn setup_vm(repo: Arc, cluster: &Cluster, node: &ClusterNode, current_cluster_hosts: HashMap) -> Result<(), String> { - let mut hosts = cluster.nodes.iter() - .map(|i| (format!("{}-{}", cluster.cluster_name, i.name), i.ip_address.clone())) - .collect::>(); - - hosts.extend(current_cluster_hosts); - - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Configure VM [{}]", node.vm_id)))?; - common::vm::setup_vm(cluster, node, &hosts)?; - Ok(()) -} diff --git a/makoon/src/operator/dispatcher/usecase/common.rs b/makoon/src/operator/dispatcher/usecase/common.rs deleted file mode 100644 index 7cb4160..0000000 --- a/makoon/src/operator/dispatcher/usecase/common.rs +++ /dev/null @@ -1,381 +0,0 @@ -pub(crate) mod vm { - use std::collections::HashMap; - use std::path::Path; - use std::sync::Arc; - - use proxmox::{ClientOperations, to_url_encoded}; - use proxmox::model::{CreateVirtualMachine, DownloadImage, DownloadImageContentType, OsType, ParamBuilder, ResizeDisk, ScsiHw, StorageContent, VmStatus}; - - use crate::operator::dispatcher::utils::retry; - use crate::operator::model::{LogEntry, Cluster, ClusterNode}; - use crate::operator::{Repository, ssh}; - - pub(crate) fn create(proxmox_client: &ClientOperations, repo: Arc, cluster: &Cluster, node: &ClusterNode) -> Result<(), String> { - let os_image_path = download_os_image(proxmox_client, repo, cluster)?; - - proxmox_client.create_virtual_machine(CreateVirtualMachine { - vm_id: node.vm_id, - node: cluster.node.clone(), - name: format!("{}-{}", cluster.cluster_name, node.name), - cores: node.cores, - memory: node.memory, - os_type: OsType::L26, - net: HashMap::from([( - "net0".to_owned(), - ParamBuilder::default() - .add_param("model", "virtio") - .add_param("bridge", &cluster.network.bridge) - .build() - )]), - scsihw: Some(ScsiHw::VirtioScsiPci), - scsi: HashMap::from([( - "scsi0".to_owned(), - ParamBuilder::default() - .add_param_with_separator(&node.storage_pool, "0", ":") - .add_param("import-from", &os_image_path) - .build() - )]), - ide: HashMap::from([( - "ide2".to_owned(), - ParamBuilder::default() - .add_param_with_separator(&node.storage_pool, "cloudinit", ":") - .build() - )]), - boot: Some( - ParamBuilder::default() - .add_param("order", "scsi0") - .build() - ), - vga: Some("serial0".to_string()), - serial: HashMap::from([( - "serial0".to_owned(), - "socket".to_owned() - )]), - ipconfig: HashMap::from([( - "ipconfig0".to_owned(), - ParamBuilder::default() - .add_param("ip", format!("{}/{}", node.ip_address, cluster.network.subnet_mask).as_str()) - .add_param("gw", cluster.network.gateway.as_str()) - .build() - )]), - nameserver: Some(cluster.network.dns.clone()), - ci_user: Some(cluster.node_username.clone()), - ci_password: Some(cluster.node_password.clone()), - ssh_keys: Some(to_url_encoded(&cluster.ssh_key.public_key)), - })?; - - retry(|| { - let locked = proxmox_client.virtual_machines(&cluster.node, Some(true))? - .iter() - .find(|i| i.vm_id == node.vm_id) - .map(|i| i.lock.is_some()) - .unwrap_or(false); - if locked { Err(format!("VM [{}] is locked", node.vm_id)) } else { Ok(()) } - })?; - - proxmox_client.resize_disk(ResizeDisk { - vm_id: node.vm_id, - node: cluster.node.clone(), - disk: "scsi0".to_string(), - size: format!("{}G", cluster.disk_size), - })?; - Ok(()) - } - - fn download_os_image(proxmox_client: &ClientOperations, repo: Arc, cluster: &Cluster) -> Result { - let os_image = cluster.os_image.clone().unwrap_or("https://cloud-images.ubuntu.com/kinetic/current/kinetic-server-cloudimg-amd64.img".to_string()); - let os_image_storage = cluster.os_image_storage.clone().unwrap_or("local".to_string()); - - let file_name = Path::new(&os_image) - .file_name() - .map(|i| i.to_string_lossy().to_string()) - .ok_or("Cannot extract file name for path".to_string())?; - - let get_storage_content = || -> Result, String> { - Ok( - proxmox_client.storage_content(&cluster.node, &os_image_storage)? - .iter() - .find(|i| i.volid.ends_with(&file_name)) - .cloned() - ) - }; - - let existing_image = get_storage_content().map_err(|e| format!("Cannot check image availability [{}]", e.to_string()))?; - - let existing_image = match existing_image { - Some(v) => v, - None => { - proxmox_client.download_image(DownloadImage { - content: DownloadImageContentType::Iso, - filename: file_name.clone(), - node: cluster.node.clone(), - storage: os_image_storage.clone(), - url: os_image, - checksum: None, - checksum_algorithm: None, - verify_certificates: None, - })?; - - let image = retry(|| { - get_storage_content() - .map_err(|e| format!("Cannot check image availability [{}]", e.to_string()))? - .ok_or("Cannot download os image".to_string()) - })?; - - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("OS image has been downloaded [{}]", file_name)))?; - image - } - }; - - proxmox_client - .storage_content_details(&cluster.node, &os_image_storage, &existing_image.volid) - .map(|i| i.path) - .map_err(|e| e.to_string()) - } - - - pub fn wait_for_shutdown(proxmox_client: &proxmox::ClientOperations, node: &str, vm_id: u32) -> Result { - let is_shutdown = retry(|| { - let status = proxmox_client - .status_vm(node, vm_id) - .map(|i| i.status) - .map_err(|e| format!("Status VM {}, error: {}", vm_id, e))?; - - match status { - VmStatus::Running => { - info!("VM [{}] is running, wait 10 sec for gracefully shutdown", vm_id); - Err("Running".to_string()) - } - VmStatus::Stopped => Ok(()) - } - }); - - match is_shutdown { - Ok(_) => Ok(true), - Err(e) => { - match e.as_str() { - "Running" => Ok(false), - _ => Err(e) - } - } - } - } - - pub fn wait_for_start(proxmox_client: &proxmox::ClientOperations, - cluster: &Cluster, - cluster_node: &ClusterNode) -> Result<(), String> { - info!("Wait for VM start"); - retry(|| { - let status = proxmox_client - .status_vm(&cluster.node, cluster_node.vm_id) - .map(|i| i.status) - .map_err(|e| format!("Status VM [{}], error: {}", cluster_node.vm_id, e))?; - - match status { - VmStatus::Running => { - info!("VM [{}] is running", cluster_node.vm_id); - Ok(()) - } - VmStatus::Stopped => { - Err(format!("VM [{}] is stopped", cluster_node.vm_id)) - } - } - })?; - - info!("Check cloud-init status for VM [{}]", cluster_node.vm_id); - retry(|| { - let mut ssh_client = crate::operator::ssh::Client::new(); - ssh_client.connect(&cluster_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - ssh_client.execute("cloud-init status --wait") - })?; - Ok(()) - } - - - pub(crate) fn get_existing_vms(proxmox_client: &ClientOperations, cluster: &Cluster) -> Result, String> { - let vms = proxmox_client - .virtual_machines(&cluster.node, None)? - .into_iter() - .map(|i| (i.vm_id.clone(), i.name.unwrap_or_default())) - .collect::>(); - - let existing_nodes = cluster.nodes.clone().into_iter() - .filter(|e| { - let name = format!("{}-{}", cluster.cluster_name, e.name); - vms.contains(&(e.vm_id.clone(), name)) - }) - .collect::>(); - Ok(existing_nodes) - } - - pub(crate) fn stop_vm(proxmox_client: &ClientOperations, node: &str, vm_id: u32) -> Result<(), String> { - proxmox_client.shutdown_vm(node, vm_id)?; - let is_shutdown = wait_for_shutdown(proxmox_client, node, vm_id)?; - if !is_shutdown { - info!("Shutdown VM [{}] is timeout, stop VM immediately", vm_id); - proxmox_client.stop_vm(node, vm_id)?; - let is_shutdown = wait_for_shutdown(proxmox_client, node, vm_id)?; - if !is_shutdown { - error!("Cannot shutdown VM [{}]", vm_id); - return Err(format!("Cannot shutdown VM [{}]", vm_id)); - } - } - Ok(()) - } - - pub(crate) fn restart_vm_if_necessary(proxmox_client: &ClientOperations, - repo: Arc, - cluster: &Cluster, - node: &ClusterNode) -> Result<(), String> { - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - let is_restart_required = ssh_client.is_file_exists("/var/run/reboot-required")?; - if !is_restart_required { - return Ok(()); - } - - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Reboot is required, shutdown VM [{}]", node.vm_id)))?; - - stop_vm(proxmox_client, &cluster.node, node.vm_id)?; - - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Starting VM [{}]", node.vm_id)))?; - proxmox_client.start_vm(&cluster.node, node.vm_id)?; - wait_for_start(proxmox_client, cluster, node).map_err(|e| format!("Cannot start VM [{}]: {}", node.vm_id, e))?; - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("VM [{}] has been started", node.vm_id)))?; - Ok(()) - } - - pub(crate) fn setup_vm(cluster: &Cluster, node: &ClusterNode, hosts: &HashMap) -> Result<(), String> { - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - ssh_client.execute("sudo systemctl enable iscsid")?; - for (host, ip) in hosts.iter() { - ssh_client.execute(format!("echo '{} {}' | sudo tee -a /etc/cloud/templates/hosts.debian.tmpl", ip, host).as_str())?; - ssh_client.execute(format!("echo '{} {}' | sudo tee -a /etc/hosts", ip, host).as_str())?; - } - Ok(()) - } -} - -pub(crate) mod cluster { - use std::sync::Arc; - use serde::{Deserialize, Serialize}; - use crate::operator::model::{Cluster, ClusterNode, ClusterNodeType, LogEntry}; - use crate::operator::{Repository, ssh}; - - pub(crate) fn install_kubernetes(repo: Arc, cluster: &Cluster, node: &ClusterNode) -> Result<(), String> { - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Install Kubernetes on VM [{}]", node.vm_id)))?; - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - ssh_client.execute(format!("sudo snap install microk8s --channel={} --classic", cluster.kube_version.clone().unwrap_or("1.24/stable".to_owned())).as_str())?; - Ok(()) - } - - - pub(crate) fn wait_for_ready_kubernetes(repo: Arc, cluster: &Cluster, node: &ClusterNode) -> Result<(), String> { - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Wait for Kubernetes on VM [{}]", node.vm_id)))?; - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - ssh_client.execute("sudo microk8s status --wait-ready")?; - Ok(()) - } - - #[derive(Serialize, Deserialize)] - pub(crate) struct JoinNode { - pub(crate) token: String, - pub(crate) urls: Vec, - } - - pub(crate) fn join_node_to_cluster(repo: Arc, cluster: &Cluster, master_node: &ClusterNode, node_to_join: &ClusterNode) -> Result<(), String> { - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Generate join token on VM [{}] ", master_node.vm_id)))?; - - let mut master_ssh_client = ssh::Client::new(); - master_ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - let token_content = master_ssh_client.execute("sudo microk8s add-node --format json")?; - let join: JoinNode = serde_json::from_str(&token_content).map_err(|e| e.to_string())?; - if join.urls.is_empty() { - return Err("Join token doesn't have urls to join node".to_string()); - } - let join = join.urls.first().ok_or("Cannot get url to join".to_string())?; - let mut worker_ssh_client = ssh::Client::new(); - worker_ssh_client.connect(&node_to_join.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - let command = format!("sudo microk8s join {}", join); - let command = match node_to_join.node_type { - ClusterNodeType::Master => command, - ClusterNodeType::Worker => format!("{} --worker", command) - }; - - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Join node [{}] with role [{}] to cluster", node_to_join.vm_id, node_to_join.node_type)))?; - worker_ssh_client.execute(command.as_str())?; - // master_ssh_client.execute( - // format!("sudo microk8s.kubectl label node {}-{} node-role.kubernetes.io/{}={}", - // cluster.cluster_name, - // node_to_join.name, - // node_to_join.node_type, - // node_to_join.node_type).as_str())?; - Ok(()) - } -} - -pub(crate) mod apps { - pub const HELM_CMD: &str = "microk8s.helm3"; - - use crate::helm; - use crate::operator::model::{ClusterResource, HelmApp}; - use crate::operator::ssh; - - pub fn install_helm_app(ssh_client: &ssh::Client, app: &HelmApp) -> Result<(), String> { - let values_file_name = app.release_name.trim().replace(" ", "_"); - - - ssh_client.execute(&helm::new(HELM_CMD) - .sudo() - .repo() - .add(&app.chart_name, &app.repository) - .build())?; - - ssh_client.execute(&helm::new(HELM_CMD) - .sudo() - .repo() - .update() - .build())?; - - if !app.values.is_empty() { - ssh_client.upload_file(format!("/tmp/{}.yaml", values_file_name).as_str(), &app.values)?; - } - - let mut command_builder = helm::new(HELM_CMD) - .sudo() - .upgrade_or_install() - .create_namespace() - .namespace(&app.namespace) - .name(&app.release_name) - .chart(&app.chart_name, &app.chart_name); - - - if !app.values.is_empty() { - command_builder = command_builder.with_values_file(&format!("/tmp/{}.yaml", values_file_name)); - } - if !app.chart_version.is_empty() { - command_builder = command_builder.version(&app.chart_version); - } - if app.wait { - command_builder = command_builder.wait(); - } - ssh_client.execute(&command_builder.build())?; - - if !app.values.is_empty() { - ssh_client.execute(format!("sudo rm /tmp/{}.yaml", values_file_name).as_str())?; - } - Ok(()) - } - - pub fn install_cluster_resource(ssh_client: &ssh::Client, resource: &ClusterResource) -> Result<(), String> { - let file_name = format!("{}_cluster_resource", resource.name.trim().replace(" ", "_")); - ssh_client.upload_file(format!("/tmp/{}.yaml", file_name).as_str(), resource.content.as_str())?; - ssh_client.execute(format!("sudo microk8s.kubectl apply -f /tmp/{}.yaml", file_name).as_str())?; - ssh_client.execute(format!("sudo rm /tmp/{}.yaml", file_name).as_str())?; - Ok(()) - } -} - diff --git a/makoon/src/operator/dispatcher/usecase/create_cluster.rs b/makoon/src/operator/dispatcher/usecase/create_cluster.rs deleted file mode 100644 index 2cfa196..0000000 --- a/makoon/src/operator/dispatcher/usecase/create_cluster.rs +++ /dev/null @@ -1,236 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use openssl::rsa::Rsa; -use pem::{encode, Pem}; - -use proxmox::{Client, ClientOperations}; -use proxmox::model::AccessData; - -use crate::operator::dispatcher::usecase::common; -use crate::operator::model::{Cluster, ClusterNode, ClusterNodeType, KeyPair, LogEntry}; -use crate::operator::repository::Repository; -use crate::operator::ssh; - -pub(crate) fn execute( - proxmox_client: Arc, - repo: Arc, - access: AccessData, - cluster_name: String) -> Result<(), String> { - info!("Cluster creation request has been received"); - let proxmox_client = proxmox_client.operations(access); - - repo.save_log(LogEntry::info(&cluster_name, "Start creating cluster".to_string()))?; - - let mut cluster = repo.get_cluster(&cluster_name)?.ok_or("Cannot find cluster")?; - let keys = generate_ssh_keys()?; - cluster.ssh_key = keys; - repo.save_cluster(cluster.clone())?; - - create_vms(&proxmox_client, &cluster, repo.clone())?; - start_vms(&proxmox_client, &cluster, repo.clone())?; - wait_for_vms_start(&proxmox_client, &cluster, repo.clone())?; - restart_vms_if_necessary(&proxmox_client, &cluster, repo.clone())?; - setup_vms(repo.clone(), &cluster)?; - install_kubernetes(repo.clone(), &cluster)?; - wait_for_ready_kubernetes(repo.clone(), &cluster)?; - join_nodes_to_cluster(repo.clone(), &cluster)?; - add_kubeconfig_to_project(repo.clone(), &mut cluster)?; - enable_microk8s_addons(repo.clone(), &cluster)?; - install_helm_apps(repo.clone(), &cluster)?; - install_cluster_resources(repo.clone(), &cluster)?; - Ok(()) -} - -fn install_cluster_resources(repo: Arc, cluster: &Cluster) -> Result<(), String> { - repo.save_log(LogEntry::info(&cluster.cluster_name, "Install Cluster resources".to_string()))?; - - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master) - .map(|i| i.clone()).ok_or("Cannot find any master node".to_string())?; - - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - - for resource in cluster.cluster_resources.iter() { - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Apply cluster resource: [{}]", resource.name)))?; - common::apps::install_cluster_resource(&ssh_client, resource)?; - } - Ok(()) -} - - -fn install_helm_apps(repo: Arc, cluster: &Cluster) -> Result<(), String> { - repo.save_log(LogEntry::info(&cluster.cluster_name, "Install Helm apps".to_string()))?; - - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master).cloned().ok_or("Cannot find any master node".to_string())?; - - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - - for app in cluster.helm_apps.iter() { - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Install Helm app: [{}]", app.release_name)))?; - common::apps::install_helm_app(&ssh_client, app)?; - } - Ok(()) -} - - -fn enable_microk8s_addons(repo: Arc, cluster: &Cluster) -> Result<(), String> { - repo.save_log(LogEntry::info(&cluster.cluster_name, "Enable MicroK8s addons: [dns, helm3]".to_string()))?; - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master).cloned().ok_or("Cannot find any master node".to_string())?; - - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - ssh_client.execute("sudo microk8s enable dns")?; - ssh_client.execute("sudo microk8s enable helm3")?; - Ok(()) -} - -fn add_kubeconfig_to_project(repo: Arc, cluster: &mut Cluster) -> Result<(), String> { - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Add kube config to project")))?; - let mut master_nodes = cluster.nodes.iter() - .filter(|i| i.node_type == ClusterNodeType::Master).cloned() - .collect::>(); - master_nodes.sort_by(|a, b| a.vm_id.cmp(&b.vm_id)); - let first_master_node = master_nodes.first().ok_or("Cannot get first master node".to_string())?; - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&first_master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - let kube_config_content = ssh_client.execute("sudo microk8s config")?; - cluster.cluster_config = kube_config_content.clone(); - let mut cluster_to_update = repo - .get_cluster(&cluster.cluster_name)? - .ok_or("Cannot read cluster from repository".to_string())?; - cluster_to_update.cluster_config = kube_config_content; - repo.save_cluster(cluster_to_update)?; - Ok(()) -} - - -fn join_nodes_to_cluster(repo: Arc, cluster: &Cluster) -> Result<(), String> { - if cluster.nodes.len() == 1 { - return Ok(()); - } - - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master).cloned().ok_or("Cannot find any master node".to_string())?; - - let nodes_to_join = cluster.nodes.clone().into_iter() - .filter(|i| i.vm_id != master_node.vm_id) - .collect::>(); - - for node_to_join in nodes_to_join.iter() { - common::cluster::join_node_to_cluster(repo.clone(), cluster, &master_node, node_to_join)?; - } - Ok(()) -} - - -pub(crate) fn install_kubernetes(repo: Arc, cluster: &Cluster) -> Result<(), String> { - info!("Install Kubernetes"); - for node in cluster.nodes.iter() { - common::cluster::install_kubernetes(repo.clone(), cluster, node)?; - } - Ok(()) -} - - -pub(crate) fn wait_for_ready_kubernetes(repo: Arc, cluster: &Cluster) -> Result<(), String> { - for node in cluster.nodes.iter() { - common::cluster::wait_for_ready_kubernetes(repo.clone(), cluster, node)?; - } - Ok(()) -} - -pub(crate) fn restart_vms_if_necessary(proxmox_client: &ClientOperations, - cluster: &Cluster, - repo: Arc) -> Result<(), String> { - info!("Restart VM's if necessary"); - for node in cluster.nodes.iter() { - common::vm::restart_vm_if_necessary(proxmox_client, repo.clone(), cluster, node)?; - } - Ok(()) -} - - -pub(crate) fn wait_for_vms_start(proxmox_client: &ClientOperations, cluster: &Cluster, repo: Arc) -> Result<(), String> { - info!("Waiting for VM's start"); - for node in cluster.nodes.iter() { - common::vm::wait_for_start(proxmox_client, cluster, node) - .map_err(|e| format!("Cannot start VM [{}]: {}", node.vm_id, e))?; - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("VM [{}] has been started", node.vm_id)))?; - } - Ok(()) -} - -fn setup_vms(repo: Arc, cluster: &Cluster) -> Result<(), String> { - info!("Setup VM's"); - let hosts = cluster.nodes.iter() - .map(|i| (format!("{}-{}", cluster.cluster_name, i.name), i.ip_address.clone())) - .collect::>(); - - - for node in cluster.nodes.iter() { - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Configure VM [{}]", node.vm_id)))?; - common::vm::setup_vm(cluster, node, &hosts)?; - } - Ok(()) -} - -fn generate_ssh_keys() -> Result { - info!("Generate SSH keys"); - let rsa = Rsa::generate(4096).map_err(|e| e.to_string())?; - let private_key = rsa.private_key_to_der().map_err(|e| e.to_string())?; - let private_key = Pem::new( - String::from("RSA PRIVATE KEY"), - private_key, - ); - let private_key: String = encode(&private_key); - - let ssh_private_key = ssh_keys::openssh::parse_private_key(private_key.as_str()).map_err(|e| e.to_string())?; - let ssh_private_key = ssh_private_key.get(0).ok_or("Cannot parse private key")?; - let ssh_public_key = ssh_private_key.public_key(); - info!("SSH keys has been generated"); - Ok(KeyPair { - public_key: ssh_public_key.to_string(), - private_key, - }) -} - -pub(crate) fn create_vms(proxmox_client: &ClientOperations, cluster: &Cluster, repo: Arc) -> Result<(), String> { - info!("Create VM's"); - let mut used_vm_ids: Vec = proxmox_client - .virtual_machines(&cluster.node, None)?.iter() - .map(|i| i.vm_id) - .collect(); - - used_vm_ids.extend(proxmox_client.lxc_containers(&cluster.node)?.iter() - .map(|i| &i.vm_id) - .map(|i| i.parse::().unwrap_or_default()) - .collect::>()); - - for node in cluster.nodes.iter() { - if used_vm_ids.contains(&node.vm_id) { - return Err(format!("VM with id [{}] already exists", node.vm_id)); - } - common::vm::create(proxmox_client, repo.clone(), cluster, node) - .map_err(|e| format!("Cannot create VM [{}]: {}", node.vm_id, e))?; - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("VM [{}] has been created", node.vm_id)))?; - }; - info!("VM's has been created"); - Ok(()) -} - -pub(crate) fn start_vms(proxmox_client: &ClientOperations, - cluster: &Cluster, - repo: Arc) -> Result<(), String> { - info!("Start VM's"); - for node in cluster.nodes.iter() { - proxmox_client.start_vm(&cluster.node, node.vm_id) - .map_err(|e| format!("Cannot start VM [{}]: {}", node.vm_id, e))?; - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Starting VM [{}]", node.vm_id)))?; - } - Ok(()) -} diff --git a/makoon/src/operator/dispatcher/usecase/delete_cluster.rs b/makoon/src/operator/dispatcher/usecase/delete_cluster.rs deleted file mode 100644 index 9e90eee..0000000 --- a/makoon/src/operator/dispatcher/usecase/delete_cluster.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::sync::Arc; - -use proxmox::ClientOperations; -use proxmox::model::AccessData; -use crate::operator::dispatcher::usecase::common; - -use crate::operator::model::{LogEntry, Cluster, ClusterNode}; -use crate::operator::repository::Repository; - -pub(crate) fn execute( - proxmox_client: Arc, - repo: Arc, - access: AccessData, - cluster_name: String) -> Result<(), String> { - let proxmox_client = proxmox_client.operations(access); - let cluster = repo.get_cluster(&cluster_name)?.ok_or("Cannot find cluster")?; - - let existing_nodes = common::vm::get_existing_vms(&proxmox_client, &cluster)?; - stop_vms(&repo, &proxmox_client, &cluster, &existing_nodes)?; - delete_vms(repo.clone(), &proxmox_client, &cluster, &existing_nodes)?; - - repo.delete_cluster(&cluster_name)?; - Ok(()) -} - - -pub(crate) fn stop_vms(repo: &Arc, proxmox_client: &ClientOperations, cluster: &Cluster, existing_nodes: &[ClusterNode]) -> Result<(), String> { - for node in existing_nodes.iter() { - proxmox_client.shutdown_vm(&cluster.node, node.vm_id).map_err(|e| format!("Shutdown VM [{}], error: [{}]", node.vm_id, e))?; - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Requested VM [{}] to shutdown", node.vm_id)))?; - } - - for node in existing_nodes.iter() { - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Wait for VM [{}] shutdown", node.vm_id)))?; - let is_shutdown = common::vm::wait_for_shutdown(proxmox_client, &cluster.node, node.vm_id)?; - if !is_shutdown { - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("VM [{}] cannot be shouted down gracefully, stop VM imminently", node.vm_id)))?; - proxmox_client.stop_vm(&cluster.node, node.vm_id)?; - common::vm::wait_for_shutdown(proxmox_client, &cluster.node, node.vm_id)?; - } - } - Ok(()) -} - -pub(crate) fn delete_vms(repo: Arc, proxmox_client: &ClientOperations, cluster: &Cluster, existing_nodes: &[ClusterNode]) -> Result<(), String> { - for node in existing_nodes.iter() { - proxmox_client.delete_vm(&cluster.node, node.vm_id).map_err(|e| format!("Delete VM [{}], error: [{}]", node.vm_id, e))?; - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("VM [{}] has been deleted", node.vm_id)))?; - } - Ok(()) -} \ No newline at end of file diff --git a/makoon/src/operator/dispatcher/usecase/delete_node_from_cluster.rs b/makoon/src/operator/dispatcher/usecase/delete_node_from_cluster.rs deleted file mode 100644 index 977ea06..0000000 --- a/makoon/src/operator/dispatcher/usecase/delete_node_from_cluster.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use proxmox::{Client, ClientOperations}; -use proxmox::model::AccessData; - -use crate::operator::{Repository, ssh}; -use crate::operator::dispatcher::usecase::common; -use crate::operator::model::{ClusterNodeType, LogEntry}; - -pub(crate) fn execute( - proxmox_client: Arc, - repo: Arc, - access: AccessData, - cluster_name: String, - node_name: String) -> Result<(), String> { - let proxmox_client = proxmox_client.operations(access); - repo.save_log(LogEntry::info(&cluster_name, format!("Start deleting node [{}-{}]", cluster_name, node_name)))?; - - let mut cluster = repo.get_cluster(&cluster_name)?.ok_or("Cannot find cluster")?; - if cluster.nodes.len() == 1 { - repo.save_log(LogEntry::error(&cluster_name, format!("Cannot delete last node, delete whole cluster instead")))?; - return Ok(()); - } - - let master_node = cluster.nodes.iter() - .find(|i| i.node_type == ClusterNodeType::Master && i.name != node_name) - .map(|i| i.clone()).ok_or("Cannot find any master node".to_string())?; - - let node_to_delete = cluster.nodes.iter() - .find(|i| i.name == node_name) - .map(|i| i.clone()).ok_or("Cannot find node to delete".to_string())?; - - - if common::vm::get_existing_vms(&proxmox_client, &cluster)?.iter() - .find(|i| i.vm_id == node_to_delete.vm_id) - .is_none() { - remove_node_from_project(repo.clone(), &cluster_name, &node_name)?; - remove_hosts_from_rest_of_nodes(repo.clone(), &proxmox_client, &cluster_name, &node_name)?; - return Ok(()); - } - - let mut master_ssh_client = ssh::Client::new(); - master_ssh_client.connect(&master_node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - - - repo.save_log(LogEntry::info(&cluster_name, format!("Drain a node [{}-{}]", cluster_name, node_name)))?; - master_ssh_client.execute(format!("sudo microk8s.kubectl drain {}-{} --ignore-daemonsets --grace-period=30 --timeout=60s", cluster_name, node_name).as_str())?; - repo.save_log(LogEntry::info(&cluster_name, "Wait 30s to gracefully shutdown pods".to_string()))?; - std::thread::sleep(Duration::from_secs(30)); - - repo.save_log(LogEntry::info(&cluster_name, format!("Detach a node [{}-{}] from the cluster", cluster_name, node_name)))?; - let mut node_to_delete_ssh_client = ssh::Client::new(); - node_to_delete_ssh_client.connect(&node_to_delete.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - node_to_delete_ssh_client.execute("sudo microk8s leave")?; - - master_ssh_client.execute(format!("sudo microk8s remove-node {}-{}", cluster_name, node_name).as_str())?; - - - cluster.nodes.retain_mut(|i| i.name == node_name); - let vm_exists = common::vm::get_existing_vms(&proxmox_client, &cluster)?.iter().any(|i| i.vm_id == node_to_delete.vm_id); - - if vm_exists { - repo.save_log(LogEntry::info(&cluster_name, format!("Removing VM [{}]", node_to_delete.vm_id)))?; - - proxmox_client.shutdown_vm(&cluster.node, node_to_delete.vm_id).map_err(|e| format!("Shutdown VM [{}], error: [{}]", node_to_delete.vm_id, e))?; - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("Requested VM [{}] to shutdown", node_to_delete.vm_id)))?; - let is_shutdown = common::vm::wait_for_shutdown(&proxmox_client, &cluster.node, node_to_delete.vm_id)?; - if !is_shutdown { - proxmox_client.stop_vm(&cluster.node, node_to_delete.vm_id)?; - common::vm::wait_for_shutdown(&proxmox_client, &cluster.node, node_to_delete.vm_id)?; - } - - proxmox_client.delete_vm(&cluster.node, node_to_delete.vm_id).map_err(|e| format!("Delete VM [{}], error: [{}]", node_to_delete.vm_id, e))?; - repo.save_log(LogEntry::info(&cluster.cluster_name, format!("VM [{}] has been deleted", node_to_delete.vm_id)))?; - } - - - remove_node_from_project(repo.clone(), &cluster_name, &node_name)?; - remove_hosts_from_rest_of_nodes(repo.clone(), &proxmox_client, &cluster_name, &node_name)?; - - Ok(()) -} - -fn remove_node_from_project(repo: Arc, cluster_name: &str, node_name: &str) -> Result<(), String> { - let mut cluster = repo.get_cluster(cluster_name)?.ok_or("Cannot find cluster")?; - cluster.nodes.retain_mut(|i| i.name != node_name); - repo.save_cluster(cluster)?; - Ok(()) -} - -fn remove_hosts_from_rest_of_nodes(repo: Arc, proxmox_client: &ClientOperations, cluster_name: &str, node_name: &str) -> Result<(), String> { - let cluster = repo.get_cluster(cluster_name)?.ok_or("Cannot find cluster")?; - let existing_nodes = common::vm::get_existing_vms(&proxmox_client, &cluster)?; - for node in existing_nodes.iter() { - let mut ssh_client = ssh::Client::new(); - ssh_client.connect(&node.ip_address, &cluster.node_username, &cluster.ssh_key.private_key, &cluster.ssh_key.public_key)?; - ssh_client.execute(format!("sudo sed -i '/{}-{}/d' /etc/cloud/templates/hosts.debian.tmpl", cluster_name, node_name).as_str())?; - ssh_client.execute(format!("sudo sed -i '/{}-{}/d' /etc/hosts", cluster_name, node_name).as_str())?; - } - Ok(()) -} diff --git a/makoon/src/operator/ssh/mod.rs b/makoon/src/operator/ssh/mod.rs deleted file mode 100644 index df8a1cb..0000000 --- a/makoon/src/operator/ssh/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod ssh; - -pub use ssh::*; \ No newline at end of file diff --git a/proxmox/Cargo.toml b/proxmox-client/Cargo.toml similarity index 93% rename from proxmox/Cargo.toml rename to proxmox-client/Cargo.toml index 2d73128..5c8ce78 100644 --- a/proxmox/Cargo.toml +++ b/proxmox-client/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "proxmox" +name = "proxmox-client" version = "0.1.0" edition = "2021" diff --git a/proxmox/src/client.rs b/proxmox-client/src/client.rs similarity index 100% rename from proxmox/src/client.rs rename to proxmox-client/src/client.rs diff --git a/proxmox/src/client_operations.rs b/proxmox-client/src/client_operations.rs similarity index 100% rename from proxmox/src/client_operations.rs rename to proxmox-client/src/client_operations.rs diff --git a/proxmox/src/error.rs b/proxmox-client/src/error.rs similarity index 100% rename from proxmox/src/error.rs rename to proxmox-client/src/error.rs diff --git a/proxmox/src/http.rs b/proxmox-client/src/http.rs similarity index 78% rename from proxmox/src/http.rs rename to proxmox-client/src/http.rs index 0a797a5..f3088df 100644 --- a/proxmox/src/http.rs +++ b/proxmox-client/src/http.rs @@ -4,8 +4,8 @@ use reqwest::blocking::RequestBuilder; use reqwest::header::{ACCEPT, CONTENT_TYPE}; use serde::{Deserialize, Serialize}; -use crate::{Error, Result}; use crate::model::Token; +use crate::{Error, Result}; pub struct HttpClient { client: reqwest::blocking::Client, @@ -31,65 +31,74 @@ impl HttpClient { pub fn host(&self) -> String { self.host.clone() } - + pub fn client(&self) -> &reqwest::blocking::Client { &self.client } pub fn get(&self, token: &Token, path: &str) -> Result - where for<'a> T: Serialize + Deserialize<'a> + where + for<'a> T: Serialize + Deserialize<'a>, { let req = self.client.get(self.url(path)); self.do_request(req, token) } - pub fn delete(&self, token: &Token, path: &str) -> Result - where for<'a> T: Serialize + Deserialize<'a> + where + for<'a> T: Serialize + Deserialize<'a>, { let req = self.client.delete(self.url(path)); self.do_request(req, token) } pub fn post(&self, token: &Token, path: &str, body: Option) -> Result - where - for<'a> T: Deserialize<'a> { + where + for<'a> T: Deserialize<'a>, + { let req = self.client.post(self.url(path)); let req = match body { Some(v) => req.header(CONTENT_TYPE, "application/json").json(&v), - None => req + None => req, }; self.do_request(req, token) } - pub fn put(&self, token: &Token, path: &str, body: Option) -> Result - where - for<'a> T: Deserialize<'a> { + where + for<'a> T: Deserialize<'a>, + { let req = self.client.put(self.url(path)); let req = match body { Some(v) => req.header(CONTENT_TYPE, "application/json").json(&v), - None => req + None => req, }; self.do_request(req, token) } fn url(&self, path: &str) -> String { - format!("https://{}:{}{}{}", self.host, self.port, self.base_path, path) + format!( + "https://{}:{}{}{}", + self.host, self.port, self.base_path, path + ) } fn do_request(&self, request: RequestBuilder, token: &Token) -> Result - where - for<'a> T: Deserialize<'a> { + where + for<'a> T: Deserialize<'a>, + { let request = request .header(ACCEPT, "application/json") .header("CSRFPreventionToken", token.csrf_prevention_token.clone()) - .header(reqwest::header::COOKIE, format!("PVEAuthCookie={}", token.ticket.clone())); + .header( + reqwest::header::COOKIE, + format!("PVEAuthCookie={}", token.ticket.clone()), + ); - trace!("Request: {:#?}",request); + trace!("Request: {:#?}", request); let response = request.send()?; let status = response.status(); let result = match status.as_u16() { @@ -104,10 +113,6 @@ impl HttpClient { } }; - serde_json::from_str(&result) - .map_err(|e| Error::BodyMalformed(e.to_string())) + serde_json::from_str(&result).map_err(|e| Error::BodyMalformed(e.to_string())) } } - - - diff --git a/proxmox/src/lib.rs b/proxmox-client/src/lib.rs similarity index 100% rename from proxmox/src/lib.rs rename to proxmox-client/src/lib.rs diff --git a/proxmox/src/model.rs b/proxmox-client/src/model.rs similarity index 100% rename from proxmox/src/model.rs rename to proxmox-client/src/model.rs diff --git a/ssh-client/Cargo.toml b/ssh-client/Cargo.toml new file mode 100644 index 0000000..5a221a5 --- /dev/null +++ b/ssh-client/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ssh-client" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ssh2 = "0.9" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +log = "0.4" diff --git a/ssh-client/src/lib.rs b/ssh-client/src/lib.rs new file mode 100644 index 0000000..89b2dc7 --- /dev/null +++ b/ssh-client/src/lib.rs @@ -0,0 +1,5 @@ +mod ssh; +pub use ssh::*; + +#[macro_use] +extern crate log; diff --git a/makoon/src/operator/ssh/ssh.rs b/ssh-client/src/ssh.rs similarity index 69% rename from makoon/src/operator/ssh/ssh.rs rename to ssh-client/src/ssh.rs index a436dc5..0345e98 100644 --- a/makoon/src/operator/ssh/ssh.rs +++ b/ssh-client/src/ssh.rs @@ -28,23 +28,30 @@ impl Client { } } } - pub fn connect(&mut self, ip_address: &str, username: &str, private_key: &str, public_key: &str) -> Result<(), String> { - let tcp = TcpStream::connect(format!("{}:{}", ip_address, self.port)).map_err(|e| e.to_string())?; + pub fn connect( + &mut self, + ip_address: &str, + username: &str, + private_key: &str, + public_key: &str, + ) -> Result<(), String> { + let tcp = TcpStream::connect(format!("{}:{}", ip_address, self.port)) + .map_err(|e| e.to_string())?; let mut session = ssh2::Session::new().map_err(|e| e.to_string())?; session.set_tcp_stream(tcp); session.handshake().map_err(|e| e.to_string())?; - session.userauth_pubkey_memory( - username, - Some(public_key), - private_key, - None, - ).map_err(|e| e.to_string())?; + session + .userauth_pubkey_memory(username, Some(public_key), private_key, None) + .map_err(|e| e.to_string())?; self.session = Some(session); Ok(()) } pub fn is_file_exists(&self, file_path: &str) -> Result { - let session = self.session.as_ref().ok_or("Client not connected".to_string())?; + let session = self + .session + .as_ref() + .ok_or("Client not connected".to_string())?; let sftp = session.sftp().unwrap(); let stats = sftp.stat(Path::new(file_path)); match stats { @@ -61,15 +68,16 @@ impl Client { } pub fn upload_file(&self, file_path: &str, content: &str) -> Result<(), String> { - let session = self.session.as_ref().ok_or("Client not connected".to_string())?; + let session = self + .session + .as_ref() + .ok_or("Client not connected".to_string())?; let content = content.as_bytes(); let content_length = u64::try_from(content.len()).map_err(|e| e.to_string())?; let mut remote_file = session - .scp_send(Path::new(file_path), - 0o644, - content_length, - None).map_err(|e| e.to_string())?; + .scp_send(Path::new(file_path), 0o644, content_length, None) + .map_err(|e| e.to_string())?; remote_file.write(content).map_err(|e| e.to_string())?; // Close the channel and wait for the whole content to be transferred remote_file.send_eof().map_err(|e| e.to_string())?; @@ -79,19 +87,30 @@ impl Client { Ok(()) } pub fn execute(&self, command: &str) -> Result { - let session = self.session.as_ref().ok_or("Client not connected".to_string())?; + let session = self + .session + .as_ref() + .ok_or("Client not connected".to_string())?; let mut channel = session.channel_session().map_err(|e| e.to_string())?; channel.exec(command).map_err(|e| e.to_string())?; let mut out = String::new(); - channel.read_to_string(&mut out).map_err(|e| e.to_string())?; + channel + .read_to_string(&mut out) + .map_err(|e| e.to_string())?; channel.wait_close().map_err(|e| e.to_string())?; - let exit_code = channel.exit_status().map_err(|e| e.to_string()).map_err(|e| e)?; + let exit_code = channel + .exit_status() + .map_err(|e| e.to_string()) + .map_err(|e| e)?; match exit_code { 0 => Ok(out), _ => { let mut err_out = String::new(); - channel.stderr().read_to_string(&mut err_out).map_err(|e| e.to_string())?; + channel + .stderr() + .read_to_string(&mut err_out) + .map_err(|e| e.to_string())?; if err_out.is_empty() { err_out = out; } @@ -100,8 +119,10 @@ impl Client { } } pub fn execute_to(&self, command: &str) -> Result - where for<'a> T: Deserialize<'a> { + where + for<'a> T: Deserialize<'a>, + { let output = self.execute(command)?; serde_json::from_str(&output).map_err(|e| e.to_string()) } -} \ No newline at end of file +} diff --git a/web/Cargo.toml b/web/Cargo.toml new file mode 100644 index 0000000..0000c84 --- /dev/null +++ b/web/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "web" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "makoon" +path = "src/main.rs" + +[features] +e2e = [] + +[dependencies] +core = { path = "../core" } +proxmox-client = { path = "../proxmox-client" } +tokio = { version = "1.32", features = ["macros", "rt-multi-thread"] } +actix-web = "4.4" +actix-session = { version = "0.8", features = ["cookie-session"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +rust-embed = "8.0" +mime_guess = "2.0" +typeshare = "1.0" +chrono = { version = "0.4", features = ["serde"] } +log = "0.4" +env_logger = "0.10" +rayon = "1.8" +uuid = { version = "1.4", features = ["v4", "fast-rng"] } + +[dev-dependencies] +reqwest = { version = "0.11", features = ["blocking", "json", "cookies"] } diff --git a/makoon/Makefile b/web/Makefile similarity index 100% rename from makoon/Makefile rename to web/Makefile diff --git a/makoon/src-web/.gitignore b/web/src-web/.gitignore similarity index 100% rename from makoon/src-web/.gitignore rename to web/src-web/.gitignore diff --git a/makoon/src-web/index.html b/web/src-web/index.html similarity index 100% rename from makoon/src-web/index.html rename to web/src-web/index.html diff --git a/makoon/src-web/package.json b/web/src-web/package.json similarity index 100% rename from makoon/src-web/package.json rename to web/src-web/package.json diff --git a/makoon/src-web/pnpm-lock.yaml b/web/src-web/pnpm-lock.yaml similarity index 100% rename from makoon/src-web/pnpm-lock.yaml rename to web/src-web/pnpm-lock.yaml index 51ad8cb..35f11ad 100644 --- a/makoon/src-web/pnpm-lock.yaml +++ b/web/src-web/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@heroicons/react': specifier: ^2.0.18 @@ -3555,3 +3551,7 @@ packages: toposort: 2.0.2 type-fest: 2.19.0 dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/makoon/src-web/postcss.config.js b/web/src-web/postcss.config.js similarity index 100% rename from makoon/src-web/postcss.config.js rename to web/src-web/postcss.config.js diff --git a/makoon/src-web/public/favicon.ico b/web/src-web/public/favicon.ico similarity index 100% rename from makoon/src-web/public/favicon.ico rename to web/src-web/public/favicon.ico diff --git a/makoon/src-web/src/App.css b/web/src-web/src/App.css similarity index 100% rename from makoon/src-web/src/App.css rename to web/src-web/src/App.css diff --git a/makoon/src-web/src/App.tsx b/web/src-web/src/App.tsx similarity index 100% rename from makoon/src-web/src/App.tsx rename to web/src-web/src/App.tsx diff --git a/makoon/src-web/src/Router.tsx b/web/src-web/src/Router.tsx similarity index 100% rename from makoon/src-web/src/Router.tsx rename to web/src-web/src/Router.tsx diff --git a/makoon/src-web/src/api/api.ts b/web/src-web/src/api/api.ts similarity index 83% rename from makoon/src-web/src/api/api.ts rename to web/src-web/src/api/api.ts index 7c95cdf..c168685 100644 --- a/makoon/src-web/src/api/api.ts +++ b/web/src-web/src/api/api.ts @@ -5,6 +5,7 @@ import { clusters } from "@/api/clusters"; import { storage } from "@/api/storage"; import { apps } from "@/api/apps"; import { cluster_resources } from "@/api/cluster_resources"; +import {settings} from "@/api/settings"; export default { networks, @@ -13,5 +14,6 @@ export default { clusters, storage, apps, - cluster_resources + cluster_resources, + settings } diff --git a/makoon/src-web/src/api/apps.ts b/web/src-web/src/api/apps.ts similarity index 100% rename from makoon/src-web/src/api/apps.ts rename to web/src-web/src/api/apps.ts diff --git a/makoon/src-web/src/api/auth.ts b/web/src-web/src/api/auth.ts similarity index 100% rename from makoon/src-web/src/api/auth.ts rename to web/src-web/src/api/auth.ts diff --git a/makoon/src-web/src/api/cluster_resources.ts b/web/src-web/src/api/cluster_resources.ts similarity index 100% rename from makoon/src-web/src/api/cluster_resources.ts rename to web/src-web/src/api/cluster_resources.ts diff --git a/makoon/src-web/src/api/clusters.ts b/web/src-web/src/api/clusters.ts similarity index 100% rename from makoon/src-web/src/api/clusters.ts rename to web/src-web/src/api/clusters.ts diff --git a/makoon/src-web/src/api/model.ts b/web/src-web/src/api/model.ts similarity index 96% rename from makoon/src-web/src/api/model.ts rename to web/src-web/src/api/model.ts index 33dd0b6..eba0250 100644 --- a/makoon/src-web/src/api/model.ts +++ b/web/src-web/src/api/model.ts @@ -180,6 +180,15 @@ export interface AvailableStorage { total?: number, } +export interface AvailableOsImage { + name: string; + url: string; +} + +export interface AvailableKubeVersion { + version: string +} + export interface ChangeNodeResourcesRequest { cores: number, memory: number, diff --git a/makoon/src-web/src/api/networks.ts b/web/src-web/src/api/networks.ts similarity index 100% rename from makoon/src-web/src/api/networks.ts rename to web/src-web/src/api/networks.ts diff --git a/makoon/src-web/src/api/nodes.ts b/web/src-web/src/api/nodes.ts similarity index 100% rename from makoon/src-web/src/api/nodes.ts rename to web/src-web/src/api/nodes.ts diff --git a/web/src-web/src/api/settings.ts b/web/src-web/src/api/settings.ts new file mode 100644 index 0000000..8609d5b --- /dev/null +++ b/web/src-web/src/api/settings.ts @@ -0,0 +1,11 @@ +import axios from "axios"; + +export namespace settings { + export function os_images() { + return axios.get("/api/v1/os-images").then(r => r.data); + } + + export function kube_versions() { + return axios.get("/api/v1/kube-versions").then(r => r.data); + } +} \ No newline at end of file diff --git a/makoon/src-web/src/api/storage.ts b/web/src-web/src/api/storage.ts similarity index 100% rename from makoon/src-web/src/api/storage.ts rename to web/src-web/src/api/storage.ts diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-Black.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-Black.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-Black.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-Black.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-BlackItalic.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-BlackItalic.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-BlackItalic.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-BlackItalic.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-Bold.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-Bold.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-Bold.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-Bold.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-BoldItalic.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-BoldItalic.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-BoldItalic.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-BoldItalic.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-ExtraBold.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-ExtraBold.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-ExtraBold.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-ExtraBold.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-ExtraBoldItalic.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-ExtraBoldItalic.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-ExtraBoldItalic.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-ExtraBoldItalic.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-ExtraLight.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-ExtraLight.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-ExtraLight.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-ExtraLight.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-ExtraLightItalic.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-ExtraLightItalic.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-ExtraLightItalic.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-ExtraLightItalic.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-Italic.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-Italic.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-Italic.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-Italic.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-Light.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-Light.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-Light.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-Light.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-LightItalic.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-LightItalic.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-LightItalic.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-LightItalic.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-Medium.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-Medium.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-Medium.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-Medium.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-MediumItalic.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-MediumItalic.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-MediumItalic.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-MediumItalic.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-Regular.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-Regular.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-Regular.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-Regular.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-SemiBold.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-SemiBold.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-SemiBold.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-SemiBold.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-SemiBoldItalic.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-SemiBoldItalic.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-SemiBoldItalic.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-SemiBoldItalic.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-Thin.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-Thin.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-Thin.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-Thin.ttf diff --git a/makoon/src-web/src/assets/fonts/Poppins/Poppins-ThinItalic.ttf b/web/src-web/src/assets/fonts/Poppins/Poppins-ThinItalic.ttf similarity index 100% rename from makoon/src-web/src/assets/fonts/Poppins/Poppins-ThinItalic.ttf rename to web/src-web/src/assets/fonts/Poppins/Poppins-ThinItalic.ttf diff --git a/makoon/src-web/src/assets/fonts/fonts.css b/web/src-web/src/assets/fonts/fonts.css similarity index 100% rename from makoon/src-web/src/assets/fonts/fonts.css rename to web/src-web/src/assets/fonts/fonts.css diff --git a/makoon/src-web/src/assets/images/makonn_logo.svg b/web/src-web/src/assets/images/makonn_logo.svg similarity index 100% rename from makoon/src-web/src/assets/images/makonn_logo.svg rename to web/src-web/src/assets/images/makonn_logo.svg diff --git a/makoon/src-web/src/assets/images/makonn_logo_wh.svg b/web/src-web/src/assets/images/makonn_logo_wh.svg similarity index 100% rename from makoon/src-web/src/assets/images/makonn_logo_wh.svg rename to web/src-web/src/assets/images/makonn_logo_wh.svg diff --git a/makoon/src-web/src/components/Content.tsx b/web/src-web/src/components/Content.tsx similarity index 100% rename from makoon/src-web/src/components/Content.tsx rename to web/src-web/src/components/Content.tsx diff --git a/makoon/src-web/src/components/ErrorPanel/ErrorPanel.css b/web/src-web/src/components/ErrorPanel/ErrorPanel.css similarity index 100% rename from makoon/src-web/src/components/ErrorPanel/ErrorPanel.css rename to web/src-web/src/components/ErrorPanel/ErrorPanel.css diff --git a/makoon/src-web/src/components/ErrorPanel/ErrorPanel.tsx b/web/src-web/src/components/ErrorPanel/ErrorPanel.tsx similarity index 100% rename from makoon/src-web/src/components/ErrorPanel/ErrorPanel.tsx rename to web/src-web/src/components/ErrorPanel/ErrorPanel.tsx diff --git a/makoon/src-web/src/components/FormError.tsx b/web/src-web/src/components/FormError.tsx similarity index 100% rename from makoon/src-web/src/components/FormError.tsx rename to web/src-web/src/components/FormError.tsx diff --git a/makoon/src-web/src/components/Header.tsx b/web/src-web/src/components/Header.tsx similarity index 100% rename from makoon/src-web/src/components/Header.tsx rename to web/src-web/src/components/Header.tsx diff --git a/makoon/src-web/src/components/HiddenPassword.tsx b/web/src-web/src/components/HiddenPassword.tsx similarity index 100% rename from makoon/src-web/src/components/HiddenPassword.tsx rename to web/src-web/src/components/HiddenPassword.tsx diff --git a/makoon/src-web/src/components/LogoContainer.tsx b/web/src-web/src/components/LogoContainer.tsx similarity index 100% rename from makoon/src-web/src/components/LogoContainer.tsx rename to web/src-web/src/components/LogoContainer.tsx diff --git a/makoon/src-web/src/components/MainContainer.tsx b/web/src-web/src/components/MainContainer.tsx similarity index 100% rename from makoon/src-web/src/components/MainContainer.tsx rename to web/src-web/src/components/MainContainer.tsx diff --git a/makoon/src-web/src/components/MainMenu.tsx b/web/src-web/src/components/MainMenu.tsx similarity index 100% rename from makoon/src-web/src/components/MainMenu.tsx rename to web/src-web/src/components/MainMenu.tsx diff --git a/makoon/src-web/src/components/Panel.tsx b/web/src-web/src/components/Panel.tsx similarity index 100% rename from makoon/src-web/src/components/Panel.tsx rename to web/src-web/src/components/Panel.tsx diff --git a/makoon/src-web/src/components/Section.tsx b/web/src-web/src/components/Section.tsx similarity index 100% rename from makoon/src-web/src/components/Section.tsx rename to web/src-web/src/components/Section.tsx diff --git a/makoon/src-web/src/components/StorageDropdownOption.tsx b/web/src-web/src/components/StorageDropdownOption.tsx similarity index 100% rename from makoon/src-web/src/components/StorageDropdownOption.tsx rename to web/src-web/src/components/StorageDropdownOption.tsx diff --git a/makoon/src-web/src/constants.ts b/web/src-web/src/constants.ts similarity index 100% rename from makoon/src-web/src/constants.ts rename to web/src-web/src/constants.ts diff --git a/makoon/src-web/src/main.tsx b/web/src-web/src/main.tsx similarity index 100% rename from makoon/src-web/src/main.tsx rename to web/src-web/src/main.tsx diff --git a/makoon/src-web/src/store/application-store.ts b/web/src-web/src/store/application-store.ts similarity index 100% rename from makoon/src-web/src/store/application-store.ts rename to web/src-web/src/store/application-store.ts diff --git a/makoon/src-web/src/store/cluster-creator-store.ts b/web/src-web/src/store/cluster-creator-store.ts similarity index 100% rename from makoon/src-web/src/store/cluster-creator-store.ts rename to web/src-web/src/store/cluster-creator-store.ts diff --git a/makoon/src-web/src/store/cluster-management-store.ts b/web/src-web/src/store/cluster-management-store.ts similarity index 100% rename from makoon/src-web/src/store/cluster-management-store.ts rename to web/src-web/src/store/cluster-management-store.ts diff --git a/makoon/src-web/src/store/clusters-list-store.ts b/web/src-web/src/store/clusters-list-store.ts similarity index 100% rename from makoon/src-web/src/store/clusters-list-store.ts rename to web/src-web/src/store/clusters-list-store.ts diff --git a/makoon/src-web/src/store/processing-indicator-store.ts b/web/src-web/src/store/processing-indicator-store.ts similarity index 100% rename from makoon/src-web/src/store/processing-indicator-store.ts rename to web/src-web/src/store/processing-indicator-store.ts diff --git a/makoon/src-web/src/style.css b/web/src-web/src/style.css similarity index 100% rename from makoon/src-web/src/style.css rename to web/src-web/src/style.css diff --git a/makoon/src-web/src/theme/theme-base/_colors.scss b/web/src-web/src/theme/theme-base/_colors.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/_colors.scss rename to web/src-web/src/theme/theme-base/_colors.scss diff --git a/makoon/src-web/src/theme/theme-base/_common.scss b/web/src-web/src/theme/theme-base/_common.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/_common.scss rename to web/src-web/src/theme/theme-base/_common.scss diff --git a/makoon/src-web/src/theme/theme-base/_components.scss b/web/src-web/src/theme/theme-base/_components.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/_components.scss rename to web/src-web/src/theme/theme-base/_components.scss diff --git a/makoon/src-web/src/theme/theme-base/_mixins.scss b/web/src-web/src/theme/theme-base/_mixins.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/_mixins.scss rename to web/src-web/src/theme/theme-base/_mixins.scss diff --git a/makoon/src-web/src/theme/theme-base/components/button/_button.scss b/web/src-web/src/theme/theme-base/components/button/_button.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/button/_button.scss rename to web/src-web/src/theme/theme-base/components/button/_button.scss diff --git a/makoon/src-web/src/theme/theme-base/components/button/_speeddial.scss b/web/src-web/src/theme/theme-base/components/button/_speeddial.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/button/_speeddial.scss rename to web/src-web/src/theme/theme-base/components/button/_speeddial.scss diff --git a/makoon/src-web/src/theme/theme-base/components/button/_splitbutton.scss b/web/src-web/src/theme/theme-base/components/button/_splitbutton.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/button/_splitbutton.scss rename to web/src-web/src/theme/theme-base/components/button/_splitbutton.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_carousel.scss b/web/src-web/src/theme/theme-base/components/data/_carousel.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_carousel.scss rename to web/src-web/src/theme/theme-base/components/data/_carousel.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_datascroller.scss b/web/src-web/src/theme/theme-base/components/data/_datascroller.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_datascroller.scss rename to web/src-web/src/theme/theme-base/components/data/_datascroller.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_datatable.scss b/web/src-web/src/theme/theme-base/components/data/_datatable.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_datatable.scss rename to web/src-web/src/theme/theme-base/components/data/_datatable.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_dataview.scss b/web/src-web/src/theme/theme-base/components/data/_dataview.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_dataview.scss rename to web/src-web/src/theme/theme-base/components/data/_dataview.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_filter.scss b/web/src-web/src/theme/theme-base/components/data/_filter.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_filter.scss rename to web/src-web/src/theme/theme-base/components/data/_filter.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_fullcalendar.scss b/web/src-web/src/theme/theme-base/components/data/_fullcalendar.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_fullcalendar.scss rename to web/src-web/src/theme/theme-base/components/data/_fullcalendar.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_orderlist.scss b/web/src-web/src/theme/theme-base/components/data/_orderlist.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_orderlist.scss rename to web/src-web/src/theme/theme-base/components/data/_orderlist.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_organizationchart.scss b/web/src-web/src/theme/theme-base/components/data/_organizationchart.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_organizationchart.scss rename to web/src-web/src/theme/theme-base/components/data/_organizationchart.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_paginator.scss b/web/src-web/src/theme/theme-base/components/data/_paginator.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_paginator.scss rename to web/src-web/src/theme/theme-base/components/data/_paginator.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_picklist.scss b/web/src-web/src/theme/theme-base/components/data/_picklist.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_picklist.scss rename to web/src-web/src/theme/theme-base/components/data/_picklist.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_timeline.scss b/web/src-web/src/theme/theme-base/components/data/_timeline.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_timeline.scss rename to web/src-web/src/theme/theme-base/components/data/_timeline.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_tree.scss b/web/src-web/src/theme/theme-base/components/data/_tree.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_tree.scss rename to web/src-web/src/theme/theme-base/components/data/_tree.scss diff --git a/makoon/src-web/src/theme/theme-base/components/data/_treetable.scss b/web/src-web/src/theme/theme-base/components/data/_treetable.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/data/_treetable.scss rename to web/src-web/src/theme/theme-base/components/data/_treetable.scss diff --git a/makoon/src-web/src/theme/theme-base/components/file/_fileupload.scss b/web/src-web/src/theme/theme-base/components/file/_fileupload.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/file/_fileupload.scss rename to web/src-web/src/theme/theme-base/components/file/_fileupload.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_autocomplete.scss b/web/src-web/src/theme/theme-base/components/input/_autocomplete.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_autocomplete.scss rename to web/src-web/src/theme/theme-base/components/input/_autocomplete.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_calendar.scss b/web/src-web/src/theme/theme-base/components/input/_calendar.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_calendar.scss rename to web/src-web/src/theme/theme-base/components/input/_calendar.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_cascadeselect.scss b/web/src-web/src/theme/theme-base/components/input/_cascadeselect.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_cascadeselect.scss rename to web/src-web/src/theme/theme-base/components/input/_cascadeselect.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_checkbox.scss b/web/src-web/src/theme/theme-base/components/input/_checkbox.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_checkbox.scss rename to web/src-web/src/theme/theme-base/components/input/_checkbox.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_chips.scss b/web/src-web/src/theme/theme-base/components/input/_chips.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_chips.scss rename to web/src-web/src/theme/theme-base/components/input/_chips.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_colorpicker.scss b/web/src-web/src/theme/theme-base/components/input/_colorpicker.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_colorpicker.scss rename to web/src-web/src/theme/theme-base/components/input/_colorpicker.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_dropdown.scss b/web/src-web/src/theme/theme-base/components/input/_dropdown.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_dropdown.scss rename to web/src-web/src/theme/theme-base/components/input/_dropdown.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_editor.scss b/web/src-web/src/theme/theme-base/components/input/_editor.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_editor.scss rename to web/src-web/src/theme/theme-base/components/input/_editor.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_inputgroup.scss b/web/src-web/src/theme/theme-base/components/input/_inputgroup.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_inputgroup.scss rename to web/src-web/src/theme/theme-base/components/input/_inputgroup.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_inputnumber.scss b/web/src-web/src/theme/theme-base/components/input/_inputnumber.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_inputnumber.scss rename to web/src-web/src/theme/theme-base/components/input/_inputnumber.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_inputswitch.scss b/web/src-web/src/theme/theme-base/components/input/_inputswitch.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_inputswitch.scss rename to web/src-web/src/theme/theme-base/components/input/_inputswitch.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_inputtext.scss b/web/src-web/src/theme/theme-base/components/input/_inputtext.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_inputtext.scss rename to web/src-web/src/theme/theme-base/components/input/_inputtext.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_listbox.scss b/web/src-web/src/theme/theme-base/components/input/_listbox.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_listbox.scss rename to web/src-web/src/theme/theme-base/components/input/_listbox.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_mention.scss b/web/src-web/src/theme/theme-base/components/input/_mention.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_mention.scss rename to web/src-web/src/theme/theme-base/components/input/_mention.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_multiselect.scss b/web/src-web/src/theme/theme-base/components/input/_multiselect.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_multiselect.scss rename to web/src-web/src/theme/theme-base/components/input/_multiselect.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_password.scss b/web/src-web/src/theme/theme-base/components/input/_password.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_password.scss rename to web/src-web/src/theme/theme-base/components/input/_password.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_radiobutton.scss b/web/src-web/src/theme/theme-base/components/input/_radiobutton.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_radiobutton.scss rename to web/src-web/src/theme/theme-base/components/input/_radiobutton.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_rating.scss b/web/src-web/src/theme/theme-base/components/input/_rating.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_rating.scss rename to web/src-web/src/theme/theme-base/components/input/_rating.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_selectbutton.scss b/web/src-web/src/theme/theme-base/components/input/_selectbutton.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_selectbutton.scss rename to web/src-web/src/theme/theme-base/components/input/_selectbutton.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_slider.scss b/web/src-web/src/theme/theme-base/components/input/_slider.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_slider.scss rename to web/src-web/src/theme/theme-base/components/input/_slider.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_togglebutton.scss b/web/src-web/src/theme/theme-base/components/input/_togglebutton.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_togglebutton.scss rename to web/src-web/src/theme/theme-base/components/input/_togglebutton.scss diff --git a/makoon/src-web/src/theme/theme-base/components/input/_treeselect.scss b/web/src-web/src/theme/theme-base/components/input/_treeselect.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/input/_treeselect.scss rename to web/src-web/src/theme/theme-base/components/input/_treeselect.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_breadcrumb.scss b/web/src-web/src/theme/theme-base/components/menu/_breadcrumb.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_breadcrumb.scss rename to web/src-web/src/theme/theme-base/components/menu/_breadcrumb.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_contextmenu.scss b/web/src-web/src/theme/theme-base/components/menu/_contextmenu.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_contextmenu.scss rename to web/src-web/src/theme/theme-base/components/menu/_contextmenu.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_dock.scss b/web/src-web/src/theme/theme-base/components/menu/_dock.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_dock.scss rename to web/src-web/src/theme/theme-base/components/menu/_dock.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_megamenu.scss b/web/src-web/src/theme/theme-base/components/menu/_megamenu.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_megamenu.scss rename to web/src-web/src/theme/theme-base/components/menu/_megamenu.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_menu.scss b/web/src-web/src/theme/theme-base/components/menu/_menu.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_menu.scss rename to web/src-web/src/theme/theme-base/components/menu/_menu.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_menubar.scss b/web/src-web/src/theme/theme-base/components/menu/_menubar.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_menubar.scss rename to web/src-web/src/theme/theme-base/components/menu/_menubar.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_panelmenu.scss b/web/src-web/src/theme/theme-base/components/menu/_panelmenu.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_panelmenu.scss rename to web/src-web/src/theme/theme-base/components/menu/_panelmenu.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_slidemenu.scss b/web/src-web/src/theme/theme-base/components/menu/_slidemenu.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_slidemenu.scss rename to web/src-web/src/theme/theme-base/components/menu/_slidemenu.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_steps.scss b/web/src-web/src/theme/theme-base/components/menu/_steps.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_steps.scss rename to web/src-web/src/theme/theme-base/components/menu/_steps.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_tabmenu.scss b/web/src-web/src/theme/theme-base/components/menu/_tabmenu.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_tabmenu.scss rename to web/src-web/src/theme/theme-base/components/menu/_tabmenu.scss diff --git a/makoon/src-web/src/theme/theme-base/components/menu/_tieredmenu.scss b/web/src-web/src/theme/theme-base/components/menu/_tieredmenu.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/menu/_tieredmenu.scss rename to web/src-web/src/theme/theme-base/components/menu/_tieredmenu.scss diff --git a/makoon/src-web/src/theme/theme-base/components/messages/_inlinemessage.scss b/web/src-web/src/theme/theme-base/components/messages/_inlinemessage.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/messages/_inlinemessage.scss rename to web/src-web/src/theme/theme-base/components/messages/_inlinemessage.scss diff --git a/makoon/src-web/src/theme/theme-base/components/messages/_message.scss b/web/src-web/src/theme/theme-base/components/messages/_message.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/messages/_message.scss rename to web/src-web/src/theme/theme-base/components/messages/_message.scss diff --git a/makoon/src-web/src/theme/theme-base/components/messages/_toast.scss b/web/src-web/src/theme/theme-base/components/messages/_toast.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/messages/_toast.scss rename to web/src-web/src/theme/theme-base/components/messages/_toast.scss diff --git a/makoon/src-web/src/theme/theme-base/components/misc/_avatar.scss b/web/src-web/src/theme/theme-base/components/misc/_avatar.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/misc/_avatar.scss rename to web/src-web/src/theme/theme-base/components/misc/_avatar.scss diff --git a/makoon/src-web/src/theme/theme-base/components/misc/_badge.scss b/web/src-web/src/theme/theme-base/components/misc/_badge.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/misc/_badge.scss rename to web/src-web/src/theme/theme-base/components/misc/_badge.scss diff --git a/makoon/src-web/src/theme/theme-base/components/misc/_blockui.scss b/web/src-web/src/theme/theme-base/components/misc/_blockui.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/misc/_blockui.scss rename to web/src-web/src/theme/theme-base/components/misc/_blockui.scss diff --git a/makoon/src-web/src/theme/theme-base/components/misc/_chip.scss b/web/src-web/src/theme/theme-base/components/misc/_chip.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/misc/_chip.scss rename to web/src-web/src/theme/theme-base/components/misc/_chip.scss diff --git a/makoon/src-web/src/theme/theme-base/components/misc/_inplace.scss b/web/src-web/src/theme/theme-base/components/misc/_inplace.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/misc/_inplace.scss rename to web/src-web/src/theme/theme-base/components/misc/_inplace.scss diff --git a/makoon/src-web/src/theme/theme-base/components/misc/_progressbar.scss b/web/src-web/src/theme/theme-base/components/misc/_progressbar.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/misc/_progressbar.scss rename to web/src-web/src/theme/theme-base/components/misc/_progressbar.scss diff --git a/makoon/src-web/src/theme/theme-base/components/misc/_scrolltop.scss b/web/src-web/src/theme/theme-base/components/misc/_scrolltop.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/misc/_scrolltop.scss rename to web/src-web/src/theme/theme-base/components/misc/_scrolltop.scss diff --git a/makoon/src-web/src/theme/theme-base/components/misc/_skeleton.scss b/web/src-web/src/theme/theme-base/components/misc/_skeleton.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/misc/_skeleton.scss rename to web/src-web/src/theme/theme-base/components/misc/_skeleton.scss diff --git a/makoon/src-web/src/theme/theme-base/components/misc/_tag.scss b/web/src-web/src/theme/theme-base/components/misc/_tag.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/misc/_tag.scss rename to web/src-web/src/theme/theme-base/components/misc/_tag.scss diff --git a/makoon/src-web/src/theme/theme-base/components/misc/_terminal.scss b/web/src-web/src/theme/theme-base/components/misc/_terminal.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/misc/_terminal.scss rename to web/src-web/src/theme/theme-base/components/misc/_terminal.scss diff --git a/makoon/src-web/src/theme/theme-base/components/multimedia/_galleria.scss b/web/src-web/src/theme/theme-base/components/multimedia/_galleria.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/multimedia/_galleria.scss rename to web/src-web/src/theme/theme-base/components/multimedia/_galleria.scss diff --git a/makoon/src-web/src/theme/theme-base/components/multimedia/_image.scss b/web/src-web/src/theme/theme-base/components/multimedia/_image.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/multimedia/_image.scss rename to web/src-web/src/theme/theme-base/components/multimedia/_image.scss diff --git a/makoon/src-web/src/theme/theme-base/components/overlay/_confirmpopup.scss b/web/src-web/src/theme/theme-base/components/overlay/_confirmpopup.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/overlay/_confirmpopup.scss rename to web/src-web/src/theme/theme-base/components/overlay/_confirmpopup.scss diff --git a/makoon/src-web/src/theme/theme-base/components/overlay/_dialog.scss b/web/src-web/src/theme/theme-base/components/overlay/_dialog.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/overlay/_dialog.scss rename to web/src-web/src/theme/theme-base/components/overlay/_dialog.scss diff --git a/makoon/src-web/src/theme/theme-base/components/overlay/_overlaypanel.scss b/web/src-web/src/theme/theme-base/components/overlay/_overlaypanel.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/overlay/_overlaypanel.scss rename to web/src-web/src/theme/theme-base/components/overlay/_overlaypanel.scss diff --git a/makoon/src-web/src/theme/theme-base/components/overlay/_sidebar.scss b/web/src-web/src/theme/theme-base/components/overlay/_sidebar.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/overlay/_sidebar.scss rename to web/src-web/src/theme/theme-base/components/overlay/_sidebar.scss diff --git a/makoon/src-web/src/theme/theme-base/components/overlay/_tooltip.scss b/web/src-web/src/theme/theme-base/components/overlay/_tooltip.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/overlay/_tooltip.scss rename to web/src-web/src/theme/theme-base/components/overlay/_tooltip.scss diff --git a/makoon/src-web/src/theme/theme-base/components/panel/_accordion.scss b/web/src-web/src/theme/theme-base/components/panel/_accordion.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/panel/_accordion.scss rename to web/src-web/src/theme/theme-base/components/panel/_accordion.scss diff --git a/makoon/src-web/src/theme/theme-base/components/panel/_card.scss b/web/src-web/src/theme/theme-base/components/panel/_card.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/panel/_card.scss rename to web/src-web/src/theme/theme-base/components/panel/_card.scss diff --git a/makoon/src-web/src/theme/theme-base/components/panel/_divider.scss b/web/src-web/src/theme/theme-base/components/panel/_divider.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/panel/_divider.scss rename to web/src-web/src/theme/theme-base/components/panel/_divider.scss diff --git a/makoon/src-web/src/theme/theme-base/components/panel/_fieldset.scss b/web/src-web/src/theme/theme-base/components/panel/_fieldset.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/panel/_fieldset.scss rename to web/src-web/src/theme/theme-base/components/panel/_fieldset.scss diff --git a/makoon/src-web/src/theme/theme-base/components/panel/_panel.scss b/web/src-web/src/theme/theme-base/components/panel/_panel.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/panel/_panel.scss rename to web/src-web/src/theme/theme-base/components/panel/_panel.scss diff --git a/makoon/src-web/src/theme/theme-base/components/panel/_scrollpanel.scss b/web/src-web/src/theme/theme-base/components/panel/_scrollpanel.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/panel/_scrollpanel.scss rename to web/src-web/src/theme/theme-base/components/panel/_scrollpanel.scss diff --git a/makoon/src-web/src/theme/theme-base/components/panel/_splitter.scss b/web/src-web/src/theme/theme-base/components/panel/_splitter.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/panel/_splitter.scss rename to web/src-web/src/theme/theme-base/components/panel/_splitter.scss diff --git a/makoon/src-web/src/theme/theme-base/components/panel/_tabview.scss b/web/src-web/src/theme/theme-base/components/panel/_tabview.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/panel/_tabview.scss rename to web/src-web/src/theme/theme-base/components/panel/_tabview.scss diff --git a/makoon/src-web/src/theme/theme-base/components/panel/_toolbar.scss b/web/src-web/src/theme/theme-base/components/panel/_toolbar.scss similarity index 100% rename from makoon/src-web/src/theme/theme-base/components/panel/_toolbar.scss rename to web/src-web/src/theme/theme-base/components/panel/_toolbar.scss diff --git a/makoon/src-web/src/theme/themes/fluent/fluent-light/_extensions.scss b/web/src-web/src/theme/themes/fluent/fluent-light/_extensions.scss similarity index 100% rename from makoon/src-web/src/theme/themes/fluent/fluent-light/_extensions.scss rename to web/src-web/src/theme/themes/fluent/fluent-light/_extensions.scss diff --git a/makoon/src-web/src/theme/themes/fluent/fluent-light/_fonts.scss b/web/src-web/src/theme/themes/fluent/fluent-light/_fonts.scss similarity index 100% rename from makoon/src-web/src/theme/themes/fluent/fluent-light/_fonts.scss rename to web/src-web/src/theme/themes/fluent/fluent-light/_fonts.scss diff --git a/makoon/src-web/src/theme/themes/fluent/fluent-light/_variables.scss b/web/src-web/src/theme/themes/fluent/fluent-light/_variables.scss similarity index 100% rename from makoon/src-web/src/theme/themes/fluent/fluent-light/_variables.scss rename to web/src-web/src/theme/themes/fluent/fluent-light/_variables.scss diff --git a/makoon/src-web/src/theme/themes/fluent/fluent-light/theme.scss b/web/src-web/src/theme/themes/fluent/fluent-light/theme.scss similarity index 100% rename from makoon/src-web/src/theme/themes/fluent/fluent-light/theme.scss rename to web/src-web/src/theme/themes/fluent/fluent-light/theme.scss diff --git a/makoon/src-web/src/utils/api.ts b/web/src-web/src/utils/api.ts similarity index 100% rename from makoon/src-web/src/utils/api.ts rename to web/src-web/src/utils/api.ts diff --git a/makoon/src-web/src/utils/hooks.ts b/web/src-web/src/utils/hooks.ts similarity index 100% rename from makoon/src-web/src/utils/hooks.ts rename to web/src-web/src/utils/hooks.ts diff --git a/makoon/src-web/src/utils/nodes.ts b/web/src-web/src/utils/nodes.ts similarity index 100% rename from makoon/src-web/src/utils/nodes.ts rename to web/src-web/src/utils/nodes.ts diff --git a/makoon/src-web/src/utils/patterns.ts b/web/src-web/src/utils/patterns.ts similarity index 100% rename from makoon/src-web/src/utils/patterns.ts rename to web/src-web/src/utils/patterns.ts diff --git a/makoon/src-web/src/utils/size.ts b/web/src-web/src/utils/size.ts similarity index 100% rename from makoon/src-web/src/utils/size.ts rename to web/src-web/src/utils/size.ts diff --git a/makoon/src-web/src/views/cluster-creator/CreatorNavigator.tsx b/web/src-web/src/views/cluster-creator/CreatorNavigator.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/CreatorNavigator.tsx rename to web/src-web/src/views/cluster-creator/CreatorNavigator.tsx diff --git a/makoon/src-web/src/views/cluster-creator/context.ts b/web/src-web/src/views/cluster-creator/context.ts similarity index 100% rename from makoon/src-web/src/views/cluster-creator/context.ts rename to web/src-web/src/views/cluster-creator/context.ts diff --git a/makoon/src-web/src/views/cluster-creator/index.tsx b/web/src-web/src/views/cluster-creator/index.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/index.tsx rename to web/src-web/src/views/cluster-creator/index.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/apps/CreatorHelmAppDialog.tsx b/web/src-web/src/views/cluster-creator/steps/apps/CreatorHelmAppDialog.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/apps/CreatorHelmAppDialog.tsx rename to web/src-web/src/views/cluster-creator/steps/apps/CreatorHelmAppDialog.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/apps/HelmAppsSection.tsx b/web/src-web/src/views/cluster-creator/steps/apps/HelmAppsSection.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/apps/HelmAppsSection.tsx rename to web/src-web/src/views/cluster-creator/steps/apps/HelmAppsSection.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/apps/index.tsx b/web/src-web/src/views/cluster-creator/steps/apps/index.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/apps/index.tsx rename to web/src-web/src/views/cluster-creator/steps/apps/index.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/cluster/index.tsx b/web/src-web/src/views/cluster-creator/steps/cluster/index.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/cluster/index.tsx rename to web/src-web/src/views/cluster-creator/steps/cluster/index.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/nodes/CreatorNodeDialog.tsx b/web/src-web/src/views/cluster-creator/steps/nodes/CreatorNodeDialog.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/nodes/CreatorNodeDialog.tsx rename to web/src-web/src/views/cluster-creator/steps/nodes/CreatorNodeDialog.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/nodes/NodesSection.tsx b/web/src-web/src/views/cluster-creator/steps/nodes/NodesSection.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/nodes/NodesSection.tsx rename to web/src-web/src/views/cluster-creator/steps/nodes/NodesSection.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/nodes/TableNodes.tsx b/web/src-web/src/views/cluster-creator/steps/nodes/TableNodes.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/nodes/TableNodes.tsx rename to web/src-web/src/views/cluster-creator/steps/nodes/TableNodes.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/nodes/index.tsx b/web/src-web/src/views/cluster-creator/steps/nodes/index.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/nodes/index.tsx rename to web/src-web/src/views/cluster-creator/steps/nodes/index.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/settings/index.tsx b/web/src-web/src/views/cluster-creator/steps/settings/index.tsx similarity index 90% rename from makoon/src-web/src/views/cluster-creator/steps/settings/index.tsx rename to web/src-web/src/views/cluster-creator/steps/settings/index.tsx index 3cbba54..d71b1ac 100644 --- a/makoon/src-web/src/views/cluster-creator/steps/settings/index.tsx +++ b/web/src-web/src/views/cluster-creator/steps/settings/index.tsx @@ -7,7 +7,7 @@ import * as Yup from "yup"; import {useOnFirstMount} from "@/utils/hooks"; import {apiCall} from "@/utils/api"; import api from "@/api/api"; -import {AvailableStorage} from "@/api/model"; +import {AvailableKubeVersion, AvailableOsImage, AvailableStorage} from "@/api/model"; import FormError from "@/components/FormError"; import StorageDropdownOption from "@/components/StorageDropdownOption"; import {ClusterCreatorStoreContext, CreatorNavigation, StepProps} from "@/views/cluster-creator/context"; @@ -22,11 +22,6 @@ const renderDescription = (description: string) => { } -type OsImageSource = { - name: string, - url: string -} - const SettingsStep = (props: StepProps, ref: any) => { useImperativeHandle(ref, () => ({ async next(): Promise { @@ -42,20 +37,8 @@ const SettingsStep = (props: StepProps, ref: any) => { const clusterStore = useContext(ClusterCreatorStoreContext) const storedModel = clusterStore.settings; const [nodes, setNodes] = useState([]); - const [osImages, setOsImages] = useState([ - // { - // name: "Ubuntu Server 22.04 LTS - jammy-server-cloudimg-amd64.img ", - // url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img" - // }, - { - name: "Ubuntu Server 22.10 - kinetic-server-cloudimg-amd64.img", - url: "https://cloud-images.ubuntu.com/kinetic/current/kinetic-server-cloudimg-amd64.img" - } - ]); - const [kubeVersions, setKubeVersions] = useState([ - "1.24/stable", - // "1.25/stable" - ]); + const [osImages, setOsImages] = useState(); + const [kubeVersions, setKubeVersions] = useState(); const [osImageStorages, setOsImageStorages] = useState([]); @@ -96,6 +79,15 @@ const SettingsStep = (props: StepProps, ref: any) => { setOsImageStorages(response); }); } + apiCall(() => api.settings.os_images()) + .then((response) => { + setOsImages(response); + }); + + apiCall(() => api.settings.kube_versions()) + .then((response) => { + setKubeVersions(response); + }); }); }, []); @@ -162,6 +154,8 @@ const SettingsStep = (props: StepProps, ref: any) => { className="w-full" value={formik.values.kubeVersion} onChange={formik.handleChange} + optionValue={"version"} + optionLabel={"version"} options={kubeVersions}/> diff --git a/makoon/src-web/src/views/cluster-creator/steps/workloads/CreatorWorkloadsDialog.tsx b/web/src-web/src/views/cluster-creator/steps/workloads/CreatorWorkloadsDialog.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/workloads/CreatorWorkloadsDialog.tsx rename to web/src-web/src/views/cluster-creator/steps/workloads/CreatorWorkloadsDialog.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/workloads/WorkloadsSection.tsx b/web/src-web/src/views/cluster-creator/steps/workloads/WorkloadsSection.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/workloads/WorkloadsSection.tsx rename to web/src-web/src/views/cluster-creator/steps/workloads/WorkloadsSection.tsx diff --git a/makoon/src-web/src/views/cluster-creator/steps/workloads/index.tsx b/web/src-web/src/views/cluster-creator/steps/workloads/index.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-creator/steps/workloads/index.tsx rename to web/src-web/src/views/cluster-creator/steps/workloads/index.tsx diff --git a/makoon/src-web/src/views/cluster-list/components/ClusterListPanel.tsx b/web/src-web/src/views/cluster-list/components/ClusterListPanel.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-list/components/ClusterListPanel.tsx rename to web/src-web/src/views/cluster-list/components/ClusterListPanel.tsx diff --git a/makoon/src-web/src/views/cluster-list/components/ClusterStatus.tsx b/web/src-web/src/views/cluster-list/components/ClusterStatus.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-list/components/ClusterStatus.tsx rename to web/src-web/src/views/cluster-list/components/ClusterStatus.tsx diff --git a/makoon/src-web/src/views/cluster-list/components/UsedResourcesPanel.tsx b/web/src-web/src/views/cluster-list/components/UsedResourcesPanel.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-list/components/UsedResourcesPanel.tsx rename to web/src-web/src/views/cluster-list/components/UsedResourcesPanel.tsx diff --git a/makoon/src-web/src/views/cluster-list/index.tsx b/web/src-web/src/views/cluster-list/index.tsx similarity index 100% rename from makoon/src-web/src/views/cluster-list/index.tsx rename to web/src-web/src/views/cluster-list/index.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/apps/HelmAppDialog.tsx b/web/src-web/src/views/clusters-management/components/apps/HelmAppDialog.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/apps/HelmAppDialog.tsx rename to web/src-web/src/views/clusters-management/components/apps/HelmAppDialog.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/apps/HelmAppStatus.tsx b/web/src-web/src/views/clusters-management/components/apps/HelmAppStatus.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/apps/HelmAppStatus.tsx rename to web/src-web/src/views/clusters-management/components/apps/HelmAppStatus.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/apps/index.tsx b/web/src-web/src/views/clusters-management/components/apps/index.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/apps/index.tsx rename to web/src-web/src/views/clusters-management/components/apps/index.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/cluster-logs/LogLevel.tsx b/web/src-web/src/views/clusters-management/components/cluster-logs/LogLevel.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/cluster-logs/LogLevel.tsx rename to web/src-web/src/views/clusters-management/components/cluster-logs/LogLevel.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/cluster-logs/index.tsx b/web/src-web/src/views/clusters-management/components/cluster-logs/index.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/cluster-logs/index.tsx rename to web/src-web/src/views/clusters-management/components/cluster-logs/index.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/nodes/AddNodeDialog.tsx b/web/src-web/src/views/clusters-management/components/nodes/AddNodeDialog.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/nodes/AddNodeDialog.tsx rename to web/src-web/src/views/clusters-management/components/nodes/AddNodeDialog.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/nodes/EditNodeDialog.tsx b/web/src-web/src/views/clusters-management/components/nodes/EditNodeDialog.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/nodes/EditNodeDialog.tsx rename to web/src-web/src/views/clusters-management/components/nodes/EditNodeDialog.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/nodes/KubeStatusInfo.tsx b/web/src-web/src/views/clusters-management/components/nodes/KubeStatusInfo.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/nodes/KubeStatusInfo.tsx rename to web/src-web/src/views/clusters-management/components/nodes/KubeStatusInfo.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/nodes/NodesTable.tsx b/web/src-web/src/views/clusters-management/components/nodes/NodesTable.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/nodes/NodesTable.tsx rename to web/src-web/src/views/clusters-management/components/nodes/NodesTable.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/nodes/VmStatusInfo.tsx b/web/src-web/src/views/clusters-management/components/nodes/VmStatusInfo.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/nodes/VmStatusInfo.tsx rename to web/src-web/src/views/clusters-management/components/nodes/VmStatusInfo.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/nodes/index.tsx b/web/src-web/src/views/clusters-management/components/nodes/index.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/nodes/index.tsx rename to web/src-web/src/views/clusters-management/components/nodes/index.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/workloads/WorkloadsDialog.tsx b/web/src-web/src/views/clusters-management/components/workloads/WorkloadsDialog.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/workloads/WorkloadsDialog.tsx rename to web/src-web/src/views/clusters-management/components/workloads/WorkloadsDialog.tsx diff --git a/makoon/src-web/src/views/clusters-management/components/workloads/index.tsx b/web/src-web/src/views/clusters-management/components/workloads/index.tsx similarity index 100% rename from makoon/src-web/src/views/clusters-management/components/workloads/index.tsx rename to web/src-web/src/views/clusters-management/components/workloads/index.tsx diff --git a/makoon/src-web/src/views/clusters-management/index.tsx b/web/src-web/src/views/clusters-management/index.tsx similarity index 98% rename from makoon/src-web/src/views/clusters-management/index.tsx rename to web/src-web/src/views/clusters-management/index.tsx index f36d81f..3ad6046 100644 --- a/makoon/src-web/src/views/clusters-management/index.tsx +++ b/web/src-web/src/views/clusters-management/index.tsx @@ -45,7 +45,8 @@ const ClusterManagement = () => { const navigate = useNavigate(); let {clusterName} = useParams(); - const locked = clusterManagementStore.cluster.status != ClusterStatus.Sync; + const locked = clusterManagementStore.cluster.status != ClusterStatus.Sync + && clusterManagementStore.cluster.status != ClusterStatus.Error useOnFirstMount(async () => { if (clusterName) { diff --git a/makoon/src-web/src/views/login/index.tsx b/web/src-web/src/views/login/index.tsx similarity index 100% rename from makoon/src-web/src/views/login/index.tsx rename to web/src-web/src/views/login/index.tsx diff --git a/makoon/src-web/src/views/settings/index.tsx b/web/src-web/src/views/settings/index.tsx similarity index 100% rename from makoon/src-web/src/views/settings/index.tsx rename to web/src-web/src/views/settings/index.tsx diff --git a/makoon/src-web/src/vite-env.d.ts b/web/src-web/src/vite-env.d.ts similarity index 100% rename from makoon/src-web/src/vite-env.d.ts rename to web/src-web/src/vite-env.d.ts diff --git a/makoon/src-web/tailwind.config.js b/web/src-web/tailwind.config.js similarity index 100% rename from makoon/src-web/tailwind.config.js rename to web/src-web/tailwind.config.js diff --git a/makoon/src-web/tsconfig.json b/web/src-web/tsconfig.json similarity index 100% rename from makoon/src-web/tsconfig.json rename to web/src-web/tsconfig.json diff --git a/makoon/src-web/tsconfig.node.json b/web/src-web/tsconfig.node.json similarity index 100% rename from makoon/src-web/tsconfig.node.json rename to web/src-web/tsconfig.node.json diff --git a/makoon/src-web/vite.config.ts b/web/src-web/vite.config.ts similarity index 100% rename from makoon/src-web/vite.config.ts rename to web/src-web/vite.config.ts diff --git a/web/src/handlers/actix.rs b/web/src/handlers/actix.rs new file mode 100644 index 0000000..b4e43e3 --- /dev/null +++ b/web/src/handlers/actix.rs @@ -0,0 +1,63 @@ +use actix_session::Session; +use serde::{Deserialize, Serialize}; + +#[macro_export] +macro_rules! logged_in { + ($session: expr, $proxmox_client: expr) => {{ + let access = crate::handlers::actix::get_session(&$session); + let access = match access { + Some(v) => v.access, + None => return Err(crate::handlers::error::HandlerError::UnAuthorized), + }; + + let access_to_get_permissions = access.clone(); + let proxmox_to_get_permissions = $proxmox_client.clone(); + let permissions_handle = tokio::task::spawn_blocking(move || { + proxmox_to_get_permissions + .operations(access_to_get_permissions) + .permissions() + }); + + match permissions_handle.await.unwrap() { + Ok(_) => access, + Err(_) => return Err(crate::handlers::error::HandlerError::UnAuthorized), + } + }}; +} + +pub mod inject { + use actix_web::web; + pub type ProxmoxClient = web::Data; + pub type Operator = web::Data; +} + +const SESSION_DATA_KEY: &str = "data"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct WebSession { + pub access: proxmox_client::model::AccessData, +} + +pub fn store_session(session: &Session, data: WebSession) { + let session_data = serde_json::to_string(&data).unwrap(); + session.insert(SESSION_DATA_KEY, session_data).unwrap(); +} + +pub fn get_session(session: &Session) -> Option { + let result = session.get::(SESSION_DATA_KEY); + let result = match result { + Ok(v) => v, + Err(_) => return None, + }; + + let result = match result { + Some(v) => v, + None => return None, + }; + + let result = serde_json::from_str(&result); + match result { + Ok(v) => Some(v), + Err(_) => None, + } +} diff --git a/makoon/src/handlers/apps.rs b/web/src/handlers/apps.rs similarity index 53% rename from makoon/src/handlers/apps.rs rename to web/src/handlers/apps.rs index cc97fab..a0852b2 100644 --- a/makoon/src/handlers/apps.rs +++ b/web/src/handlers/apps.rs @@ -1,13 +1,17 @@ use actix_session::Session; -use actix_web::{delete, get, HttpResponse, post, put, Responder, web, http::header::ContentType}; +use actix_web::{delete, get, http::header::ContentType, post, put, web, HttpResponse, Responder}; -use crate::logged_in; use crate::handlers::actix::inject; use crate::handlers::error::HandlerError; -use crate::operator::model::HelmApp; +use crate::logged_in; #[get("/api/v1/clusters/{name}/apps/status")] -pub async fn apps_status(path: web::Path, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn apps_status( + path: web::Path, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let name = path.into_inner(); @@ -17,7 +21,13 @@ pub async fn apps_status(path: web::Path, session: Session, proxmox_clie } #[post("/api/v1/clusters/{name}/apps")] -pub async fn save_helm_app(path: web::Path, body: web::Json, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn save_helm_app( + path: web::Path, + body: web::Json, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let name = path.into_inner(); @@ -29,7 +39,13 @@ pub async fn save_helm_app(path: web::Path, body: web::Json, se } #[put("/api/v1/clusters/{name}/apps")] -pub async fn update_helm_app(path: web::Path, body: web::Json, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn update_helm_app( + path: web::Path, + body: web::Json, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let name = path.into_inner(); @@ -39,7 +55,12 @@ pub async fn update_helm_app(path: web::Path, body: web::Json, } #[delete("/api/v1/clusters/{name}/apps/{app_id}")] -pub async fn delete_helm_app(path: web::Path<(String, String)>, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn delete_helm_app( + path: web::Path<(String, String)>, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let (name, app_id) = path.into_inner(); @@ -48,9 +69,13 @@ pub async fn delete_helm_app(path: web::Path<(String, String)>, session: Session Ok(HttpResponse::Ok().finish()) } - #[post("/api/v1/clusters/{name}/apps/{app_id}/install")] -pub async fn install_helm_app(path: web::Path<(String, String)>, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn install_helm_app( + path: web::Path<(String, String)>, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let (name, app_id) = path.into_inner(); @@ -60,7 +85,12 @@ pub async fn install_helm_app(path: web::Path<(String, String)>, session: Sessio } #[delete("/api/v1/clusters/{name}/apps/{app_id}/uninstall")] -pub async fn uninstall_helm_app(path: web::Path<(String, String)>, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn uninstall_helm_app( + path: web::Path<(String, String)>, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let (name, app_id) = path.into_inner(); diff --git a/makoon/src/handlers/auth.rs b/web/src/handlers/auth.rs similarity index 68% rename from makoon/src/handlers/auth.rs rename to web/src/handlers/auth.rs index e2ed20a..e53a4db 100644 --- a/makoon/src/handlers/auth.rs +++ b/web/src/handlers/auth.rs @@ -1,8 +1,8 @@ use actix_session::Session; -use actix_web::{get, HttpResponse, post, Responder, web}; +use actix_web::{get, post, web, HttpResponse, Responder}; -use proxmox::{Client, model::LoginRequest as ProxmoxLoginRequest}; -use proxmox::model::AccessData; +use proxmox_client::model::AccessData; +use proxmox_client::{model::LoginRequest as ProxmoxLoginRequest, Client}; use crate::handlers::actix; use crate::handlers::actix::{get_session, WebSession}; @@ -10,9 +10,11 @@ use crate::handlers::error::HandlerError; use crate::handlers::model::LoginRequest; #[post("/api/v1/login")] -pub async fn login(body: web::Json, - session: Session, - proxmox_client: web::Data) -> actix_web::Result { +pub async fn login( + body: web::Json, + session: Session, + proxmox_client: web::Data, +) -> actix_web::Result { let access_data = web::block(move || { let result = proxmox_client.login(ProxmoxLoginRequest { host: body.host.clone(), @@ -22,11 +24,15 @@ pub async fn login(body: web::Json, password: body.password.clone(), })?; Ok::(result) - }).await??; + }) + .await??; - actix::store_session(&session, WebSession { - access: access_data, - }); + actix::store_session( + &session, + WebSession { + access: access_data, + }, + ); Ok(HttpResponse::Ok()) } @@ -44,4 +50,4 @@ pub async fn get_host_ip(session: Session) -> impl Responder { .map(|i| i.access.host) .map(|i| HttpResponse::Ok().content_type("text/plain").body(i)) .unwrap_or(HttpResponse::Unauthorized().finish()) -} \ No newline at end of file +} diff --git a/web/src/handlers/cluster.rs b/web/src/handlers/cluster.rs new file mode 100644 index 0000000..c51d08c --- /dev/null +++ b/web/src/handlers/cluster.rs @@ -0,0 +1,233 @@ +use actix_session::Session; +use actix_web::{delete, get, post, put, web, HttpResponse, Responder}; + +use proxmox_client::model::VirtualMachine; + +use crate::handlers::actix::inject; +use crate::handlers::error::HandlerError; +use crate::handlers::model::{ChangeNodeResourcesRequest, ClusterNodeVmStatus}; +use crate::logged_in; + +#[get("/api/v1/clusters/{cluster_name}/nodes")] +pub async fn get_nodes( + path: web::Path, + session: Session, + operator: inject::Operator, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let _ = logged_in!(session, proxmox_client); + + let cluster_name = path.into_inner(); + + let result = operator.get_nodes(&cluster_name)?; + + Ok(HttpResponse::Ok().json(result)) +} + +#[post("/api/v1/clusters/{cluster_name}/nodes")] +pub async fn add_node_to_cluster( + body: web::Json, + path: web::Path, + session: Session, + operator: inject::Operator, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let access = logged_in!(session, proxmox_client); + let cluster_name = path.into_inner(); + + let added_node = operator.add_node_cluster(access, cluster_name, body.0)?; + Ok(HttpResponse::Created().json(added_node)) +} + +#[put("/api/v1/clusters/{cluster_name}/nodes/{node_name}/resources")] +pub async fn change_node_resources( + body: web::Json, + path: web::Path<(String, String)>, + session: Session, + operator: inject::Operator, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let access = logged_in!(session, proxmox_client); + let (cluster_name, node_name) = path.into_inner(); + operator.change_node_resources(access, cluster_name, node_name, body.cores, body.memory)?; + Ok(HttpResponse::Accepted()) +} + +#[post("/api/v1/clusters")] +pub async fn create_cluster( + body: web::Json, + session: Session, + operator: inject::Operator, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let access = logged_in!(session, proxmox_client); + + operator.create_cluster(access, body.0)?; + Ok(HttpResponse::Created().finish()) +} + +#[get("/api/v1/clusters")] +pub async fn get_clusters( + session: Session, + operator: inject::Operator, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let _ = logged_in!(session, proxmox_client); + let result = operator.get_clusters()?; + Ok(HttpResponse::Ok().json(result)) +} + +#[get("/api/v1/clusters/{name}")] +pub async fn get_cluster( + path: web::Path, + session: Session, + operator: inject::Operator, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let _ = logged_in!(session, proxmox_client); + + let name = path.into_inner(); + + let result = operator + .get_cluster(&name)? + .ok_or(HandlerError::NotFound("Cluster not found".to_string()))?; + + Ok(HttpResponse::Ok().json(result)) +} + +#[delete("/api/v1/clusters/{name}")] +pub async fn delete_cluster( + path: web::Path, + session: Session, + operator: inject::Operator, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let access = logged_in!(session, proxmox_client); + let name = path.into_inner(); + + operator.delete_cluster(access, name)?; + Ok(HttpResponse::Ok().finish()) +} + +#[delete("/api/v1/clusters/{cluster_name}/nodes/{node_name}")] +pub async fn delete_node_from_cluster( + path: web::Path<(String, String)>, + session: Session, + operator: inject::Operator, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let access = logged_in!(session, proxmox_client); + let (cluster_name, node_name) = path.into_inner(); + + let deleted_node = operator.delete_node_from_cluster(access, cluster_name, node_name)?; + Ok(HttpResponse::Ok().json(deleted_node)) +} + +#[get("/api/v1/clusters/generate")] +pub async fn generate_default_cluster_configuration( + session: Session, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let access = logged_in!(session, proxmox_client); + + let result = web::block(move || { + let generator = + core::DefaultClusterConfigurationGenerator::new(proxmox_client.operations(access)); + Ok::(generator.generate()?) + }) + .await??; + + Ok(HttpResponse::Ok().json(result)) +} + +#[get("/api/v1/clusters/{name}/logs")] +pub async fn logs_for_cluster( + path: web::Path, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { + let _ = logged_in!(session, proxmox_client); + + let name = path.into_inner(); + + let result = + web::block(move || Ok::, HandlerError>(operator.logs_for_cluster(&name)?)) + .await??; + + Ok(HttpResponse::Ok().json(result)) +} + +#[delete("/api/v1/clusters/{name}/logs")] +pub async fn clear_logs_for_cluster( + path: web::Path, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { + let _ = logged_in!(session, proxmox_client); + + let name = path.into_inner(); + + web::block(move || { + operator.clear_logs_for_cluster(&name)?; + Ok::<(), HandlerError>(()) + }) + .await??; + + Ok(HttpResponse::Ok()) +} + +#[get("/api/v1/clusters/{name}/status/vms")] +pub async fn cluster_vm_status( + path: web::Path, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { + let access = logged_in!(session, proxmox_client); + let name = path.into_inner(); + + let cluster = web::block(move || { + let result = operator.get_cluster(&name)?; + Ok::, HandlerError>(result) + }) + .await?? + .ok_or(HandlerError::NotFound("Cluster not found".to_string()))?; + + let vms = web::block(move || { + let result = proxmox_client + .operations(access) + .virtual_machines(&cluster.node, None)?; + Ok::, HandlerError>(result) + }) + .await??; + + let cluster_vm_ids = cluster.nodes.iter().map(|i| i.vm_id).collect::>(); + + let result = vms + .iter() + .filter(|i| cluster_vm_ids.contains(&i.vm_id)) + .map(|i| ClusterNodeVmStatus { + vm_id: i.vm_id, + status: i.status.clone(), + }) + .collect::>(); + + Ok(HttpResponse::Ok().json(result)) +} + +#[get("/api/v1/clusters/{name}/status/kube")] +pub async fn cluster_kube_status( + path: web::Path, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { + let _ = logged_in!(session, proxmox_client); + let name = path.into_inner(); + + let result = web::block(move || operator.cluster_status(&name)).await??; + + Ok(HttpResponse::Ok().json(result)) +} diff --git a/makoon/src/handlers/cluster_resources.rs b/web/src/handlers/cluster_resources.rs similarity index 53% rename from makoon/src/handlers/cluster_resources.rs rename to web/src/handlers/cluster_resources.rs index 7a8ba45..fd98568 100644 --- a/makoon/src/handlers/cluster_resources.rs +++ b/web/src/handlers/cluster_resources.rs @@ -1,13 +1,18 @@ use actix_session::Session; -use actix_web::{delete, http::header::ContentType, HttpResponse, post, put, Responder, web}; +use actix_web::{delete, http::header::ContentType, post, put, web, HttpResponse, Responder}; use crate::handlers::actix::inject; use crate::handlers::error::HandlerError; use crate::logged_in; -use crate::operator::model::ClusterResource; #[post("/api/v1/clusters/{name}/cluster-resources")] -pub async fn save_cluster_resources(path: web::Path, body: web::Json, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn save_cluster_resources( + path: web::Path, + body: web::Json, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let name = path.into_inner(); @@ -19,7 +24,13 @@ pub async fn save_cluster_resources(path: web::Path, body: web::Json, body: web::Json, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn update_cluster_resources( + path: web::Path, + body: web::Json, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let name = path.into_inner(); @@ -29,7 +40,12 @@ pub async fn update_cluster_resources(path: web::Path, body: web::Json, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn delete_cluster_resources( + path: web::Path<(String, String)>, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let (name, res_id) = path.into_inner(); @@ -38,9 +54,13 @@ pub async fn delete_cluster_resources(path: web::Path<(String, String)>, session Ok(HttpResponse::Ok()) } - #[post("/api/v1/clusters/{name}/cluster-resources/{res_id}/install")] -pub async fn install_cluster_resources(path: web::Path<(String, String)>, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn install_cluster_resources( + path: web::Path<(String, String)>, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let (name, res_id) = path.into_inner(); @@ -50,7 +70,12 @@ pub async fn install_cluster_resources(path: web::Path<(String, String)>, sessio } #[delete("/api/v1/clusters/{name}/cluster-resources/{res_id}/uninstall")] -pub async fn uninstall_cluster_resources(path: web::Path<(String, String)>, session: Session, proxmox_client: inject::ProxmoxClient, operator: inject::Operator) -> actix_web::Result { +pub async fn uninstall_cluster_resources( + path: web::Path<(String, String)>, + session: Session, + proxmox_client: inject::ProxmoxClient, + operator: inject::Operator, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let (name, res_id) = path.into_inner(); diff --git a/makoon/src/handlers/error.rs b/web/src/handlers/error.rs similarity index 57% rename from makoon/src/handlers/error.rs rename to web/src/handlers/error.rs index a6a4c30..5ca1627 100644 --- a/makoon/src/handlers/error.rs +++ b/web/src/handlers/error.rs @@ -5,8 +5,6 @@ use actix_web::error::BlockingError; use actix_web::http::StatusCode; use serde::{Deserialize, Serialize}; -use crate::operator; - #[derive(Serialize, Deserialize, Debug)] pub enum HandlerError { BadRequest(String), @@ -15,14 +13,13 @@ pub enum HandlerError { InternalServerError(String), } - impl Display for HandlerError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { HandlerError::UnAuthorized => write!(f, ""), HandlerError::NotFound(e) => write!(f, "{}", e), HandlerError::InternalServerError(e) => write!(f, "{}", e), - HandlerError::BadRequest(e) => write!(f, "{}", e) + HandlerError::BadRequest(e) => write!(f, "{}", e), } } } @@ -33,12 +30,11 @@ impl actix_web::error::ResponseError for HandlerError { HandlerError::UnAuthorized => StatusCode::UNAUTHORIZED, HandlerError::NotFound(_) => StatusCode::NOT_FOUND, HandlerError::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR, - HandlerError::BadRequest(_) => StatusCode::BAD_REQUEST + HandlerError::BadRequest(_) => StatusCode::BAD_REQUEST, } } } - impl From> for HandlerError { fn from(value: PoisonError) -> Self { error!("{}", value); @@ -53,24 +49,32 @@ impl From for HandlerError { } } -impl From for HandlerError { - fn from(value: proxmox::Error) -> Self { +impl From for HandlerError { + fn from(value: proxmox_client::Error) -> Self { match value { - proxmox::Error::Generic(e) => HandlerError::InternalServerError(e), - proxmox::Error::CannotConnectToProxmox(_) => HandlerError::UnAuthorized, - proxmox::Error::CredentialsInvalid => HandlerError::UnAuthorized, - proxmox::Error::HttpError { status, reason, response } => HandlerError::InternalServerError(format!("{}-{}: {}", status, reason, response)), - proxmox::Error::BodyMalformed(e) => HandlerError::InternalServerError(e), + proxmox_client::Error::Generic(e) => HandlerError::InternalServerError(e), + proxmox_client::Error::CannotConnectToProxmox(_) => HandlerError::UnAuthorized, + proxmox_client::Error::CredentialsInvalid => HandlerError::UnAuthorized, + proxmox_client::Error::HttpError { + status, + reason, + response, + } => HandlerError::InternalServerError(format!("{}-{}: {}", status, reason, response)), + proxmox_client::Error::BodyMalformed(e) => HandlerError::InternalServerError(e), } } } -impl From for HandlerError { - fn from(value: operator::Error) -> Self { +impl From for HandlerError { + fn from(value: core::Error) -> Self { match value { - operator::Error::ResourceAlreadyExists => HandlerError::BadRequest("Cluster already exists".to_string()), - operator::Error::Generic(e) => HandlerError::InternalServerError(e), - operator::Error::ResourceNotFound => HandlerError::NotFound("Cluster not found".to_string()), + core::Error::ResourceAlreadyExists => { + HandlerError::BadRequest("Cluster already exists".to_string()) + } + core::Error::Generic(e) => HandlerError::InternalServerError(e), + core::Error::ResourceNotFound => { + HandlerError::NotFound("Cluster not found".to_string()) + } } } -} \ No newline at end of file +} diff --git a/makoon/src/handlers/export.rs b/web/src/handlers/export.rs similarity index 70% rename from makoon/src/handlers/export.rs rename to web/src/handlers/export.rs index 4793a2c..5dd5b7e 100644 --- a/makoon/src/handlers/export.rs +++ b/web/src/handlers/export.rs @@ -1,7 +1,7 @@ use actix_session::Session; -use actix_web::{get, HttpResponse, web}; use actix_web::http::header::CONTENT_DISPOSITION; use actix_web::http::StatusCode; +use actix_web::{get, web, HttpResponse}; use mime_guess::mime::TEXT_PLAIN; use serde::{Deserialize, Serialize}; @@ -18,11 +18,17 @@ pub enum ExportType { } #[get("/api/v1/clusters/{cluster_name}/export/{export_type}")] -pub async fn export_cluster_data(path: web::Path<(String, ExportType)>, session: Session, operator: inject::Operator, proxmox_client: inject::ProxmoxClient) -> actix_web::Result { +pub async fn export_cluster_data( + path: web::Path<(String, ExportType)>, + session: Session, + operator: inject::Operator, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { let _ = logged_in!(session, proxmox_client); let (cluster_name, export_type) = path.into_inner(); - let cluster = operator.get_cluster(&cluster_name)? + let cluster = operator + .get_cluster(&cluster_name)? .ok_or(HandlerError::NotFound("Cluster not found".to_string()))?; let (key, file_name) = match export_type { @@ -33,7 +39,10 @@ pub async fn export_cluster_data(path: web::Path<(String, ExportType)>, session: let response = HttpResponse::build(StatusCode::OK) .content_type(TEXT_PLAIN) - .append_header((CONTENT_DISPOSITION, format!("attachment; filename=\"{}\"", file_name))) + .append_header(( + CONTENT_DISPOSITION, + format!("attachment; filename=\"{}\"", file_name), + )) .body(key); Ok(response) } diff --git a/makoon/src/handlers/mod.rs b/web/src/handlers/mod.rs similarity index 91% rename from makoon/src/handlers/mod.rs rename to web/src/handlers/mod.rs index e408bbc..b199cbd 100644 --- a/makoon/src/handlers/mod.rs +++ b/web/src/handlers/mod.rs @@ -1,11 +1,12 @@ -pub mod auth; -pub mod model; pub mod actix; +pub mod apps; +pub mod auth; pub mod cluster; +pub mod cluster_resources; +pub mod error; +pub mod export; +pub mod model; pub mod network; pub mod nodes; pub mod storage; -pub mod export; -pub mod error; -pub mod apps; -pub mod cluster_resources; +pub mod settings; \ No newline at end of file diff --git a/makoon/src/handlers/model.rs b/web/src/handlers/model.rs similarity index 67% rename from makoon/src/handlers/model.rs rename to web/src/handlers/model.rs index 2638e26..9d1d31c 100644 --- a/makoon/src/handlers/model.rs +++ b/web/src/handlers/model.rs @@ -1,6 +1,6 @@ +use proxmox_client::model::VmStatus; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use proxmox::model::VmStatus; #[typeshare] #[derive(Serialize, Deserialize, Debug)] @@ -30,7 +30,6 @@ pub struct LoginRequest { pub password: String, } - #[typeshare] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -65,3 +64,35 @@ pub struct ChangeNodeResourcesRequest { pub cores: u16, pub memory: u64, } + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AvailableOsImage { + pub name: String, + pub url: String, +} + +impl AvailableOsImage { + pub fn new(name: &str, url: &str) -> Self { + AvailableOsImage { + name: name.to_string(), + url: url.to_string(), + } + } +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AvailableKubeVersion { + pub version: String, +} + +impl AvailableKubeVersion { + pub fn new(version: &str) -> Self { + AvailableKubeVersion { + version: version.to_string() + } + } +} \ No newline at end of file diff --git a/web/src/handlers/network.rs b/web/src/handlers/network.rs new file mode 100644 index 0000000..0326f48 --- /dev/null +++ b/web/src/handlers/network.rs @@ -0,0 +1,35 @@ +use actix_session::Session; +use actix_web::{get, web, HttpResponse, Responder}; + +use crate::handlers::actix::inject; +use crate::handlers::error::HandlerError; +use crate::handlers::model::AvailableNetwork; +use crate::logged_in; + +#[get("/api/v1/nodes/{node}/networks/bridges")] +pub async fn networks_bridges( + path: web::Path, + session: Session, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let node = path.into_inner(); + let access = logged_in!(session, proxmox_client); + + let result = web::block(move || { + let result = proxmox_client + .operations(access) + .networks(&node, Some(proxmox_client::model::NetworkType::Bridge))?; + Ok::, HandlerError>(result) + }) + .await??; + + let result = result + .into_iter() + .map(|i| AvailableNetwork { + iface: i.iface, + address: i.address, + }) + .collect::>(); + + Ok(HttpResponse::Ok().json(result)) +} diff --git a/web/src/handlers/nodes.rs b/web/src/handlers/nodes.rs new file mode 100644 index 0000000..ec1914c --- /dev/null +++ b/web/src/handlers/nodes.rs @@ -0,0 +1,26 @@ +use actix_session::Session; +use actix_web::{get, web, HttpResponse, Responder}; + +use proxmox_client::model::Node; + +use crate::handlers::actix::inject; +use crate::handlers::error::HandlerError; +use crate::logged_in; + +#[get("/api/v1/nodes")] +pub async fn nodes( + session: Session, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { + let access = logged_in!(session, proxmox_client); + + let result = web::block(move || { + let result = proxmox_client.operations(access).nodes()?; + Ok::, HandlerError>(result) + }) + .await??; + + let result: Vec = result.into_iter().map(|i| i.node).collect(); + + Ok(HttpResponse::Ok().json(result)) +} diff --git a/web/src/handlers/settings.rs b/web/src/handlers/settings.rs new file mode 100644 index 0000000..e940821 --- /dev/null +++ b/web/src/handlers/settings.rs @@ -0,0 +1,22 @@ +use actix_web::{get, HttpResponse, Responder}; +use crate::handlers::error::HandlerError; +use crate::handlers::model::AvailableKubeVersion; +use crate::handlers::model::AvailableOsImage; + +#[get("/api/v1/os-images")] +pub async fn os_images() -> actix_web::Result { + let result: Vec = core::supported::os_images().iter() + .map(|(k, v)| AvailableOsImage::new(k, v)) + .collect(); + + Ok(HttpResponse::Ok().json(result)) +} + +#[get("/api/v1/kube-versions")] +pub async fn kube_versions() -> actix_web::Result { + let result: Vec = core::supported::kube_versions().iter() + .map(|e| AvailableKubeVersion::new(e)) + .collect(); + + Ok(HttpResponse::Ok().json(result)) +} \ No newline at end of file diff --git a/makoon/src/handlers/storage.rs b/web/src/handlers/storage.rs similarity index 63% rename from makoon/src/handlers/storage.rs rename to web/src/handlers/storage.rs index 3525b61..ab25668 100644 --- a/makoon/src/handlers/storage.rs +++ b/web/src/handlers/storage.rs @@ -1,6 +1,5 @@ use actix_session::Session; -use actix_web::{get, HttpResponse, Responder, web}; - +use actix_web::{get, web, HttpResponse, Responder}; use crate::handlers::actix::inject; use crate::handlers::error::HandlerError; @@ -8,7 +7,11 @@ use crate::handlers::model::AvailableStorage; use crate::logged_in; #[get("/api/v1/nodes/{node}/storage/{storage_content_type}")] -pub async fn storage(path: web::Path<(String, proxmox::model::StorageContentType)>, session: Session, proxmox_client: inject::ProxmoxClient) -> actix_web::Result { +pub async fn storage( + path: web::Path<(String, proxmox_client::model::StorageContentType)>, + session: Session, + proxmox_client: inject::ProxmoxClient, +) -> actix_web::Result { let (node, storage_content_type) = path.into_inner(); let access = logged_in!(session, proxmox_client); @@ -16,10 +19,12 @@ pub async fn storage(path: web::Path<(String, proxmox::model::StorageContentType let result = proxmox_client .operations(access) .storage(&node, Some(storage_content_type))?; - Ok::, HandlerError>(result) - }).await??; + Ok::, HandlerError>(result) + }) + .await??; - let result: Vec = result.into_iter() + let result: Vec = result + .into_iter() .map(|i| AvailableStorage { storage: i.storage, avail: i.avail, @@ -29,4 +34,4 @@ pub async fn storage(path: web::Path<(String, proxmox::model::StorageContentType .collect(); Ok(HttpResponse::Ok().json(result)) -} \ No newline at end of file +} diff --git a/web/src/lib.rs b/web/src/lib.rs new file mode 100644 index 0000000..29aa4d4 --- /dev/null +++ b/web/src/lib.rs @@ -0,0 +1,17 @@ +#[macro_use] +extern crate log; + +mod handlers; + +pub use handlers::model; + +pub mod core { + pub mod model { + pub use core::model::{ + ClusterHeader, + ClusterStatus, + ClusterRequest, + Cluster, + }; + } +} \ No newline at end of file diff --git a/makoon/src/main.rs b/web/src/main.rs similarity index 76% rename from makoon/src/main.rs rename to web/src/main.rs index 0f842e6..3d4afbb 100644 --- a/makoon/src/main.rs +++ b/web/src/main.rs @@ -5,28 +5,23 @@ use std::env; use std::io::ErrorKind; use std::sync::Arc; -use actix_session::SessionMiddleware; use actix_session::storage::CookieSessionStore; -use actix_web::{App, get, HttpResponse, HttpServer, Responder, web}; +use actix_session::SessionMiddleware; use actix_web::cookie::Key; use actix_web::middleware::Logger; +use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; use env_logger::Env; use mime_guess::from_path; use rust_embed::RustEmbed; -use operator::{Config, Dispatcher, Operator}; use crate::handlers::actix::inject; mod handlers; -mod operator; -mod helm; - #[derive(RustEmbed)] #[folder = "src-web/dist/"] struct Asset; - fn handle_embedded_file(path: &str) -> HttpResponse { match Asset::get(path) { Some(content) => HttpResponse::Ok() @@ -38,26 +33,33 @@ fn handle_embedded_file(path: &str) -> HttpResponse { #[get("/{_:.*}")] async fn static_files(path: web::Path) -> impl Responder { - handle_embedded_file(if path.is_empty() { "index.html" } else { path.as_str() }) + handle_embedded_file(if path.is_empty() { + "index.html" + } else { + path.as_str() + }) } - #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init_from_env(Env::default().default_filter_or("info")); let db_location = env::var("MAKOON_DB_PATH").unwrap_or("./makoon".to_string()); - let server_port: u16 = env::var("MAKOON_SERVER_PORT").unwrap_or("8080".to_string()) + let server_port: u16 = env::var("MAKOON_SERVER_PORT") + .unwrap_or("8080".to_string()) .parse() .map_err(|_| std::io::Error::from(ErrorKind::InvalidInput))?; - let proxmox_client = Arc::new(proxmox::Client::new()); - let repo = Arc::new(operator::Repository::new(&db_location) - .map_err(|_| std::io::Error::from(ErrorKind::InvalidData))?); - - let operator = Operator::new( - Config::default(), - Dispatcher::new(proxmox_client.clone(), repo.clone()), - repo.clone()); + let proxmox_client = Arc::new(proxmox_client::Client::new()); + let repo = Arc::new( + core::Repository::new(&db_location) + .map_err(|_| std::io::Error::from(ErrorKind::InvalidData))?, + ); + + let operator = core::Operator::new( + core::Config::default(), + core::Dispatcher::new(proxmox_client.clone(), repo.clone()), + repo.clone(), + ); let operator = inject::Operator::new(operator); let session_encryption_key = Key::generate(); @@ -65,10 +67,15 @@ async fn main() -> std::io::Result<()> { App::new() .app_data(inject::ProxmoxClient::from(proxmox_client.clone())) .app_data(operator.clone()) - .wrap(SessionMiddleware::builder(CookieSessionStore::default(), session_encryption_key.clone()) + .wrap( + SessionMiddleware::builder( + CookieSessionStore::default(), + session_encryption_key.clone(), + ) .cookie_http_only(false) .cookie_secure(false) - .build()) + .build(), + ) .wrap(Logger::default()) .service(handlers::auth::get_host_ip) .service(handlers::auth::login) @@ -101,11 +108,11 @@ async fn main() -> std::io::Result<()> { .service(handlers::nodes::nodes) .service(handlers::storage::storage) .service(handlers::export::export_cluster_data) + .service(handlers::settings::os_images) + .service(handlers::settings::kube_versions) .service(static_files) }) - .bind(("0.0.0.0", server_port))? - .run() - .await + .bind(("0.0.0.0", server_port))? + .run() + .await } - - diff --git a/web/tests/common/makoon_client.rs b/web/tests/common/makoon_client.rs new file mode 100644 index 0000000..298de51 --- /dev/null +++ b/web/tests/common/makoon_client.rs @@ -0,0 +1,98 @@ +use reqwest::header::CONTENT_TYPE; +use web::core::model::{Cluster, ClusterHeader, ClusterRequest}; +use web::model::{AvailableKubeVersion, AvailableOsImage, LoginRequest}; + +pub struct Client { + client: reqwest::blocking::Client, + host: String, +} + +impl Client { + pub fn new(host: String) -> Client { + Client { + host, + client: reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(true) + .pool_max_idle_per_host(0) + .cookie_store(true) + .build() + .unwrap(), + } + } + + pub fn login(&self, login: LoginRequest) { + let url = format!("{}{}", self.host, "/api/v1/login"); + + let request = self.client.post(url).json(&login); + let response = request.send().unwrap(); + if !response.status().is_success() { + panic!("Cannot login"); + } + } + pub fn clusters(&self) -> Vec { + let url = format!("{}{}", self.host, "/api/v1/clusters"); + + let request = self.client.get(url); + let response = request.send().unwrap(); + let clusters: Vec = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + clusters + } + + pub fn generate_default_cluster(&self) -> ClusterRequest { + let url = format!("{}{}", self.host, "/api/v1/clusters/generate"); + + let request = self.client.get(url); + let response = request.send().unwrap(); + let clusters: ClusterRequest = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + clusters + } + + pub fn create_cluster(&self, request: &ClusterRequest) { + let url = format!("{}{}", self.host, "/api/v1/clusters"); + + let request = self.client.post(url) + .header(CONTENT_TYPE, "application/json") + .json(request); + + let response = request.send().unwrap(); + if !response.status().is_success() { + panic!("Cannot create cluster [{}] - [{}]", response.status().to_string(), response.text().unwrap()); + } + } + + pub fn get_cluster(&self, cluster_name: &str) -> Cluster { + let url = format!("{}{}{}", self.host, "/api/v1/clusters/", cluster_name); + + let request = self.client.get(url); + let response = request.send().unwrap(); + let cluster: Cluster = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + cluster + } + pub fn delete_cluster(&self, cluster_name: &str) { + let url = format!("{}{}{}", self.host, "/api/v1/clusters/", cluster_name); + + let request = self.client.delete(url); + + let response = request.send().unwrap(); + if !response.status().is_success() { + panic!("Cannot delete cluster"); + } + } + pub fn os_images(&self) -> Vec { + let url = format!("{}{}", self.host, "/api/v1/os-images"); + + let request = self.client.get(url); + let response = request.send().unwrap(); + let result: Vec = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + result + } + + pub fn kube_versions(&self) -> Vec { + let url = format!("{}{}", self.host, "/api/v1/kube-versions"); + + let request = self.client.get(url); + let response = request.send().unwrap(); + let result: Vec = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + result + } +} \ No newline at end of file diff --git a/web/tests/common/mod.rs b/web/tests/common/mod.rs new file mode 100644 index 0000000..80b1336 --- /dev/null +++ b/web/tests/common/mod.rs @@ -0,0 +1,2 @@ +mod makoon_client; +pub use makoon_client::Client; diff --git a/web/tests/e2e_create_cluster.rs b/web/tests/e2e_create_cluster.rs new file mode 100644 index 0000000..dcd76a7 --- /dev/null +++ b/web/tests/e2e_create_cluster.rs @@ -0,0 +1,90 @@ +use std::time::Duration; +use web::core::model::ClusterStatus; +use web::model::LoginRequest; + +mod common; + +const CLUSTER_NAME:&str = "e2e-test"; + +#[cfg(feature = "e2e")] +#[test] +fn test_add_and_delete_small_cluster() { + let makoon_host = std::env::var("MAKOON_HOST").unwrap_or("http://localhost:5173".to_string()); + let proxmox_host = std::env::var("PROXMOX_HOST").unwrap(); + let proxmox_user = std::env::var("PROXMOX_USER").unwrap(); + let proxmox_password = std::env::var("PROXMOX_PASSWORD").unwrap(); + + + + let makoon_client = common::Client::new(makoon_host); + makoon_client.login(LoginRequest { + host: proxmox_host, + port: 8006, + username: proxmox_user, + password: proxmox_password, + }); + + + let os_images = makoon_client.os_images(); + let kube_versions = makoon_client.kube_versions(); + + for os_image in os_images.iter() { + for kube_version in kube_versions.iter() { + println!("-> Cluster test: OS [{}], kube version: [{}]", &os_image.name, &kube_version.version); + clean_clusters(&makoon_client); + create_and_destroy(&makoon_client, &os_image.url, &kube_version.version) + } + } + +} +fn create_and_destroy(makoon_client: &common::Client, os_image: &str, kube_version: &str) { + let mut cluster_request = makoon_client.generate_default_cluster(); + cluster_request.cluster_name = CLUSTER_NAME.to_string(); + cluster_request.nodes[0].ip_address = "192.168.1.210".to_string(); + cluster_request.os_image = os_image.to_string(); + cluster_request.kube_version = kube_version.to_string(); + + makoon_client.create_cluster(&cluster_request); + let mut probes = 40; + loop { + if probes == 0 { + panic!("Cannot create cluster"); + } + let cluster = makoon_client.get_cluster(&cluster_request.cluster_name); + if cluster.status == ClusterStatus::Sync { + println!("Cluster created"); + break; + } else if cluster.status == ClusterStatus::Error { + panic!("Cannot create cluster"); + } else { + println!("Cluster in status [{:?}], wait 10 sec", cluster.status); + std::thread::sleep(Duration::from_secs(10)); + probes -= 1; + } + } + makoon_client.delete_cluster(&cluster_request.cluster_name) +} +fn clean_clusters(client: &common::Client) { + println!("Setup"); + let clusters = client.clusters(); + let exists = clusters.iter().find(|e|e.name == CLUSTER_NAME).is_some(); + if exists { + println!("Cluster [{}] already exists, delete cluster to prepare environment", CLUSTER_NAME); + client.delete_cluster(CLUSTER_NAME); + } + let mut probes = 10; + loop { + let clusters = client.clusters(); + let exists = clusters.iter().find(|e|e.name == CLUSTER_NAME).is_some(); + if exists { + std::thread::sleep(Duration::from_secs(3)); + probes -= 1; + } else { + break; + } + if probes == 0 { + panic!("Cannot delete cluster"); + } + } + +} \ No newline at end of file diff --git a/makoon/typeshare.toml b/web/typeshare.toml similarity index 100% rename from makoon/typeshare.toml rename to web/typeshare.toml