├── .cargo └── config.toml ├── .devcontainer ├── dev.compose.yaml ├── devcontainer.json └── postCreate.sh ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode ├── extensions.json ├── resolver.code-snippets └── tasks.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── bin ├── binaries.Dockerfile ├── cli │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── args.rs │ │ ├── exec.rs │ │ ├── helpers.rs │ │ ├── main.rs │ │ └── state.rs ├── core │ ├── Cargo.toml │ ├── aio.Dockerfile │ ├── debian-deps.sh │ ├── multi-arch.Dockerfile │ ├── single-arch.Dockerfile │ ├── src │ │ ├── alert │ │ │ ├── discord.rs │ │ │ ├── mod.rs │ │ │ ├── ntfy.rs │ │ │ ├── pushover.rs │ │ │ └── slack.rs │ │ ├── api │ │ │ ├── auth.rs │ │ │ ├── execute │ │ │ │ ├── action.rs │ │ │ │ ├── alerter.rs │ │ │ │ ├── build.rs │ │ │ │ ├── deployment.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── procedure.rs │ │ │ │ ├── repo.rs │ │ │ │ ├── server.rs │ │ │ │ ├── stack.rs │ │ │ │ └── sync.rs │ │ │ ├── mod.rs │ │ │ ├── read │ │ │ │ ├── action.rs │ │ │ │ ├── alert.rs │ │ │ │ ├── alerter.rs │ │ │ │ ├── build.rs │ │ │ │ ├── builder.rs │ │ │ │ ├── deployment.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── permission.rs │ │ │ │ ├── procedure.rs │ │ │ │ ├── provider.rs │ │ │ │ ├── repo.rs │ │ │ │ ├── server.rs │ │ │ │ ├── stack.rs │ │ │ │ ├── sync.rs │ │ │ │ ├── tag.rs │ │ │ │ ├── toml.rs │ │ │ │ ├── update.rs │ │ │ │ ├── user.rs │ │ │ │ ├── user_group.rs │ │ │ │ └── variable.rs │ │ │ ├── terminal.rs │ │ │ ├── user.rs │ │ │ └── write │ │ │ │ ├── action.rs │ │ │ │ ├── alerter.rs │ │ │ │ ├── build.rs │ │ │ │ ├── builder.rs │ │ │ │ ├── deployment.rs │ │ │ │ ├── description.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── permissions.rs │ │ │ │ ├── procedure.rs │ │ │ │ ├── provider.rs │ │ │ │ ├── repo.rs │ │ │ │ ├── server.rs │ │ │ │ ├── service_user.rs │ │ │ │ ├── stack.rs │ │ │ │ ├── sync.rs │ │ │ │ ├── tag.rs │ │ │ │ ├── user.rs │ │ │ │ ├── user_group.rs │ │ │ │ └── variable.rs │ │ ├── auth │ │ │ ├── github │ │ │ │ ├── client.rs │ │ │ │ └── mod.rs │ │ │ ├── google │ │ │ │ ├── client.rs │ │ │ │ └── mod.rs │ │ │ ├── jwt.rs │ │ │ ├── local.rs │ │ │ ├── mod.rs │ │ │ └── oidc │ │ │ │ ├── client.rs │ │ │ │ └── mod.rs │ │ ├── cloud │ │ │ ├── aws │ │ │ │ ├── ec2.rs │ │ │ │ └── mod.rs │ │ │ └── mod.rs │ │ ├── config.rs │ │ ├── db.rs │ │ ├── helpers │ │ │ ├── action_state.rs │ │ │ ├── builder.rs │ │ │ ├── cache.rs │ │ │ ├── channel.rs │ │ │ ├── interpolate.rs │ │ │ ├── matcher.rs │ │ │ ├── mod.rs │ │ │ ├── procedure.rs │ │ │ ├── prune.rs │ │ │ ├── query.rs │ │ │ └── update.rs │ │ ├── listener │ │ │ ├── integrations │ │ │ │ ├── github.rs │ │ │ │ ├── gitlab.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── resources.rs │ │ │ └── router.rs │ │ ├── main.rs │ │ ├── monitor │ │ │ ├── alert │ │ │ │ ├── deployment.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── server.rs │ │ │ │ └── stack.rs │ │ │ ├── helpers.rs │ │ │ ├── lists.rs │ │ │ ├── mod.rs │ │ │ ├── record.rs │ │ │ └── resources.rs │ │ ├── permission.rs │ │ ├── resource │ │ │ ├── action.rs │ │ │ ├── alerter.rs │ │ │ ├── build.rs │ │ │ ├── builder.rs │ │ │ ├── deployment.rs │ │ │ ├── mod.rs │ │ │ ├── procedure.rs │ │ │ ├── refresh.rs │ │ │ ├── repo.rs │ │ │ ├── server.rs │ │ │ ├── stack.rs │ │ │ └── sync.rs │ │ ├── schedule.rs │ │ ├── stack │ │ │ ├── execute.rs │ │ │ ├── mod.rs │ │ │ ├── remote.rs │ │ │ └── services.rs │ │ ├── startup.rs │ │ ├── state.rs │ │ ├── sync │ │ │ ├── deploy.rs │ │ │ ├── execute.rs │ │ │ ├── file.rs │ │ │ ├── mod.rs │ │ │ ├── remote.rs │ │ │ ├── resources.rs │ │ │ ├── toml.rs │ │ │ ├── user_groups.rs │ │ │ ├── variables.rs │ │ │ └── view.rs │ │ ├── ts_client.rs │ │ └── ws │ │ │ ├── container.rs │ │ │ ├── deployment.rs │ │ │ ├── mod.rs │ │ │ ├── stack.rs │ │ │ ├── terminal.rs │ │ │ └── update.rs │ └── starship.toml ├── periphery │ ├── Cargo.toml │ ├── aio.Dockerfile │ ├── debian-deps.sh │ ├── multi-arch.Dockerfile │ ├── single-arch.Dockerfile │ ├── src │ │ ├── api │ │ │ ├── build.rs │ │ │ ├── compose.rs │ │ │ ├── container.rs │ │ │ ├── deploy.rs │ │ │ ├── git.rs │ │ │ ├── image.rs │ │ │ ├── mod.rs │ │ │ ├── network.rs │ │ │ ├── router.rs │ │ │ ├── stats.rs │ │ │ ├── terminal.rs │ │ │ └── volume.rs │ │ ├── compose.rs │ │ ├── config.rs │ │ ├── docker.rs │ │ ├── helpers.rs │ │ ├── main.rs │ │ ├── ssl.rs │ │ ├── stats.rs │ │ └── terminal.rs │ └── starship.toml └── util │ ├── Cargo.toml │ ├── aio.Dockerfile │ ├── docs │ └── copy-database.md │ ├── multi-arch.Dockerfile │ ├── single-arch.Dockerfile │ └── src │ ├── copy_database.rs │ └── main.rs ├── client ├── core │ ├── rs │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ │ ├── api │ │ │ ├── auth.rs │ │ │ ├── execute │ │ │ │ ├── action.rs │ │ │ │ ├── alerter.rs │ │ │ │ ├── build.rs │ │ │ │ ├── deployment.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── procedure.rs │ │ │ │ ├── repo.rs │ │ │ │ ├── server.rs │ │ │ │ ├── stack.rs │ │ │ │ └── sync.rs │ │ │ ├── mod.rs │ │ │ ├── read │ │ │ │ ├── action.rs │ │ │ │ ├── alert.rs │ │ │ │ ├── alerter.rs │ │ │ │ ├── build.rs │ │ │ │ ├── builder.rs │ │ │ │ ├── deployment.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── permission.rs │ │ │ │ ├── procedure.rs │ │ │ │ ├── provider.rs │ │ │ │ ├── repo.rs │ │ │ │ ├── server.rs │ │ │ │ ├── stack.rs │ │ │ │ ├── sync.rs │ │ │ │ ├── tag.rs │ │ │ │ ├── toml.rs │ │ │ │ ├── update.rs │ │ │ │ ├── user.rs │ │ │ │ ├── user_group.rs │ │ │ │ └── variable.rs │ │ │ ├── terminal.rs │ │ │ ├── user.rs │ │ │ └── write │ │ │ │ ├── action.rs │ │ │ │ ├── alerter.rs │ │ │ │ ├── api_key.rs │ │ │ │ ├── build.rs │ │ │ │ ├── builder.rs │ │ │ │ ├── deployment.rs │ │ │ │ ├── description.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── permissions.rs │ │ │ │ ├── procedure.rs │ │ │ │ ├── provider.rs │ │ │ │ ├── repo.rs │ │ │ │ ├── server.rs │ │ │ │ ├── stack.rs │ │ │ │ ├── sync.rs │ │ │ │ ├── tags.rs │ │ │ │ ├── user.rs │ │ │ │ ├── user_group.rs │ │ │ │ └── variable.rs │ │ │ ├── busy.rs │ │ │ ├── deserializers │ │ │ ├── conversion.rs │ │ │ ├── environment.rs │ │ │ ├── file_contents.rs │ │ │ ├── labels.rs │ │ │ ├── maybe_string_i64.rs │ │ │ ├── mod.rs │ │ │ ├── permission.rs │ │ │ ├── string_list.rs │ │ │ └── term_signal_labels.rs │ │ │ ├── entities │ │ │ ├── action.rs │ │ │ ├── alert.rs │ │ │ ├── alerter.rs │ │ │ ├── api_key.rs │ │ │ ├── build.rs │ │ │ ├── builder.rs │ │ │ ├── config │ │ │ │ ├── core.rs │ │ │ │ ├── mod.rs │ │ │ │ └── periphery.rs │ │ │ ├── deployment.rs │ │ │ ├── docker │ │ │ │ ├── container.rs │ │ │ │ ├── image.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── network.rs │ │ │ │ └── volume.rs │ │ │ ├── logger.rs │ │ │ ├── mod.rs │ │ │ ├── permission.rs │ │ │ ├── procedure.rs │ │ │ ├── provider.rs │ │ │ ├── repo.rs │ │ │ ├── resource.rs │ │ │ ├── server.rs │ │ │ ├── stack.rs │ │ │ ├── stats.rs │ │ │ ├── sync.rs │ │ │ ├── tag.rs │ │ │ ├── toml.rs │ │ │ ├── update.rs │ │ │ ├── user.rs │ │ │ ├── user_group.rs │ │ │ └── variable.rs │ │ │ ├── lib.rs │ │ │ ├── parsers.rs │ │ │ ├── request.rs │ │ │ ├── terminal.rs │ │ │ └── ws.rs │ └── ts │ │ ├── README.md │ │ ├── generate_types.mjs │ │ ├── package.json │ │ ├── src │ │ ├── lib.ts │ │ ├── responses.ts │ │ └── types.ts │ │ ├── tsconfig.json │ │ └── yarn.lock └── periphery │ └── rs │ ├── Cargo.toml │ └── src │ ├── api │ ├── build.rs │ ├── compose.rs │ ├── container.rs │ ├── git.rs │ ├── image.rs │ ├── mod.rs │ ├── network.rs │ ├── stats.rs │ ├── terminal.rs │ └── volume.rs │ ├── lib.rs │ └── terminal.rs ├── compose ├── compose.env ├── ferretdb.compose.yaml ├── mongo.compose.yaml └── periphery.compose.yaml ├── config ├── core.config.toml └── periphery.config.toml ├── dev.compose.yaml ├── docsite ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── api.md │ ├── build-images │ │ ├── builders.md │ │ ├── configuration.md │ │ ├── index.mdx │ │ ├── pre-build.md │ │ └── versioning.md │ ├── connect-servers.mdx │ ├── deploy-containers │ │ ├── configuration.md │ │ ├── index.mdx │ │ └── lifetime-management.md │ ├── development.md │ ├── docker-compose.md │ ├── file-paths.md │ ├── intro.md │ ├── other-resources.md │ ├── permissioning.md │ ├── procedures.md │ ├── resources.md │ ├── setup │ │ ├── advanced.mdx │ │ ├── ferretdb.mdx │ │ ├── index.mdx │ │ └── mongo.mdx │ ├── sync-resources.md │ ├── variables.md │ ├── version-upgrades.md │ └── webhooks.md ├── docusaurus.config.ts ├── package-lock.json ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ ├── ComposeAndEnv.tsx │ │ ├── Divider.tsx │ │ ├── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── KomodoLogo.tsx │ │ ├── RemoteCodeFile.tsx │ │ └── SummaryImg.tsx │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ └── index.tsx ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.ico │ │ ├── komodo-512x512.png │ │ └── monitor-lizard.png ├── tsconfig.json └── yarn.lock ├── example ├── alerter │ ├── Cargo.toml │ ├── Dockerfile │ ├── README.md │ └── src │ │ └── main.rs └── update_logger │ ├── Cargo.toml │ ├── Dockerfile │ └── src │ └── main.rs ├── expose.compose.yaml ├── frontend ├── .eslintrc.cjs ├── .gitignore ├── Dockerfile ├── README.md ├── components.json ├── index.html ├── package.json ├── postcss.config.js ├── public │ ├── apple-touch-icon.png │ ├── client │ │ ├── lib.d.ts │ │ ├── lib.js │ │ ├── responses.d.ts │ │ ├── responses.js │ │ ├── types.d.ts │ │ └── types.js │ ├── deno.d.ts │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── favicon.svg │ ├── icons │ │ ├── github.svg │ │ └── google.svg │ ├── index.d.ts │ ├── komodo-192x192.png │ ├── komodo-2q2code.png │ ├── komodo-512x512.png │ ├── komodo-logo.svg │ ├── manifest.json │ ├── monitor-lizard.png │ └── robots.txt ├── src │ ├── components │ │ ├── alert │ │ │ ├── details.tsx │ │ │ ├── index.tsx │ │ │ ├── table.tsx │ │ │ └── topbar.tsx │ │ ├── config │ │ │ ├── env_vars.tsx │ │ │ ├── index.tsx │ │ │ └── util.tsx │ │ ├── export.tsx │ │ ├── group-actions.tsx │ │ ├── inspect.tsx │ │ ├── keys │ │ │ └── table.tsx │ │ ├── layouts.tsx │ │ ├── log.tsx │ │ ├── monaco.tsx │ │ ├── omnibar.tsx │ │ ├── resources │ │ │ ├── action │ │ │ │ ├── config.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── info.tsx │ │ │ │ └── table.tsx │ │ │ ├── alerter │ │ │ │ ├── config │ │ │ │ │ ├── alert_types.tsx │ │ │ │ │ ├── endpoint.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── resources.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── table.tsx │ │ │ ├── build │ │ │ │ ├── actions.tsx │ │ │ │ ├── chart.tsx │ │ │ │ ├── config.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── info.tsx │ │ │ │ └── table.tsx │ │ │ ├── builder │ │ │ │ ├── config.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── table.tsx │ │ │ ├── common.tsx │ │ │ ├── deployment │ │ │ │ ├── actions.tsx │ │ │ │ ├── config │ │ │ │ │ ├── components │ │ │ │ │ │ ├── image.tsx │ │ │ │ │ │ ├── network.tsx │ │ │ │ │ │ ├── restart.tsx │ │ │ │ │ │ └── term-signal.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── inspect.tsx │ │ │ │ ├── log.tsx │ │ │ │ └── table.tsx │ │ │ ├── index.tsx │ │ │ ├── procedure │ │ │ │ ├── config.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── table.tsx │ │ │ ├── repo │ │ │ │ ├── actions.tsx │ │ │ │ ├── config.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── table.tsx │ │ │ ├── resource-sync │ │ │ │ ├── actions.tsx │ │ │ │ ├── config.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── info.tsx │ │ │ │ ├── pending.tsx │ │ │ │ └── table.tsx │ │ │ ├── server │ │ │ │ ├── actions.tsx │ │ │ │ ├── config.tsx │ │ │ │ ├── hooks.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── info │ │ │ │ │ ├── containers.tsx │ │ │ │ │ ├── images.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── networks.tsx │ │ │ │ │ └── volumes.tsx │ │ │ │ ├── stat-chart.tsx │ │ │ │ ├── stats.tsx │ │ │ │ └── table.tsx │ │ │ └── stack │ │ │ │ ├── actions.tsx │ │ │ │ ├── config.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── info.tsx │ │ │ │ ├── log.tsx │ │ │ │ ├── services.tsx │ │ │ │ └── table.tsx │ │ ├── sidebar.tsx │ │ ├── tags │ │ │ └── index.tsx │ │ ├── terminal │ │ │ ├── container.tsx │ │ │ ├── index.tsx │ │ │ └── server.tsx │ │ ├── topbar.tsx │ │ ├── updates │ │ │ ├── details.tsx │ │ │ ├── resource.tsx │ │ │ ├── table.tsx │ │ │ └── topbar.tsx │ │ ├── users │ │ │ ├── hooks.ts │ │ │ ├── new.tsx │ │ │ ├── permissions-selector.tsx │ │ │ ├── permissions-table.tsx │ │ │ ├── service-api-key.tsx │ │ │ └── table.tsx │ │ └── util.tsx │ ├── globals.css │ ├── lib │ │ ├── color.ts │ │ ├── formatting.ts │ │ ├── hooks.ts │ │ ├── socket.tsx │ │ └── utils.ts │ ├── main.tsx │ ├── monaco │ │ ├── index.ts │ │ ├── init.ts │ │ ├── key_value.ts │ │ ├── shell.ts │ │ ├── string_list.ts │ │ ├── theme.ts │ │ ├── toml.ts │ │ └── yaml.ts │ ├── pages │ │ ├── alerts.tsx │ │ ├── containers.tsx │ │ ├── home │ │ │ ├── all_resources.tsx │ │ │ ├── dashboard.tsx │ │ │ ├── index.tsx │ │ │ └── tree.tsx │ │ ├── login.tsx │ │ ├── resource-notifications.tsx │ │ ├── resource.tsx │ │ ├── resources.tsx │ │ ├── server-info │ │ │ ├── container │ │ │ │ ├── actions.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── inspect.tsx │ │ │ │ └── log.tsx │ │ │ ├── image.tsx │ │ │ ├── network.tsx │ │ │ └── volume.tsx │ │ ├── settings │ │ │ ├── index.tsx │ │ │ ├── profile.tsx │ │ │ ├── providers.tsx │ │ │ ├── tags.tsx │ │ │ ├── users.tsx │ │ │ └── variables.tsx │ │ ├── stack-service │ │ │ ├── index.tsx │ │ │ ├── inspect.tsx │ │ │ └── log.tsx │ │ ├── updates.tsx │ │ ├── user-group.tsx │ │ ├── user.tsx │ │ └── user_disabled.tsx │ ├── router.tsx │ ├── types.d.ts │ ├── ui │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── data-table.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── json.tsx │ │ ├── label.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── theme.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── komodo.code-workspace ├── lib ├── cache │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── command │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── environment_file │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── formatting │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── git │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── clone.rs │ │ ├── commit.rs │ │ ├── environment.rs │ │ ├── init.rs │ │ ├── lib.rs │ │ ├── pull.rs │ │ └── pull_or_clone.rs ├── logger │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ └── otel.rs └── response │ ├── Cargo.toml │ └── src │ └── lib.rs ├── readme.md ├── roadmap.md ├── runfile.toml ├── rustfmt.toml ├── screenshots ├── Dark-Compose.png ├── Dark-Dashboard.png ├── Dark-Env.png ├── Dark-Export.png ├── Dark-Stack.png ├── Dark-Stats.png ├── Dark-Sync.png ├── Dark-Update.png ├── Light-Compose.png ├── Light-Dashboard.png ├── Light-Env.png ├── Light-Export.png ├── Light-Stack.png ├── Light-Stats.png ├── Light-Sync.png └── Light-Update.png ├── scripts ├── readme.md └── setup-periphery.py └── typeshare.toml /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-Wunused-crate-dependencies"] -------------------------------------------------------------------------------- /.devcontainer/dev.compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | dev: 3 | image: mcr.microsoft.com/devcontainers/rust:1-1-bullseye 4 | volumes: 5 | # Mount the root folder that contains .git 6 | - ../:/workspace:cached 7 | - /var/run/docker.sock:/var/run/docker.sock 8 | - /proc:/proc 9 | - repos:/etc/komodo/repos 10 | - stacks:/etc/komodo/stacks 11 | command: sleep infinity 12 | ports: 13 | - "9121:9121" 14 | environment: 15 | KOMODO_FIRST_SERVER: http://localhost:8120 16 | KOMODO_DATABASE_ADDRESS: db 17 | KOMODO_ENABLE_NEW_USERS: true 18 | KOMODO_LOCAL_AUTH: true 19 | KOMODO_JWT_SECRET: a_random_secret 20 | links: 21 | - db 22 | # ... 23 | 24 | db: 25 | extends: 26 | file: ../dev.compose.yaml 27 | service: ferretdb 28 | 29 | volumes: 30 | data: 31 | repo-cache: 32 | repos: 33 | stacks: -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "Komodo", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | //"image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye", 7 | "dockerComposeFile": ["dev.compose.yaml"], 8 | "workspaceFolder": "/workspace", 9 | "service": "dev", 10 | // Features to add to the dev container. More info: https://containers.dev/features. 11 | "features": { 12 | "ghcr.io/devcontainers/features/node:1": { 13 | "version": "20.12.2" 14 | }, 15 | "ghcr.io/devcontainers-community/features/deno:1": { 16 | 17 | } 18 | }, 19 | 20 | // Use 'mounts' to make the cargo cache persistent in a Docker Volume. 21 | "mounts": [ 22 | { 23 | "source": "devcontainer-cargo-cache-${devcontainerId}", 24 | "target": "/usr/local/cargo", 25 | "type": "volume" 26 | } 27 | ], 28 | 29 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 30 | "forwardPorts": [ 31 | 9121 32 | ], 33 | 34 | // Use 'postCreateCommand' to run commands after the container is created. 35 | "postCreateCommand": "./.devcontainer/postCreate.sh", 36 | 37 | "runServices": [ 38 | "db" 39 | ] 40 | 41 | // Configure tool-specific properties. 42 | // "customizations": {}, 43 | 44 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 45 | // "remoteUser": "root" 46 | } 47 | -------------------------------------------------------------------------------- /.devcontainer/postCreate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cargo install typeshare-cli -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository 2 | open_collective: komodo 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | node_modules 3 | dist 4 | .env 5 | .env.development 6 | .DS_Store 7 | .idea 8 | 9 | /frontend/build 10 | /lib/ts_client/build 11 | 12 | creds.toml 13 | .dev 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "rust-lang.rust-analyzer", 4 | "tamasfe.even-better-toml", 5 | "vadimcn.vscode-lldb", 6 | "denoland.vscode-deno" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/resolver.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "resolve": { 3 | "scope": "rust", 4 | "prefix": "resolve", 5 | "body": [ 6 | "impl Resolve<${1}, User> for State {", 7 | "\tasync fn resolve(&self, ${1} { ${0} }: ${1}, _: User) -> anyhow::Result<${2}> {", 8 | "\t\ttodo!()", 9 | "\t}", 10 | "}" 11 | ] 12 | }, 13 | "static": { 14 | "scope": "rust", 15 | "prefix": "static", 16 | "body": [ 17 | "fn ${1}() -> &'static ${2} {", 18 | "\tstatic ${3}: OnceLock<${2}> = OnceLock::new();", 19 | "\t${3}.get_or_init(|| {", 20 | "\t\t${0}", 21 | "\t})", 22 | "}" 23 | ] 24 | } 25 | } -------------------------------------------------------------------------------- /bin/binaries.Dockerfile: -------------------------------------------------------------------------------- 1 | ## Builds the Komodo Core, Periphery, and Util binaries 2 | ## for a specific architecture. 3 | 4 | FROM rust:1.87.0-bullseye AS builder 5 | 6 | WORKDIR /builder 7 | COPY Cargo.toml Cargo.lock ./ 8 | COPY ./lib ./lib 9 | COPY ./client/core/rs ./client/core/rs 10 | COPY ./client/periphery ./client/periphery 11 | COPY ./bin/core ./bin/core 12 | COPY ./bin/periphery ./bin/periphery 13 | COPY ./bin/util ./bin/util 14 | 15 | # Compile bin 16 | RUN \ 17 | cargo build -p komodo_core --release && \ 18 | cargo build -p komodo_periphery --release && \ 19 | cargo build -p komodo_util --release 20 | 21 | # Copy just the binaries to scratch image 22 | FROM scratch 23 | 24 | COPY --from=builder /builder/target/release/core /core 25 | COPY --from=builder /builder/target/release/periphery /periphery 26 | COPY --from=builder /builder/target/release/util /util 27 | 28 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 29 | LABEL org.opencontainers.image.description="Komodo Binaries" 30 | LABEL org.opencontainers.image.licenses=GPL-3.0 -------------------------------------------------------------------------------- /bin/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "komodo_cli" 3 | description = "Command line tool to execute Komodo actions" 4 | version.workspace = true 5 | edition.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | 11 | [[bin]] 12 | name = "komodo" 13 | path = "src/main.rs" 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | # local 19 | # komodo_client = "1.16.12" 20 | komodo_client.workspace = true 21 | # external 22 | tracing-subscriber.workspace = true 23 | merge_config_files.workspace = true 24 | futures.workspace = true 25 | tracing.workspace = true 26 | colored.workspace = true 27 | anyhow.workspace = true 28 | tokio.workspace = true 29 | serde.workspace = true 30 | clap.workspace = true 31 | -------------------------------------------------------------------------------- /bin/cli/src/args.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use komodo_client::api::execute::Execution; 3 | use serde::Deserialize; 4 | 5 | #[derive(Parser, Debug)] 6 | #[command(version, about, long_about = None)] 7 | pub struct CliArgs { 8 | /// Sync or Exec 9 | #[command(subcommand)] 10 | pub command: Command, 11 | 12 | /// The path to a creds file. 13 | /// 14 | /// Note: If each of `url`, `key` and `secret` are passed, 15 | /// no file is required at this path. 16 | #[arg(long, default_value_t = default_creds())] 17 | pub creds: String, 18 | 19 | /// Pass url in args instead of creds file 20 | #[arg(long)] 21 | pub url: Option, 22 | /// Pass api key in args instead of creds file 23 | #[arg(long)] 24 | pub key: Option, 25 | /// Pass api secret in args instead of creds file 26 | #[arg(long)] 27 | pub secret: Option, 28 | 29 | /// Always continue on user confirmation prompts. 30 | #[arg(long, short, default_value_t = false)] 31 | pub yes: bool, 32 | } 33 | 34 | fn default_creds() -> String { 35 | let home = 36 | std::env::var("HOME").unwrap_or_else(|_| String::from("/root")); 37 | format!("{home}/.config/komodo/creds.toml") 38 | } 39 | 40 | #[derive(Debug, Clone, Subcommand)] 41 | pub enum Command { 42 | /// Runs an execution 43 | Execute { 44 | #[command(subcommand)] 45 | execution: Execution, 46 | }, 47 | // Room for more 48 | } 49 | 50 | #[derive(Debug, Deserialize)] 51 | pub struct CredsFile { 52 | pub url: String, 53 | pub key: String, 54 | pub secret: String, 55 | } 56 | -------------------------------------------------------------------------------- /bin/cli/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | 3 | use anyhow::Context; 4 | use colored::Colorize; 5 | 6 | pub fn wait_for_enter(press_enter_to: &str) -> anyhow::Result<()> { 7 | println!( 8 | "\nPress {} to {}\n", 9 | "ENTER".green(), 10 | press_enter_to.bold() 11 | ); 12 | let buffer = &mut [0u8]; 13 | std::io::stdin() 14 | .read_exact(buffer) 15 | .context("failed to read ENTER")?; 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /bin/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate tracing; 3 | 4 | use colored::Colorize; 5 | use komodo_client::api::read::GetVersion; 6 | 7 | mod args; 8 | mod exec; 9 | mod helpers; 10 | mod state; 11 | 12 | #[tokio::main] 13 | async fn main() -> anyhow::Result<()> { 14 | tracing_subscriber::fmt().with_target(false).init(); 15 | 16 | info!( 17 | "Komodo CLI version: {}", 18 | env!("CARGO_PKG_VERSION").blue().bold() 19 | ); 20 | 21 | let version = 22 | state::komodo_client().read(GetVersion {}).await?.version; 23 | info!("Komodo Core version: {}", version.blue().bold()); 24 | 25 | match &state::cli_args().command { 26 | args::Command::Execute { execution } => { 27 | exec::run(execution.to_owned()).await? 28 | } 29 | } 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /bin/cli/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use clap::Parser; 4 | use komodo_client::KomodoClient; 5 | use merge_config_files::parse_config_file; 6 | 7 | pub fn cli_args() -> &'static crate::args::CliArgs { 8 | static CLI_ARGS: OnceLock = OnceLock::new(); 9 | CLI_ARGS.get_or_init(crate::args::CliArgs::parse) 10 | } 11 | 12 | pub fn komodo_client() -> &'static KomodoClient { 13 | static KOMODO_CLIENT: OnceLock = OnceLock::new(); 14 | KOMODO_CLIENT.get_or_init(|| { 15 | let args = cli_args(); 16 | let crate::args::CredsFile { url, key, secret } = 17 | match (&args.url, &args.key, &args.secret) { 18 | (Some(url), Some(key), Some(secret)) => { 19 | crate::args::CredsFile { 20 | url: url.clone(), 21 | key: key.clone(), 22 | secret: secret.clone(), 23 | } 24 | } 25 | (url, key, secret) => { 26 | let mut creds: crate::args::CredsFile = 27 | parse_config_file(cli_args().creds.as_str()) 28 | .expect("failed to parse Komodo credentials"); 29 | 30 | if let Some(url) = url { 31 | creds.url.clone_from(url); 32 | } 33 | if let Some(key) = key { 34 | creds.key.clone_from(key); 35 | } 36 | if let Some(secret) = secret { 37 | creds.secret.clone_from(secret); 38 | } 39 | 40 | creds 41 | } 42 | }; 43 | futures::executor::block_on( 44 | KomodoClient::new(url, key, secret).with_healthcheck(), 45 | ) 46 | .expect("failed to initialize Komodo client") 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /bin/core/aio.Dockerfile: -------------------------------------------------------------------------------- 1 | ## All in one, multi stage compile + runtime Docker build for your architecture. 2 | 3 | # Build Core 4 | FROM rust:1.87.0-bullseye AS core-builder 5 | 6 | WORKDIR /builder 7 | COPY Cargo.toml Cargo.lock ./ 8 | COPY ./lib ./lib 9 | COPY ./client/core/rs ./client/core/rs 10 | COPY ./client/periphery ./client/periphery 11 | COPY ./bin/core ./bin/core 12 | 13 | # Compile app 14 | RUN cargo build -p komodo_core --release 15 | 16 | # Build Frontend 17 | FROM node:20.12-alpine AS frontend-builder 18 | WORKDIR /builder 19 | COPY ./frontend ./frontend 20 | COPY ./client/core/ts ./client 21 | RUN cd client && yarn && yarn build && yarn link 22 | RUN cd frontend && yarn link komodo_client && yarn && yarn build 23 | 24 | # Final Image 25 | FROM debian:bullseye-slim 26 | 27 | COPY ./bin/core/starship.toml /config/starship.toml 28 | COPY ./bin/core/debian-deps.sh . 29 | RUN sh ./debian-deps.sh && rm ./debian-deps.sh 30 | 31 | # Setup an application directory 32 | WORKDIR /app 33 | 34 | # Copy 35 | COPY ./config/core.config.toml /config/config.toml 36 | COPY --from=frontend-builder /builder/frontend/dist /app/frontend 37 | COPY --from=core-builder /builder/target/release/core /usr/local/bin/core 38 | COPY --from=denoland/deno:bin /deno /usr/local/bin/deno 39 | 40 | # Set $DENO_DIR and preload external Deno deps 41 | ENV DENO_DIR=/action-cache/deno 42 | RUN mkdir /action-cache && \ 43 | cd /action-cache && \ 44 | deno install jsr:@std/yaml jsr:@std/toml 45 | 46 | # Hint at the port 47 | EXPOSE 9120 48 | 49 | # Label for Ghcr 50 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 51 | LABEL org.opencontainers.image.description="Komodo Core" 52 | LABEL org.opencontainers.image.licenses=GPL-3.0 53 | 54 | ENTRYPOINT [ "core" ] 55 | -------------------------------------------------------------------------------- /bin/core/debian-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Core deps installer 4 | 5 | apt-get update 6 | apt-get install -y git curl ca-certificates 7 | 8 | rm -rf /var/lib/apt/lists/* 9 | 10 | # Starship prompt 11 | curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin 12 | echo 'export STARSHIP_CONFIG=/config/starship.toml' >> /root/.bashrc 13 | echo 'eval "$(starship init bash)"' >> /root/.bashrc 14 | 15 | -------------------------------------------------------------------------------- /bin/core/multi-arch.Dockerfile: -------------------------------------------------------------------------------- 1 | ## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile). 2 | ## Sets up the necessary runtime container dependencies for Komodo Core. 3 | ## Since theres no heavy build here, QEMU multi-arch builds are fine for this image. 4 | 5 | ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest 6 | ARG FRONTEND_IMAGE=ghcr.io/moghtech/komodo-frontend:latest 7 | ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64 8 | ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64 9 | 10 | # This is required to work with COPY --from 11 | FROM ${X86_64_BINARIES} AS x86_64 12 | FROM ${AARCH64_BINARIES} AS aarch64 13 | FROM ${FRONTEND_IMAGE} AS frontend 14 | 15 | # Final Image 16 | FROM debian:bullseye-slim 17 | 18 | COPY ./bin/core/starship.toml /config/starship.toml 19 | COPY ./bin/core/debian-deps.sh . 20 | RUN sh ./debian-deps.sh && rm ./debian-deps.sh 21 | 22 | WORKDIR /app 23 | 24 | # Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM. 25 | COPY --from=x86_64 /core /app/arch/linux/amd64 26 | COPY --from=aarch64 /core /app/arch/linux/arm64 27 | ARG TARGETPLATFORM 28 | RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/core && rm -r /app/arch 29 | 30 | # Copy default config / static frontend / deno binary 31 | COPY ./config/core.config.toml /config/config.toml 32 | COPY --from=frontend /frontend /app/frontend 33 | COPY --from=denoland/deno:bin /deno /usr/local/bin/deno 34 | 35 | # Set $DENO_DIR and preload external Deno deps 36 | ENV DENO_DIR=/action-cache/deno 37 | RUN mkdir /action-cache && \ 38 | cd /action-cache && \ 39 | deno install jsr:@std/yaml jsr:@std/toml 40 | 41 | # Hint at the port 42 | EXPOSE 9120 43 | 44 | # Label for Ghcr 45 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 46 | LABEL org.opencontainers.image.description="Komodo Core" 47 | LABEL org.opencontainers.image.licenses=GPL-3.0 48 | 49 | CMD [ "core" ] -------------------------------------------------------------------------------- /bin/core/single-arch.Dockerfile: -------------------------------------------------------------------------------- 1 | ## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile). 2 | ## Sets up the necessary runtime container dependencies for Komodo Core. 3 | 4 | ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest 5 | 6 | # This is required to work with COPY --from 7 | FROM ${BINARIES_IMAGE} AS binaries 8 | 9 | # Build Frontend 10 | FROM node:20.12-alpine AS frontend-builder 11 | WORKDIR /builder 12 | COPY ./frontend ./frontend 13 | COPY ./client/core/ts ./client 14 | RUN cd client && yarn && yarn build && yarn link 15 | RUN cd frontend && yarn link komodo_client && yarn && yarn build 16 | 17 | FROM debian:bullseye-slim 18 | 19 | COPY ./bin/core/starship.toml /config/starship.toml 20 | COPY ./bin/core/debian-deps.sh . 21 | RUN sh ./debian-deps.sh && rm ./debian-deps.sh 22 | 23 | # Copy 24 | COPY ./config/core.config.toml /config/config.toml 25 | COPY --from=frontend-builder /builder/frontend/dist /app/frontend 26 | COPY --from=binaries /core /usr/local/bin/core 27 | COPY --from=denoland/deno:bin /deno /usr/local/bin/deno 28 | 29 | # Set $DENO_DIR and preload external Deno deps 30 | ENV DENO_DIR=/action-cache/deno 31 | RUN mkdir /action-cache && \ 32 | cd /action-cache && \ 33 | deno install jsr:@std/yaml jsr:@std/toml 34 | 35 | # Hint at the port 36 | EXPOSE 9120 37 | 38 | # Label for Ghcr 39 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 40 | LABEL org.opencontainers.image.description="Komodo Core" 41 | LABEL org.opencontainers.image.licenses=GPL-3.0 42 | 43 | CMD [ "core" ] -------------------------------------------------------------------------------- /bin/core/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod execute; 3 | pub mod read; 4 | pub mod terminal; 5 | pub mod user; 6 | pub mod write; 7 | 8 | #[derive(serde::Deserialize)] 9 | struct Variant { 10 | variant: String, 11 | } 12 | -------------------------------------------------------------------------------- /bin/core/src/api/read/tag.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use komodo_client::{ 3 | api::read::{GetTag, ListTags}, 4 | entities::tag::Tag, 5 | }; 6 | use mongo_indexed::doc; 7 | use mungos::{find::find_collect, mongodb::options::FindOptions}; 8 | use resolver_api::Resolve; 9 | 10 | use crate::{helpers::query::get_tag, state::db_client}; 11 | 12 | use super::ReadArgs; 13 | 14 | impl Resolve for GetTag { 15 | async fn resolve(self, _: &ReadArgs) -> serror::Result { 16 | Ok(get_tag(&self.tag).await?) 17 | } 18 | } 19 | 20 | impl Resolve for ListTags { 21 | async fn resolve(self, _: &ReadArgs) -> serror::Result> { 22 | let res = find_collect( 23 | &db_client().tags, 24 | self.query, 25 | FindOptions::builder().sort(doc! { "name": 1 }).build(), 26 | ) 27 | .await 28 | .context("failed to get tags from db")?; 29 | Ok(res) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bin/core/src/api/read/user_group.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use anyhow::Context; 4 | use komodo_client::api::read::*; 5 | use mungos::{ 6 | find::find_collect, 7 | mongodb::{ 8 | bson::{Document, doc, oid::ObjectId}, 9 | options::FindOptions, 10 | }, 11 | }; 12 | use resolver_api::Resolve; 13 | 14 | use crate::state::db_client; 15 | 16 | use super::ReadArgs; 17 | 18 | impl Resolve for GetUserGroup { 19 | async fn resolve( 20 | self, 21 | ReadArgs { user }: &ReadArgs, 22 | ) -> serror::Result { 23 | let mut filter = match ObjectId::from_str(&self.user_group) { 24 | Ok(id) => doc! { "_id": id }, 25 | Err(_) => doc! { "name": &self.user_group }, 26 | }; 27 | // Don't allow non admin users to get UserGroups they aren't a part of. 28 | if !user.admin { 29 | // Filter for only UserGroups which contain the users id 30 | filter.insert("users", &user.id); 31 | } 32 | let res = db_client() 33 | .user_groups 34 | .find_one(filter) 35 | .await 36 | .context("failed to query db for user groups")? 37 | .context("no UserGroup found with given name or id")?; 38 | Ok(res) 39 | } 40 | } 41 | 42 | impl Resolve for ListUserGroups { 43 | async fn resolve( 44 | self, 45 | ReadArgs { user }: &ReadArgs, 46 | ) -> serror::Result { 47 | let mut filter = Document::new(); 48 | if !user.admin { 49 | filter.insert("users", &user.id); 50 | } 51 | let res = find_collect( 52 | &db_client().user_groups, 53 | filter, 54 | FindOptions::builder().sort(doc! { "name": 1 }).build(), 55 | ) 56 | .await 57 | .context("failed to query db for UserGroups")?; 58 | Ok(res) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /bin/core/src/api/read/variable.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use komodo_client::api::read::*; 3 | use mongo_indexed::doc; 4 | use mungos::{find::find_collect, mongodb::options::FindOptions}; 5 | use resolver_api::Resolve; 6 | 7 | use crate::{helpers::query::get_variable, state::db_client}; 8 | 9 | use super::ReadArgs; 10 | 11 | impl Resolve for GetVariable { 12 | async fn resolve( 13 | self, 14 | ReadArgs { user }: &ReadArgs, 15 | ) -> serror::Result { 16 | let mut variable = get_variable(&self.name).await?; 17 | if !variable.is_secret || user.admin { 18 | return Ok(variable); 19 | } 20 | variable.value = "#".repeat(variable.value.len()); 21 | Ok(variable) 22 | } 23 | } 24 | 25 | impl Resolve for ListVariables { 26 | async fn resolve( 27 | self, 28 | ReadArgs { user }: &ReadArgs, 29 | ) -> serror::Result { 30 | let variables = find_collect( 31 | &db_client().variables, 32 | None, 33 | FindOptions::builder().sort(doc! { "name": 1 }).build(), 34 | ) 35 | .await 36 | .context("failed to query db for variables")?; 37 | if user.admin { 38 | return Ok(variables); 39 | } 40 | let variables = variables 41 | .into_iter() 42 | .map(|mut variable| { 43 | if variable.is_secret { 44 | variable.value = "#".repeat(variable.value.len()); 45 | } 46 | variable 47 | }) 48 | .collect(); 49 | Ok(variables) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /bin/core/src/api/terminal.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use axum::{Extension, Router, middleware, routing::post}; 3 | use komodo_client::{ 4 | api::terminal::ExecuteTerminalBody, 5 | entities::{ 6 | permission::PermissionLevel, server::Server, user::User, 7 | }, 8 | }; 9 | use serror::Json; 10 | use uuid::Uuid; 11 | 12 | use crate::{ 13 | auth::auth_request, helpers::periphery_client, 14 | permission::get_check_permissions, 15 | }; 16 | 17 | pub fn router() -> Router { 18 | Router::new() 19 | .route("/execute", post(execute)) 20 | .layer(middleware::from_fn(auth_request)) 21 | } 22 | 23 | async fn execute( 24 | Extension(user): Extension, 25 | Json(request): Json, 26 | ) -> serror::Result { 27 | execute_inner(Uuid::new_v4(), request, user).await 28 | } 29 | 30 | #[instrument( 31 | name = "ExecuteTerminal", 32 | skip(user), 33 | fields( 34 | user_id = user.id, 35 | ) 36 | )] 37 | async fn execute_inner( 38 | req_id: Uuid, 39 | ExecuteTerminalBody { 40 | server, 41 | terminal, 42 | command, 43 | }: ExecuteTerminalBody, 44 | user: User, 45 | ) -> serror::Result { 46 | info!("/terminal request | user: {}", user.username); 47 | 48 | let res = async { 49 | let server = get_check_permissions::( 50 | &server, 51 | &user, 52 | PermissionLevel::Read.terminal(), 53 | ) 54 | .await?; 55 | 56 | let periphery = periphery_client(&server)?; 57 | 58 | let stream = periphery 59 | .execute_terminal(terminal, command) 60 | .await 61 | .context("Failed to execute command on periphery")?; 62 | 63 | anyhow::Ok(stream) 64 | } 65 | .await; 66 | 67 | let stream = match res { 68 | Ok(stream) => stream, 69 | Err(e) => { 70 | warn!("/terminal request {req_id} error: {e:#}"); 71 | return Err(e.into()); 72 | } 73 | }; 74 | 75 | Ok(axum::body::Body::from_stream(stream.into_line_stream())) 76 | } 77 | -------------------------------------------------------------------------------- /bin/core/src/api/write/action.rs: -------------------------------------------------------------------------------- 1 | use komodo_client::{ 2 | api::write::*, 3 | entities::{ 4 | action::Action, permission::PermissionLevel, update::Update, 5 | }, 6 | }; 7 | use resolver_api::Resolve; 8 | 9 | use crate::{permission::get_check_permissions, resource}; 10 | 11 | use super::WriteArgs; 12 | 13 | impl Resolve for CreateAction { 14 | #[instrument(name = "CreateAction", skip(user))] 15 | async fn resolve( 16 | self, 17 | WriteArgs { user }: &WriteArgs, 18 | ) -> serror::Result { 19 | Ok( 20 | resource::create::(&self.name, self.config, user) 21 | .await?, 22 | ) 23 | } 24 | } 25 | 26 | impl Resolve for CopyAction { 27 | #[instrument(name = "CopyAction", skip(user))] 28 | async fn resolve( 29 | self, 30 | WriteArgs { user }: &WriteArgs, 31 | ) -> serror::Result { 32 | let Action { config, .. } = get_check_permissions::( 33 | &self.id, 34 | user, 35 | PermissionLevel::Write.into(), 36 | ) 37 | .await?; 38 | Ok( 39 | resource::create::(&self.name, config.into(), user) 40 | .await?, 41 | ) 42 | } 43 | } 44 | 45 | impl Resolve for UpdateAction { 46 | #[instrument(name = "UpdateAction", skip(user))] 47 | async fn resolve( 48 | self, 49 | WriteArgs { user }: &WriteArgs, 50 | ) -> serror::Result { 51 | Ok(resource::update::(&self.id, self.config, user).await?) 52 | } 53 | } 54 | 55 | impl Resolve for RenameAction { 56 | #[instrument(name = "RenameAction", skip(user))] 57 | async fn resolve( 58 | self, 59 | WriteArgs { user }: &WriteArgs, 60 | ) -> serror::Result { 61 | Ok(resource::rename::(&self.id, &self.name, user).await?) 62 | } 63 | } 64 | 65 | impl Resolve for DeleteAction { 66 | #[instrument(name = "DeleteAction", skip(args))] 67 | async fn resolve(self, args: &WriteArgs) -> serror::Result { 68 | Ok(resource::delete::(&self.id, args).await?) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /bin/core/src/cloud/aws/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ec2; 2 | -------------------------------------------------------------------------------- /bin/core/src/cloud/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod aws; 2 | 3 | #[derive(Debug)] 4 | pub enum BuildCleanupData { 5 | /// Nothing to clean up 6 | Server, 7 | /// Clean up AWS instance 8 | Aws { instance_id: String, region: String }, 9 | } 10 | -------------------------------------------------------------------------------- /bin/core/src/helpers/channel.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use komodo_client::entities::update::{Update, UpdateListItem}; 4 | use tokio::sync::{Mutex, broadcast}; 5 | 6 | /// A channel sending (build_id, update_id) 7 | pub fn build_cancel_channel() 8 | -> &'static BroadcastChannel<(String, Update)> { 9 | static BUILD_CANCEL_CHANNEL: OnceLock< 10 | BroadcastChannel<(String, Update)>, 11 | > = OnceLock::new(); 12 | BUILD_CANCEL_CHANNEL.get_or_init(|| BroadcastChannel::new(100)) 13 | } 14 | 15 | /// A channel sending (repo_id, update_id) 16 | pub fn repo_cancel_channel() 17 | -> &'static BroadcastChannel<(String, Update)> { 18 | static REPO_CANCEL_CHANNEL: OnceLock< 19 | BroadcastChannel<(String, Update)>, 20 | > = OnceLock::new(); 21 | REPO_CANCEL_CHANNEL.get_or_init(|| BroadcastChannel::new(100)) 22 | } 23 | 24 | pub fn update_channel() -> &'static BroadcastChannel { 25 | static UPDATE_CHANNEL: OnceLock> = 26 | OnceLock::new(); 27 | UPDATE_CHANNEL.get_or_init(|| BroadcastChannel::new(100)) 28 | } 29 | 30 | pub struct BroadcastChannel { 31 | pub sender: Mutex>, 32 | pub receiver: broadcast::Receiver, 33 | } 34 | 35 | impl BroadcastChannel { 36 | pub fn new(capacity: usize) -> BroadcastChannel { 37 | let (sender, receiver) = broadcast::channel(capacity); 38 | BroadcastChannel { 39 | sender: sender.into(), 40 | receiver, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /bin/core/src/helpers/matcher.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | 3 | pub enum Matcher<'a> { 4 | Wildcard(wildcard::Wildcard<'a>), 5 | Regex(regex::Regex), 6 | } 7 | 8 | impl<'a> Matcher<'a> { 9 | pub fn new(pattern: &'a str) -> anyhow::Result { 10 | if pattern.starts_with('\\') && pattern.ends_with('\\') { 11 | let inner = &pattern[1..(pattern.len() - 1)]; 12 | let regex = regex::Regex::new(inner) 13 | .with_context(|| format!("invalid regex. got: {inner}"))?; 14 | Ok(Self::Regex(regex)) 15 | } else { 16 | let wildcard = wildcard::Wildcard::new(pattern.as_bytes()) 17 | .with_context(|| { 18 | format!("invalid wildcard. got: {pattern}") 19 | })?; 20 | Ok(Self::Wildcard(wildcard)) 21 | } 22 | } 23 | 24 | pub fn is_match(&self, source: &str) -> bool { 25 | match self { 26 | Matcher::Wildcard(wildcard) => { 27 | wildcard.is_match(source.as_bytes()) 28 | } 29 | Matcher::Regex(regex) => regex.is_match(source), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bin/core/src/listener/integrations/gitlab.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, anyhow}; 2 | use serde::Deserialize; 3 | 4 | use crate::{ 5 | config::core_config, 6 | listener::{VerifyBranch, VerifySecret}, 7 | }; 8 | 9 | /// Listener implementation for Gitlab type API 10 | pub struct Gitlab; 11 | 12 | impl VerifySecret for Gitlab { 13 | #[instrument("VerifyGitlabSecret", skip_all)] 14 | fn verify_secret( 15 | headers: axum::http::HeaderMap, 16 | _body: &str, 17 | custom_secret: &str, 18 | ) -> anyhow::Result<()> { 19 | let token = headers 20 | .get("x-gitlab-token") 21 | .context("No gitlab token in headers")?; 22 | let token = 23 | token.to_str().context("Failed to get token as string")?; 24 | let secret = if custom_secret.is_empty() { 25 | core_config().webhook_secret.as_str() 26 | } else { 27 | custom_secret 28 | }; 29 | if token == secret { 30 | Ok(()) 31 | } else { 32 | Err(anyhow!("Webhook secret does not match expected.")) 33 | } 34 | } 35 | } 36 | 37 | #[derive(Deserialize)] 38 | struct GitlabWebhookBody { 39 | #[serde(rename = "ref")] 40 | branch: String, 41 | } 42 | 43 | impl VerifyBranch for Gitlab { 44 | fn verify_branch( 45 | body: &str, 46 | expected_branch: &str, 47 | ) -> anyhow::Result<()> { 48 | let branch = serde_json::from_str::(body) 49 | .context("Failed to parse gitlab request body")? 50 | .branch 51 | .replace("refs/heads/", ""); 52 | if branch == expected_branch { 53 | Ok(()) 54 | } else { 55 | Err(anyhow!("request branch does not match expected")) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bin/core/src/listener/integrations/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod github; 2 | pub mod gitlab; 3 | -------------------------------------------------------------------------------- /bin/core/src/listener/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use axum::{Router, http::HeaderMap}; 4 | use komodo_client::entities::resource::Resource; 5 | use tokio::sync::Mutex; 6 | 7 | use crate::{helpers::cache::Cache, resource::KomodoResource}; 8 | 9 | mod integrations; 10 | mod resources; 11 | mod router; 12 | 13 | use integrations::*; 14 | 15 | pub fn router() -> Router { 16 | Router::new() 17 | .nest("/github", router::router::()) 18 | .nest("/gitlab", router::router::()) 19 | } 20 | 21 | type ListenerLockCache = Cache>>; 22 | 23 | /// Implemented for all resources which can recieve webhook. 24 | trait CustomSecret: KomodoResource { 25 | fn custom_secret( 26 | resource: &Resource, 27 | ) -> &str; 28 | } 29 | 30 | /// Implemented on the integration struct, eg [integrations::github::Github] 31 | trait VerifySecret { 32 | fn verify_secret( 33 | headers: HeaderMap, 34 | body: &str, 35 | custom_secret: &str, 36 | ) -> anyhow::Result<()>; 37 | } 38 | 39 | /// Implemented on the integration struct, eg [integrations::github::Github] 40 | trait VerifyBranch { 41 | /// Returns Err if the branch extracted from request 42 | /// body does not match the expected branch. 43 | fn verify_branch( 44 | body: &str, 45 | expected_branch: &str, 46 | ) -> anyhow::Result<()>; 47 | } 48 | 49 | /// For Procedures and Actions, incoming webhook 50 | /// can be triggered by any branch by using `__ANY__` 51 | /// as the branch in the webhook URL. 52 | const ANY_BRANCH: &str = "__ANY__"; 53 | -------------------------------------------------------------------------------- /bin/core/src/monitor/alert/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::Context; 4 | use komodo_client::entities::{ 5 | permission::PermissionLevel, resource::ResourceQuery, 6 | server::Server, user::User, 7 | }; 8 | 9 | use crate::resource; 10 | 11 | mod deployment; 12 | mod server; 13 | mod stack; 14 | 15 | // called after cache update 16 | #[instrument(level = "debug")] 17 | pub async fn check_alerts(ts: i64) { 18 | let (servers, server_names) = match get_all_servers_map().await { 19 | Ok(res) => res, 20 | Err(e) => { 21 | error!("{e:#?}"); 22 | return; 23 | } 24 | }; 25 | 26 | tokio::join!( 27 | server::alert_servers(ts, servers), 28 | deployment::alert_deployments(ts, &server_names), 29 | stack::alert_stacks(ts, &server_names) 30 | ); 31 | } 32 | 33 | #[instrument(level = "debug")] 34 | async fn get_all_servers_map() 35 | -> anyhow::Result<(HashMap, HashMap)> 36 | { 37 | let servers = resource::list_full_for_user::( 38 | ResourceQuery::default(), 39 | &User { 40 | admin: true, 41 | ..Default::default() 42 | }, 43 | PermissionLevel::Read.into(), 44 | &[], 45 | ) 46 | .await 47 | .context("failed to get servers from db (in alert_servers)")?; 48 | 49 | let servers = servers 50 | .into_iter() 51 | .map(|server| (server.id.clone(), server)) 52 | .collect::>(); 53 | 54 | let server_names = servers 55 | .iter() 56 | .map(|(id, server)| (id.clone(), server.name.clone())) 57 | .collect::>(); 58 | 59 | Ok((servers, server_names)) 60 | } 61 | -------------------------------------------------------------------------------- /bin/core/src/monitor/lists.rs: -------------------------------------------------------------------------------- 1 | use komodo_client::entities::{ 2 | docker::{ 3 | container::ContainerListItem, image::ImageListItem, 4 | network::NetworkListItem, volume::VolumeListItem, 5 | }, 6 | stack::ComposeProject, 7 | }; 8 | use periphery_client::{ 9 | PeripheryClient, 10 | api::{GetDockerLists, GetDockerListsResponse}, 11 | }; 12 | 13 | pub async fn get_docker_lists( 14 | periphery: &PeripheryClient, 15 | ) -> anyhow::Result<( 16 | Vec, 17 | Vec, 18 | Vec, 19 | Vec, 20 | Vec, 21 | )> { 22 | let GetDockerListsResponse { 23 | containers, 24 | networks, 25 | images, 26 | volumes, 27 | projects, 28 | } = periphery.request(GetDockerLists {}).await?; 29 | // TODO: handle the errors 30 | let ( 31 | mut containers, 32 | mut networks, 33 | mut images, 34 | mut volumes, 35 | mut projects, 36 | ) = ( 37 | containers.unwrap_or_default(), 38 | networks.unwrap_or_default(), 39 | images.unwrap_or_default(), 40 | volumes.unwrap_or_default(), 41 | projects.unwrap_or_default(), 42 | ); 43 | 44 | containers.sort_by(|a, b| a.name.cmp(&b.name)); 45 | networks.sort_by(|a, b| a.name.cmp(&b.name)); 46 | images.sort_by(|a, b| a.name.cmp(&b.name)); 47 | volumes.sort_by(|a, b| a.name.cmp(&b.name)); 48 | projects.sort_by(|a, b| a.name.cmp(&b.name)); 49 | 50 | Ok((containers, networks, images, volumes, projects)) 51 | } 52 | -------------------------------------------------------------------------------- /bin/core/src/monitor/record.rs: -------------------------------------------------------------------------------- 1 | use komodo_client::entities::stats::{ 2 | SystemStatsRecord, TotalDiskUsage, sum_disk_usage, 3 | }; 4 | 5 | use crate::state::{db_client, server_status_cache}; 6 | 7 | #[instrument(level = "debug")] 8 | pub async fn record_server_stats(ts: i64) { 9 | let status = server_status_cache().get_list().await; 10 | let records = status 11 | .into_iter() 12 | .filter_map(|status| { 13 | let stats = status.stats.as_ref()?; 14 | 15 | let TotalDiskUsage { 16 | used_gb: disk_used_gb, 17 | total_gb: disk_total_gb, 18 | } = sum_disk_usage(&stats.disks); 19 | 20 | Some(SystemStatsRecord { 21 | ts, 22 | sid: status.id.clone(), 23 | cpu_perc: stats.cpu_perc, 24 | mem_total_gb: stats.mem_total_gb, 25 | mem_used_gb: stats.mem_used_gb, 26 | disk_total_gb, 27 | disk_used_gb, 28 | disks: stats.disks.clone(), 29 | network_ingress_bytes: stats.network_ingress_bytes, 30 | network_egress_bytes: stats.network_egress_bytes, 31 | }) 32 | }) 33 | .collect::>(); 34 | if !records.is_empty() { 35 | let res = db_client().stats.insert_many(records).await; 36 | if let Err(e) = res { 37 | error!("failed to record server stats | {e:#}"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bin/core/src/stack/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, anyhow}; 2 | use komodo_client::entities::{ 3 | permission::PermissionLevelAndSpecifics, 4 | server::{Server, ServerState}, 5 | stack::Stack, 6 | user::User, 7 | }; 8 | use regex::Regex; 9 | 10 | use crate::{ 11 | helpers::query::get_server_with_state, 12 | permission::get_check_permissions, 13 | }; 14 | 15 | pub mod execute; 16 | pub mod remote; 17 | pub mod services; 18 | 19 | pub async fn get_stack_and_server( 20 | stack: &str, 21 | user: &User, 22 | permissions: PermissionLevelAndSpecifics, 23 | block_if_server_unreachable: bool, 24 | ) -> anyhow::Result<(Stack, Server)> { 25 | let stack = 26 | get_check_permissions::(stack, user, permissions).await?; 27 | 28 | if stack.config.server_id.is_empty() { 29 | return Err(anyhow!("Stack has no server configured")); 30 | } 31 | 32 | let (server, status) = 33 | get_server_with_state(&stack.config.server_id).await?; 34 | if block_if_server_unreachable && status != ServerState::Ok { 35 | return Err(anyhow!( 36 | "cannot send action when server is unreachable or disabled" 37 | )); 38 | } 39 | 40 | Ok((stack, server)) 41 | } 42 | 43 | pub fn compose_container_match_regex( 44 | container_name: &str, 45 | ) -> anyhow::Result { 46 | let regex = format!("^{container_name}-?[0-9]*$"); 47 | Regex::new(®ex).with_context(|| { 48 | format!("failed to construct valid regex from {regex}") 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /bin/core/src/ts_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, anyhow}; 2 | use axum::{ 3 | Router, 4 | extract::Path, 5 | http::{HeaderMap, HeaderValue}, 6 | routing::get, 7 | }; 8 | use reqwest::StatusCode; 9 | use serde::Deserialize; 10 | use serror::AddStatusCodeError; 11 | use tokio::fs; 12 | 13 | use crate::config::core_config; 14 | 15 | pub fn router() -> Router { 16 | Router::new().route("/{path}", get(serve_client_file)) 17 | } 18 | 19 | const ALLOWED_FILES: &[&str] = &[ 20 | "lib.js", 21 | "lib.d.ts", 22 | "types.js", 23 | "types.d.ts", 24 | "responses.js", 25 | "responses.d.ts", 26 | ]; 27 | 28 | #[derive(Deserialize)] 29 | struct FilePath { 30 | path: String, 31 | } 32 | 33 | #[axum::debug_handler] 34 | async fn serve_client_file( 35 | Path(FilePath { path }): Path, 36 | ) -> serror::Result<(HeaderMap, String)> { 37 | if !ALLOWED_FILES.contains(&path.as_str()) { 38 | return Err( 39 | anyhow!("File {path} not found.") 40 | .status_code(StatusCode::NOT_FOUND), 41 | ); 42 | } 43 | 44 | let contents = fs::read_to_string(format!( 45 | "{}/client/{path}", 46 | core_config().frontend_path 47 | )) 48 | .await 49 | .with_context(|| format!("Failed to read file: {path}"))?; 50 | 51 | let mut headers = HeaderMap::new(); 52 | 53 | if path.ends_with(".js") { 54 | headers.insert( 55 | "X-TypeScript-Types", 56 | HeaderValue::from_str(&format!( 57 | "/client/{}", 58 | path.replace(".js", ".d.ts") 59 | )) 60 | .context("?? Invalid Header Value")?, 61 | ); 62 | } 63 | 64 | Ok((headers, contents)) 65 | } 66 | -------------------------------------------------------------------------------- /bin/core/src/ws/container.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::{Query, WebSocketUpgrade, ws::Message}, 3 | response::IntoResponse, 4 | }; 5 | use futures::SinkExt; 6 | use komodo_client::{ 7 | api::terminal::ConnectContainerExecQuery, 8 | entities::{permission::PermissionLevel, server::Server}, 9 | }; 10 | 11 | use crate::permission::get_check_permissions; 12 | 13 | #[instrument(name = "ConnectContainerExec", skip(ws))] 14 | pub async fn terminal( 15 | Query(ConnectContainerExecQuery { 16 | server, 17 | container, 18 | shell, 19 | }): Query, 20 | ws: WebSocketUpgrade, 21 | ) -> impl IntoResponse { 22 | ws.on_upgrade(|socket| async move { 23 | let Some((mut client_socket, user)) = 24 | super::ws_login(socket).await 25 | else { 26 | return; 27 | }; 28 | 29 | let server = match get_check_permissions::( 30 | &server, 31 | &user, 32 | PermissionLevel::Read.terminal(), 33 | ) 34 | .await 35 | { 36 | Ok(server) => server, 37 | Err(e) => { 38 | debug!("could not get server | {e:#}"); 39 | let _ = client_socket 40 | .send(Message::text(format!("ERROR: {e:#}"))) 41 | .await; 42 | let _ = client_socket.close().await; 43 | return; 44 | } 45 | }; 46 | 47 | super::handle_container_terminal( 48 | client_socket, 49 | &server, 50 | container, 51 | shell, 52 | ) 53 | .await 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /bin/core/src/ws/deployment.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::{Query, WebSocketUpgrade, ws::Message}, 3 | response::IntoResponse, 4 | }; 5 | use futures::SinkExt; 6 | use komodo_client::{ 7 | api::terminal::ConnectDeploymentExecQuery, 8 | entities::{ 9 | deployment::Deployment, permission::PermissionLevel, 10 | server::Server, 11 | }, 12 | }; 13 | 14 | use crate::{permission::get_check_permissions, resource::get}; 15 | 16 | #[instrument(name = "ConnectDeploymentExec", skip(ws))] 17 | pub async fn terminal( 18 | Query(ConnectDeploymentExecQuery { deployment, shell }): Query< 19 | ConnectDeploymentExecQuery, 20 | >, 21 | ws: WebSocketUpgrade, 22 | ) -> impl IntoResponse { 23 | ws.on_upgrade(|socket| async move { 24 | let Some((mut client_socket, user)) = 25 | super::ws_login(socket).await 26 | else { 27 | return; 28 | }; 29 | 30 | let deployment = match get_check_permissions::( 31 | &deployment, 32 | &user, 33 | PermissionLevel::Read.terminal(), 34 | ) 35 | .await 36 | { 37 | Ok(deployment) => deployment, 38 | Err(e) => { 39 | debug!("could not get deployment | {e:#}"); 40 | let _ = client_socket 41 | .send(Message::text(format!("ERROR: {e:#}"))) 42 | .await; 43 | let _ = client_socket.close().await; 44 | return; 45 | } 46 | }; 47 | 48 | let server = 49 | match get::(&deployment.config.server_id).await { 50 | Ok(server) => server, 51 | Err(e) => { 52 | debug!("could not get server | {e:#}"); 53 | let _ = client_socket 54 | .send(Message::text(format!("ERROR: {e:#}"))) 55 | .await; 56 | let _ = client_socket.close().await; 57 | return; 58 | } 59 | }; 60 | 61 | super::handle_container_terminal( 62 | client_socket, 63 | &server, 64 | deployment.name, 65 | shell, 66 | ) 67 | .await 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /bin/core/starship.toml: -------------------------------------------------------------------------------- 1 | ## This is used to customize the shell prompt in Periphery container for Terminals 2 | 3 | "$schema" = 'https://starship.rs/config-schema.json' 4 | 5 | add_newline = true 6 | 7 | format = "$time$hostname$container$memory_usage$all" 8 | 9 | [character] 10 | success_symbol = "[❯](bright-blue bold)" 11 | error_symbol = "[❯](bright-red bold)" 12 | 13 | [package] 14 | disabled = true 15 | 16 | [time] 17 | format = "[❯$time](white dimmed) " 18 | time_format = "%l:%M %p" 19 | utc_time_offset = '-5' 20 | disabled = true 21 | 22 | [username] 23 | format = "[❯ $user]($style) " 24 | style_user = "bright-green" 25 | show_always = true 26 | 27 | [hostname] 28 | format = "[❯ $hostname]($style) " 29 | style = "bright-blue" 30 | ssh_only = false 31 | 32 | [directory] 33 | format = "[❯ $path]($style)[$read_only]($read_only_style) " 34 | style = "bright-cyan" 35 | 36 | [git_branch] 37 | format = "[❯ $symbol$branch(:$remote_branch)]($style) " 38 | style = "bright-purple" 39 | 40 | [git_status] 41 | style = "bright-purple" 42 | 43 | [rust] 44 | format = "[❯ $symbol($version )]($style)" 45 | symbol = "rustc " 46 | style = "bright-red" 47 | 48 | [nodejs] 49 | format = "[❯ $symbol($version )]($style)" 50 | symbol = "nodejs " 51 | style = "bright-red" 52 | 53 | [memory_usage] 54 | format = "[❯ mem ${ram} ${ram_pct}]($style) " 55 | threshold = -1 56 | style = "white" 57 | 58 | [cmd_duration] 59 | format = "[❯ $duration]($style)" 60 | style = "bright-yellow" 61 | 62 | [container] 63 | format = "[❯ 🦎 core container ]($style)" 64 | style = "bright-green" 65 | 66 | [aws] 67 | disabled = true 68 | -------------------------------------------------------------------------------- /bin/periphery/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "komodo_periphery" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | 10 | [[bin]] 11 | name = "periphery" 12 | path = "src/main.rs" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | # local 18 | komodo_client.workspace = true 19 | periphery_client.workspace = true 20 | environment_file.workspace = true 21 | formatting.workspace = true 22 | response.workspace = true 23 | command.workspace = true 24 | logger.workspace = true 25 | cache.workspace = true 26 | git.workspace = true 27 | # mogh 28 | serror = { workspace = true, features = ["axum"] } 29 | merge_config_files.workspace = true 30 | async_timing_util.workspace = true 31 | derive_variants.workspace = true 32 | resolver_api.workspace = true 33 | run_command.workspace = true 34 | svi.workspace = true 35 | # external 36 | pin-project-lite.workspace = true 37 | tokio-stream.workspace = true 38 | portable-pty.workspace = true 39 | axum-server.workspace = true 40 | serde_json.workspace = true 41 | serde_yaml.workspace = true 42 | tokio-util.workspace = true 43 | futures.workspace = true 44 | tracing.workspace = true 45 | bollard.workspace = true 46 | sysinfo.workspace = true 47 | dotenvy.workspace = true 48 | anyhow.workspace = true 49 | rustls.workspace = true 50 | tokio.workspace = true 51 | serde.workspace = true 52 | bytes.workspace = true 53 | axum.workspace = true 54 | clap.workspace = true 55 | envy.workspace = true 56 | uuid.workspace = true 57 | rand.workspace = true 58 | -------------------------------------------------------------------------------- /bin/periphery/aio.Dockerfile: -------------------------------------------------------------------------------- 1 | ## All in one, multi stage compile + runtime Docker build for your architecture. 2 | 3 | FROM rust:1.87.0-bullseye AS builder 4 | 5 | WORKDIR /builder 6 | COPY Cargo.toml Cargo.lock ./ 7 | COPY ./lib ./lib 8 | COPY ./client/core/rs ./client/core/rs 9 | COPY ./client/periphery ./client/periphery 10 | COPY ./bin/periphery ./bin/periphery 11 | 12 | # Compile app 13 | RUN cargo build -p komodo_periphery --release 14 | 15 | # Final Image 16 | FROM debian:bullseye-slim 17 | 18 | COPY ./bin/periphery/starship.toml /config/starship.toml 19 | COPY ./bin/periphery/debian-deps.sh . 20 | RUN sh ./debian-deps.sh && rm ./debian-deps.sh 21 | 22 | COPY --from=builder /builder/target/release/periphery /usr/local/bin/periphery 23 | 24 | EXPOSE 8120 25 | 26 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 27 | LABEL org.opencontainers.image.description="Komodo Periphery" 28 | LABEL org.opencontainers.image.licenses=GPL-3.0 29 | 30 | CMD [ "periphery" ] 31 | -------------------------------------------------------------------------------- /bin/periphery/debian-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## Periphery deps installer 4 | 5 | apt-get update 6 | apt-get install -y git curl wget ca-certificates 7 | 8 | install -m 0755 -d /etc/apt/keyrings 9 | curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc 10 | chmod a+r /etc/apt/keyrings/docker.asc 11 | 12 | echo \ 13 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ 14 | $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ 15 | tee /etc/apt/sources.list.d/docker.list > /dev/null 16 | 17 | apt-get update 18 | 19 | # apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 20 | apt-get install -y docker-ce-cli docker-buildx-plugin docker-compose-plugin 21 | 22 | rm -rf /var/lib/apt/lists/* 23 | 24 | # Starship prompt 25 | curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /usr/local/bin 26 | echo 'export STARSHIP_CONFIG=/config/starship.toml' >> /root/.bashrc 27 | echo 'eval "$(starship init bash)"' >> /root/.bashrc 28 | 29 | -------------------------------------------------------------------------------- /bin/periphery/multi-arch.Dockerfile: -------------------------------------------------------------------------------- 1 | ## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile). 2 | ## Sets up the necessary runtime container dependencies for Komodo Periphery. 3 | ## Since theres no heavy build here, QEMU multi-arch builds are fine for this image. 4 | 5 | ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest 6 | ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64 7 | ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64 8 | 9 | # This is required to work with COPY --from 10 | FROM ${X86_64_BINARIES} AS x86_64 11 | FROM ${AARCH64_BINARIES} AS aarch64 12 | 13 | FROM debian:bullseye-slim 14 | 15 | COPY ./bin/periphery/starship.toml /config/starship.toml 16 | COPY ./bin/periphery/debian-deps.sh . 17 | RUN sh ./debian-deps.sh && rm ./debian-deps.sh 18 | 19 | WORKDIR /app 20 | 21 | ## Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM. 22 | COPY --from=x86_64 /periphery /app/arch/linux/amd64 23 | COPY --from=aarch64 /periphery /app/arch/linux/arm64 24 | 25 | ARG TARGETPLATFORM 26 | RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/periphery && rm -r /app/arch 27 | 28 | EXPOSE 8120 29 | 30 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 31 | LABEL org.opencontainers.image.description="Komodo Periphery" 32 | LABEL org.opencontainers.image.licenses=GPL-3.0 33 | 34 | CMD [ "periphery" ] -------------------------------------------------------------------------------- /bin/periphery/single-arch.Dockerfile: -------------------------------------------------------------------------------- 1 | ## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile). 2 | ## Sets up the necessary runtime container dependencies for Komodo Periphery. 3 | 4 | ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest 5 | 6 | # This is required to work with COPY --from 7 | FROM ${BINARIES_IMAGE} AS binaries 8 | 9 | FROM debian:bullseye-slim 10 | 11 | COPY ./bin/periphery/starship.toml /config/starship.toml 12 | COPY ./bin/periphery/debian-deps.sh . 13 | RUN sh ./debian-deps.sh && rm ./debian-deps.sh 14 | 15 | COPY --from=binaries /periphery /usr/local/bin/periphery 16 | 17 | EXPOSE 8120 18 | 19 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 20 | LABEL org.opencontainers.image.description="Komodo Periphery" 21 | LABEL org.opencontainers.image.licenses=GPL-3.0 22 | 23 | CMD [ "periphery" ] -------------------------------------------------------------------------------- /bin/periphery/src/api/network.rs: -------------------------------------------------------------------------------- 1 | use command::run_komodo_command; 2 | use komodo_client::entities::{ 3 | docker::network::Network, update::Log, 4 | }; 5 | use periphery_client::api::network::*; 6 | use resolver_api::Resolve; 7 | 8 | use crate::docker::docker_client; 9 | 10 | // 11 | 12 | impl Resolve for InspectNetwork { 13 | #[instrument(name = "InspectNetwork", level = "debug")] 14 | async fn resolve(self, _: &super::Args) -> serror::Result { 15 | Ok(docker_client().inspect_network(&self.name).await?) 16 | } 17 | } 18 | 19 | // 20 | 21 | impl Resolve for CreateNetwork { 22 | #[instrument(name = "CreateNetwork", skip(self))] 23 | async fn resolve(self, _: &super::Args) -> serror::Result { 24 | let CreateNetwork { name, driver } = self; 25 | let driver = match driver { 26 | Some(driver) => format!(" -d {driver}"), 27 | None => String::new(), 28 | }; 29 | let command = format!("docker network create{driver} {name}"); 30 | Ok(run_komodo_command("Create Network", None, command).await) 31 | } 32 | } 33 | 34 | // 35 | 36 | impl Resolve for DeleteNetwork { 37 | #[instrument(name = "DeleteNetwork", skip(self))] 38 | async fn resolve(self, _: &super::Args) -> serror::Result { 39 | let command = format!("docker network rm {}", self.name); 40 | Ok(run_komodo_command("Delete Network", None, command).await) 41 | } 42 | } 43 | 44 | // 45 | 46 | impl Resolve for PruneNetworks { 47 | #[instrument(name = "PruneNetworks", skip(self))] 48 | async fn resolve(self, _: &super::Args) -> serror::Result { 49 | let command = String::from("docker network prune -f"); 50 | Ok(run_komodo_command("Prune Networks", None, command).await) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bin/periphery/src/api/stats.rs: -------------------------------------------------------------------------------- 1 | use komodo_client::entities::stats::{ 2 | SystemInformation, SystemProcess, SystemStats, 3 | }; 4 | use periphery_client::api::stats::{ 5 | GetSystemInformation, GetSystemProcesses, GetSystemStats, 6 | }; 7 | use resolver_api::Resolve; 8 | 9 | use crate::stats::stats_client; 10 | 11 | impl Resolve for GetSystemInformation { 12 | #[instrument( 13 | name = "GetSystemInformation", 14 | level = "debug", 15 | skip_all 16 | )] 17 | async fn resolve( 18 | self, 19 | _: &super::Args, 20 | ) -> serror::Result { 21 | Ok(stats_client().read().await.info.clone()) 22 | } 23 | } 24 | 25 | // 26 | 27 | impl Resolve for GetSystemStats { 28 | #[instrument(name = "GetSystemStats", level = "debug", skip_all)] 29 | async fn resolve( 30 | self, 31 | _: &super::Args, 32 | ) -> serror::Result { 33 | Ok(stats_client().read().await.stats.clone()) 34 | } 35 | } 36 | 37 | // 38 | 39 | impl Resolve for GetSystemProcesses { 40 | #[instrument(name = "GetSystemProcesses", level = "debug")] 41 | async fn resolve( 42 | self, 43 | _: &super::Args, 44 | ) -> serror::Result> { 45 | Ok(stats_client().read().await.get_processes()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bin/periphery/src/api/volume.rs: -------------------------------------------------------------------------------- 1 | use command::run_komodo_command; 2 | use komodo_client::entities::{docker::volume::Volume, update::Log}; 3 | use periphery_client::api::volume::*; 4 | use resolver_api::Resolve; 5 | 6 | use crate::docker::docker_client; 7 | 8 | // 9 | 10 | impl Resolve for InspectVolume { 11 | #[instrument(name = "InspectVolume", level = "debug")] 12 | async fn resolve(self, _: &super::Args) -> serror::Result { 13 | Ok(docker_client().inspect_volume(&self.name).await?) 14 | } 15 | } 16 | 17 | // 18 | 19 | impl Resolve for DeleteVolume { 20 | #[instrument(name = "DeleteVolume")] 21 | async fn resolve(self, _: &super::Args) -> serror::Result { 22 | let command = format!("docker volume rm {}", self.name); 23 | Ok(run_komodo_command("Delete Volume", None, command).await) 24 | } 25 | } 26 | 27 | // 28 | 29 | impl Resolve for PruneVolumes { 30 | #[instrument(name = "PruneVolumes")] 31 | async fn resolve(self, _: &super::Args) -> serror::Result { 32 | let command = String::from("docker volume prune -a -f"); 33 | Ok(run_komodo_command("Prune Volumes", None, command).await) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bin/periphery/src/ssl.rs: -------------------------------------------------------------------------------- 1 | use crate::config::periphery_config; 2 | 3 | pub async fn ensure_certs() { 4 | let config = periphery_config(); 5 | if !config.ssl_cert_file().is_file() 6 | || !config.ssl_key_file().is_file() 7 | { 8 | generate_self_signed_ssl_certs().await 9 | } 10 | } 11 | 12 | #[instrument] 13 | async fn generate_self_signed_ssl_certs() { 14 | info!("Generating certs..."); 15 | 16 | let config = periphery_config(); 17 | 18 | let ssl_key_file = config.ssl_key_file(); 19 | let ssl_cert_file = config.ssl_cert_file(); 20 | 21 | // ensure cert folders exist 22 | if let Some(parent) = ssl_key_file.parent() { 23 | let _ = std::fs::create_dir_all(parent); 24 | } 25 | if let Some(parent) = ssl_cert_file.parent() { 26 | let _ = std::fs::create_dir_all(parent); 27 | } 28 | 29 | let key_path = ssl_key_file.display(); 30 | let cert_path = ssl_cert_file.display(); 31 | 32 | let command = format!( 33 | "openssl req -x509 -newkey rsa:4096 -keyout {key_path} -out {cert_path} -sha256 -days 3650 -nodes -subj \"/C=XX/CN=periphery\"" 34 | ); 35 | let log = run_command::async_run_command(&command).await; 36 | 37 | if log.success() { 38 | info!("✅ SSL Certs generated"); 39 | } else { 40 | panic!( 41 | "🚨 Failed to generate SSL Certs | stdout: {} | stderr: {}", 42 | log.stdout, log.stderr 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bin/periphery/starship.toml: -------------------------------------------------------------------------------- 1 | ## This is used to customize the shell prompt in Periphery container for Terminals 2 | 3 | "$schema" = 'https://starship.rs/config-schema.json' 4 | 5 | add_newline = true 6 | 7 | format = "$time$hostname$container$memory_usage$all" 8 | 9 | [character] 10 | success_symbol = "[❯](bright-blue bold)" 11 | error_symbol = "[❯](bright-red bold)" 12 | 13 | [package] 14 | disabled = true 15 | 16 | [time] 17 | format = "[❯$time](white dimmed) " 18 | time_format = "%l:%M %p" 19 | utc_time_offset = '-5' 20 | disabled = true 21 | 22 | [username] 23 | format = "[❯ $user]($style) " 24 | style_user = "bright-green" 25 | show_always = true 26 | 27 | [hostname] 28 | format = "[❯ $hostname]($style) " 29 | style = "bright-blue" 30 | ssh_only = false 31 | 32 | [directory] 33 | format = "[❯ $path]($style)[$read_only]($read_only_style) " 34 | style = "bright-cyan" 35 | 36 | [git_branch] 37 | format = "[❯ $symbol$branch(:$remote_branch)]($style) " 38 | style = "bright-purple" 39 | 40 | [git_status] 41 | style = "bright-purple" 42 | 43 | [rust] 44 | format = "[❯ $symbol($version )]($style)" 45 | symbol = "rustc " 46 | style = "bright-red" 47 | 48 | [nodejs] 49 | format = "[❯ $symbol($version )]($style)" 50 | symbol = "nodejs " 51 | style = "bright-red" 52 | 53 | [memory_usage] 54 | format = "[❯ mem ${ram} ${ram_pct}]($style) " 55 | threshold = -1 56 | style = "white" 57 | 58 | [cmd_duration] 59 | format = "[❯ $duration]($style)" 60 | style = "bright-yellow" 61 | 62 | [container] 63 | format = "[❯ 🦎 periphery container ]($style)" 64 | style = "bright-green" 65 | 66 | [aws] 67 | disabled = true 68 | -------------------------------------------------------------------------------- /bin/util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "komodo_util" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | homepage.workspace = true 9 | 10 | [[bin]] 11 | name = "util" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | tracing-subscriber.workspace = true 16 | futures-util.workspace = true 17 | dotenvy.workspace = true 18 | tracing.workspace = true 19 | anyhow.workspace = true 20 | mungos.workspace = true 21 | tokio.workspace = true 22 | serde.workspace = true 23 | envy.workspace = true -------------------------------------------------------------------------------- /bin/util/aio.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.87.0-bullseye AS builder 2 | 3 | WORKDIR /builder 4 | COPY Cargo.toml Cargo.lock ./ 5 | COPY ./lib ./lib 6 | COPY ./client/core/rs ./client/core/rs 7 | COPY ./client/periphery ./client/periphery 8 | COPY ./bin/util ./bin/util 9 | 10 | # Compile bin 11 | RUN cargo build -p komodo_util --release 12 | 13 | # Copy binaries to distroless base 14 | FROM gcr.io/distroless/cc 15 | 16 | COPY --from=builder /builder/target/release/util /usr/local/bin/util 17 | 18 | CMD [ "util" ] 19 | 20 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 21 | LABEL org.opencontainers.image.description="Komodo Util" 22 | LABEL org.opencontainers.image.licenses=GPL-3.0 -------------------------------------------------------------------------------- /bin/util/multi-arch.Dockerfile: -------------------------------------------------------------------------------- 1 | ## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile). 2 | ## Since theres no heavy build here, QEMU multi-arch builds are fine for this image. 3 | 4 | ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest 5 | ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64 6 | ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64 7 | 8 | # This is required to work with COPY --from 9 | FROM ${X86_64_BINARIES} AS x86_64 10 | FROM ${AARCH64_BINARIES} AS aarch64 11 | 12 | FROM debian:bullseye-slim 13 | 14 | WORKDIR /app 15 | 16 | ## Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM. 17 | COPY --from=x86_64 /util /app/arch/linux/amd64 18 | COPY --from=aarch64 /util /app/arch/linux/arm64 19 | 20 | ARG TARGETPLATFORM 21 | RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/util && rm -r /app/arch 22 | 23 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 24 | LABEL org.opencontainers.image.description="Komodo Util" 25 | LABEL org.opencontainers.image.licenses=GPL-3.0 26 | 27 | CMD [ "util" ] -------------------------------------------------------------------------------- /bin/util/single-arch.Dockerfile: -------------------------------------------------------------------------------- 1 | ## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile). 2 | 3 | ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest 4 | 5 | # This is required to work with COPY --from 6 | FROM ${BINARIES_IMAGE} AS binaries 7 | 8 | FROM gcr.io/distroless/cc 9 | 10 | COPY --from=binaries /util /usr/local/bin/util 11 | 12 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 13 | LABEL org.opencontainers.image.description="Komodo Util" 14 | LABEL org.opencontainers.image.licenses=GPL-3.0 15 | 16 | CMD [ "util" ] -------------------------------------------------------------------------------- /bin/util/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate tracing; 3 | 4 | use serde::Deserialize; 5 | 6 | mod copy_database; 7 | 8 | #[derive(Deserialize, Debug, Default)] 9 | enum Mode { 10 | #[default] 11 | CopyDatabase, 12 | } 13 | 14 | #[derive(Deserialize)] 15 | struct Env { 16 | mode: Mode, 17 | } 18 | 19 | async fn app() -> anyhow::Result<()> { 20 | dotenvy::dotenv().ok(); 21 | tracing_subscriber::fmt::init(); 22 | 23 | let env = envy::from_env::()?; 24 | 25 | info!("Komodo Util version: v{}", env!("CARGO_PKG_VERSION")); 26 | info!("Mode: {:?}", env.mode); 27 | 28 | match env.mode { 29 | Mode::CopyDatabase => copy_database::main().await, 30 | } 31 | } 32 | 33 | #[tokio::main] 34 | async fn main() -> anyhow::Result<()> { 35 | let mut term_signal = tokio::signal::unix::signal( 36 | tokio::signal::unix::SignalKind::terminate(), 37 | )?; 38 | tokio::select! { 39 | res = tokio::spawn(app()) => res?, 40 | _ = term_signal.recv() => Ok(()), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/core/rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "komodo_client" 3 | description = "Client for the Komodo build and deployment system" 4 | version.workspace = true 5 | edition.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [features] 14 | # default = ["blocking"] # use to dev client blocking mode 15 | mongo = ["dep:mongo_indexed"] 16 | blocking = ["reqwest/blocking"] 17 | 18 | [dependencies] 19 | # mogh 20 | mongo_indexed = { workspace = true, optional = true } 21 | serror = { workspace = true, features = ["axum"]} 22 | derive_default_builder.workspace = true 23 | derive_empty_traits.workspace = true 24 | async_timing_util.workspace = true 25 | partial_derive2.workspace = true 26 | derive_variants.workspace = true 27 | resolver_api.workspace = true 28 | # external 29 | tokio-tungstenite.workspace = true 30 | derive_builder.workspace = true 31 | serde_json.workspace = true 32 | tokio-util.workspace = true 33 | thiserror.workspace = true 34 | typeshare.workspace = true 35 | indexmap.workspace = true 36 | futures.workspace = true 37 | reqwest.workspace = true 38 | tracing.workspace = true 39 | anyhow.workspace = true 40 | serde.workspace = true 41 | tokio.workspace = true 42 | strum.workspace = true 43 | envy.workspace = true 44 | uuid.workspace = true 45 | clap.workspace = true 46 | bson.workspace = true -------------------------------------------------------------------------------- /client/core/rs/README.md: -------------------------------------------------------------------------------- 1 | # Komodo 2 | *A system to build and deploy software across many servers*. [https://komo.do](https://komo.do) 3 | 4 | Docs: [https://docs.rs/komodo_client/latest/komodo_client](https://docs.rs/komodo_client/latest/komodo_client). 5 | 6 | This is a client library for the Komodo Core API. 7 | It contains: 8 | - Definitions for the application [api](https://docs.rs/komodo_client/latest/komodo_client/api/index.html) 9 | and [entities](https://docs.rs/komodo_client/latest/komodo_client/entities/index.html). 10 | - A [client](https://docs.rs/komodo_client/latest/komodo_client/struct.KomodoClient.html) 11 | to interact with the Komodo Core API. 12 | - Information on configuring Komodo 13 | [Core](https://docs.rs/komodo_client/latest/komodo_client/entities/config/core/index.html) and 14 | [Periphery](https://docs.rs/komodo_client/latest/komodo_client/entities/config/periphery/index.html). 15 | 16 | ## Client Configuration 17 | 18 | The client includes a convenenience method to parse the Komodo API url and credentials from the environment: 19 | - `KOMODO_ADDRESS` 20 | - `KOMODO_API_KEY` 21 | - `KOMODO_API_SECRET` 22 | 23 | ## Client Example 24 | ```rust 25 | dotenvy::dotenv().ok(); 26 | 27 | let client = KomodoClient::new_from_env()?; 28 | 29 | // Get all the deployments 30 | let deployments = client.read(ListDeployments::default()).await?; 31 | 32 | println!("{deployments:#?}"); 33 | 34 | let update = client.execute(RunBuild { build: "test-build".to_string() }).await?: 35 | ``` -------------------------------------------------------------------------------- /client/core/rs/src/api/execute/action.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use derive_empty_traits::EmptyTraits; 3 | use resolver_api::Resolve; 4 | use serde::{Deserialize, Serialize}; 5 | use typeshare::typeshare; 6 | 7 | use crate::entities::update::Update; 8 | 9 | use super::{BatchExecutionResponse, KomodoExecuteRequest}; 10 | 11 | /// Runs the target Action. Response: [Update] 12 | #[typeshare] 13 | #[derive( 14 | Debug, 15 | Clone, 16 | PartialEq, 17 | Serialize, 18 | Deserialize, 19 | Resolve, 20 | EmptyTraits, 21 | Parser, 22 | )] 23 | #[empty_traits(KomodoExecuteRequest)] 24 | #[response(Update)] 25 | #[error(serror::Error)] 26 | pub struct RunAction { 27 | /// Id or name 28 | pub action: String, 29 | } 30 | 31 | /// Runs multiple Actions in parallel that match pattern. Response: [BatchExecutionResponse] 32 | #[typeshare] 33 | #[derive( 34 | Debug, 35 | Clone, 36 | PartialEq, 37 | Serialize, 38 | Deserialize, 39 | Resolve, 40 | EmptyTraits, 41 | Parser, 42 | )] 43 | #[empty_traits(KomodoExecuteRequest)] 44 | #[response(BatchExecutionResponse)] 45 | #[error(serror::Error)] 46 | pub struct BatchRunAction { 47 | /// Id or name or wildcard pattern or regex. 48 | /// Supports multiline and comma delineated combinations of the above. 49 | /// 50 | /// Example: 51 | /// ``` 52 | /// # match all foo-* actions 53 | /// foo-* 54 | /// # add some more 55 | /// extra-action-1, extra-action-2 56 | /// ``` 57 | pub pattern: String, 58 | } 59 | -------------------------------------------------------------------------------- /client/core/rs/src/api/execute/alerter.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use derive_empty_traits::EmptyTraits; 3 | use resolver_api::Resolve; 4 | use serde::{Deserialize, Serialize}; 5 | use typeshare::typeshare; 6 | 7 | use crate::entities::update::Update; 8 | 9 | use super::KomodoExecuteRequest; 10 | 11 | /// Tests an Alerters ability to reach the configured endpoint. Response: [Update] 12 | #[typeshare] 13 | #[derive( 14 | Serialize, 15 | Deserialize, 16 | Debug, 17 | Clone, 18 | PartialEq, 19 | Resolve, 20 | EmptyTraits, 21 | Parser, 22 | )] 23 | #[empty_traits(KomodoExecuteRequest)] 24 | #[response(Update)] 25 | #[error(serror::Error)] 26 | pub struct TestAlerter { 27 | /// Name or id 28 | pub alerter: String, 29 | } 30 | -------------------------------------------------------------------------------- /client/core/rs/src/api/execute/procedure.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use derive_empty_traits::EmptyTraits; 3 | use resolver_api::Resolve; 4 | use serde::{Deserialize, Serialize}; 5 | use typeshare::typeshare; 6 | 7 | use crate::entities::update::Update; 8 | 9 | use super::{BatchExecutionResponse, KomodoExecuteRequest}; 10 | 11 | /// Runs the target Procedure. Response: [Update] 12 | #[typeshare] 13 | #[derive( 14 | Debug, 15 | Clone, 16 | PartialEq, 17 | Serialize, 18 | Deserialize, 19 | Resolve, 20 | EmptyTraits, 21 | Parser, 22 | )] 23 | #[empty_traits(KomodoExecuteRequest)] 24 | #[response(Update)] 25 | #[error(serror::Error)] 26 | pub struct RunProcedure { 27 | /// Id or name 28 | pub procedure: String, 29 | } 30 | 31 | /// Runs multiple Procedures in parallel that match pattern. Response: [BatchExecutionResponse]. 32 | #[typeshare] 33 | #[derive( 34 | Debug, 35 | Clone, 36 | PartialEq, 37 | Serialize, 38 | Deserialize, 39 | Resolve, 40 | EmptyTraits, 41 | Parser, 42 | )] 43 | #[empty_traits(KomodoExecuteRequest)] 44 | #[response(BatchExecutionResponse)] 45 | #[error(serror::Error)] 46 | pub struct BatchRunProcedure { 47 | /// Id or name or wildcard pattern or regex. 48 | /// Supports multiline and comma delineated combinations of the above. 49 | /// 50 | /// Example: 51 | /// ``` 52 | /// # match all foo-* procedures 53 | /// foo-* 54 | /// # add some more 55 | /// extra-procedure-1, extra-procedure-2 56 | /// ``` 57 | pub pattern: String, 58 | } 59 | -------------------------------------------------------------------------------- /client/core/rs/src/api/execute/sync.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use derive_empty_traits::EmptyTraits; 3 | use resolver_api::Resolve; 4 | use serde::{Deserialize, Serialize}; 5 | use typeshare::typeshare; 6 | 7 | use crate::entities::{ResourceTargetVariant, update::Update}; 8 | 9 | use super::KomodoExecuteRequest; 10 | 11 | /// Runs the target resource sync. Response: [Update] 12 | #[typeshare] 13 | #[derive( 14 | Debug, 15 | Clone, 16 | PartialEq, 17 | Serialize, 18 | Deserialize, 19 | Resolve, 20 | EmptyTraits, 21 | Parser, 22 | )] 23 | #[empty_traits(KomodoExecuteRequest)] 24 | #[response(Update)] 25 | #[error(serror::Error)] 26 | pub struct RunSync { 27 | /// Id or name 28 | pub sync: String, 29 | /// Only execute sync on a specific resource type. 30 | /// Combine with `resource_id` to specify resource. 31 | pub resource_type: Option, 32 | /// Only execute sync on a specific resources. 33 | /// Combine with `resource_type` to specify resources. 34 | /// Supports name or id. 35 | pub resources: Option>, 36 | } 37 | -------------------------------------------------------------------------------- /client/core/rs/src/api/read/permission.rs: -------------------------------------------------------------------------------- 1 | use derive_empty_traits::EmptyTraits; 2 | use resolver_api::Resolve; 3 | use serde::{Deserialize, Serialize}; 4 | use typeshare::typeshare; 5 | 6 | use crate::entities::{ 7 | ResourceTarget, 8 | permission::{Permission, PermissionLevelAndSpecifics, UserTarget}, 9 | }; 10 | 11 | use super::KomodoReadRequest; 12 | 13 | /// List permissions for the calling user. 14 | /// Does not include any permissions on UserGroups they may be a part of. 15 | /// Response: [ListPermissionsResponse] 16 | #[typeshare] 17 | #[derive( 18 | Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, 19 | )] 20 | #[empty_traits(KomodoReadRequest)] 21 | #[response(ListPermissionsResponse)] 22 | #[error(serror::Error)] 23 | pub struct ListPermissions {} 24 | 25 | #[typeshare] 26 | pub type ListPermissionsResponse = Vec; 27 | 28 | // 29 | 30 | /// Gets the calling user's permission level on a specific resource. 31 | /// Factors in any UserGroup's permissions they may be a part of. 32 | /// Response: [PermissionLevel] 33 | #[typeshare] 34 | #[derive( 35 | Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, 36 | )] 37 | #[empty_traits(KomodoReadRequest)] 38 | #[response(GetPermissionResponse)] 39 | #[error(serror::Error)] 40 | pub struct GetPermission { 41 | /// The target to get user permission on. 42 | pub target: ResourceTarget, 43 | } 44 | 45 | #[typeshare] 46 | pub type GetPermissionResponse = PermissionLevelAndSpecifics; 47 | 48 | // 49 | 50 | /// List permissions for a specific user. **Admin only**. 51 | /// Response: [ListUserTargetPermissionsResponse] 52 | #[typeshare] 53 | #[derive( 54 | Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, 55 | )] 56 | #[empty_traits(KomodoReadRequest)] 57 | #[response(ListUserTargetPermissionsResponse)] 58 | #[error(serror::Error)] 59 | pub struct ListUserTargetPermissions { 60 | /// Specify either a user or a user group. 61 | pub user_target: UserTarget, 62 | } 63 | 64 | #[typeshare] 65 | pub type ListUserTargetPermissionsResponse = Vec; 66 | -------------------------------------------------------------------------------- /client/core/rs/src/api/read/tag.rs: -------------------------------------------------------------------------------- 1 | use derive_empty_traits::EmptyTraits; 2 | use resolver_api::Resolve; 3 | use serde::{Deserialize, Serialize}; 4 | use typeshare::typeshare; 5 | 6 | use crate::entities::{MongoDocument, tag::Tag}; 7 | 8 | use super::KomodoReadRequest; 9 | 10 | // 11 | 12 | /// Get data for a specific tag. Response [Tag]. 13 | #[typeshare] 14 | #[derive( 15 | Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, 16 | )] 17 | #[empty_traits(KomodoReadRequest)] 18 | #[response(GetTagResponse)] 19 | #[error(serror::Error)] 20 | pub struct GetTag { 21 | /// Id or name 22 | #[serde(alias = "id", alias = "name")] 23 | pub tag: String, 24 | } 25 | 26 | #[typeshare] 27 | pub type GetTagResponse = Tag; 28 | 29 | // 30 | 31 | /// List data for tags matching optional mongo query. 32 | /// Response: [ListTagsResponse]. 33 | #[typeshare] 34 | #[derive( 35 | Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits, 36 | )] 37 | #[empty_traits(KomodoReadRequest)] 38 | #[response(ListTagsResponse)] 39 | #[error(serror::Error)] 40 | pub struct ListTags { 41 | pub query: Option, 42 | } 43 | 44 | #[typeshare] 45 | pub type ListTagsResponse = Vec; 46 | -------------------------------------------------------------------------------- /client/core/rs/src/api/read/update.rs: -------------------------------------------------------------------------------- 1 | use derive_empty_traits::EmptyTraits; 2 | use resolver_api::Resolve; 3 | use serde::{Deserialize, Serialize}; 4 | use typeshare::typeshare; 5 | 6 | use crate::entities::{ 7 | MongoDocument, 8 | update::{Update, UpdateListItem}, 9 | }; 10 | 11 | use super::KomodoReadRequest; 12 | 13 | /// Get all data for the target update. 14 | /// Response: [Update]. 15 | #[typeshare] 16 | #[derive( 17 | Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, 18 | )] 19 | #[empty_traits(KomodoReadRequest)] 20 | #[response(GetUpdateResponse)] 21 | #[error(serror::Error)] 22 | pub struct GetUpdate { 23 | /// The update id. 24 | pub id: String, 25 | } 26 | 27 | #[typeshare] 28 | pub type GetUpdateResponse = Update; 29 | 30 | // 31 | 32 | /// Paginated endpoint for updates matching optional query. 33 | /// More recent updates will be returned first. 34 | #[typeshare] 35 | #[derive( 36 | Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, 37 | )] 38 | #[empty_traits(KomodoReadRequest)] 39 | #[response(ListUpdatesResponse)] 40 | #[error(serror::Error)] 41 | pub struct ListUpdates { 42 | /// An optional mongo query to filter the updates. 43 | pub query: Option, 44 | /// Page of updates. Default is 0, which is the most recent data. 45 | /// Use with the `next_page` field of the response. 46 | #[serde(default)] 47 | pub page: u32, 48 | } 49 | 50 | /// Response for [ListUpdates]. 51 | #[typeshare] 52 | #[derive(Serialize, Deserialize, Debug, Clone)] 53 | pub struct ListUpdatesResponse { 54 | /// The page of updates, sorted by timestamp descending. 55 | pub updates: Vec, 56 | /// If there is a next page of data, pass this to `page` to get it. 57 | pub next_page: Option, 58 | } 59 | -------------------------------------------------------------------------------- /client/core/rs/src/api/read/user_group.rs: -------------------------------------------------------------------------------- 1 | use derive_empty_traits::EmptyTraits; 2 | use resolver_api::Resolve; 3 | use serde::{Deserialize, Serialize}; 4 | use typeshare::typeshare; 5 | 6 | use crate::entities::user_group::UserGroup; 7 | 8 | use super::KomodoReadRequest; 9 | 10 | /// Get a specific user group by name or id. 11 | /// Response: [UserGroup]. 12 | #[typeshare] 13 | #[derive( 14 | Debug, Clone, Serialize, Deserialize, Resolve, EmptyTraits, 15 | )] 16 | #[empty_traits(KomodoReadRequest)] 17 | #[response(GetUserGroupResponse)] 18 | #[error(serror::Error)] 19 | pub struct GetUserGroup { 20 | /// Name or Id 21 | pub user_group: String, 22 | } 23 | 24 | #[typeshare] 25 | pub type GetUserGroupResponse = UserGroup; 26 | 27 | // 28 | 29 | /// List all user groups which user can see. Response: [ListUserGroupsResponse]. 30 | /// 31 | /// Admins can see all user groups, 32 | /// and users can see user groups to which they belong. 33 | #[typeshare] 34 | #[derive( 35 | Debug, Clone, Default, Serialize, Deserialize, Resolve, EmptyTraits, 36 | )] 37 | #[empty_traits(KomodoReadRequest)] 38 | #[response(ListUserGroupsResponse)] 39 | #[error(serror::Error)] 40 | pub struct ListUserGroups {} 41 | 42 | #[typeshare] 43 | pub type ListUserGroupsResponse = Vec; 44 | -------------------------------------------------------------------------------- /client/core/rs/src/api/read/variable.rs: -------------------------------------------------------------------------------- 1 | use derive_empty_traits::EmptyTraits; 2 | use resolver_api::Resolve; 3 | use serde::{Deserialize, Serialize}; 4 | use typeshare::typeshare; 5 | 6 | use crate::entities::variable::Variable; 7 | 8 | use super::KomodoReadRequest; 9 | 10 | /// List all available global variables. 11 | /// Response: [Variable] 12 | /// 13 | /// Note. For non admin users making this call, 14 | /// secret variables will have their values obscured. 15 | #[typeshare] 16 | #[derive( 17 | Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, 18 | )] 19 | #[empty_traits(KomodoReadRequest)] 20 | #[response(GetVariableResponse)] 21 | #[error(serror::Error)] 22 | pub struct GetVariable { 23 | /// The name of the variable to get. 24 | pub name: String, 25 | } 26 | 27 | #[typeshare] 28 | pub type GetVariableResponse = Variable; 29 | 30 | // 31 | 32 | /// List all available global variables. 33 | /// Response: [ListVariablesResponse] 34 | /// 35 | /// Note. For non admin users making this call, 36 | /// secret variables will have their values obscured. 37 | #[typeshare] 38 | #[derive( 39 | Serialize, Deserialize, Debug, Clone, Default, Resolve, EmptyTraits, 40 | )] 41 | #[empty_traits(KomodoReadRequest)] 42 | #[response(ListVariablesResponse)] 43 | #[error(serror::Error)] 44 | pub struct ListVariables {} 45 | 46 | #[typeshare] 47 | pub type ListVariablesResponse = Vec; 48 | -------------------------------------------------------------------------------- /client/core/rs/src/api/write/api_key.rs: -------------------------------------------------------------------------------- 1 | use derive_empty_traits::EmptyTraits; 2 | use resolver_api::Resolve; 3 | use serde::{Deserialize, Serialize}; 4 | use typeshare::typeshare; 5 | 6 | use crate::{ 7 | api::user::CreateApiKeyResponse, 8 | entities::{I64, NoData}, 9 | }; 10 | 11 | use super::KomodoWriteRequest; 12 | 13 | // 14 | 15 | /// Admin only method to create an api key for a service user. 16 | /// Response: [CreateApiKeyResponse]. 17 | #[typeshare] 18 | #[derive( 19 | Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, 20 | )] 21 | #[empty_traits(KomodoWriteRequest)] 22 | #[response(CreateApiKeyForServiceUserResponse)] 23 | #[error(serror::Error)] 24 | pub struct CreateApiKeyForServiceUser { 25 | /// Must be service user 26 | pub user_id: String, 27 | /// The name for the api key 28 | pub name: String, 29 | /// A unix timestamp in millseconds specifying api key expire time. 30 | /// Default is 0, which means no expiry. 31 | #[serde(default)] 32 | pub expires: I64, 33 | } 34 | 35 | #[typeshare] 36 | pub type CreateApiKeyForServiceUserResponse = CreateApiKeyResponse; 37 | 38 | // 39 | 40 | /// Admin only method to delete an api key for a service user. 41 | /// Response: [NoData]. 42 | #[typeshare] 43 | #[derive( 44 | Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, 45 | )] 46 | #[empty_traits(KomodoWriteRequest)] 47 | #[response(DeleteApiKeyForServiceUserResponse)] 48 | #[error(serror::Error)] 49 | pub struct DeleteApiKeyForServiceUser { 50 | pub key: String, 51 | } 52 | 53 | #[typeshare] 54 | pub type DeleteApiKeyForServiceUserResponse = NoData; 55 | -------------------------------------------------------------------------------- /client/core/rs/src/api/write/description.rs: -------------------------------------------------------------------------------- 1 | use derive_empty_traits::EmptyTraits; 2 | use resolver_api::Resolve; 3 | use serde::{Deserialize, Serialize}; 4 | use typeshare::typeshare; 5 | 6 | use crate::entities::{NoData, ResourceTarget}; 7 | 8 | use super::KomodoWriteRequest; 9 | 10 | /// Update a resources description. 11 | /// Response: [NoData]. 12 | #[typeshare] 13 | #[derive( 14 | Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, 15 | )] 16 | #[empty_traits(KomodoWriteRequest)] 17 | #[response(UpdateDescriptionResponse)] 18 | #[error(serror::Error)] 19 | pub struct UpdateDescription { 20 | /// The target resource to set description for. 21 | pub target: ResourceTarget, 22 | /// The new description. 23 | pub description: String, 24 | } 25 | 26 | #[typeshare] 27 | pub type UpdateDescriptionResponse = NoData; 28 | -------------------------------------------------------------------------------- /client/core/rs/src/api/write/mod.rs: -------------------------------------------------------------------------------- 1 | mod action; 2 | mod alerter; 3 | mod api_key; 4 | mod build; 5 | mod builder; 6 | mod deployment; 7 | mod description; 8 | mod permissions; 9 | mod procedure; 10 | mod provider; 11 | mod repo; 12 | mod server; 13 | mod stack; 14 | mod sync; 15 | mod tags; 16 | mod user; 17 | mod user_group; 18 | mod variable; 19 | 20 | pub use action::*; 21 | pub use alerter::*; 22 | pub use api_key::*; 23 | pub use build::*; 24 | pub use builder::*; 25 | pub use deployment::*; 26 | pub use description::*; 27 | pub use permissions::*; 28 | pub use procedure::*; 29 | pub use provider::*; 30 | pub use repo::*; 31 | pub use server::*; 32 | pub use stack::*; 33 | pub use sync::*; 34 | pub use tags::*; 35 | pub use user::*; 36 | pub use user_group::*; 37 | pub use variable::*; 38 | 39 | pub trait KomodoWriteRequest: resolver_api::HasResponse {} 40 | -------------------------------------------------------------------------------- /client/core/rs/src/busy.rs: -------------------------------------------------------------------------------- 1 | use crate::entities::{ 2 | action::ActionActionState, build::BuildActionState, 3 | deployment::DeploymentActionState, procedure::ProcedureActionState, 4 | repo::RepoActionState, server::ServerActionState, 5 | stack::StackActionState, sync::ResourceSyncActionState, 6 | }; 7 | 8 | pub trait Busy { 9 | fn busy(&self) -> bool; 10 | } 11 | 12 | impl Busy for ServerActionState { 13 | fn busy(&self) -> bool { 14 | self.pruning_containers 15 | || self.pruning_images 16 | || self.pruning_networks 17 | || self.pruning_volumes 18 | || self.starting_containers 19 | || self.restarting_containers 20 | || self.pausing_containers 21 | || self.unpausing_containers 22 | || self.stopping_containers 23 | } 24 | } 25 | 26 | impl Busy for DeploymentActionState { 27 | fn busy(&self) -> bool { 28 | self.deploying 29 | || self.starting 30 | || self.restarting 31 | || self.pausing 32 | || self.unpausing 33 | || self.stopping 34 | || self.destroying 35 | || self.renaming 36 | } 37 | } 38 | 39 | impl Busy for StackActionState { 40 | fn busy(&self) -> bool { 41 | self.deploying 42 | || self.starting 43 | || self.restarting 44 | || self.pausing 45 | || self.unpausing 46 | || self.stopping 47 | || self.destroying 48 | } 49 | } 50 | 51 | impl Busy for BuildActionState { 52 | fn busy(&self) -> bool { 53 | self.building 54 | } 55 | } 56 | 57 | impl Busy for RepoActionState { 58 | fn busy(&self) -> bool { 59 | self.cloning || self.pulling || self.building 60 | } 61 | } 62 | 63 | impl Busy for ProcedureActionState { 64 | fn busy(&self) -> bool { 65 | self.running 66 | } 67 | } 68 | 69 | impl Busy for ActionActionState { 70 | fn busy(&self) -> bool { 71 | self.running 72 | } 73 | } 74 | 75 | impl Busy for ResourceSyncActionState { 76 | fn busy(&self) -> bool { 77 | self.syncing 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /client/core/rs/src/deserializers/file_contents.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserializer, de::Visitor}; 2 | 3 | /// Using this ensures the file contents end with trailing '\n' 4 | pub fn file_contents_deserializer<'de, D>( 5 | deserializer: D, 6 | ) -> Result 7 | where 8 | D: Deserializer<'de>, 9 | { 10 | deserializer.deserialize_any(FileContentsVisitor) 11 | } 12 | 13 | /// Using this ensures the file contents end with trailing '\n' 14 | pub fn option_file_contents_deserializer<'de, D>( 15 | deserializer: D, 16 | ) -> Result, D::Error> 17 | where 18 | D: Deserializer<'de>, 19 | { 20 | deserializer.deserialize_any(OptionFileContentsVisitor) 21 | } 22 | 23 | struct FileContentsVisitor; 24 | 25 | impl Visitor<'_> for FileContentsVisitor { 26 | type Value = String; 27 | 28 | fn expecting( 29 | &self, 30 | formatter: &mut std::fmt::Formatter, 31 | ) -> std::fmt::Result { 32 | write!(formatter, "string") 33 | } 34 | 35 | fn visit_str(self, v: &str) -> Result 36 | where 37 | E: serde::de::Error, 38 | { 39 | let out = v.trim_end().to_string(); 40 | if out.is_empty() { 41 | Ok(out) 42 | } else { 43 | Ok(out + "\n") 44 | } 45 | } 46 | } 47 | 48 | struct OptionFileContentsVisitor; 49 | 50 | impl Visitor<'_> for OptionFileContentsVisitor { 51 | type Value = Option; 52 | 53 | fn expecting( 54 | &self, 55 | formatter: &mut std::fmt::Formatter, 56 | ) -> std::fmt::Result { 57 | write!(formatter, "null or string") 58 | } 59 | 60 | fn visit_str(self, v: &str) -> Result 61 | where 62 | E: serde::de::Error, 63 | { 64 | FileContentsVisitor.visit_str(v).map(Some) 65 | } 66 | 67 | fn visit_none(self) -> Result 68 | where 69 | E: serde::de::Error, 70 | { 71 | Ok(None) 72 | } 73 | 74 | fn visit_unit(self) -> Result 75 | where 76 | E: serde::de::Error, 77 | { 78 | Ok(None) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/core/rs/src/deserializers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Deserializers for custom behavior and backward compatibility. 2 | 3 | mod conversion; 4 | mod environment; 5 | mod file_contents; 6 | mod labels; 7 | mod maybe_string_i64; 8 | mod permission; 9 | mod string_list; 10 | mod term_signal_labels; 11 | 12 | pub use conversion::*; 13 | pub use environment::*; 14 | pub use file_contents::*; 15 | pub use labels::*; 16 | pub use maybe_string_i64::*; 17 | pub use string_list::*; 18 | pub use term_signal_labels::*; 19 | -------------------------------------------------------------------------------- /client/core/rs/src/entities/api_key.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use typeshare::typeshare; 3 | 4 | use super::I64; 5 | 6 | /// An api key used to authenticate requests via request headers. 7 | #[typeshare] 8 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 9 | #[cfg_attr( 10 | feature = "mongo", 11 | derive(mongo_indexed::derive::MongoIndexed) 12 | )] 13 | pub struct ApiKey { 14 | /// Unique key associated with secret 15 | #[cfg_attr(feature = "mongo", unique_index)] 16 | pub key: String, 17 | 18 | /// Hash of the secret 19 | pub secret: String, 20 | 21 | /// User associated with the api key 22 | #[cfg_attr(feature = "mongo", index)] 23 | pub user_id: String, 24 | 25 | /// Name associated with the api key for management 26 | pub name: String, 27 | 28 | /// Timestamp of key creation 29 | pub created_at: I64, 30 | 31 | /// Expiry of key, or 0 if never expires 32 | pub expires: I64, 33 | } 34 | 35 | impl ApiKey { 36 | pub fn sanitize(&mut self) { 37 | self.secret.clear() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/core/rs/src/entities/user_group.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | use typeshare::typeshare; 4 | 5 | use crate::deserializers::string_list_deserializer; 6 | 7 | use super::{ 8 | I64, MongoId, ResourceTargetVariant, 9 | permission::PermissionLevelAndSpecifics, 10 | }; 11 | 12 | /// Permission users at the group level. 13 | /// 14 | /// All users that are part of a group inherit the group's permissions. 15 | /// A user can be a part of multiple groups. A user's permission on a particular resource 16 | /// will be resolved to be the maximum permission level between the user's own permissions and 17 | /// any groups they are a part of. 18 | #[typeshare] 19 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 20 | #[cfg_attr( 21 | feature = "mongo", 22 | derive(mongo_indexed::derive::MongoIndexed) 23 | )] 24 | pub struct UserGroup { 25 | /// The Mongo ID of the UserGroup. 26 | /// This field is de/serialized from/to JSON as 27 | /// `{ "_id": { "$oid": "..." }, ...(rest of serialized User) }` 28 | #[serde( 29 | default, 30 | rename = "_id", 31 | skip_serializing_if = "String::is_empty", 32 | with = "bson::serde_helpers::hex_string_as_object_id" 33 | )] 34 | pub id: MongoId, 35 | 36 | /// A name for the user group 37 | #[cfg_attr(feature = "mongo", unique_index)] 38 | pub name: String, 39 | 40 | /// Whether all users will implicitly have the permissions in this group. 41 | #[cfg_attr(feature = "mongo", index)] 42 | #[serde(default)] 43 | pub everyone: bool, 44 | 45 | /// User ids of group members 46 | #[cfg_attr(feature = "mongo", index)] 47 | #[serde(default, deserialize_with = "string_list_deserializer")] 48 | pub users: Vec, 49 | 50 | /// Give the user group elevated permissions on all resources of a certain type 51 | #[serde(default)] 52 | pub all: 53 | IndexMap, 54 | 55 | /// Unix time (ms) when user group last updated 56 | #[serde(default)] 57 | pub updated_at: I64, 58 | } 59 | -------------------------------------------------------------------------------- /client/core/rs/src/entities/variable.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use typeshare::typeshare; 3 | 4 | /// A non-secret global variable which can be interpolated into deployment 5 | /// environment variable values and build argument values. 6 | #[typeshare] 7 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 8 | #[cfg_attr( 9 | feature = "mongo", 10 | derive(mongo_indexed::derive::MongoIndexed) 11 | )] 12 | pub struct Variable { 13 | /// Unique name associated with the variable. 14 | /// Instances of '[[variable.name]]' in value will be replaced with 'variable.value'. 15 | #[cfg_attr(feature = "mongo", unique_index)] 16 | pub name: String, 17 | /// A description for the variable. 18 | #[serde(default, skip_serializing_if = "String::is_empty")] 19 | pub description: String, 20 | /// The value associated with the variable. 21 | #[serde(default)] 22 | pub value: String, 23 | /// If marked as secret, the variable value will be hidden in updates / logs. 24 | /// Additionally the value will not be served in read requests by non admin users. 25 | /// 26 | /// Note that the value is NOT encrypted in the database, and will likely show up in database logs. 27 | /// The security of these variables comes down to the security 28 | /// of the database (system level encryption, network isolation, etc.) 29 | #[serde(default)] 30 | pub is_secret: bool, 31 | } 32 | -------------------------------------------------------------------------------- /client/core/rs/src/terminal.rs: -------------------------------------------------------------------------------- 1 | use futures::{Stream, StreamExt, TryStreamExt}; 2 | 3 | pub struct TerminalStreamResponse(pub reqwest::Response); 4 | 5 | impl TerminalStreamResponse { 6 | pub fn into_line_stream( 7 | self, 8 | ) -> impl Stream> 9 | { 10 | tokio_util::codec::FramedRead::new( 11 | tokio_util::io::StreamReader::new( 12 | self.0.bytes_stream().map_err(std::io::Error::other), 13 | ), 14 | tokio_util::codec::LinesCodec::new(), 15 | ) 16 | .map(|line| line.map(|line| line + "\n")) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/core/ts/README.md: -------------------------------------------------------------------------------- 1 | # Komodo 2 | 3 | _A system to build and deploy software across many servers_. [https://komo.do](https://komo.do) 4 | 5 | ```sh 6 | npm install komodo_client 7 | ``` 8 | 9 | or 10 | 11 | ```sh 12 | yarn add komodo_client 13 | ``` 14 | 15 | ```ts 16 | import { KomodoClient, Types } from "komodo_client"; 17 | 18 | const komodo = KomodoClient("https://demo.komo.do", { 19 | type: "api-key", 20 | params: { 21 | key: "your_key", 22 | secret: "your secret", 23 | }, 24 | }); 25 | 26 | // Inferred as Types.StackListItem[] 27 | const stacks = await komodo.read("ListStacks", {}); 28 | 29 | // Inferred as Types.Stack 30 | const stack = await komodo.read("GetStack", { 31 | stack: stacks[0].name, 32 | }); 33 | ``` 34 | -------------------------------------------------------------------------------- /client/core/ts/generate_types.mjs: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import { readFileSync, writeFileSync } from "fs"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | 8 | console.log("generating typescript types..."); 9 | 10 | const gen_command = 11 | "RUST_BACKTRACE=1 typeshare . --lang=typescript --output-file=./client/core/ts/src/types.ts"; 12 | 13 | exec(gen_command, (error, _stdout, _stderr) => { 14 | if (error) { 15 | console.error(error); 16 | return; 17 | } 18 | console.log("generated types using typeshare"); 19 | fix_types(); 20 | console.log("finished."); 21 | }); 22 | 23 | function fix_types() { 24 | const types_path = __dirname + "/src/types.ts"; 25 | const contents = readFileSync(types_path); 26 | const fixed = contents 27 | .toString() 28 | // Replace Variants 29 | .replaceAll("ResourceTargetVariant", 'ResourceTarget["type"]') 30 | .replaceAll("AlerterEndpointVariant", 'AlerterEndpoint["type"]') 31 | .replaceAll("AlertDataVariant", 'AlertData["type"]') 32 | .replaceAll("ServerTemplateConfigVariant", 'ServerTemplateConfig["type"]') 33 | // Add '| string' to env vars 34 | .replaceAll("EnvironmentVar[]", "EnvironmentVar[] | string") 35 | .replaceAll("IndexSet", "Array") 36 | .replaceAll( 37 | ": PermissionLevelAndSpecifics", 38 | ": PermissionLevelAndSpecifics | PermissionLevel" 39 | ) 40 | .replaceAll( 41 | ", PermissionLevelAndSpecifics", 42 | ", PermissionLevelAndSpecifics | PermissionLevel" 43 | ) 44 | .replaceAll("IndexMap", "Record"); 45 | writeFileSync(types_path, fixed); 46 | } 47 | -------------------------------------------------------------------------------- /client/core/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "komodo_client", 3 | "version": "1.18.0", 4 | "description": "Komodo client package", 5 | "homepage": "https://komo.do", 6 | "main": "dist/lib.js", 7 | "type": "module", 8 | "license": "GPL-3.0", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "build": "tsc" 14 | }, 15 | "dependencies": {}, 16 | "devDependencies": { 17 | "typescript": "^5.6.3" 18 | }, 19 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 20 | } 21 | -------------------------------------------------------------------------------- /client/core/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "isolatedModules": true, 10 | "outDir": "dist", 11 | "declaration": true 12 | } 13 | } -------------------------------------------------------------------------------- /client/core/ts/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | typescript@^5.6.3: 6 | version "5.6.3" 7 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" 8 | integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== 9 | -------------------------------------------------------------------------------- /client/periphery/rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "periphery_client" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | # local 14 | komodo_client.workspace = true 15 | # mogh 16 | resolver_api.workspace = true 17 | serror.workspace = true 18 | # external 19 | tokio-tungstenite.workspace = true 20 | serde_json.workspace = true 21 | serde_qs.workspace = true 22 | reqwest.workspace = true 23 | tracing.workspace = true 24 | anyhow.workspace = true 25 | rustls.workspace = true 26 | tokio.workspace = true 27 | serde.workspace = true -------------------------------------------------------------------------------- /client/periphery/rs/src/api/image.rs: -------------------------------------------------------------------------------- 1 | use komodo_client::entities::{ 2 | docker::image::{Image, ImageHistoryResponseItem}, 3 | update::Log, 4 | }; 5 | use resolver_api::Resolve; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | // 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize, Resolve)] 11 | #[response(Image)] 12 | #[error(serror::Error)] 13 | pub struct InspectImage { 14 | pub name: String, 15 | } 16 | 17 | // 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize, Resolve)] 20 | #[response(Vec)] 21 | #[error(serror::Error)] 22 | pub struct ImageHistory { 23 | pub name: String, 24 | } 25 | 26 | // 27 | 28 | #[derive(Debug, Clone, Serialize, Deserialize, Resolve)] 29 | #[response(Log)] 30 | #[error(serror::Error)] 31 | pub struct PullImage { 32 | /// The name of the image. 33 | pub name: String, 34 | /// Optional account to use to pull the image 35 | pub account: Option, 36 | /// Override registry token for account with one sent from core. 37 | pub token: Option, 38 | } 39 | 40 | // 41 | 42 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 43 | #[response(Log)] 44 | #[error(serror::Error)] 45 | pub struct DeleteImage { 46 | /// Id or name 47 | pub name: String, 48 | } 49 | 50 | // 51 | 52 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 53 | #[response(Log)] 54 | #[error(serror::Error)] 55 | pub struct PruneImages {} 56 | -------------------------------------------------------------------------------- /client/periphery/rs/src/api/network.rs: -------------------------------------------------------------------------------- 1 | use komodo_client::entities::{ 2 | docker::network::Network, update::Log, 3 | }; 4 | use resolver_api::Resolve; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | // 8 | 9 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 10 | #[response(Network)] 11 | #[error(serror::Error)] 12 | pub struct InspectNetwork { 13 | pub name: String, 14 | } 15 | 16 | // 17 | 18 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 19 | #[response(Log)] 20 | #[error(serror::Error)] 21 | pub struct CreateNetwork { 22 | pub name: String, 23 | pub driver: Option, 24 | } 25 | 26 | // 27 | 28 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 29 | #[response(Log)] 30 | #[error(serror::Error)] 31 | pub struct DeleteNetwork { 32 | /// Id or name 33 | pub name: String, 34 | } 35 | 36 | // 37 | 38 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 39 | #[response(Log)] 40 | #[error(serror::Error)] 41 | pub struct PruneNetworks {} 42 | -------------------------------------------------------------------------------- /client/periphery/rs/src/api/stats.rs: -------------------------------------------------------------------------------- 1 | use komodo_client::entities::stats::{ 2 | SystemInformation, SystemProcess, SystemStats, 3 | }; 4 | use resolver_api::Resolve; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | // 8 | 9 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 10 | #[response(SystemInformation)] 11 | #[error(serror::Error)] 12 | pub struct GetSystemInformation {} 13 | 14 | // 15 | 16 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 17 | #[response(SystemStats)] 18 | #[error(serror::Error)] 19 | pub struct GetSystemStats {} 20 | 21 | // 22 | 23 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 24 | #[response(Vec)] 25 | #[error(serror::Error)] 26 | pub struct GetSystemProcesses {} 27 | 28 | // 29 | -------------------------------------------------------------------------------- /client/periphery/rs/src/api/volume.rs: -------------------------------------------------------------------------------- 1 | use komodo_client::entities::{docker::volume::Volume, update::Log}; 2 | use resolver_api::Resolve; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | // 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize, Resolve)] 8 | #[response(Volume)] 9 | #[error(serror::Error)] 10 | pub struct InspectVolume { 11 | pub name: String, 12 | } 13 | 14 | // 15 | 16 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 17 | #[response(Log)] 18 | #[error(serror::Error)] 19 | pub struct DeleteVolume { 20 | /// Id or name 21 | pub name: String, 22 | } 23 | 24 | // 25 | 26 | #[derive(Serialize, Deserialize, Debug, Clone, Resolve)] 27 | #[response(Log)] 28 | #[error(serror::Error)] 29 | pub struct PruneVolumes {} 30 | -------------------------------------------------------------------------------- /dev.compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | core: 3 | build: 4 | context: . 5 | dockerfile: bin/core/aio.Dockerfile 6 | restart: unless-stopped 7 | logging: 8 | driver: local 9 | networks: 10 | - default 11 | environment: 12 | KOMODO_FIRST_SERVER: https://periphery:8120 13 | KOMODO_DATABASE_ADDRESS: ferretdb 14 | KOMODO_ENABLE_NEW_USERS: true 15 | KOMODO_LOCAL_AUTH: true 16 | KOMODO_JWT_SECRET: a_random_secret 17 | volumes: 18 | - repo-cache:/repo-cache 19 | 20 | periphery: 21 | build: 22 | context: . 23 | dockerfile: bin/periphery/aio.Dockerfile 24 | restart: unless-stopped 25 | logging: 26 | driver: local 27 | networks: 28 | - default 29 | volumes: 30 | - /var/run/docker.sock:/var/run/docker.sock 31 | - /proc:/proc 32 | - repos:/etc/komodo/repos 33 | - stacks:/etc/komodo/stacks 34 | environment: 35 | PERIPHERY_INCLUDE_DISK_MOUNTS: /etc/hostname 36 | 37 | ferretdb: 38 | image: ghcr.io/ferretdb/ferretdb:1 39 | restart: unless-stopped 40 | logging: 41 | driver: local 42 | networks: 43 | - default 44 | environment: 45 | - FERRETDB_HANDLER=sqlite 46 | volumes: 47 | - data:/state 48 | 49 | networks: 50 | default: {} 51 | 52 | volumes: 53 | data: 54 | repo-cache: 55 | repos: 56 | stacks: 57 | -------------------------------------------------------------------------------- /docsite/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docsite/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docsite/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docsite/docs/api.md: -------------------------------------------------------------------------------- 1 | # API and Clients 2 | 3 | Komodo Core exposes an RPC-like HTTP API to read data, write configuration, and execute actions. 4 | There are typesafe clients available in 5 | [**Rust**](/docs/api#rust-client) and [**Typescript**](/docs/api#typescript-client). 6 | 7 | The full API documentation is [**available here**](https://docs.rs/komodo_client/latest/komodo_client/api/index.html). 8 | 9 | ## Rust Client 10 | 11 | The Rust client is published to crates.io at [komodo_client](https://crates.io/crates/komodo_client). 12 | 13 | ```rust 14 | let komodo = KomodoClient::new("https://demo.komo.do", "your_key", "your_secret") 15 | .with_healthcheck() 16 | .await?; 17 | 18 | let stacks = komodo.read(ListStacks::default()).await?; 19 | 20 | let update = komodo 21 | .execute(DeployStack { 22 | stack: stacks[0].name.clone(), 23 | stop_time: None 24 | }) 25 | .await?; 26 | ``` 27 | 28 | ## Typescript Client 29 | 30 | The Typescript client is published to NPM at [komodo_client](https://www.npmjs.com/package/komodo_client). 31 | 32 | ```ts 33 | import { KomodoClient, Types } from "komodo_client"; 34 | 35 | const komodo = KomodoClient("https://demo.komo.do", { 36 | type: "api-key", 37 | params: { 38 | key: "your_key", 39 | secret: "your secret", 40 | }, 41 | }); 42 | 43 | // Inferred as Types.StackListItem[] 44 | const stacks = await komodo.read("ListStacks", {}); 45 | 46 | // Inferred as Types.Update 47 | const update = await komodo.execute("DeployStack", { 48 | stack: stacks[0].name, 49 | }); 50 | ``` 51 | -------------------------------------------------------------------------------- /docsite/docs/build-images/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: /build-images 3 | --- 4 | 5 | # Building Images 6 | 7 | Komodo builds docker images by cloning the source repository from the configured git provider, running `docker build`, 8 | and pushing the resulting image to the configured docker registry. Any repo containing a `Dockerfile` is buildable using this method. 9 | 10 | ```mdx-code-block 11 | import DocCardList from '@theme/DocCardList'; 12 | 13 | 14 | ``` -------------------------------------------------------------------------------- /docsite/docs/build-images/pre-build.md: -------------------------------------------------------------------------------- 1 | # Pre-build command 2 | 3 | Sometimes a command needs to be run before running ```docker build```, you can configure this in the *pre build* section. 4 | 5 | There are two fields to pass for *pre build*. the first is *path*, which changes the working directory. To run the command in the root of the repo, just pass ```.```. The second field is *command*, this is the shell command to be executed after the repo is cloned. 6 | 7 | For example, say your repo had a folder in it called ```scripts``` with a shell script ```on-clone.sh```. You would give *path* as ```scripts``` and command as ```sh on-clone.sh```. Or you could make *path* just ```.``` and then the command would be ```sh scripts/on-clone.sh```. Either way works fine. -------------------------------------------------------------------------------- /docsite/docs/build-images/versioning.md: -------------------------------------------------------------------------------- 1 | # Image Versioning 2 | 3 | Komodo uses a major.minor.patch versioning scheme to Build versioning. By default, every RunBuild will auto increment the Build's version patch number, and push the image to docker hub with the version tag, as well as the `latest` tag. A tag containing the latest short commit hash at the time the repo was cloned will also be created. 4 | 5 | You can also turn off the auto incrementing feature, and manage the version yourself. In addition, you can configure a "version tag" on the build. This will postfix the version tag / commit hash tag with a custom label. For example, an image tag of `dev` will produce tags like `image_name:1.1.1-dev` and `image_name:h3c87c-dev`. -------------------------------------------------------------------------------- /docsite/docs/deploy-containers/index.mdx: -------------------------------------------------------------------------------- 1 | # Deploy Containers 2 | 3 | Komodo can deploy any docker images that it can access with the configured docker accounts. 4 | It works by parsing the deployment configuration into a `docker run` command, which is then run on the target system. 5 | The configuration is stored on MongoDB, and records of all actions (update config, deploy, stop, etc.) are stored as well. 6 | 7 | ```mdx-code-block 8 | import DocCardList from '@theme/DocCardList'; 9 | 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /docsite/docs/deploy-containers/lifetime-management.md: -------------------------------------------------------------------------------- 1 | # Container Management 2 | 3 | The lifetime of a docker container is more like a virtual machine. They can be created, started, stopped, and destroyed. Komodo will display the state of the container and provides an API to manage all your container's lifetimes. 4 | 5 | This is achieved internally by running the appropriate docker command for the requested action (docker stop, docker start, etc). 6 | 7 | ### Stopping a Container 8 | 9 | Sometimes you want to stop a running application but preserve its logs and configuration, either to be restarted later or to view the logs at a later time. It is more like *pausing* the application with its current config, as no configuration (like environment variable, volume mounts, etc.) will be changed when the container is started again. 10 | 11 | Note that in order to restart an application with updated configuration, it must be *redeployed*. stopping and starting a container again will keep all configuration as it was when the container was initially created. 12 | 13 | ### Container Redeploy 14 | 15 | Redeploying is the action of destroying a container and recreating it. If you update deployment config, these changes will not take effect until the container is redeployed. Just note this will destroy the previous containers logs along with the container itself. -------------------------------------------------------------------------------- /docsite/docs/file-paths.md: -------------------------------------------------------------------------------- 1 | # File Paths 2 | 3 | When working with Komodo, you might have to configure file or directory paths. 4 | 5 | ## Relative Paths 6 | 7 | Where possible, it is better to use relative file paths. Using relative file paths removes the connection between the process being run and the particular server it runs on, making it easier to move things between servers. 8 | 9 | Where you see relative paths: 10 | 11 | - setting the build directory and path of the Dockerfile 12 | - setting a pre build command path 13 | 14 | For all of the above, the path can be given relative to the root of the configured repo 15 | 16 | The one exception is the Dockerfile path, which is given relative to the build directory (This is done by Docker itself, and this pattern matches usage of the Docker CLI). 17 | 18 | There are 3 kinds of paths to pass: 19 | 20 | 1. to specify the root of the repo, use `.` as the path 21 | 2. to specify a folder in the repo, pass it with **no** preceding `/`. For example, `example_folder` or `folder1/folder2` 22 | 3. to specify an absolute path on the servers filesystem, use a preceding slash, eg. `/home/ubuntu/example`. This way should only be used if absolutely necessary, like when passing host paths when configuring docker volumes. 23 | 24 | ### Implementation 25 | 26 | Relative file paths are joined with the path of the repo on the system using a Rust [PathBuf](https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.push). 27 | 28 | ## Docker Volume Paths 29 | 30 | These are passed directly to the Docker CLI using `--volume /path/on/system:/path/in/container`. So for these, the same rules apply as when using Docker on the command line. Paths here should usually be given as absolute. It's also probably best to avoid usage of `~` or environment variables like `$HOME`, as this may lead to unexpected behavior. 31 | -------------------------------------------------------------------------------- /docsite/docs/other-resources.md: -------------------------------------------------------------------------------- 1 | # Other Resources 2 | 3 | ### Posts and Guides 4 | - [Migrating to Komodo](https://blog.foxxmd.dev/posts/migrating-to-komodo) by [FoxxMD](https://github.com/FoxxMD) 5 | - [FAQ, Tips, and Tricks](https://blog.foxxmd.dev/posts/komodo-tips-tricks) by [FoxxMD](https://github.com/FoxxMD) 6 | - [Compose Environments Explained](https://blog.foxxmd.dev/posts/compose-envs-explained) by [FoxxMD](https://github.com/FoxxMD) 7 | - [How To: Automate version updates for your self-hosted Docker containers with Gitea, Renovate, and Komodo](https://nickcunningh.am/blog/how-to-automate-version-updates-for-your-self-hosted-docker-containers-with-gitea-renovate-and-komodo) by [TheNickOfTime](https://github.com/TheNickOfTime) 8 | - [Setting up Komodo, comparison to Portainer, and FAQ](https://skyblog.one/komodo-the-better-alternative-to-portainer-for-container-management) by [Skyfay](https://skyblog.one/authors/) 9 | ### Community Alerters 10 | These provide alerting implementations which can be used with the `Custom` Alerter type. 11 | - [Discord](https://github.com/FoxxMD/deploy-discord-alerter) by [FoxxMD](https://github.com/FoxxMD) 12 | - [Telegram](https://github.com/mattsmallman/komodo-alert-to-telgram) by [mattsmallman](https://github.com/mattsmallman) 13 | - [Ntfy](https://github.com/FoxxMD/deploy-ntfy-alerter) by [FoxxMD](https://github.com/FoxxMD) 14 | - [Gotify](https://github.com/FoxxMD/deploy-gotify-alerter) by [FoxxMD](https://github.com/FoxxMD) 15 | - [Apprise](https://github.com/FoxxMD/deploy-apprise-alerter) by [FoxxMD](https://github.com/FoxxMD) -------------------------------------------------------------------------------- /docsite/docs/setup/ferretdb.mdx: -------------------------------------------------------------------------------- 1 | # FerretDB 2 | 3 | :::info 4 | - If you setup Komodo using **Postgres** or **Sqlite** options prior to [**Komodo v1.18.0**](https://github.com/moghtech/komodo/releases/tag/v1.18.0), you are using **FerretDB v1**. 5 | - Komodo now uses **FerretDB v2**. For existing users, [**upgrading requires a migration**](https://github.com/moghtech/komodo/blob/main/bin/util/docs/copy-database.md#ferretdb-v2-update-guide). 6 | ::: 7 | 8 | [**FerretDB**](https://www.ferretdb.com) is a MongoDB-compatible database backed by [Postgres + DocumentDB extension](https://github.com/microsoft/documentdb). 9 | It is a solid option with performance comparable to MongoDB, and can also be run on some systems which [do not support MongoDB](https://github.com/moghtech/komodo/issues/59). 10 | 11 | 1. Copy `komodo/ferretdb.compose.yaml` and `komodo/compose.env` to your host: 12 | ```bash 13 | wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/ferretdb.compose.yaml && \ 14 | wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/compose.env 15 | ``` 16 | 2. Edit the variables in `komodo/compose.env`. 17 | 3. Deploy: 18 | 19 | ```bash 20 | docker compose -p komodo -f komodo/ferretdb.compose.yaml --env-file komodo/compose.env up -d 21 | ``` 22 | 23 | ```mdx-code-block 24 | import ComposeAndEnv from "@site/src/components/ComposeAndEnv"; 25 | 26 | 27 | ``` -------------------------------------------------------------------------------- /docsite/docs/setup/index.mdx: -------------------------------------------------------------------------------- 1 | # Setup Komodo 2 | 3 | To run Komodo, you will need Docker. See [the docker install docs](https://docs.docker.com/engine/install/). 4 | 5 | ### Deploy with Docker Compose 6 | 7 | - [**Using MongoDB**](./mongo.mdx) 8 | - Lower CPU usage, Higher RAM usage. 9 | - Some systems [do not support running the latest MongoDB versions](https://github.com/moghtech/komodo/issues/59). 10 | - [**Using FerretDB** (Postgres)](./ferretdb.mdx) 11 | - Lower RAM usage, Higher CPU usage. 12 | 13 | :::info 14 | **FerretDB v1** users: 15 | There is an [**upgrade guide for FerretDB v2** available here](https://github.com/moghtech/komodo/blob/main/bin/util/docs/copy-database.md#ferretdb-v2-update-guide). 16 | ::: 17 | 18 | ### First login 19 | 20 | Core should now be accessible on the specified port and navigating to `http://
:` will display the login page. 21 | Enter your preferred admin username and password, and click **"Sign Up"**, _not_ "Log In", to create your admin user for Komodo. 22 | Any additional users to create accounts will be disabled by default, and must be enabled by an admin. 23 | 24 | ### Https 25 | 26 | Komodo Core only supports http, so a reverse proxy like [caddy](https://caddyserver.com/) should be used for https. 27 | 28 | ```mdx-code-block 29 | import DocCardList from '@theme/DocCardList'; 30 | 31 | 32 | ``` 33 | -------------------------------------------------------------------------------- /docsite/docs/setup/mongo.mdx: -------------------------------------------------------------------------------- 1 | # MongoDB 2 | 3 | [**MongoDB**](https://www.mongodb.com) is the traditional database for Komodo. 4 | Komodo Core communicates with the database using the MongoDB driver. 5 | 6 | 1. Copy `komodo/mongo.compose.yaml` and `komodo/compose.env` to your host: 7 | ```bash 8 | wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/mongo.compose.yaml && \ 9 | wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/compose.env 10 | ``` 11 | 2. Edit the variables in `komodo/compose.env`. 12 | 3. Deploy: 13 | 14 | ```bash 15 | docker compose -p komodo -f komodo/mongo.compose.yaml --env-file komodo/compose.env up -d 16 | ``` 17 | 18 | ```mdx-code-block 19 | import ComposeAndEnv from "@site/src/components/ComposeAndEnv"; 20 | 21 | 22 | ``` -------------------------------------------------------------------------------- /docsite/docs/version-upgrades.md: -------------------------------------------------------------------------------- 1 | # Version Upgrades 2 | 3 | Most version upgrades only require a redeployment of the Core container after pulling the latest version, and are fully backward compatible with the periphery clients, which may be updated later on as convenient. This is the default, and will be the case unless specifically mentioned in the [version release notes](https://github.com/moghtech/komodo/releases). 4 | 5 | Some Core API upgrades may change behavior such as building / cloning, and require updating the Periphery binaries to match the Core version before this functionality can be restored. This will be specifically mentioned in the release notes. 6 | 7 | Additionally, some Core API upgrades may include database schema changes, and require a database migration. This can be accomplished by using the [komodo migrator](https://github.com/moghtech/komodo/blob/main/bin/migrator/README.md) for the particular version upgrade before upgrading the Core API container. -------------------------------------------------------------------------------- /docsite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docsite", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "^3.5.2", 19 | "@docusaurus/preset-classic": "^3.5.2", 20 | "@mdx-js/react": "^3.0.1", 21 | "clsx": "^2.1.1", 22 | "prism-react-renderer": "^2.3.1", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1" 25 | }, 26 | "devDependencies": { 27 | "@docusaurus/module-type-aliases": "^3.5.2", 28 | "@docusaurus/tsconfig": "^3.5.2", 29 | "@docusaurus/types": "^3.5.2", 30 | "dotenv": "^16.4.5", 31 | "typescript": "^5.4.5" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.5%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 3 chrome version", 41 | "last 3 firefox version", 42 | "last 5 safari version" 43 | ] 44 | }, 45 | "engines": { 46 | "node": ">=18.0" 47 | }, 48 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 49 | } 50 | -------------------------------------------------------------------------------- /docsite/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | docs: [ 15 | "intro", 16 | "resources", 17 | { 18 | type: "category", 19 | label: "Setup Komodo Core", 20 | link: { 21 | type: "doc", 22 | id: "setup/index", 23 | }, 24 | items: [ 25 | "setup/mongo", 26 | "setup/ferretdb", 27 | "setup/advanced", 28 | ], 29 | }, 30 | "connect-servers", 31 | { 32 | type: "category", 33 | label: "Build Images", 34 | link: { 35 | type: "doc", 36 | id: "build-images/index", 37 | }, 38 | items: [ 39 | "build-images/configuration", 40 | "build-images/pre-build", 41 | "build-images/builders", 42 | "build-images/versioning", 43 | ], 44 | }, 45 | { 46 | type: "category", 47 | label: "Deploy Containers", 48 | link: { 49 | type: "doc", 50 | id: "deploy-containers/index", 51 | }, 52 | items: [ 53 | "deploy-containers/configuration", 54 | "deploy-containers/lifetime-management", 55 | // "deploy-containers/choosing-builder", 56 | // "deploy-containers/versioning", 57 | ], 58 | }, 59 | "docker-compose", 60 | "variables", 61 | "procedures", 62 | "permissioning", 63 | "sync-resources", 64 | "webhooks", 65 | "version-upgrades", 66 | "api", 67 | "development", 68 | "other-resources" 69 | ], 70 | }; 71 | 72 | export default sidebars; 73 | -------------------------------------------------------------------------------- /docsite/src/components/ComposeAndEnv.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RemoteCodeFile from "./RemoteCodeFile"; 3 | import Tabs from "@theme/Tabs"; 4 | import TabItem from "@theme/TabItem"; 5 | 6 | export default function ComposeAndEnv({ 7 | file_name, 8 | }: { 9 | file_name: string; 10 | }) { 11 | return ( 12 | 13 | 14 | 19 | 20 | 21 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /docsite/src/components/Divider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Divider() { 4 | return ( 5 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /docsite/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | description: JSX.Element; 8 | }; 9 | 10 | const FeatureList: FeatureItem[] = [ 11 | { 12 | title: "Automated builds 🛠️", 13 | description: ( 14 | <> 15 | Build auto versioned docker images from git repos, trigger builds on 16 | git push 17 | 18 | ), 19 | }, 20 | { 21 | title: "Deploy docker containers 🚀", 22 | description: ( 23 | <> 24 | Deploy containers, deploy docker compose, see uptime and logs across all 25 | your servers 26 | 27 | ), 28 | }, 29 | { 30 | title: "Powered by Rust 🦀", 31 | description: <>The core API and periphery agent are written in Rust, 32 | }, 33 | ]; 34 | 35 | function Feature({title, description}: FeatureItem) { 36 | return ( 37 |
38 |
39 | {title} 40 |

{description}

41 |
42 |
43 | ); 44 | } 45 | 46 | export default function HomepageFeatures(): JSX.Element { 47 | return ( 48 |
49 |
50 |
51 | {FeatureList.map((props, idx) => ( 52 | 53 | ))} 54 |
55 |
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /docsite/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 4rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docsite/src/components/KomodoLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function KomodoLogo({ width = "4rem" }) { 4 | return ( 5 | monitor-lizard 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /docsite/src/components/RemoteCodeFile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import CodeBlock from "@theme/CodeBlock"; 3 | 4 | async function fetch_text_set(url: string, set: (text: string) => void) { 5 | const res = await fetch(url); 6 | const text = await res.text(); 7 | set(text); 8 | } 9 | 10 | export default function RemoteCodeFile({ 11 | url, 12 | language, 13 | title, 14 | }: { 15 | url: string; 16 | language?: string; 17 | title?: string; 18 | }) { 19 | const [file, setFile] = useState(""); 20 | useEffect(() => { 21 | fetch_text_set(url, setFile); 22 | }, []); 23 | return ( 24 | 25 | {file} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /docsite/src/components/SummaryImg.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function SummaryImg() { 4 | return ( 5 |
6 | monitor-summary 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /docsite/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /docsite/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: grid; 21 | gap: 1rem; 22 | grid-template-columns: 1fr 1fr; 23 | width: fit-content; 24 | } 25 | -------------------------------------------------------------------------------- /docsite/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/docsite/static/.nojekyll -------------------------------------------------------------------------------- /docsite/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/docsite/static/img/favicon.ico -------------------------------------------------------------------------------- /docsite/static/img/komodo-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/docsite/static/img/komodo-512x512.png -------------------------------------------------------------------------------- /docsite/static/img/monitor-lizard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/docsite/static/img/monitor-lizard.png -------------------------------------------------------------------------------- /docsite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/alerter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "alerter" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | # local 14 | komodo_client.workspace = true 15 | logger.workspace = true 16 | # external 17 | tokio.workspace = true 18 | tracing.workspace = true 19 | axum.workspace = true 20 | anyhow.workspace = true 21 | serde.workspace = true 22 | dotenvy.workspace = true 23 | envy.workspace = true -------------------------------------------------------------------------------- /example/alerter/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.87.0 as builder 2 | WORKDIR /builder 3 | 4 | COPY . . 5 | 6 | RUN cargo build -p alert_logger --release 7 | 8 | FROM gcr.io/distroless/debian-cc 9 | 10 | COPY --from=builder /builder/target/release/alert_logger / 11 | 12 | EXPOSE 7000 13 | 14 | CMD ["./alert_logger"] -------------------------------------------------------------------------------- /example/alerter/README.md: -------------------------------------------------------------------------------- 1 | # Alerter 2 | 3 | This crate sets up a basic axum server that listens for incoming alert POSTs. 4 | It can be used as a Komodo alerting endpoint, and serves as a template for other custom alerter implementations. -------------------------------------------------------------------------------- /example/alerter/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate tracing; 3 | 4 | use std::{net::SocketAddr, str::FromStr}; 5 | 6 | use anyhow::Context; 7 | use axum::{routing::post, Json, Router}; 8 | use komodo_client::entities::alert::{Alert, SeverityLevel}; 9 | use serde::Deserialize; 10 | 11 | /// Entrypoint for handling each incoming alert. 12 | async fn handle_incoming_alert(Json(alert): Json) { 13 | if alert.resolved { 14 | info!("Alert Resolved!: {alert:?}"); 15 | return; 16 | } 17 | match alert.level { 18 | SeverityLevel::Ok => info!("{alert:?}"), 19 | SeverityLevel::Warning => warn!("{alert:?}"), 20 | SeverityLevel::Critical => error!("{alert:?}"), 21 | } 22 | } 23 | 24 | /// ======================== 25 | /// Http server boilerplate. 26 | /// ======================== 27 | 28 | #[derive(Deserialize)] 29 | struct Env { 30 | #[serde(default = "default_port")] 31 | port: u16, 32 | } 33 | 34 | fn default_port() -> u16 { 35 | 7000 36 | } 37 | 38 | async fn app() -> anyhow::Result<()> { 39 | dotenvy::dotenv().ok(); 40 | logger::init(&Default::default())?; 41 | 42 | let Env { port } = 43 | envy::from_env().context("failed to parse env")?; 44 | 45 | let socket_addr = SocketAddr::from_str(&format!("0.0.0.0:{port}")) 46 | .context("invalid socket addr")?; 47 | 48 | info!("v {} | {socket_addr}", env!("CARGO_PKG_VERSION")); 49 | 50 | let app = Router::new().route("/", post(handle_incoming_alert)); 51 | 52 | let listener = tokio::net::TcpListener::bind(socket_addr) 53 | .await 54 | .context("failed to bind tcp listener")?; 55 | 56 | axum::serve(listener, app).await.context("server crashed") 57 | } 58 | 59 | #[tokio::main] 60 | async fn main() -> anyhow::Result<()> { 61 | let mut term_signal = tokio::signal::unix::signal( 62 | tokio::signal::unix::SignalKind::terminate(), 63 | )?; 64 | 65 | let app = tokio::spawn(app()); 66 | 67 | tokio::select! { 68 | res = app => return res?, 69 | _ = term_signal.recv() => {}, 70 | } 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /example/update_logger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "update_logger" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | # local 14 | komodo_client.workspace = true 15 | logger.workspace = true 16 | # external 17 | tokio.workspace = true 18 | tracing.workspace = true 19 | anyhow.workspace = true -------------------------------------------------------------------------------- /example/update_logger/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.87.0 as builder 2 | WORKDIR /builder 3 | 4 | COPY . . 5 | 6 | RUN cargo build -p update_logger --release 7 | 8 | FROM gcr.io/distroless/debian-cc 9 | 10 | COPY --from=builder /builder/target/release/update_logger / 11 | 12 | EXPOSE 7000 13 | 14 | CMD ["./update_logger"] -------------------------------------------------------------------------------- /example/update_logger/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate tracing; 3 | 4 | use komodo_client::{ws::UpdateWsMessage, KomodoClient}; 5 | 6 | /// Entrypoint for handling each incoming update. 7 | async fn handle_incoming_update(update: UpdateWsMessage) { 8 | info!("{update:?}"); 9 | } 10 | 11 | /// ======================== 12 | /// Ws Listener boilerplate. 13 | /// ======================== 14 | 15 | async fn app() -> anyhow::Result<()> { 16 | logger::init(&Default::default())?; 17 | 18 | info!("v {}", env!("CARGO_PKG_VERSION")); 19 | 20 | let komodo = 21 | KomodoClient::new_from_env()?.with_healthcheck().await?; 22 | 23 | let (mut rx, _) = komodo.subscribe_to_updates()?; 24 | 25 | loop { 26 | let update = match rx.recv().await { 27 | Ok(msg) => msg, 28 | Err(e) => { 29 | error!("🚨 recv error | {e:?}"); 30 | break; 31 | } 32 | }; 33 | handle_incoming_update(update).await 34 | } 35 | 36 | Ok(()) 37 | } 38 | 39 | #[tokio::main] 40 | async fn main() -> anyhow::Result<()> { 41 | let mut term_signal = tokio::signal::unix::signal( 42 | tokio::signal::unix::SignalKind::terminate(), 43 | )?; 44 | 45 | let app = tokio::spawn(app()); 46 | 47 | tokio::select! { 48 | res = app => return res?, 49 | _ = term_signal.recv() => {}, 50 | } 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /expose.compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | core: 3 | ports: 4 | - 9120:9120 5 | environment: 6 | KOMODO_FIRST_SERVER: http://periphery:8120 -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": [ 14 | "warn", 15 | { allowConstantExport: true }, 16 | ], 17 | "@typescript-eslint/no-explicit-any": "off", 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.12-alpine AS builder 2 | 3 | WORKDIR /builder 4 | 5 | COPY ./frontend ./frontend 6 | COPY ./client/core/ts ./client 7 | 8 | # Optionally specify a specific Komodo host. 9 | ARG VITE_KOMODO_HOST="" 10 | ENV VITE_KOMODO_HOST=$VITE_KOMODO_HOST 11 | 12 | # Build and link the client 13 | RUN cd client && yarn && yarn build && yarn link 14 | RUN cd frontend && yarn link komodo_client && yarn && yarn build 15 | 16 | # Copy just the static frontend to scratch image 17 | FROM scratch 18 | 19 | COPY --from=builder /builder/frontend/dist /frontend 20 | 21 | LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo 22 | LABEL org.opencontainers.image.description="Komodo Frontend" 23 | LABEL org.opencontainers.image.licenses=GPL-3.0 -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Komodo Frontend 2 | 3 | Komodo JS stack uses Yarn + Vite + React + Tailwind + shadcn/ui 4 | 5 | ## Setup Dev Environment 6 | 7 | The frontend depends on the local package `komodo_client` located at `/client/core/ts`. 8 | This must first be built and prepared for yarn link. 9 | 10 | The following command should setup everything up (run with /frontend as working directory): 11 | 12 | ```sh 13 | cd ../client/core/ts && yarn && yarn build && yarn link && \ 14 | cd ../../../frontend && yarn link komodo_client && yarn 15 | ``` 16 | 17 | You can make a new file `.env.development` (gitignored) which holds: 18 | ```sh 19 | VITE_KOMODO_HOST=https://demo.komo.do 20 | ``` 21 | You can point it to any Komodo host you like, including the demo. 22 | 23 | Now you can start the dev frontend server: 24 | ```sh 25 | yarn dev 26 | ``` -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/", 15 | "utils": "@lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Komodo 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/frontend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/client/responses.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /frontend/public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/frontend/public/favicon-96x96.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/icons/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | -------------------------------------------------------------------------------- /frontend/public/komodo-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/frontend/public/komodo-192x192.png -------------------------------------------------------------------------------- /frontend/public/komodo-2q2code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/frontend/public/komodo-2q2code.png -------------------------------------------------------------------------------- /frontend/public/komodo-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/frontend/public/komodo-512x512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Komodo", 3 | "short_name": "Komodo", 4 | "icons": [ 5 | { 6 | "src": "/komodo-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/komodo-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#000000", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /frontend/public/monitor-lizard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moghtech/komodo/4165e253325ed63c35b99efd8fc55362d7c4089f/frontend/public/monitor-lizard.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/components/alert/index.tsx: -------------------------------------------------------------------------------- 1 | import { Section } from "@components/layouts"; 2 | import { alert_level_intention } from "@lib/color"; 3 | import { useRead, useLocalStorage } from "@lib/hooks"; 4 | import { Types } from "komodo_client"; 5 | import { Button } from "@ui/button"; 6 | import { AlertTriangle } from "lucide-react"; 7 | import { AlertsTable } from "./table"; 8 | import { StatusBadge } from "@components/util"; 9 | 10 | export const OpenAlerts = () => { 11 | const [open, setOpen] = useLocalStorage("open-alerts-v0", true); 12 | const alerts = useRead("ListAlerts", { query: { resolved: false } }).data 13 | ?.alerts; 14 | if (!alerts || alerts.length === 0) return null; 15 | return ( 16 |
} 19 | actions={ 20 | 23 | } 24 | > 25 | {open && } 26 |
27 | ); 28 | }; 29 | 30 | export const AlertLevel = ({ 31 | level, 32 | }: { 33 | level: Types.SeverityLevel | undefined; 34 | }) => { 35 | if (!level) return null; 36 | return ; 37 | }; 38 | -------------------------------------------------------------------------------- /frontend/src/components/alert/table.tsx: -------------------------------------------------------------------------------- 1 | import { Types } from "komodo_client"; 2 | import { DataTable } from "@ui/data-table"; 3 | import { AlertLevel } from "."; 4 | import { AlertDetailsDialog } from "./details"; 5 | import { UsableResource } from "@types"; 6 | import { ResourceLink } from "@components/resources/common"; 7 | import { 8 | alert_level_intention, 9 | text_color_class_by_intention, 10 | } from "@lib/color"; 11 | 12 | export const AlertsTable = ({ 13 | alerts, 14 | showResolved, 15 | }: { 16 | alerts: Types.Alert[]; 17 | showResolved?: boolean; 18 | }) => { 19 | return ( 20 | 27 | row.original._id?.$oid && ( 28 | 29 | ), 30 | }, 31 | { 32 | header: "Resource", 33 | cell: ({ row }) => { 34 | const type = row.original.target.type as UsableResource; 35 | return ; 36 | }, 37 | }, 38 | showResolved && { 39 | header: "Status", 40 | cell: ({ row }) => { 41 | return ( 42 |
49 | {row.original.resolved ? "RESOLVED" : "OPEN"} 50 |
51 | ); 52 | }, 53 | }, 54 | { 55 | header: "Level", 56 | cell: ({ row }) => , 57 | }, 58 | { 59 | header: "Alert Type", 60 | accessorKey: "data.type", 61 | }, 62 | ]} 63 | /> 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /frontend/src/components/inspect.tsx: -------------------------------------------------------------------------------- 1 | import { Types } from "komodo_client"; 2 | import { Loader2 } from "lucide-react"; 3 | import { MonacoEditor } from "./monaco"; 4 | 5 | export const InspectContainerView = ({ 6 | container, 7 | error, 8 | isPending, 9 | isError, 10 | }: { 11 | container: Types.Container | undefined; 12 | error: unknown; 13 | isPending: boolean; 14 | isError: boolean; 15 | }) => { 16 | if (isPending) { 17 | return ( 18 |
19 | 20 |
21 | ); 22 | } 23 | if (isError) { 24 | return ( 25 |
26 |

Failed to inspect container.

27 | {(error ?? undefined) && ( 28 | 33 | )} 34 |
35 | ); 36 | } 37 | return ( 38 |
39 | 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /frontend/src/components/keys/table.tsx: -------------------------------------------------------------------------------- 1 | import { CopyButton } from "@components/util"; 2 | import { Types } from "komodo_client"; 3 | import { DataTable } from "@ui/data-table"; 4 | import { Input } from "@ui/input"; 5 | import { ReactNode } from "react"; 6 | 7 | const ONE_DAY_MS = 1000 * 60 * 60 * 24; 8 | 9 | export const KeysTable = ({ 10 | keys, 11 | DeleteKey, 12 | }: { 13 | keys: Types.ApiKey[]; 14 | DeleteKey: (params: { api_key: string }) => ReactNode; 15 | }) => { 16 | return ( 17 | { 29 | return ( 30 |
31 | 36 | 37 |
38 | ); 39 | }, 40 | }, 41 | { 42 | header: "Expires", 43 | accessorFn: ({ expires }) => 44 | expires 45 | ? "In " + 46 | ((expires - Date.now()) / ONE_DAY_MS).toFixed() + 47 | " Days" 48 | : "Never", 49 | }, 50 | { 51 | header: "Delete", 52 | cell: ({ row }) => , 53 | }, 54 | ]} 55 | /> 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /frontend/src/components/resources/action/table.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable, SortableHeader } from "@ui/data-table"; 2 | import { TableTags } from "@components/tags"; 3 | import { ResourceLink } from "../common"; 4 | import { ActionComponents } from "."; 5 | import { Types } from "komodo_client"; 6 | import { useSelectedResources } from "@lib/hooks"; 7 | 8 | export const ActionTable = ({ 9 | actions, 10 | }: { 11 | actions: Types.ActionListItem[]; 12 | }) => { 13 | const [_, setSelectedResources] = useSelectedResources("Action"); 14 | 15 | return ( 16 | name, 21 | onSelect: setSelectedResources, 22 | }} 23 | columns={[ 24 | { 25 | accessorKey: "name", 26 | header: ({ column }) => ( 27 | 28 | ), 29 | cell: ({ row }) => ( 30 | 31 | ), 32 | }, 33 | { 34 | accessorKey: "info.state", 35 | header: ({ column }) => ( 36 | 37 | ), 38 | cell: ({ row }) => , 39 | }, 40 | { 41 | header: "Tags", 42 | cell: ({ row }) => , 43 | }, 44 | ]} 45 | /> 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /frontend/src/components/resources/alerter/table.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable, SortableHeader } from "@ui/data-table"; 2 | import { ResourceLink } from "../common"; 3 | import { TableTags } from "@components/tags"; 4 | import { Types } from "komodo_client"; 5 | import { useSelectedResources } from "@lib/hooks"; 6 | 7 | export const AlerterTable = ({ 8 | alerters, 9 | }: { 10 | alerters: Types.AlerterListItem[]; 11 | }) => { 12 | const [_, setSelectedResources] = useSelectedResources("Alerter"); 13 | return ( 14 | name, 19 | onSelect: setSelectedResources, 20 | }} 21 | columns={[ 22 | { 23 | accessorKey: "name", 24 | header: ({ column }) => ( 25 | 26 | ), 27 | cell: ({ row }) => ( 28 | 29 | ), 30 | }, 31 | { 32 | accessorKey: "info.endpoint_type", 33 | header: ({ column }) => ( 34 | 35 | ), 36 | }, 37 | { 38 | accessorKey: "info.enabled", 39 | header: ({ column }) => ( 40 | 41 | ), 42 | }, 43 | { 44 | header: "Tags", 45 | cell: ({ row }) => , 46 | }, 47 | ]} 48 | /> 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /frontend/src/components/resources/builder/table.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable, SortableHeader } from "@ui/data-table"; 2 | import { ResourceLink } from "../common"; 3 | import { TableTags } from "@components/tags"; 4 | import { BuilderInstanceType } from "."; 5 | import { Types } from "komodo_client"; 6 | import { useSelectedResources } from "@lib/hooks"; 7 | 8 | export const BuilderTable = ({ 9 | builders, 10 | }: { 11 | builders: Types.BuilderListItem[]; 12 | }) => { 13 | const [_, setSelectedResources] = useSelectedResources("Builder"); 14 | return ( 15 | name, 20 | onSelect: setSelectedResources, 21 | }} 22 | columns={[ 23 | { 24 | accessorKey: "name", 25 | header: ({ column }) => ( 26 | 27 | ), 28 | cell: ({ row }) => ( 29 | 30 | ), 31 | }, 32 | { 33 | accessorKey: "info.builder_type", 34 | header: ({ column }) => ( 35 | 36 | ), 37 | }, 38 | { 39 | accessorKey: "info.instance_type", 40 | header: ({ column }) => ( 41 | 42 | ), 43 | cell: ({ row }) => , 44 | }, 45 | { 46 | header: "Tags", 47 | cell: ({ row }) => , 48 | }, 49 | ]} 50 | /> 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /frontend/src/components/resources/deployment/config/components/restart.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigItem } from "@components/config/util"; 2 | import { Types } from "komodo_client"; 3 | import { 4 | Select, 5 | SelectContent, 6 | SelectItem, 7 | SelectTrigger, 8 | SelectValue, 9 | } from "@ui/select"; 10 | import { object_keys } from "@lib/utils"; 11 | 12 | const format_mode = (m: string) => m.split("-").join(" "); 13 | 14 | export const RestartModeSelector = ({ 15 | selected, 16 | set, 17 | disabled, 18 | }: { 19 | selected: Types.RestartMode | undefined; 20 | set: (input: Partial) => void; 21 | disabled: boolean; 22 | }) => ( 23 | 28 | 50 | 51 | ); 52 | -------------------------------------------------------------------------------- /frontend/src/components/resources/deployment/inspect.tsx: -------------------------------------------------------------------------------- 1 | import { usePermissions, useRead } from "@lib/hooks"; 2 | import { ReactNode } from "react"; 3 | import { Types } from "komodo_client"; 4 | import { Section } from "@components/layouts"; 5 | import { InspectContainerView } from "@components/inspect"; 6 | 7 | export const DeploymentInspect = ({ 8 | id, 9 | titleOther, 10 | }: { 11 | id: string; 12 | titleOther: ReactNode; 13 | }) => { 14 | const { specific } = usePermissions({ type: "Deployment", id }); 15 | if (!specific.includes(Types.SpecificPermission.Inspect)) { 16 | return ( 17 |
18 |
19 |

User does not have permission to inspect this Deployment.

20 |
21 |
22 | ); 23 | } 24 | return ( 25 |
26 | 27 |
28 | ); 29 | }; 30 | 31 | const DeploymentInspectInner = ({ id }: { id: string }) => { 32 | const { 33 | data: container, 34 | error, 35 | isPending, 36 | isError, 37 | } = useRead("InspectDeploymentContainer", { 38 | deployment: id, 39 | }); 40 | return ( 41 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /frontend/src/components/resources/index.tsx: -------------------------------------------------------------------------------- 1 | import { RequiredResourceComponents, UsableResource } from "@types"; 2 | import { AlerterComponents } from "./alerter"; 3 | import { BuildComponents } from "./build"; 4 | import { BuilderComponents } from "./builder"; 5 | import { DeploymentComponents } from "./deployment"; 6 | import { RepoComponents } from "./repo"; 7 | import { ServerComponents } from "./server"; 8 | import { ProcedureComponents } from "./procedure/index"; 9 | import { ResourceSyncComponents } from "./resource-sync"; 10 | import { StackComponents } from "./stack"; 11 | import { ActionComponents } from "./action"; 12 | 13 | export const ResourceComponents: { 14 | [key in UsableResource]: RequiredResourceComponents; 15 | } = { 16 | Server: ServerComponents, 17 | Stack: StackComponents, 18 | Deployment: DeploymentComponents, 19 | Build: BuildComponents, 20 | Repo: RepoComponents, 21 | Procedure: ProcedureComponents, 22 | Action: ActionComponents, 23 | ResourceSync: ResourceSyncComponents, 24 | Builder: BuilderComponents, 25 | Alerter: AlerterComponents, 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/src/components/resources/procedure/table.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable, SortableHeader } from "@ui/data-table"; 2 | import { TableTags } from "@components/tags"; 3 | import { ResourceLink } from "../common"; 4 | import { ProcedureComponents } from "."; 5 | import { Types } from "komodo_client"; 6 | import { useSelectedResources } from "@lib/hooks"; 7 | 8 | export const ProcedureTable = ({ 9 | procedures, 10 | }: { 11 | procedures: Types.ProcedureListItem[]; 12 | }) => { 13 | const [_, setSelectedResources] = useSelectedResources("Procedure"); 14 | 15 | return ( 16 | name, 21 | onSelect: setSelectedResources, 22 | }} 23 | columns={[ 24 | { 25 | accessorKey: "name", 26 | header: ({ column }) => ( 27 | 28 | ), 29 | cell: ({ row }) => ( 30 | 31 | ), 32 | }, 33 | { 34 | accessorKey: "info.stages", 35 | header: ({ column }) => ( 36 | 37 | ), 38 | }, 39 | { 40 | accessorKey: "info.state", 41 | header: ({ column }) => ( 42 | 43 | ), 44 | cell: ({ row }) => , 45 | }, 46 | { 47 | header: "Tags", 48 | cell: ({ row }) => , 49 | }, 50 | ]} 51 | /> 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /frontend/src/components/resources/repo/table.tsx: -------------------------------------------------------------------------------- 1 | import { DataTable, SortableHeader } from "@ui/data-table"; 2 | import { ResourceLink } from "../common"; 3 | import { TableTags } from "@components/tags"; 4 | import { RepoComponents } from "."; 5 | import { Types } from "komodo_client"; 6 | import { useSelectedResources } from "@lib/hooks"; 7 | 8 | export const RepoTable = ({ repos }: { repos: Types.RepoListItem[] }) => { 9 | const [_, setSelectedResources] = useSelectedResources("Repo"); 10 | 11 | return ( 12 | name, 17 | onSelect: setSelectedResources, 18 | }} 19 | columns={[ 20 | { 21 | accessorKey: "name", 22 | header: ({ column }) => ( 23 | 24 | ), 25 | cell: ({ row }) => , 26 | size: 200, 27 | }, 28 | { 29 | accessorKey: "info.repo", 30 | header: ({ column }) => ( 31 | 32 | ), 33 | size: 200, 34 | }, 35 | { 36 | accessorKey: "info.branch", 37 | header: ({ column }) => ( 38 | 39 | ), 40 | size: 200, 41 | }, 42 | { 43 | accessorKey: "info.state", 44 | header: ({ column }) => ( 45 | 46 | ), 47 | cell: ({ row }) => , 48 | size: 120, 49 | }, 50 | { 51 | header: "Tags", 52 | cell: ({ row }) => , 53 | }, 54 | ]} 55 | /> 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /frontend/src/components/resources/server/hooks.ts: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from "@lib/hooks"; 2 | import { Types } from "komodo_client"; 3 | import { useAtom } from "jotai"; 4 | 5 | const statsGranularityAtom = atomWithStorage( 6 | "stats-granularity-v0", 7 | Types.Timelength.FiveMinutes 8 | ); 9 | 10 | export const useStatsGranularity = () => useAtom(statsGranularityAtom); 11 | -------------------------------------------------------------------------------- /frontend/src/components/resources/server/info/containers.tsx: -------------------------------------------------------------------------------- 1 | import { DockerContainersSection } from "@components/util"; 2 | import { useRead } from "@lib/hooks"; 3 | import { ReactNode } from "react"; 4 | 5 | export const Containers = ({ 6 | id, 7 | titleOther 8 | }: { 9 | id: string; 10 | titleOther: ReactNode 11 | }) => { 12 | const containers = 13 | useRead("ListDockerContainers", { server: id }, { refetchInterval: 10_000 }) 14 | .data ?? []; 15 | return ( 16 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/components/users/new.tsx: -------------------------------------------------------------------------------- 1 | import { NewLayout } from "@components/layouts"; 2 | import { useInvalidate, useWrite } from "@lib/hooks"; 3 | import { Input } from "@ui/input"; 4 | import { useToast } from "@ui/use-toast"; 5 | import { useState } from "react"; 6 | 7 | export const NewUserGroup = () => { 8 | const { toast } = useToast(); 9 | const inv = useInvalidate(); 10 | const { mutateAsync } = useWrite("CreateUserGroup", { 11 | onSuccess: () => { 12 | inv(["ListUserGroups"]); 13 | toast({ title: "Created User Group" }); 14 | }, 15 | }); 16 | const [name, setName] = useState(""); 17 | return ( 18 | mutateAsync({ name })} 21 | enabled={!!name} 22 | onOpenChange={() => setName("")} 23 | > 24 |
25 | Name 26 | setName(e.target.value)} 30 | /> 31 |
32 |
33 | ); 34 | }; 35 | 36 | export const NewServiceUser = () => { 37 | const { toast } = useToast(); 38 | const inv = useInvalidate(); 39 | const { mutateAsync } = useWrite("CreateServiceUser", { 40 | onSuccess: () => { 41 | inv(["ListUsers"]); 42 | toast({ title: "Created Service User" }); 43 | }, 44 | }); 45 | const [username, setUsername] = useState(""); 46 | return ( 47 | mutateAsync({ username, description: "" })} 50 | enabled={!!username} 51 | onOpenChange={() => setUsername("")} 52 | > 53 |
54 | Username 55 | setUsername(e.target.value)} 59 | /> 60 |
61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import "globals.css"; 2 | import ReactDOM from "react-dom/client"; 3 | import { ThemeProvider } from "@ui/theme"; 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | import { Router } from "@router"; 6 | import { WebsocketProvider } from "@lib/socket"; 7 | import { Toaster } from "@ui/toaster"; 8 | import { atomWithStorage } from "@lib/hooks"; 9 | // Run monaco setup 10 | import "./monaco"; 11 | import { init_monaco } from "./monaco/init"; 12 | 13 | export const AUTH_TOKEN_STORAGE_KEY = "komodo-auth-token"; 14 | 15 | export const KOMODO_BASE_URL = 16 | import.meta.env.VITE_KOMODO_HOST ?? location.origin; 17 | 18 | export const UPDATE_WS_URL = 19 | KOMODO_BASE_URL.replace("http", "ws") + "/ws/update"; 20 | 21 | const query_client = new QueryClient({ 22 | defaultOptions: { queries: { retry: false } }, 23 | }); 24 | 25 | export type HomeView = "Dashboard" | "Tree" | "Resources"; 26 | 27 | export const homeViewAtom = atomWithStorage( 28 | "home-view-v1", 29 | "Dashboard" 30 | ); 31 | 32 | init_monaco().then(() => 33 | ReactDOM.createRoot(document.getElementById("root")!).render( 34 | // 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | // 44 | ) 45 | ); 46 | -------------------------------------------------------------------------------- /frontend/src/monaco/index.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from "monaco-editor"; 2 | import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; 3 | import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; 4 | import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; 5 | import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; 6 | import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; 7 | 8 | self.MonacoEnvironment = { 9 | getWorker(_, label) { 10 | if (label === "json") { 11 | return new jsonWorker(); 12 | } 13 | if (label === "css" || label === "scss" || label === "less") { 14 | return new cssWorker(); 15 | } 16 | if (label === "html" || label === "handlebars" || label === "razor") { 17 | return new htmlWorker(); 18 | } 19 | if (label === "typescript" || label === "javascript") { 20 | return new tsWorker(); 21 | } 22 | return new editorWorker(); 23 | }, 24 | }; 25 | 26 | import { loader } from "@monaco-editor/react"; 27 | loader.config({ monaco }); 28 | 29 | // Load the themes 30 | import "./theme"; 31 | // Load the parsers 32 | import "./yaml"; 33 | import "./toml"; 34 | import "./shell" 35 | import "./key_value"; 36 | import "./string_list"; 37 | -------------------------------------------------------------------------------- /frontend/src/monaco/init.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from "monaco-editor"; 2 | 3 | export async function init_monaco() { 4 | const promises = ["lib", "responses", "types"].map((file) => 5 | Promise.all( 6 | [".js", ".d.ts"].map((extension) => 7 | fetch(`/client/${file}${extension}`) 8 | .then((res) => res.text()) 9 | .then((dts) => 10 | monaco.languages.typescript.typescriptDefaults.addExtraLib( 11 | dts, 12 | `file:///client/${file}${extension}` 13 | ) 14 | ) 15 | ) 16 | ) 17 | ); 18 | promises.push( 19 | Promise.all( 20 | ["index.d.ts", "deno.d.ts"].map((file) => 21 | fetch(`/${file}`) 22 | .then((res) => res.text()) 23 | .then((dts) => 24 | monaco.languages.typescript.typescriptDefaults.addExtraLib( 25 | dts, 26 | `file:///${file}` 27 | ) 28 | ) 29 | ) 30 | ) 31 | ); 32 | 33 | await Promise.all(promises); 34 | 35 | type ExtraOptions = { 36 | allowTopLevelAwait?: boolean; 37 | moduleDetection?: "force" | "auto" | "legacy" | 3 | 2 | 1; // string or numeric enum 38 | }; 39 | 40 | monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ 41 | module: monaco.languages.typescript.ModuleKind.ESNext, 42 | target: monaco.languages.typescript.ScriptTarget.ESNext, 43 | allowNonTsExtensions: true, 44 | moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, 45 | typeRoots: ["index.d.ts"], 46 | allowTopLevelAwait: true, 47 | moduleDetection: "force", 48 | } as monaco.languages.typescript.CompilerOptions & ExtraOptions); 49 | 50 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ 51 | diagnosticCodesToIgnore: [ 52 | // Allows top level await 53 | 1375, 54 | // Allows top level return 55 | 1108, 56 | ], 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/monaco/string_list.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from "monaco-editor"; 2 | 3 | const string_list_conf: monaco.languages.LanguageConfiguration = { 4 | comments: { 5 | lineComment: "#", 6 | }, 7 | autoClosingPairs: [ 8 | { open: '"', close: '"' }, 9 | { open: "'", close: "'" }, 10 | ], 11 | surroundingPairs: [ 12 | { open: '"', close: '"' }, 13 | { open: "'", close: "'" }, 14 | ], 15 | }; 16 | 17 | const string_list_language = { 18 | defaultToken: "", 19 | tokenPostfix: ".string_list", 20 | 21 | tokenizer: { 22 | root: [ 23 | // Comments 24 | [/#.*$/, "comment"], 25 | 26 | // Comma as a delimiter 27 | [/,/, "comment"], 28 | [/\*/, "keyword"], 29 | [/\?/, "keyword"], 30 | 31 | // Special syntax: text surrounded by \ 32 | // [/\\[^\\]*\\/, "keyword"], 33 | [/\\/, { token: "keyword", next: "@regex" }], 34 | 35 | // Main strings separated by spaces or newlines 36 | [/[^\*\?,#\\\s]+/, ""], 37 | 38 | // Whitespace 39 | [/[ \t\r\n]+/, ""], 40 | ], 41 | regex: [ 42 | // Regex tokens 43 | [/\[[^\]]*\]/, ""], // Character classes like [abc] 44 | [/[*+?\.]+/, "keyword"], // Quantifiers like *, +, ? 45 | [/\\./, "string.regexp constant.character.escape"], // Escape sequences like \d, \w 46 | [/[^\\]/, "string"], // Any other regex content 47 | [/\\/, { token: "keyword", next: "@pop" }], // Closing backslash returns to root 48 | ], 49 | }, 50 | }; 51 | 52 | // Register the custom language and configuration with Monaco 53 | monaco.languages.register({ id: "string_list" }); 54 | monaco.languages.setLanguageConfiguration("string_list", string_list_conf); 55 | monaco.languages.setMonarchTokensProvider("string_list", string_list_language); 56 | -------------------------------------------------------------------------------- /frontend/src/pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { homeViewAtom } from "@main"; 2 | import { useAtom } from "jotai"; 3 | import { Dashboard } from "./dashboard"; 4 | import { AllResources } from "./all_resources"; 5 | import { Tree } from "./tree"; 6 | import { useSetTitle } from "@lib/hooks"; 7 | 8 | export const Home = () => { 9 | useSetTitle(); 10 | const [view] = useAtom(homeViewAtom); 11 | switch (view) { 12 | case "Dashboard": 13 | return ; 14 | case "Resources": 15 | return ; 16 | case "Tree": 17 | return ; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/pages/server-info/container/inspect.tsx: -------------------------------------------------------------------------------- 1 | import { usePermissions, useRead } from "@lib/hooks"; 2 | import { ReactNode } from "react"; 3 | import { Types } from "komodo_client"; 4 | import { Section } from "@components/layouts"; 5 | import { InspectContainerView } from "@components/inspect"; 6 | 7 | export const ContainerInspect = ({ 8 | id, 9 | container, 10 | titleOther, 11 | }: { 12 | id: string; 13 | container: string; 14 | titleOther: ReactNode; 15 | }) => { 16 | const { specific } = usePermissions({ type: "Server", id }); 17 | if (!specific.includes(Types.SpecificPermission.Inspect)) { 18 | return ( 19 |
20 |
21 |

User does not have permission to inspect this Server.

22 |
23 |
24 | ); 25 | } 26 | return ( 27 |
28 | 29 |
30 | ); 31 | }; 32 | 33 | const ContainerInspectInner = ({ 34 | id, 35 | container, 36 | }: { 37 | id: string; 38 | container: string; 39 | }) => { 40 | const { 41 | data: inspect_container, 42 | error, 43 | isPending, 44 | isError, 45 | } = useRead("InspectDockerContainer", { 46 | server: id, 47 | container, 48 | }); 49 | return ( 50 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /frontend/src/pages/stack-service/inspect.tsx: -------------------------------------------------------------------------------- 1 | import { usePermissions, useRead } from "@lib/hooks"; 2 | import { ReactNode } from "react"; 3 | import { Types } from "komodo_client"; 4 | import { Section } from "@components/layouts"; 5 | import { InspectContainerView } from "@components/inspect"; 6 | 7 | export const StackServiceInspect = ({ 8 | id, 9 | service, 10 | titleOther, 11 | }: { 12 | id: string; 13 | service: string; 14 | titleOther: ReactNode; 15 | }) => { 16 | const { specific } = usePermissions({ type: "Stack", id }); 17 | if (!specific.includes(Types.SpecificPermission.Inspect)) { 18 | return ( 19 |
20 |
21 |

User does not have permission to inspect this Stack service.

22 |
23 |
24 | ); 25 | } 26 | return ( 27 |
28 | 29 |
30 | ); 31 | }; 32 | 33 | const StackServiceInspectInner = ({ 34 | id, 35 | service, 36 | }: { 37 | id: string; 38 | service: string; 39 | }) => { 40 | const { 41 | data: container, 42 | error, 43 | isPending, 44 | isError, 45 | } = useRead("InspectStackContainer", { 46 | stack: id, 47 | service, 48 | }); 49 | return ( 50 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /frontend/src/pages/user_disabled.tsx: -------------------------------------------------------------------------------- 1 | import { AUTH_TOKEN_STORAGE_KEY } from "@main"; 2 | import { Button } from "@ui/button"; 3 | import { UserX } from "lucide-react"; 4 | 5 | export const UserDisabled = () => { 6 | return ( 7 |
8 |
9 | 10 | User Not Enabled 11 | 20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/types.d.ts: -------------------------------------------------------------------------------- 1 | import { Types } from "komodo_client"; 2 | 3 | export type UsableResource = Exclude; 4 | 5 | type IdComponent = React.FC<{ id: string }>; 6 | type OptionalIdComponent = React.FC<{ id?: string }>; 7 | 8 | export interface RequiredResourceComponents { 9 | list_item: (id: string) => Types.ResourceListItem | undefined; 10 | resource_links: ( 11 | resource: Types.Resource 12 | ) => Array | undefined; 13 | 14 | Description: React.FC; 15 | 16 | /** Header for individual resource pages */ 17 | ResourcePageHeader: IdComponent; 18 | 19 | /** Summary card for use in dashboard */ 20 | Dashboard: React.FC; 21 | 22 | /** New resource button / dialog */ 23 | New: React.FC<{ server_id?: string; build_id?: string }>; 24 | 25 | /** A table component to view resource list */ 26 | Table: React.FC<{ resources: Types.ResourceListItem[] }>; 27 | 28 | /** Dropdown menu to trigger group actions for selected resources */ 29 | GroupActions: React.FC; 30 | 31 | /** Icon for the component */ 32 | Icon: OptionalIdComponent; 33 | BigIcon: OptionalIdComponent; 34 | 35 | State: IdComponent; 36 | 37 | /** status metrics, like deployment state / status */ 38 | Status: { [status: string]: IdComponent }; 39 | 40 | /** 41 | * Some config items shown in header, like deployment server /image 42 | * or build repo / branch 43 | */ 44 | Info: { [info: string]: IdComponent }; 45 | 46 | /** Action buttons */ 47 | Actions: { [action: string]: IdComponent }; 48 | 49 | /** Resource specific sections */ 50 | Page: { [section: string]: IdComponent }; 51 | 52 | /** Config component for resource */ 53 | Config: IdComponent; 54 | 55 | /** Danger zone for resource, containing eg rename, delete */ 56 | DangerZone: IdComponent; 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /frontend/src/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 3 | import { CheckIcon } from "@radix-ui/react-icons" 4 | 5 | import { cn } from "@lib/utils" 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )) 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 27 | 28 | export { Checkbox } 29 | -------------------------------------------------------------------------------- /frontend/src/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 3 | 4 | import { cn } from "@lib/utils" 5 | 6 | const HoverCard = HoverCardPrimitive.Root 7 | 8 | const HoverCardTrigger = HoverCardPrimitive.Trigger 9 | 10 | const HoverCardContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 24 | )) 25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName 26 | 27 | export { HoverCard, HoverCardTrigger, HoverCardContent } 28 | -------------------------------------------------------------------------------- /frontend/src/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /frontend/src/ui/json.tsx: -------------------------------------------------------------------------------- 1 | export const Json = ({ json }: any) => { 2 | if (!json) { 3 | return

null

; 4 | } 5 | 6 | const type = typeof json; 7 | 8 | if (type === "function") { 9 | return

??function??

; 10 | } 11 | 12 | // null case 13 | if (type === "undefined") { 14 | return

null

; 15 | } 16 | 17 | // base cases 18 | if ( 19 | type === "bigint" || 20 | type === "boolean" || 21 | type === "number" || 22 | type === "string" || 23 | type === "symbol" 24 | ) { 25 | return

{json}

; 26 | } 27 | 28 | // Type is object or array 29 | if (Array.isArray(json)) { 30 | return ( 31 |
32 | {(json as any[]).map((json) => ( 33 | 34 | ))} 35 |
36 | ); 37 | } 38 | 39 | return ( 40 |
41 | {Object.keys(json).map((key) => ( 42 |
43 |

{key}

: 44 |
45 | ))} 46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /frontend/src/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /frontend/src/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as PopoverPrimitive from "@radix-ui/react-popover" 3 | 4 | import { cn } from "@lib/utils" 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverAnchor = PopoverPrimitive.Anchor 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 32 | -------------------------------------------------------------------------------- /frontend/src/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ProgressPrimitive from "@radix-ui/react-progress" 3 | 4 | import { cn } from "@lib/utils" 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 18 | 22 | 23 | )) 24 | Progress.displayName = ProgressPrimitive.Root.displayName 25 | 26 | export { Progress } 27 | -------------------------------------------------------------------------------- /frontend/src/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@lib/utils" 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref 13 | ) => ( 14 | 25 | ) 26 | ) 27 | Separator.displayName = SeparatorPrimitive.Root.displayName 28 | 29 | export { Separator } 30 | -------------------------------------------------------------------------------- /frontend/src/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SwitchPrimitives from "@radix-ui/react-switch" 3 | 4 | import { cn } from "@lib/utils" 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )) 25 | Switch.displayName = SwitchPrimitives.Root.displayName 26 | 27 | export { Switch } 28 | -------------------------------------------------------------------------------- /frontend/src/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |