├── cmd ├── serve │ ├── front │ │ ├── build │ │ │ └── .keep │ │ ├── .npmrc │ │ ├── src │ │ │ ├── lib │ │ │ │ ├── collections.ts │ │ │ │ ├── pagination.ts │ │ │ │ ├── config.ts │ │ │ │ ├── select.test.ts │ │ │ │ ├── resources │ │ │ │ │ ├── healthcheck.ts │ │ │ │ │ ├── sessions.ts │ │ │ │ │ └── users.ts │ │ │ │ ├── select.ts │ │ │ │ ├── path.ts │ │ │ │ ├── aria.ts │ │ │ │ ├── actions.ts │ │ │ │ ├── fetcher │ │ │ │ │ ├── data.test.ts │ │ │ │ │ └── set.test.ts │ │ │ │ └── curl.ts │ │ │ ├── routes │ │ │ │ ├── +layout.ts │ │ │ │ ├── +layout.svelte │ │ │ │ ├── (main) │ │ │ │ │ ├── +page.ts │ │ │ │ │ ├── jobs │ │ │ │ │ │ ├── +page.ts │ │ │ │ │ │ └── cancel-button.svelte │ │ │ │ │ ├── registries │ │ │ │ │ │ ├── +page.ts │ │ │ │ │ │ ├── [id] │ │ │ │ │ │ │ └── edit │ │ │ │ │ │ │ │ └── +page.ts │ │ │ │ │ │ ├── +error.svelte │ │ │ │ │ │ ├── new │ │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ ├── targets │ │ │ │ │ │ ├── +page.ts │ │ │ │ │ │ ├── [id] │ │ │ │ │ │ │ └── edit │ │ │ │ │ │ │ │ └── +page.ts │ │ │ │ │ │ ├── +error.svelte │ │ │ │ │ │ ├── new │ │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ ├── apps │ │ │ │ │ │ ├── new │ │ │ │ │ │ │ ├── +page.ts │ │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ │ ├── [id] │ │ │ │ │ │ │ ├── +layout.ts │ │ │ │ │ │ │ ├── +page.ts │ │ │ │ │ │ │ ├── deployments │ │ │ │ │ │ │ │ └── [number] │ │ │ │ │ │ │ │ │ ├── +page.ts │ │ │ │ │ │ │ │ │ └── +error.svelte │ │ │ │ │ │ │ └── edit │ │ │ │ │ │ │ │ └── +page.ts │ │ │ │ │ │ ├── service-url.svelte │ │ │ │ │ │ └── +error.svelte │ │ │ │ │ ├── +layout.ts │ │ │ │ │ ├── progress-bar.svelte │ │ │ │ │ ├── +layout.svelte │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── bottom-bar.svelte │ │ │ │ │ └── account.svelte │ │ │ │ └── +error.svelte │ │ │ ├── assets │ │ │ │ └── icons │ │ │ │ │ ├── menu.svelte │ │ │ │ │ ├── arrow-down.svelte │ │ │ │ │ ├── arrow-left.svelte │ │ │ │ │ ├── arrow-right.svelte │ │ │ │ │ ├── external-launch.svelte │ │ │ │ │ ├── file.svelte │ │ │ │ │ ├── archive.svelte │ │ │ │ │ └── git.svelte │ │ │ ├── components │ │ │ │ ├── page-title.svelte │ │ │ │ ├── cards-grid.svelte │ │ │ │ ├── input-help.svelte │ │ │ │ ├── cleanup-notice.svelte │ │ │ │ ├── button.svelte │ │ │ │ ├── blank-slate.svelte │ │ │ │ ├── form-errors.svelte │ │ │ │ ├── display.svelte │ │ │ │ ├── form.svelte │ │ │ │ ├── prose.svelte │ │ │ │ ├── registry-card.svelte │ │ │ │ ├── environment-card.svelte │ │ │ │ ├── input-file.svelte │ │ │ │ ├── link.svelte │ │ │ │ ├── form-section.svelte │ │ │ │ ├── loader.svelte │ │ │ │ ├── stack.svelte │ │ │ │ ├── text-area.svelte │ │ │ │ ├── checkbox.svelte │ │ │ │ ├── status-indicator.svelte │ │ │ │ ├── deployment-pill.svelte │ │ │ │ ├── service-info.svelte │ │ │ │ ├── target-card.svelte │ │ │ │ ├── text-input.svelte │ │ │ │ ├── pagination.svelte │ │ │ │ └── card.svelte │ │ │ └── app.html │ │ ├── .gitignore │ │ ├── .eslintignore │ │ ├── .prettierignore │ │ ├── .prettierrc │ │ ├── vite.config.ts │ │ ├── .eslintrc.cjs │ │ ├── tsconfig.json │ │ ├── README.md │ │ ├── svelte.config.js │ │ └── package.json │ ├── healthcheck.go │ ├── cmd.go │ ├── jobs.go │ └── sessions.go ├── version │ └── version.go └── root.go ├── examples ├── go-api │ ├── .dockerignore │ ├── Makefile │ ├── go-api.tar.gz │ ├── README.md │ ├── Dockerfile │ ├── compose.yml │ └── go.mod ├── multiple-ports │ ├── .dockerignore │ ├── Makefile │ ├── go.mod │ ├── multiple-ports.tar.gz │ ├── compose.yml │ ├── Dockerfile │ └── README.md ├── sveltekit-hello │ ├── .npmrc │ ├── .dockerignore │ ├── static │ │ └── favicon.png │ ├── Makefile │ ├── sveltekit-hello.tar.gz │ ├── compose.yml │ ├── README.md │ ├── vite.config.ts │ ├── .gitignore │ ├── Dockerfile │ ├── .eslintignore │ ├── .prettierignore │ ├── .prettierrc │ ├── src │ │ ├── app.d.ts │ │ ├── app.html │ │ └── routes │ │ │ └── +page.svelte │ ├── tsconfig.json │ ├── .eslintrc.cjs │ ├── svelte.config.js │ └── package.json ├── minimal │ ├── README.md │ └── compose.yml └── README.md ├── docs ├── seelf-home.jpeg ├── .vitepress │ └── theme │ │ ├── index.js │ │ └── custom.css ├── reference │ ├── providers.md │ ├── registries.md │ ├── faq.md │ ├── deployments.md │ ├── jobs.md │ └── api.md ├── contributing │ ├── docs.md │ ├── donating.md │ └── frontend.md ├── index.md └── guide │ └── updating.md ├── .dockerignore ├── pkg ├── domain │ ├── mod.go │ ├── action.go │ ├── interval.go │ ├── interval_test.go │ └── action_test.go ├── types │ ├── is.go │ └── is_test.go ├── id │ ├── id.go │ └── id_test.go ├── must │ ├── panic.go │ └── panic_test.go ├── flag │ ├── flag.go │ └── flag_test.go ├── crypto │ ├── random_test.go │ └── random.go ├── storage │ ├── sqlite │ │ └── builder │ │ │ ├── helpers.go │ │ │ ├── result.go │ │ │ └── loader.go │ ├── paginated.go │ ├── secret_string.go │ └── secret_string_test.go ├── validate │ ├── numbers │ │ ├── numbers.go │ │ └── numbers_test.go │ └── strings │ │ └── strings.go ├── ostools │ ├── dir.go │ └── file.go ├── ssh │ ├── private_key.go │ ├── host.go │ └── host_test.go ├── bus │ ├── sqlite │ │ └── migrations │ │ │ ├── 1702544287_create_scheduled_jobs.up.sql │ │ │ └── 1706004450_add_resource_id.up.sql │ ├── message_test.go │ └── spy │ │ └── dispatcher.go ├── monad │ └── patch.go └── event │ └── event_test.go ├── .gitignore ├── internal ├── deployment │ ├── infra │ │ ├── sqlite │ │ │ ├── migrations │ │ │ │ ├── 1699945320_remove_deployment_path.up.sql │ │ │ │ ├── 1691321458_rename_trigger_to_source.up.sql │ │ │ │ ├── 1699476081_rename_source_columns.up.sql │ │ │ │ ├── 1716482091_create_registries.up.sql │ │ │ │ ├── 1702544287_delete_worker_stuff.up.sql │ │ │ │ ├── 1714119553_add_target_entrypoints.up.sql │ │ │ │ └── 1660649329_create_apps_deployments.up.sql │ │ │ └── mod.go │ │ ├── provider │ │ │ └── docker │ │ │ │ └── body.go │ │ ├── source │ │ │ ├── raw │ │ │ │ └── data.go │ │ │ ├── archive │ │ │ │ └── data.go │ │ │ └── facade.go │ │ └── artifact │ │ │ └── logger.go │ ├── domain │ │ ├── logger.go │ │ ├── deployment_id_test.go │ │ ├── deployment_id.go │ │ ├── credentials.go │ │ ├── appname.go │ │ ├── version_control.go │ │ ├── credentials_test.go │ │ ├── source.go │ │ ├── appname_test.go │ │ └── artifact_manager.go │ ├── app │ │ ├── get_registries │ │ │ └── get_registries.go │ │ ├── get_targets │ │ │ └── get_targets.go │ │ ├── query.go │ │ ├── cleanup_target │ │ │ ├── on_target_cleanup_requested.go │ │ │ └── cleanup_target.go │ │ ├── group.go │ │ ├── delete_app │ │ │ ├── on_app_cleanup_requested.go │ │ │ └── delete_app.go │ │ ├── configure_target │ │ │ ├── on_target_created.go │ │ │ ├── on_target_state_changed.go │ │ │ ├── on_deployment_state_changed.go │ │ │ ├── on_app_env_changed.go │ │ │ └── on_app_cleanup_requested.go │ │ ├── delete_target │ │ │ ├── on_target_cleanup_requested.go │ │ │ └── delete_target.go │ │ ├── fail_pending_deployments │ │ │ ├── on_target_delete_requested.go │ │ │ ├── on_app_cleanup_requested.go │ │ │ └── on_app_env_changed.go │ │ ├── cleanup_app │ │ │ ├── on_app_env_changed.go │ │ │ └── on_app_cleanup_requested.go │ │ ├── deploy │ │ │ └── on_deployment_created.go │ │ ├── delete_registry │ │ │ └── delete_registry.go │ │ ├── reconfigure_target │ │ │ └── reconfigure_target.go │ │ ├── request_app_cleanup │ │ │ └── request_app_cleanup.go │ │ ├── get_deployment_log │ │ │ └── get_deployment_log.go │ │ ├── get_registry │ │ │ └── get_registry.go │ │ ├── request_target_cleanup │ │ │ └── request_target_cleanup.go │ │ ├── get_apps │ │ │ └── get_apps.go │ │ ├── redeploy │ │ │ ├── on_app_env_changed.go │ │ │ └── redeploy.go │ │ ├── promote │ │ │ └── promote.go │ │ └── get_app_deployments │ │ │ └── get_app_deployments.go │ └── fixture │ │ ├── registry.go │ │ └── registry_test.go └── auth │ ├── infra │ ├── sqlite │ │ ├── mod.go │ │ ├── migrations │ │ │ └── 1660649329_create_users.up.sql │ │ └── gateway.go │ ├── crypto │ │ ├── api_key_generator.go │ │ ├── api_key_generator_test.go │ │ ├── bcrypt_hasher.go │ │ └── bcrypt_hasher_test.go │ └── mod.go │ ├── domain │ ├── email.go │ ├── email_test.go │ ├── requirement.go │ ├── context.go │ └── context_test.go │ ├── app │ ├── get_profile │ │ └── get_profile.go │ ├── refresh_api_key │ │ └── refresh_api_key.go │ └── login │ │ └── login.go │ └── fixture │ ├── database_test.go │ └── user.go ├── main.go ├── package.json ├── .vscode └── launch.json ├── .github └── FUNDING.yml ├── Dockerfile ├── .releaserc ├── compose.yml ├── Makefile └── README.md /cmd/serve/front/build/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cmd/serve/front/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /examples/go-api/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | compose.yml -------------------------------------------------------------------------------- /examples/multiple-ports/.dockerignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | Dockerfile 3 | compose.yml -------------------------------------------------------------------------------- /examples/sveltekit-hello/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /examples/go-api/Makefile: -------------------------------------------------------------------------------- 1 | archive: 2 | rm -f go-api.tar.gz 3 | tar czf go-api.tar.gz * -------------------------------------------------------------------------------- /docs/seelf-home.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuukanOO/seelf/HEAD/docs/seelf-home.jpeg -------------------------------------------------------------------------------- /examples/sveltekit-hello/.dockerignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | Dockerfile 3 | compose.yml 4 | node_modules -------------------------------------------------------------------------------- /examples/go-api/go-api.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuukanOO/seelf/HEAD/examples/go-api/go-api.tar.gz -------------------------------------------------------------------------------- /examples/multiple-ports/Makefile: -------------------------------------------------------------------------------- 1 | archive: 2 | rm -rf multiple-ports.tar.gz 3 | tar czf multiple-ports.tar.gz * -------------------------------------------------------------------------------- /examples/multiple-ports/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/YuukanOO/seelf/examples/multiple-ports 2 | 3 | go 1.21.5 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | examples 2 | Dockerfile 3 | compose.yml 4 | docs 5 | node_modules 6 | **/Dockerfile 7 | **/node_modules -------------------------------------------------------------------------------- /pkg/domain/mod.go: -------------------------------------------------------------------------------- 1 | // Package domain provides a set of types commonly used in the domain layer. 2 | package domain 3 | -------------------------------------------------------------------------------- /cmd/serve/front/src/lib/collections.ts: -------------------------------------------------------------------------------- 1 | export function isSet(data: Maybe): data is T { 2 | return !!data; 3 | } 4 | -------------------------------------------------------------------------------- /examples/minimal/README.md: -------------------------------------------------------------------------------- 1 | # minimal example 2 | 3 | Minimal example to deploy multiple services and use profiles set by seelf. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | seelf 3 | .vscode 4 | !.vscode/launch.json 5 | node_modules 6 | docs/.vitepress/dist 7 | docs/.vitepress/cache -------------------------------------------------------------------------------- /cmd/serve/front/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuukanOO/seelf/HEAD/examples/sveltekit-hello/static/favicon.png -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | import "./custom.css"; 3 | 4 | export default DefaultTheme; 5 | -------------------------------------------------------------------------------- /examples/multiple-ports/multiple-ports.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuukanOO/seelf/HEAD/examples/multiple-ports/multiple-ports.tar.gz -------------------------------------------------------------------------------- /examples/sveltekit-hello/Makefile: -------------------------------------------------------------------------------- 1 | archive: 2 | rm -rf sveltekit-hello.tar.gz .svelte-kit build node_modules 3 | tar czf sveltekit-hello.tar.gz * -------------------------------------------------------------------------------- /examples/sveltekit-hello/sveltekit-hello.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YuukanOO/seelf/HEAD/examples/sveltekit-hello/sveltekit-hello.tar.gz -------------------------------------------------------------------------------- /examples/sveltekit-hello/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | restart: unless-stopped 4 | build: . 5 | ports: 6 | - '3000:3000' 7 | -------------------------------------------------------------------------------- /pkg/types/is.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Checks if the given obj is of type T. 4 | func Is[T any](obj any) bool { 5 | _, ok := obj.(T) 6 | return ok 7 | } 8 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = false; 2 | export const trailingSlash = 'always'; // Enable pretty URLs 3 | export const ssr = false; 4 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-brand-1: #10b981; 3 | --vp-c-brand-2: #34d399; 4 | 5 | --vp-button-brand-bg: var(--vp-c-brand-1); 6 | } -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /internal/deployment/infra/sqlite/migrations/1699945320_remove_deployment_path.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE deployments DROP COLUMN path; 2 | ALTER TABLE deployments DROP COLUMN state_logfile; 3 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/README.md: -------------------------------------------------------------------------------- 1 | # sveltekit-hello 2 | 3 | Tiny example of a sveltekit app. 4 | 5 | To quickly check it out, you can deploy the `sveltekit-hello.tar.gz` to your seelf instance. -------------------------------------------------------------------------------- /pkg/id/id.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | import "github.com/segmentio/ksuid" 4 | 5 | // Generates a new random unique identifier. 6 | func New[T ~string]() T { 7 | return T(ksuid.New().String()) 8 | } 9 | -------------------------------------------------------------------------------- /cmd/serve/front/src/lib/pagination.ts: -------------------------------------------------------------------------------- 1 | export type Paginated = { 2 | data: T[]; 3 | total: number; 4 | page: number; 5 | first_page: boolean; 6 | last_page: boolean; 7 | per_page: number; 8 | }; 9 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | WORKDIR /app 3 | COPY package*.json . 4 | RUN npm ci 5 | COPY . . 6 | RUN npm run build 7 | WORKDIR /app/build 8 | EXPOSE 3000 9 | CMD ["node", "index.js"] -------------------------------------------------------------------------------- /internal/deployment/infra/sqlite/migrations/1691321458_rename_trigger_to_source.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE deployments RENAME COLUMN trigger_kind TO source_kind; 2 | ALTER TABLE deployments RENAME COLUMN trigger_data TO source_data; -------------------------------------------------------------------------------- /internal/deployment/infra/sqlite/migrations/1699476081_rename_source_columns.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE deployments RENAME COLUMN source_kind TO source_discriminator; 2 | ALTER TABLE deployments RENAME COLUMN source_data TO source; -------------------------------------------------------------------------------- /cmd/serve/front/src/assets/icons/menu.svelte: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/page-title.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | seelf - {title} 8 | 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/YuukanOO/seelf/cmd" 7 | ) 8 | 9 | func main() { 10 | cmd := cmd.Root() 11 | if err := cmd.Execute(); err != nil { 12 | os.Exit(1) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cmd/serve/front/src/assets/icons/arrow-down.svelte: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /cmd/serve/front/src/assets/icons/arrow-left.svelte: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /cmd/serve/front/src/assets/icons/arrow-right.svelte: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /cmd/serve/front/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /cmd/serve/front/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /cmd/serve/front/src/lib/config.ts: -------------------------------------------------------------------------------- 1 | /** Default polling interval for "realtime" data. */ 2 | export const POLLING_INTERVAL_MS = 5000; 3 | 4 | /** Polling interval for running deployments */ 5 | export const RUNNING_DEPLOYMENT_POLLING_INTERVAL_MS = 2500; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "vitepress": "^1.1.0" 4 | }, 5 | "scripts": { 6 | "docs:dev": "vitepress dev docs", 7 | "docs:build": "vitepress build docs", 8 | "docs:preview": "vitepress preview docs" 9 | } 10 | } -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/+page.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/apps'; 2 | 3 | export const load = async ({ fetch, depends }) => { 4 | const apps = await service.fetchAll({ fetch, depends }); 5 | 6 | return { 7 | apps 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /docs/reference/providers.md: -------------------------------------------------------------------------------- 1 | # Providers 2 | 3 | Depending on which provider you choose for your [target](/reference/targets), you'll have access to different parameters. See the provider reference for more information: 4 | 5 | - [Docker](/reference/providers/docker) 6 | -------------------------------------------------------------------------------- /examples/go-api/README.md: -------------------------------------------------------------------------------- 1 | # go-api-example 2 | 3 | Tiny example with a Golang API connected to a PostgreSQL database which logs the date at which the application has been started. 4 | 5 | To quickly check it out, you can deploy the `go-api.tar.gz` to your seelf instance. 6 | -------------------------------------------------------------------------------- /pkg/must/panic.go: -------------------------------------------------------------------------------- 1 | package must 2 | 3 | // Panic if an err is given, else returns the T. This is a handy helper in a lot of 4 | // situations! 5 | func Panic[T any](value T, err error) T { 6 | if err != nil { 7 | panic(err) 8 | } 9 | 10 | return value 11 | } 12 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/jobs/+page.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/jobs'; 2 | 3 | export const load = async ({ fetch, depends }) => { 4 | const targets = await service.fetchAll(1, { fetch, depends }); 5 | 6 | return { 7 | targets 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /pkg/flag/flag.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | type Flag interface { 4 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 5 | } 6 | 7 | // Check if the given flag value has one of the given flags. 8 | func IsSet[T Flag](value, check T) bool { 9 | return value&check == check 10 | } 11 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/registries/+page.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/registries'; 2 | 3 | export const load = async ({ fetch, depends }) => { 4 | const registries = await service.fetchAll({ fetch, depends }); 5 | 6 | return { 7 | registries 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/targets/+page.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/targets'; 2 | 3 | export const load = async ({ fetch, depends }) => { 4 | const targets = await service.fetchAll(undefined, { fetch, depends }); 5 | 6 | return { 7 | targets 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Example applications 2 | 3 | Each examples provide a tiny application you can deploy with seelf to test how it behaves. 4 | 5 | [go-api](go-api/README.md) and [sveltekit-hello](sveltekit-hello/README.md) provides a `tar.gz` archive which you can deploy directly to seelf. 6 | -------------------------------------------------------------------------------- /cmd/serve/front/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/apps/new/+page.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/targets'; 2 | 3 | export const load = async ({ fetch, depends }) => { 4 | const targets = await service.fetchAll({ active_only: true }, { fetch, depends }); 5 | 6 | return { 7 | targets 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/cards-grid.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 13 | -------------------------------------------------------------------------------- /internal/auth/infra/sqlite/mod.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/YuukanOO/seelf/pkg/storage/sqlite" 7 | ) 8 | 9 | //go:embed migrations/*.sql 10 | var migrations embed.FS 11 | 12 | var Migrations = sqlite.NewMigrationsModule("auth", "migrations", migrations) 13 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /examples/minimal/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | one: 3 | restart: unless-stopped 4 | image: traefik/whoami 5 | ports: 6 | - "8888:80" 7 | 8 | two: 9 | restart: unless-stopped 10 | image: traefik/whoami 11 | ports: 12 | - "8889:80" 13 | profiles: 14 | - staging 15 | -------------------------------------------------------------------------------- /internal/deployment/infra/sqlite/mod.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/YuukanOO/seelf/pkg/storage/sqlite" 7 | ) 8 | 9 | //go:embed migrations/*.sql 10 | var migrations embed.FS 11 | 12 | var Migrations = sqlite.NewMigrationsModule("deployment", "migrations", migrations) 13 | -------------------------------------------------------------------------------- /cmd/serve/front/src/lib/select.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import select from './select'; 3 | 4 | describe('the select function', () => { 5 | test('should returns a default value if not found', () => { 6 | expect(select('foo', { default: 'default' })).toEqual('default'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /cmd/serve/front/src/assets/icons/external-launch.svelte: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /pkg/crypto/random_test.go: -------------------------------------------------------------------------------- 1 | package crypto_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/YuukanOO/seelf/pkg/assert" 7 | "github.com/YuukanOO/seelf/pkg/crypto" 8 | ) 9 | 10 | func Test_RandomKey(t *testing.T) { 11 | key, err := crypto.RandomKey[string](32) 12 | assert.Nil(t, err) 13 | assert.HasNRunes(t, 32, key) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/builder/helpers.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import "github.com/YuukanOO/seelf/pkg/storage" 4 | 5 | // Tiny mapper when all you have to do is retrieve a single value from a query. 6 | func valueMapper[T any](scanner storage.Scanner) (value T, err error) { 7 | err = scanner.Scan(&value) 8 | return value, err 9 | } 10 | -------------------------------------------------------------------------------- /cmd/serve/front/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /internal/deployment/domain/logger.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "io" 4 | 5 | // Specific logger interface use by deployment jobs to document the deployment process. 6 | type DeploymentLogger interface { 7 | io.WriteCloser 8 | 9 | Stepf(string, ...any) 10 | Warnf(string, ...any) 11 | Infof(string, ...any) 12 | Error(error) 13 | } 14 | -------------------------------------------------------------------------------- /examples/multiple-ports/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: . 4 | command: -web 8080,8081,8082 -tcp 8083,8084,8085 -udp 8086,8087,8088 5 | ports: 6 | - "8080:8080" 7 | - "8081-8082:8081-8082" 8 | - "8083:8083/tcp" 9 | - "8084-8085:8084-8085/tcp" 10 | - "8086:8086/udp" 11 | - "8087-8088:8087-8088/udp" -------------------------------------------------------------------------------- /docs/contributing/docs.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | The documentation you're seeing right now is built using [Vitepress](https://vitepress.dev). If you find something to fix or want to improve it, feel free to submit a **pull request**! 4 | 5 | ## Useful commands 6 | 7 | - `make serve-docs`: Serve the Vitepress dev server 8 | - `make build-docs`: Build the documentation 9 | -------------------------------------------------------------------------------- /pkg/storage/paginated.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | // Represents a paginated data set. 4 | type Paginated[T any] struct { 5 | Data []T `json:"data"` 6 | Page int `json:"page"` 7 | IsFirstPage bool `json:"first_page"` 8 | IsLastPage bool `json:"last_page"` 9 | PerPage int `json:"per_page"` 10 | Total int `json:"total"` 11 | } 12 | -------------------------------------------------------------------------------- /docs/contributing/donating.md: -------------------------------------------------------------------------------- 1 | # Donating 2 | 3 | If you wish to **contribute with some money** to help me find more time to work on this project, feel free to do so on: 4 | 5 | - [Github Sponsors](https://github.com/sponsors/YuukanOO) 6 | - [Liberapay](https://liberapay.com/YuukanOO) 7 | - [Buy Me A Coffee](https://buymeacoffee.com/yuukanoo) 8 | 9 | Thanks ❤️ ! 10 | -------------------------------------------------------------------------------- /examples/multiple-ports/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine AS builder 2 | WORKDIR /app 3 | COPY go.* ./ 4 | RUN go mod download 5 | COPY . . 6 | RUN go build -o app main.go 7 | 8 | FROM alpine:3.16 AS runner 9 | RUN addgroup --system app 10 | RUN adduser --system app 11 | USER app 12 | WORKDIR /app 13 | COPY --from=builder --chown=app:app /app/app ./ 14 | ENTRYPOINT ["./app"] -------------------------------------------------------------------------------- /internal/deployment/app/get_registries/get_registries.go: -------------------------------------------------------------------------------- 1 | package get_registries 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/internal/deployment/app/get_registry" 5 | "github.com/YuukanOO/seelf/pkg/bus" 6 | ) 7 | 8 | type Query struct { 9 | bus.Query[[]get_registry.Registry] 10 | } 11 | 12 | func (Query) Name_() string { return "deployment.query.get_registries" } 13 | -------------------------------------------------------------------------------- /examples/go-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine AS builder 2 | WORKDIR /app 3 | COPY go.* ./ 4 | RUN go mod download 5 | COPY . . 6 | RUN go build -o api main.go 7 | 8 | FROM alpine:3.16 AS runner 9 | RUN addgroup --system app 10 | RUN adduser --system app 11 | USER app 12 | WORKDIR /app 13 | COPY --from=builder --chown=app:app /app/api ./ 14 | EXPOSE 8080 15 | CMD ["./api"] -------------------------------------------------------------------------------- /docs/reference/registries.md: -------------------------------------------------------------------------------- 1 | # Custom registries 2 | 3 | If you need to deploy images hosted on private registries (such as ones using the [registry](https://hub.docker.com/_/registry) image), you can declare them on **seelf** on the appropriate page. 4 | 5 | Registries are **shared** across [targets](/reference/targets) and are used during the deployment process as soon as they are configured. 6 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/apps/[id]/+layout.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/apps'; 2 | import { error } from '@sveltejs/kit'; 3 | 4 | export const load = async ({ params, fetch, depends }) => { 5 | try { 6 | const app = await service.fetchById(params.id, { fetch, depends }); 7 | 8 | return { 9 | app 10 | }; 11 | } catch { 12 | throw error(404); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/apps/[id]/+page.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/apps'; 2 | import { error } from '@sveltejs/kit'; 3 | 4 | export const load = async ({ params, fetch, depends }) => { 5 | try { 6 | const app = await service.fetchById(params.id, { fetch, depends }); 7 | 8 | return { 9 | app 10 | }; 11 | } catch { 12 | throw error(404); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /pkg/validate/numbers/numbers.go: -------------------------------------------------------------------------------- 1 | package numbers 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/pkg/apperr" 5 | "github.com/YuukanOO/seelf/pkg/validate" 6 | ) 7 | 8 | var ErrMin = apperr.New("min") 9 | 10 | func Min(minValue int) validate.Validator[int] { 11 | return func(value int) error { 12 | if value < minValue { 13 | return ErrMin 14 | } 15 | 16 | return nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/+layout.ts: -------------------------------------------------------------------------------- 1 | import auth from '$lib/auth'; 2 | import service from '$lib/resources/healthcheck'; 3 | 4 | export const load = async ({ fetch }) => { 5 | const user = await auth.checkSession({ fetch, cache: 'no-cache' }); // Don't know why, the cache option is mandatory here 6 | const health = await service.check({ fetch }); 7 | 8 | return { user, health }; 9 | }; 10 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/targets/[id]/edit/+page.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/targets'; 2 | import { error } from '@sveltejs/kit'; 3 | 4 | export const load = async ({ params, fetch, depends }) => { 5 | try { 6 | const target = await service.fetchById(params.id, { fetch, depends }); 7 | 8 | return { 9 | target 10 | }; 11 | } catch { 12 | throw error(404); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/input-help.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/registries/[id]/edit/+page.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/registries'; 2 | import { error } from '@sveltejs/kit'; 3 | 4 | export const load = async ({ params, fetch, depends }) => { 5 | try { 6 | const registry = await service.fetchById(params.id, { fetch, depends }); 7 | 8 | return { 9 | registry 10 | }; 11 | } catch { 12 | throw error(404); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /internal/auth/infra/sqlite/migrations/1660649329_create_users.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id TEXT NOT NULL, 3 | email TEXT NOT NULL, 4 | password_hash TEXT NOT NULL, 5 | api_key TEXT NOT NULL, 6 | registered_at DATETIME NOT NULL, 7 | 8 | CONSTRAINT pk_users PRIMARY KEY(id), 9 | CONSTRAINT unique_users_email UNIQUE(email), 10 | CONSTRAINT unique_users_api_key UNIQUE(api_key) 11 | ); 12 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/cleanup-notice.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if requested_at} 9 | 10 |

{l.translate('cleanup_requested.description', [requested_at])}

11 |
12 | {/if} 13 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/apps/service-url.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if scheme} 12 | {scheme}{prefix}{appName}{suffix}.{host} 13 | {:else} 14 | - ({l.translate('target.manual_proxy')}) 15 | {/if} 16 | -------------------------------------------------------------------------------- /cmd/serve/healthcheck.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/cmd/version" 5 | "github.com/YuukanOO/seelf/pkg/http" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type healthCheckResponse struct { 10 | Version string `json:"version"` 11 | } 12 | 13 | func (s *server) healthcheckHandler(ctx *gin.Context) { 14 | _ = http.Ok(ctx, healthCheckResponse{ 15 | Version: version.Current(), 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /docs/reference/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently asked question 2 | 3 | ## How does seelf know which services to expose from a `compose.yml` file? 4 | 5 | See the [providers page](/reference/providers/docker#exposing-services) for more information. 6 | 7 | ## Integrating seelf in your Continuous Integration / Continuous Deployment pipeline 8 | 9 | See the [CI / CD page](/guide/continuous-integration-deployment) for more information on how to do it. 10 | -------------------------------------------------------------------------------- /internal/deployment/app/get_targets/get_targets.go: -------------------------------------------------------------------------------- 1 | package get_targets 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/internal/deployment/app/get_target" 5 | "github.com/YuukanOO/seelf/pkg/bus" 6 | ) 7 | 8 | // Retrieve all available targets. 9 | type Query struct { 10 | bus.Query[[]get_target.Target] 11 | 12 | ActiveOnly bool `form:"active_only"` 13 | } 14 | 15 | func (Query) Name_() string { return "deployment.query.get_targets" } 16 | -------------------------------------------------------------------------------- /pkg/ostools/dir.go: -------------------------------------------------------------------------------- 1 | package ostools 2 | 3 | import "os" 4 | 5 | // Tiny wrapper around the default os.MkdirAll but apply standard permissions. 6 | func MkdirAll(path string) error { 7 | return os.MkdirAll(path, defaultPermissions) 8 | } 9 | 10 | // Totally removes and recreates the given path. 11 | func EmptyDir(path string) error { 12 | if err := os.RemoveAll(path); err != nil { 13 | return err 14 | } 15 | 16 | return MkdirAll(path) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/id/id_test.go: -------------------------------------------------------------------------------- 1 | package id_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/YuukanOO/seelf/pkg/assert" 7 | "github.com/YuukanOO/seelf/pkg/id" 8 | ) 9 | 10 | type someDomainID string 11 | 12 | func Test_ID_GeneratesANonEmptyUniqueIdentifier(t *testing.T) { 13 | id1 := id.New[someDomainID]() 14 | id2 := id.New[someDomainID]() 15 | 16 | assert.HasNRunes(t, 27, id1) 17 | assert.HasNRunes(t, 27, id2) 18 | assert.NotEqual(t, id1, id2) 19 | } 20 | -------------------------------------------------------------------------------- /cmd/serve/front/src/assets/icons/file.svelte: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /cmd/serve/front/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit()], 7 | server: { 8 | proxy: { 9 | // During development, proxy api requests to the go server to make things transparent 10 | '/api/v1': 'http://127.0.0.1:8080' 11 | } 12 | }, 13 | test: { 14 | include: ['src/**/*.{test,spec}.{js,ts}'] 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /cmd/serve/front/src/assets/icons/archive.svelte: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /internal/auth/infra/crypto/api_key_generator.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/internal/auth/domain" 5 | "github.com/YuukanOO/seelf/pkg/crypto" 6 | ) 7 | 8 | const apiKeyLengthInBytes = 64 9 | 10 | type keyGenerator struct{} 11 | 12 | func NewKeyGenerator() domain.KeyGenerator { 13 | return &keyGenerator{} 14 | } 15 | 16 | func (*keyGenerator) Generate() (domain.APIKey, error) { 17 | return crypto.RandomKey[domain.APIKey](apiKeyLengthInBytes) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/ssh/private_key.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/pkg/apperr" 5 | "golang.org/x/crypto/ssh" 6 | ) 7 | 8 | var ErrInvalidSSHKey = apperr.New("invalid_ssh_key") 9 | 10 | type PrivateKey string 11 | 12 | // Parses a raw ssh private key. 13 | func ParsePrivateKey(value string) (PrivateKey, error) { 14 | if _, err := ssh.ParsePrivateKey([]byte(value)); err != nil { 15 | return "", ErrInvalidSSHKey 16 | } 17 | 18 | return PrivateKey(value), nil 19 | } 20 | -------------------------------------------------------------------------------- /internal/auth/domain/email.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "net/mail" 5 | 6 | "github.com/YuukanOO/seelf/pkg/apperr" 7 | ) 8 | 9 | var ErrInvalidEmail = apperr.New("invalid_email") 10 | 11 | type Email string 12 | 13 | // Try to constructs an email from a raw string 14 | func EmailFrom(value string) (Email, error) { 15 | addr, err := mail.ParseAddress(value) 16 | 17 | if err != nil { 18 | return "", ErrInvalidEmail 19 | } 20 | 21 | return Email(addr.Address), nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/auth/infra/crypto/api_key_generator_test.go: -------------------------------------------------------------------------------- 1 | package crypto_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/YuukanOO/seelf/internal/auth/infra/crypto" 7 | "github.com/YuukanOO/seelf/pkg/assert" 8 | ) 9 | 10 | func Test_KeyGenerator(t *testing.T) { 11 | t.Run("should generate an API key", func(t *testing.T) { 12 | generator := crypto.NewKeyGenerator() 13 | key, err := generator.Generate() 14 | 15 | assert.Nil(t, err) 16 | assert.HasNRunes(t, 64, key) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/button.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | {l.translate(text)} 15 | 16 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/blank-slate.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /cmd/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "runtime/debug" 5 | ) 6 | 7 | var version = "2.4.2" 8 | 9 | // Retrieve the currentVersion version with additional vcs info if any. 10 | func Current() string { 11 | var suffix string 12 | 13 | if info, ok := debug.ReadBuildInfo(); ok { 14 | for _, setting := range info.Settings { 15 | if setting.Key == "vcs.revision" { 16 | suffix = "-" + setting.Value 17 | break 18 | } 19 | } 20 | } 21 | 22 | return version + suffix 23 | } 24 | -------------------------------------------------------------------------------- /internal/auth/domain/email_test.go: -------------------------------------------------------------------------------- 1 | package domain_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/YuukanOO/seelf/internal/auth/domain" 7 | "github.com/YuukanOO/seelf/pkg/assert" 8 | ) 9 | 10 | func Test_Email_ValidatesAnEmail(t *testing.T) { 11 | r, err := domain.EmailFrom("") 12 | 13 | assert.Equal(t, "", r) 14 | assert.ErrorIs(t, domain.ErrInvalidEmail, err) 15 | 16 | r, err = domain.EmailFrom("agood@email.com") 17 | 18 | assert.Equal(t, "agood@email.com", r) 19 | assert.Nil(t, err) 20 | } 21 | -------------------------------------------------------------------------------- /internal/deployment/infra/provider/docker/body.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import "github.com/YuukanOO/seelf/pkg/monad" 4 | 5 | // Request payload when wanting to instantiate a ProviderConfig 6 | // If the Host field is omitted, the provider will consider it's a local target. 7 | type Body struct { 8 | Host monad.Maybe[string] `json:"host"` 9 | Port monad.Maybe[int] `json:"port"` 10 | User monad.Maybe[string] `json:"user"` 11 | PrivateKey monad.Patch[string] `json:"private_key"` 12 | } 13 | -------------------------------------------------------------------------------- /examples/go-api/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | restart: unless-stopped 4 | build: . 5 | environment: 6 | - DSN=postgres://app:apppa55word@db/app?sslmode=disable 7 | depends_on: 8 | - db 9 | ports: 10 | - "8080:8080" 11 | db: 12 | restart: unless-stopped 13 | image: postgres:14-alpine 14 | volumes: 15 | - dbdata:/var/lib/postgresql/data 16 | environment: 17 | - POSTGRES_USER=app 18 | - POSTGRES_PASSWORD=apppa55word 19 | volumes: 20 | dbdata: 21 | -------------------------------------------------------------------------------- /pkg/bus/sqlite/migrations/1702544287_create_scheduled_jobs.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE scheduled_jobs ( 2 | id TEXT NOT NULL, 3 | dedupe_name TEXT NOT NULL, 4 | message_name TEXT NOT NULL, 5 | message_data TEXT NOT NULL, 6 | policy INTEGER NOT NULL, 7 | queued_at DATETIME NOT NULL, 8 | errcode TEXT NULL, 9 | retrieved BOOLEAN NOT NULL DEFAULT false, 10 | 11 | CONSTRAINT pk_scheduled_jobs PRIMARY KEY(id) 12 | ); 13 | 14 | CREATE INDEX idx_scheduled_jobs_dedupe_name ON scheduled_jobs(dedupe_name); 15 | -------------------------------------------------------------------------------- /internal/auth/domain/requirement.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type EmailRequirement struct { 4 | email Email 5 | unique bool 6 | } 7 | 8 | func NewEmailRequirement(email Email, unique bool) EmailRequirement { 9 | return EmailRequirement{ 10 | email: email, 11 | unique: unique, 12 | } 13 | } 14 | 15 | func (e EmailRequirement) Error() error { 16 | if !e.unique { 17 | return ErrEmailAlreadyTaken 18 | } 19 | 20 | return nil 21 | } 22 | 23 | func (e EmailRequirement) Met() (Email, error) { return e.email, e.Error() } 24 | -------------------------------------------------------------------------------- /internal/deployment/app/query.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "github.com/YuukanOO/seelf/pkg/monad" 4 | 5 | type ( 6 | UserSummary struct { 7 | ID string `json:"id"` 8 | Email string `json:"email"` 9 | } 10 | 11 | TargetSummary struct { 12 | ID string `json:"id"` 13 | Name string `json:"name"` 14 | Url monad.Maybe[string] `json:"url"` 15 | } 16 | 17 | LatestDeployments[T any] struct { 18 | Production monad.Maybe[T] `json:"production"` 19 | Staging monad.Maybe[T] `json:"staging"` 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /internal/deployment/infra/sqlite/migrations/1716482091_create_registries.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE registries ( 2 | id TEXT NOT NULL 3 | ,name TEXT NOT NULL 4 | ,url TEXT NOT NULL 5 | ,credentials_username TEXT NULL 6 | ,credentials_password TEXT NULL 7 | ,created_at DATETIME NOT NULL 8 | ,created_by TEXT NOT NULL 9 | ,CONSTRAINT pk_registries PRIMARY KEY(id) 10 | ,CONSTRAINT unique_registries_url UNIQUE(url) 11 | ,CONSTRAINT fk_registries_created_by FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE CASCADE 12 | ); -------------------------------------------------------------------------------- /pkg/types/is_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/YuukanOO/seelf/pkg/assert" 7 | "github.com/YuukanOO/seelf/pkg/types" 8 | ) 9 | 10 | type ( 11 | type1 struct{} 12 | type2 struct{} 13 | ) 14 | 15 | func Test_Is(t *testing.T) { 16 | t.Run("should be able to return if an object is of a given type", func(t *testing.T) { 17 | var ( 18 | t1 any = type1{} 19 | t2 any = type2{} 20 | ) 21 | 22 | assert.True(t, types.Is[type1](t1)) 23 | assert.False(t, types.Is[type1](t2)) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /internal/deployment/app/cleanup_target/on_target_cleanup_requested.go: -------------------------------------------------------------------------------- 1 | package cleanup_target 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/YuukanOO/seelf/internal/deployment/domain" 7 | "github.com/YuukanOO/seelf/pkg/bus" 8 | ) 9 | 10 | func OnTargetCleanupRequestedHandler(scheduler bus.Scheduler) bus.SignalHandler[domain.TargetCleanupRequested] { 11 | return func(ctx context.Context, evt domain.TargetCleanupRequested) error { 12 | return scheduler.Queue(ctx, Command{ 13 | ID: string(evt.ID), 14 | }, bus.WithPolicy(bus.JobPolicyCancellable)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/auth/app/get_profile/get_profile.go: -------------------------------------------------------------------------------- 1 | package get_profile 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/YuukanOO/seelf/pkg/bus" 7 | ) 8 | 9 | type ( 10 | // Retrieve the user profile. 11 | Query struct { 12 | bus.Query[Profile] 13 | 14 | ID string `json:"-"` 15 | } 16 | 17 | Profile struct { 18 | ID string `json:"id"` 19 | Email string `json:"email"` 20 | RegisteredAt time.Time `json:"registered_at"` 21 | APIKey string `json:"api_key"` 22 | } 23 | ) 24 | 25 | func (Query) Name_() string { return "auth.query.get_profile" } 26 | -------------------------------------------------------------------------------- /examples/multiple-ports/README.md: -------------------------------------------------------------------------------- 1 | # multiple-ports 2 | 3 | Quick & dirty project to test the **seelf** ability to handle multiple ports of specific types and check how they are managed by traefik. 4 | 5 | ## Usage 6 | 7 | ```sh 8 | go run main.go -web 8080,8081 -tcp 9854,9855 -udp 9856,9857 9 | ``` 10 | 11 | ## How to test 12 | 13 | Quick & dirty ways to test if the server is actually listening. 14 | 15 | ```sh 16 | curl http://localhost:8080 # HTTP 17 | curl telnet://localhost:9855 <<< text # TCP 18 | nc -v localhost 9855 # Other way for TCP 19 | nc -v -u localhost 9856 # UDP 20 | ``` 21 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/apps/[id]/deployments/[number]/+page.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/deployments'; 2 | import appService from '$lib/resources/apps'; 3 | import { error } from '@sveltejs/kit'; 4 | 5 | export const load = async ({ fetch, params: { id, number }, depends }) => { 6 | try { 7 | const deployment = await service.fetchByAppAndNumber(id, parseInt(number), { fetch, depends }); 8 | const app = await appService.fetchById(id, { fetch, depends }); 9 | 10 | return { 11 | app, 12 | deployment 13 | }; 14 | } catch { 15 | throw error(404); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /internal/deployment/app/group.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/internal/deployment/domain" 5 | ) 6 | 7 | // Group for deployment to prevent multiple deployment at the same time on the same 8 | // environment. 9 | func DeploymentGroup(config domain.ConfigSnapshot) string { 10 | return "deployment.deployment.deploy." + config.ProjectName() 11 | } 12 | 13 | // Group for target operation to prevent multiple target configuration at the same time. 14 | func TargetConfigurationGroup(id domain.TargetID) string { 15 | return "deployment.target.configure." + string(id) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/serve/front/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript') 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /docs/contributing/frontend.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | The frontend part of **seelf** is written using [SvelteKit](https://kit.svelte.dev/) for its **low bundle size** and **performances**. 4 | 5 | The frontend stuff is located in the `cmd/serve/front` directory and embedded inside the final executable at build time. 6 | 7 | ## Useful commands 8 | 9 | These commands must be executed from the root folder. 10 | 11 | - `make serve-front`: Serve the Sveltekit dev server 12 | - `make test`: Run all test suites (front & back), if you only wish to launch the frontend ones, just run `npm test` (in the `cmd/serve/front`) instead 13 | -------------------------------------------------------------------------------- /cmd/serve/front/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /internal/deployment/domain/deployment_id_test.go: -------------------------------------------------------------------------------- 1 | package domain_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/YuukanOO/seelf/internal/deployment/domain" 7 | "github.com/YuukanOO/seelf/pkg/assert" 8 | ) 9 | 10 | func Test_DeploymentID(t *testing.T) { 11 | t.Run("could be created from an appid and a deployment number", func(t *testing.T) { 12 | var ( 13 | app domain.AppID = "1" 14 | number domain.DeploymentNumber = 1 15 | ) 16 | 17 | id := domain.DeploymentIDFrom(app, number) 18 | 19 | assert.Equal(t, app, id.AppID()) 20 | assert.Equal(t, number, id.DeploymentNumber()) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /pkg/validate/numbers/numbers_test.go: -------------------------------------------------------------------------------- 1 | package numbers_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/YuukanOO/seelf/pkg/assert" 7 | "github.com/YuukanOO/seelf/pkg/validate/numbers" 8 | ) 9 | 10 | func Test_Min(t *testing.T) { 11 | t.Run("should fail on value lesser than the required min", func(t *testing.T) { 12 | assert.ErrorIs(t, numbers.ErrMin, numbers.Min(3)(2)) 13 | assert.ErrorIs(t, numbers.ErrMin, numbers.Min(3)(1)) 14 | }) 15 | 16 | t.Run("should succeed on value greater then the required min", func(t *testing.T) { 17 | assert.Nil(t, numbers.Min(3)(4)) 18 | assert.Nil(t, numbers.Min(3)(3)) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /internal/deployment/infra/sqlite/migrations/1702544287_delete_worker_stuff.up.sql: -------------------------------------------------------------------------------- 1 | -- Since this migration remove the old jobs table, fail all pending deployments because the job will be lost 2 | UPDATE deployments 3 | SET 4 | state_status = 2, 5 | state_errcode = 'seelf_incompatible_upgrade', 6 | state_started_at = datetime('now'), 7 | state_finished_at = datetime('now') 8 | WHERE state_status = 0; -- Running jobs will be failed with the server_reset error so no need to handle them here 9 | 10 | -- Delete unused worker stuff since I now use a scheduler adapter 11 | DROP TABLE IF EXISTS jobs; 12 | DROP TABLE IF EXISTS worker_schema_migrations; -------------------------------------------------------------------------------- /pkg/crypto/random.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 9 | 10 | // Generates a random key (from: https://gist.github.com/dopey/c69559607800d2f2f90b1b1ed4e550fb) 11 | func RandomKey[T ~string](lengthInBytes int) (T, error) { 12 | ret := make([]byte, lengthInBytes) 13 | for i := 0; i < lengthInBytes; i++ { 14 | num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) 15 | if err != nil { 16 | return "", err 17 | } 18 | ret[i] = letters[num.Int64()] 19 | } 20 | 21 | return T(ret), nil 22 | } 23 | -------------------------------------------------------------------------------- /cmd/serve/front/README.md: -------------------------------------------------------------------------------- 1 | # seelf front-end 2 | 3 | _I may have to refactor things a bit to make test writable (by using some view model maybe) but that's not a priority for now._ 4 | 5 | ## Developing 6 | 7 | Run the following commands to serve the development server and start building. 8 | 9 | _In development, call to the `/api/v1` will be proxified to `http://127.0.0.1:8080/api/v1` which is the default port for the seelf backend._ 10 | 11 | ```bash 12 | npm install 13 | npm run dev 14 | ``` 15 | 16 | ## Building 17 | 18 | _You should probably use the `make build` at the root of the seelf project instead._ 19 | 20 | ```bash 21 | npm run build 22 | ``` 23 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/apps/[id]/edit/+page.ts: -------------------------------------------------------------------------------- 1 | import service from '$lib/resources/apps'; 2 | import targetsService from '$lib/resources/targets'; 3 | import { error } from '@sveltejs/kit'; 4 | 5 | export const load = async ({ params, fetch, depends }) => { 6 | try { 7 | // Retrieve the last version of the app because the one used in the layout load may be outdated. 8 | const app = await service.fetchById(params.id, { fetch, depends }); 9 | const targets = await targetsService.fetchAll({ active_only: true }, { fetch, depends }); 10 | 11 | return { 12 | app, 13 | targets 14 | }; 15 | } catch { 16 | throw error(404); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /internal/deployment/app/delete_app/on_app_cleanup_requested.go: -------------------------------------------------------------------------------- 1 | package delete_app 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/YuukanOO/seelf/internal/deployment/domain" 7 | "github.com/YuukanOO/seelf/pkg/bus" 8 | ) 9 | 10 | // Upon receiving a cleanup request, queue a job to remove everything related to the application. 11 | func OnAppCleanupRequestedHandler(scheduler bus.Scheduler) bus.SignalHandler[domain.AppCleanupRequested] { 12 | return func(ctx context.Context, evt domain.AppCleanupRequested) error { 13 | return scheduler.Queue(ctx, Command{ 14 | ID: string(evt.ID), 15 | }, bus.WithPolicy(bus.JobPolicyWaitForOthersResourceID)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/apps/+error.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 17 |

18 | {l.translate('app.not_found')} 19 | {l.translate('app.not_found.cta')}. 20 |

21 |
22 | -------------------------------------------------------------------------------- /pkg/must/panic_test.go: -------------------------------------------------------------------------------- 1 | package must_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/YuukanOO/seelf/pkg/assert" 8 | "github.com/YuukanOO/seelf/pkg/must" 9 | ) 10 | 11 | func Test_Panic(t *testing.T) { 12 | t.Run("should panic if an error is given", func(t *testing.T) { 13 | err := errors.New("some error") 14 | defer func() { 15 | r := recover() 16 | 17 | assert.NotNil(t, r) 18 | assert.ErrorIs(t, err, r.(error)) 19 | }() 20 | 21 | must.Panic(42, err) 22 | }) 23 | 24 | t.Run("should return the value if no error is given", func(t *testing.T) { 25 | value := must.Panic(42, nil) 26 | 27 | assert.Equal(t, 42, value) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "seelf:serve", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "debug", 12 | "program": "main.go", 13 | "env": { 14 | "LOG_LEVEL": "debug", 15 | "LOG_FORMAT": "console", 16 | "ADMIN_EMAIL": "admin@example.com", 17 | "ADMIN_PASSWORD": "admin" 18 | }, 19 | "args": ["serve"] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /internal/deployment/infra/source/raw/data.go: -------------------------------------------------------------------------------- 1 | package raw 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/internal/deployment/app/get_deployment" 5 | "github.com/YuukanOO/seelf/internal/deployment/domain" 6 | ) 7 | 8 | type Data string 9 | 10 | func (p Data) Kind() string { return "raw" } 11 | func (p Data) NeedVersionControl() bool { return false } 12 | 13 | func init() { 14 | domain.SourceDataTypes.Register(Data(""), func(value string) (domain.SourceData, error) { 15 | return Data(value), nil 16 | }) 17 | 18 | get_deployment.SourceDataTypes.Register(Data(""), func(value string) (get_deployment.SourceData, error) { 19 | return Data(value), nil 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/domain/action.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "time" 4 | 5 | // Action represents an action done by a user at a given time. 6 | type Action[T ~string] struct { 7 | by T 8 | at time.Time 9 | } 10 | 11 | func NewAction[T ~string](by T) (a Action[T]) { 12 | a.by = by 13 | a.at = time.Now().UTC() 14 | return a 15 | } 16 | 17 | // Builds an action from both the user and the time, required when rehydrating 18 | // the struct from the storage. 19 | func ActionFrom[T ~string](by T, at time.Time) (a Action[T]) { 20 | a.by = by 21 | a.at = at 22 | return a 23 | } 24 | 25 | func (a Action[T]) By() T { return a.by } 26 | func (a Action[T]) At() time.Time { return a.at } 27 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/targets/+error.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 17 |

18 | {l.translate('target.not_found')} 19 | {l.translate('target.not_found.cta')}. 20 |

21 |
22 | -------------------------------------------------------------------------------- /internal/deployment/app/configure_target/on_target_created.go: -------------------------------------------------------------------------------- 1 | package configure_target 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/YuukanOO/seelf/internal/deployment/app" 7 | "github.com/YuukanOO/seelf/internal/deployment/domain" 8 | "github.com/YuukanOO/seelf/pkg/bus" 9 | ) 10 | 11 | func OnTargetCreatedHandler(scheduler bus.Scheduler) bus.SignalHandler[domain.TargetCreated] { 12 | return func(ctx context.Context, evt domain.TargetCreated) error { 13 | return scheduler.Queue(ctx, Command{ 14 | ID: string(evt.ID), 15 | Version: evt.State.Version(), 16 | }, bus.WithGroup(app.TargetConfigurationGroup(evt.ID)), bus.WithPolicy(bus.JobPolicyMerge)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/domain/interval.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/YuukanOO/seelf/pkg/apperr" 7 | ) 8 | 9 | var ErrInvalidTimeInterval = apperr.New("invalid_time_interval") 10 | 11 | type TimeInterval struct { 12 | from time.Time 13 | to time.Time 14 | } 15 | 16 | // Builds up a new time interval. 17 | func NewTimeInterval(from, to time.Time) (TimeInterval, error) { 18 | if from.After(to) { 19 | return TimeInterval{}, ErrInvalidTimeInterval 20 | } 21 | 22 | return TimeInterval{ 23 | from: from, 24 | to: to, 25 | }, nil 26 | } 27 | 28 | func (i TimeInterval) From() time.Time { return i.from } 29 | func (i TimeInterval) To() time.Time { return i.to } 30 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: YuukanOO 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: YuukanOO 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | buy_me_a_coffee: yuukanoo 14 | -------------------------------------------------------------------------------- /internal/deployment/app/delete_target/on_target_cleanup_requested.go: -------------------------------------------------------------------------------- 1 | package delete_target 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/YuukanOO/seelf/internal/deployment/domain" 7 | "github.com/YuukanOO/seelf/pkg/bus" 8 | ) 9 | 10 | // Upon receiving a cleanup request, queue a job to delete the target record when every other tasks are done. 11 | func OnTargetCleanupRequestedHandler(scheduler bus.Scheduler) bus.SignalHandler[domain.TargetCleanupRequested] { 12 | return func(ctx context.Context, evt domain.TargetCleanupRequested) error { 13 | return scheduler.Queue(ctx, Command{ 14 | ID: string(evt.ID), 15 | }, bus.WithPolicy(bus.JobPolicyWaitForOthersResourceID)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/deployment/infra/source/archive/data.go: -------------------------------------------------------------------------------- 1 | package archive 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/internal/deployment/app/get_deployment" 5 | "github.com/YuukanOO/seelf/internal/deployment/domain" 6 | ) 7 | 8 | type Data string 9 | 10 | func (p Data) Kind() string { return "archive" } 11 | func (p Data) NeedVersionControl() bool { return false } 12 | 13 | func init() { 14 | domain.SourceDataTypes.Register(Data(""), func(value string) (domain.SourceData, error) { 15 | return Data(value), nil 16 | }) 17 | 18 | get_deployment.SourceDataTypes.Register(Data(""), func(value string) (get_deployment.SourceData, error) { 19 | return Data(value), nil 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/registries/+error.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 17 |

18 | {l.translate('registry.not_found')} 19 | {l.translate('registry.not_found.cta')}. 20 |

21 |
22 | -------------------------------------------------------------------------------- /internal/deployment/domain/deployment_id.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type ( 4 | // The deployment unique identifier is a composite key 5 | // based on the app id and the deployment number. 6 | DeploymentNumber int 7 | 8 | DeploymentID struct { 9 | appID AppID 10 | deploymentNumber DeploymentNumber 11 | } 12 | ) 13 | 14 | // Construct a deployment id from an app and a deployment number 15 | func DeploymentIDFrom(app AppID, number DeploymentNumber) DeploymentID { 16 | return DeploymentID{app, number} 17 | } 18 | 19 | func (i DeploymentID) AppID() AppID { return i.appID } 20 | func (i DeploymentID) DeploymentNumber() DeploymentNumber { return i.deploymentNumber } 21 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:svelte/recommended', 7 | 'prettier' 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint'], 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020, 14 | extraFileExtensions: ['.svelte'] 15 | }, 16 | env: { 17 | browser: true, 18 | es2017: true, 19 | node: true 20 | }, 21 | overrides: [ 22 | { 23 | files: ['*.svelte'], 24 | parser: 'svelte-eslint-parser', 25 | parserOptions: { 26 | parser: '@typescript-eslint/parser' 27 | } 28 | } 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /pkg/flag/flag_test.go: -------------------------------------------------------------------------------- 1 | package flag_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/YuukanOO/seelf/pkg/assert" 7 | "github.com/YuukanOO/seelf/pkg/flag" 8 | ) 9 | 10 | type flagType uint 11 | 12 | const ( 13 | flagA flagType = 1 << iota 14 | flagB 15 | flagC 16 | ) 17 | 18 | func Test_IsSet(t *testing.T) { 19 | assert.True(t, flag.IsSet(flagA, flagA)) 20 | assert.False(t, flag.IsSet(flagA, flagB)) 21 | assert.True(t, flag.IsSet(flagA|flagB, flagA)) 22 | assert.True(t, flag.IsSet(flagA|flagB, flagB|flagA)) 23 | assert.True(t, flag.IsSet(flagA|flagB|flagC, flagB|flagA)) 24 | assert.False(t, flag.IsSet(flagA, flagB|flagA)) 25 | assert.False(t, flag.IsSet(flagA|flagC, flagB|flagA)) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |

{l.translate('unexpected_error')}

8 |
9 | {@html l.translate('unexpected_error.description')} 10 |
11 |
12 | 13 | 27 | -------------------------------------------------------------------------------- /pkg/ssh/host.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "net" 5 | "regexp" 6 | 7 | "github.com/YuukanOO/seelf/pkg/apperr" 8 | ) 9 | 10 | var ( 11 | ErrInvalidHost = apperr.New("invalid_host") 12 | 13 | hostRegex = regexp.MustCompile(`^([a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62}){1}(\.[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})*?$`) 14 | ) 15 | 16 | type Host string 17 | 18 | // Parses a raw value as a host. Allow ipv4 & ipv6 addresses and domain names without port number. 19 | func ParseHost(value string) (Host, error) { 20 | if net.ParseIP(value) == nil && !hostRegex.MatchString(value) { 21 | return "", ErrInvalidHost 22 | } 23 | 24 | return Host(value), nil 25 | } 26 | 27 | func (h Host) String() string { return string(h) } 28 | -------------------------------------------------------------------------------- /cmd/serve/front/src/lib/resources/healthcheck.ts: -------------------------------------------------------------------------------- 1 | import fetcher, { type FetchOptions, type FetchService } from '$lib/fetcher'; 2 | 3 | export type HealthCheckResult = { 4 | version: string; 5 | }; 6 | 7 | export interface HealthCheckService { 8 | check(options?: FetchOptions): Promise; 9 | } 10 | 11 | export class RemoteHealthCheckService implements HealthCheckService { 12 | constructor(private readonly _fetcher: FetchService) {} 13 | 14 | check(options?: FetchOptions): Promise { 15 | return this._fetcher.get('/api/v1/healthcheck', options); 16 | } 17 | } 18 | 19 | const service: HealthCheckService = new RemoteHealthCheckService(fetcher); 20 | 21 | export default service; 22 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/progress-bar.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | {#if visible} 6 |
7 | {/if} 8 | 9 | 35 | -------------------------------------------------------------------------------- /internal/deployment/app/fail_pending_deployments/on_target_delete_requested.go: -------------------------------------------------------------------------------- 1 | package fail_pending_deployments 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/YuukanOO/seelf/internal/deployment/domain" 7 | "github.com/YuukanOO/seelf/pkg/bus" 8 | "github.com/YuukanOO/seelf/pkg/monad" 9 | ) 10 | 11 | func OnTargetDeleteRequestedHandler(writer domain.DeploymentsWriter) bus.SignalHandler[domain.TargetCleanupRequested] { 12 | return func(ctx context.Context, evt domain.TargetCleanupRequested) error { 13 | return writer.FailDeployments(ctx, domain.ErrTargetCleanupRequested, domain.FailCriteria{ 14 | Status: monad.Value(domain.DeploymentStatusPending), 15 | Target: monad.Value(evt.ID), 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/reference/deployments.md: -------------------------------------------------------------------------------- 1 | # Deployments 2 | 3 | Created from an [application](/reference/applications) for a specific [environment](/reference/applications#environments), represents an actual deployment on a [target](/reference/targets). 4 | 5 | ## Sources {#sources} 6 | 7 | Deployments can be created from a number of sources. 8 | 9 | ### Archive (`tar.gz`) 10 | 11 | An archive containing your project files to be deployed. 12 | 13 | ### Raw file 14 | 15 | A raw file. For example, a `compose.yml` content when using the [Docker provider](/reference/providers/docker). 16 | 17 | ### Git 18 | 19 | A valid **branch** and an optional specific **commit** if the application has been configured with a version control system. 20 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/form-errors.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | {#if globalErrorText} 18 | 19 | {globalErrorText} 20 | 21 | {/if} 22 | -------------------------------------------------------------------------------- /internal/auth/infra/crypto/bcrypt_hasher.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/internal/auth/domain" 5 | "golang.org/x/crypto/bcrypt" 6 | ) 7 | 8 | type bcryptHasher struct{} 9 | 10 | func NewBCryptHasher() domain.PasswordHasher { 11 | return &bcryptHasher{} 12 | } 13 | 14 | func (*bcryptHasher) Hash(value string) (domain.PasswordHash, error) { 15 | data, err := bcrypt.GenerateFromPassword([]byte(value), bcrypt.DefaultCost) 16 | 17 | if err != nil { 18 | return "", err 19 | } 20 | 21 | return domain.PasswordHash(data), nil 22 | } 23 | 24 | func (*bcryptHasher) Compare(value string, hash domain.PasswordHash) error { 25 | return bcrypt.CompareHashAndPassword([]byte(hash), []byte(value)) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/display.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
{l.translate(label)}
15 | 16 |
17 | 18 | 30 | -------------------------------------------------------------------------------- /pkg/storage/sqlite/builder/result.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import "golang.org/x/exp/maps" 4 | 5 | type ( 6 | keysMapping map[string]int 7 | 8 | // Represents a key indexed set of data. 9 | keyedResult[T any] struct { 10 | data []T 11 | indexByKeys keysMapping 12 | } 13 | ) 14 | 15 | // Keys returns the list of keys contained in this dataset. 16 | func (r *keyedResult[T]) Keys() []string { return maps.Keys(r.indexByKeys) } 17 | 18 | // Update the result with the given key by applying the given function if it exists. 19 | func (r *keyedResult[T]) Update(targetKey string, updateFn func(T) T) { 20 | idx, found := r.indexByKeys[targetKey] 21 | 22 | if !found { 23 | return 24 | } 25 | 26 | r.data[idx] = updateFn(r.data[idx]) 27 | } 28 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/form.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |
20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /cmd/serve/front/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import preprocess from 'svelte-preprocess'; 3 | import { cssModules, linearPreprocess } from 'svelte-preprocess-cssmodules'; 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://github.com/sveltejs/svelte-preprocess 8 | // for more information about preprocessors 9 | preprocess: linearPreprocess([preprocess(), cssModules()]), 10 | 11 | kit: { 12 | alias: { 13 | $components: 'src/components', 14 | $assets: 'src/assets' 15 | }, 16 | adapter: adapter({ 17 | fallback: 'fallback.html' // Enable true SPA mode since some pages could not be pregenerated (ex: apps pages) 18 | }) 19 | } 20 | }; 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /examples/sveltekit-hello/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { vitePreprocess } from '@sveltejs/kit/vite'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter() 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /cmd/serve/cmd.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "github.com/YuukanOO/seelf/cmd/startup" 5 | "github.com/YuukanOO/seelf/pkg/log" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type Options interface { 10 | ServerOptions 11 | startup.ServerOptions 12 | } 13 | 14 | // Returns the root serve command 15 | func Root(opts Options, logger log.Logger) *cobra.Command { 16 | serveCmd := &cobra.Command{ 17 | Use: "serve", 18 | Short: "Launch the web application!", 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | root, err := startup.Server(opts, logger) 21 | 22 | if err != nil { 23 | return err 24 | } 25 | 26 | defer root.Cleanup() 27 | 28 | return newHttpServer(opts, root).Listen() 29 | }, 30 | } 31 | 32 | return serveCmd 33 | } 34 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/prose.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 38 | -------------------------------------------------------------------------------- /internal/deployment/app/configure_target/on_target_state_changed.go: -------------------------------------------------------------------------------- 1 | package configure_target 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/YuukanOO/seelf/internal/deployment/app" 7 | "github.com/YuukanOO/seelf/internal/deployment/domain" 8 | "github.com/YuukanOO/seelf/pkg/bus" 9 | ) 10 | 11 | func OnTargetStateChangedHandler(scheduler bus.Scheduler) bus.SignalHandler[domain.TargetStateChanged] { 12 | return func(ctx context.Context, evt domain.TargetStateChanged) error { 13 | if !evt.WentToConfiguringState() { 14 | return nil 15 | } 16 | 17 | return scheduler.Queue(ctx, Command{ 18 | ID: string(evt.ID), 19 | Version: evt.State.Version(), 20 | }, bus.WithGroup(app.TargetConfigurationGroup(evt.ID)), bus.WithPolicy(bus.JobPolicyMerge)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/deployment/app/fail_pending_deployments/on_app_cleanup_requested.go: -------------------------------------------------------------------------------- 1 | package fail_pending_deployments 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/YuukanOO/seelf/internal/deployment/domain" 7 | "github.com/YuukanOO/seelf/pkg/bus" 8 | "github.com/YuukanOO/seelf/pkg/monad" 9 | ) 10 | 11 | // When an app is about to be deleted, cancel all pending deployments 12 | func OnAppCleanupRequestedHandler(writer domain.DeploymentsWriter) bus.SignalHandler[domain.AppCleanupRequested] { 13 | return func(ctx context.Context, evt domain.AppCleanupRequested) error { 14 | return writer.FailDeployments(ctx, domain.ErrAppCleanupRequested, domain.FailCriteria{ 15 | Status: monad.Value(domain.DeploymentStatusPending), 16 | App: monad.Value(evt.ID), 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cmd/serve/front/src/assets/icons/git.svelte: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /internal/deployment/app/cleanup_app/on_app_env_changed.go: -------------------------------------------------------------------------------- 1 | package cleanup_app 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/YuukanOO/seelf/internal/deployment/domain" 8 | "github.com/YuukanOO/seelf/pkg/bus" 9 | ) 10 | 11 | func OnAppEnvChangedHandler(scheduler bus.Scheduler) bus.SignalHandler[domain.AppEnvChanged] { 12 | return func(ctx context.Context, evt domain.AppEnvChanged) error { 13 | if !evt.TargetHasChanged() { 14 | return nil 15 | } 16 | 17 | return scheduler.Queue(ctx, Command{ 18 | AppID: string(evt.ID), 19 | TargetID: string(evt.OldConfig.Target()), 20 | Environment: string(evt.Environment), 21 | From: evt.OldConfig.Version(), 22 | To: time.Now().UTC(), 23 | }, bus.WithPolicy(bus.JobPolicyCancellable)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cmd/serve/front/src/lib/select.ts: -------------------------------------------------------------------------------- 1 | /** Select an option based on the provided value. */ 2 | export default function select( 3 | value: TValue, 4 | options: Partial> & { default: TOutput } 5 | ): TOutput; 6 | export default function select( 7 | value: TValue, 8 | options: Partial> & { default?: TOutput } 9 | ): Maybe; 10 | export default function select( 11 | value: TValue, 12 | options: Partial> & { default?: TOutput } 13 | ): Maybe { 14 | const output = options[value]; 15 | 16 | if (!output) { 17 | return options.default; 18 | } 19 | 20 | return output; 21 | } 22 | -------------------------------------------------------------------------------- /pkg/domain/interval_test.go: -------------------------------------------------------------------------------- 1 | package domain_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/YuukanOO/seelf/pkg/assert" 8 | "github.com/YuukanOO/seelf/pkg/domain" 9 | ) 10 | 11 | func Test_TimeInterval(t *testing.T) { 12 | t.Run("should fail if the from date is after the to date", func(t *testing.T) { 13 | _, err := domain.NewTimeInterval(time.Now(), time.Now().Add(-time.Second)) 14 | 15 | assert.ErrorIs(t, domain.ErrInvalidTimeInterval, err) 16 | }) 17 | 18 | t.Run("should succeed if the from date is before the to date", func(t *testing.T) { 19 | from := time.Now() 20 | to := time.Now().Add(time.Second) 21 | ti, err := domain.NewTimeInterval(from, to) 22 | 23 | assert.Nil(t, err) 24 | assert.Equal(t, from, ti.From()) 25 | assert.Equal(t, to, ti.To()) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /internal/deployment/app/deploy/on_deployment_created.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/YuukanOO/seelf/internal/deployment/app" 7 | "github.com/YuukanOO/seelf/internal/deployment/domain" 8 | "github.com/YuukanOO/seelf/pkg/bus" 9 | ) 10 | 11 | // Upon receiving a deployment created event, queue a job to deploy the application. 12 | func OnDeploymentCreatedHandler(scheduler bus.Scheduler) bus.SignalHandler[domain.DeploymentCreated] { 13 | return func(ctx context.Context, evt domain.DeploymentCreated) error { 14 | return scheduler.Queue(ctx, Command{ 15 | AppID: string(evt.ID.AppID()), 16 | DeploymentNumber: int(evt.ID.DeploymentNumber()), 17 | }, bus.WithGroup(app.DeploymentGroup(evt.Config)), bus.WithPolicy(bus.JobPolicyRetryPreserveOrder)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/deployment/domain/credentials.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | // Represents basic credentials used by a registry. 4 | type Credentials struct { 5 | username string 6 | password string 7 | } 8 | 9 | // Builds new credentials with the provided username and password. 10 | func NewCredentials(username, password string) Credentials { 11 | return Credentials{ 12 | username: username, 13 | password: password, 14 | } 15 | } 16 | 17 | // Updates the username. 18 | func (c *Credentials) HasUsername(username string) { 19 | c.username = username 20 | } 21 | 22 | // Updates the password. 23 | func (c *Credentials) HasPassword(password string) { 24 | c.password = password 25 | } 26 | 27 | func (c Credentials) Username() string { return c.username } 28 | func (c Credentials) Password() string { return c.password } 29 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/registry-card.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 |

{data.name}

12 |
{data.url}
13 |
14 | 15 | 31 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 | 20 | 34 | -------------------------------------------------------------------------------- /internal/auth/domain/context.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/YuukanOO/seelf/pkg/monad" 7 | ) 8 | 9 | type contextKey string 10 | 11 | const currentUserContextKey contextKey = "current-user" 12 | 13 | // Attach the given UserID to the given context. Will be used everywhere when trying 14 | // to determine which user is currently executing an action. 15 | func WithUserID(ctx context.Context, uid UserID) context.Context { 16 | return context.WithValue(ctx, currentUserContextKey, uid) 17 | } 18 | 19 | // Retrieve the current user attached to the given context if any. 20 | func CurrentUser(ctx context.Context) (m monad.Maybe[UserID]) { 21 | val := ctx.Value(currentUserContextKey) 22 | 23 | if val == nil { 24 | return m 25 | } 26 | 27 | m.Set(val.(UserID)) 28 | 29 | return m 30 | } 31 | -------------------------------------------------------------------------------- /internal/auth/domain/context_test.go: -------------------------------------------------------------------------------- 1 | package domain_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/YuukanOO/seelf/internal/auth/domain" 8 | "github.com/YuukanOO/seelf/pkg/assert" 9 | "github.com/YuukanOO/seelf/pkg/monad" 10 | ) 11 | 12 | func Test_AuthContext(t *testing.T) { 13 | t.Run("should embed a user id into the context", func(t *testing.T) { 14 | ctx := context.Background() 15 | uid := domain.UserID("a_user_id") 16 | 17 | newCtx := domain.WithUserID(ctx, uid) 18 | 19 | assert.Equal(t, uid, domain.CurrentUser(newCtx).MustGet()) 20 | }) 21 | 22 | t.Run("should returns an empty monad.Maybe if no user id has been attached to the context", func(t *testing.T) { 23 | uid := domain.CurrentUser(context.Background()) 24 | 25 | assert.Equal(t, monad.None[domain.UserID](), uid) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /cmd/serve/front/src/routes/(main)/jobs/cancel-button.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 19 |
20 | 21 | 22 | 50 | -------------------------------------------------------------------------------- /internal/deployment/fixture/registry.go: -------------------------------------------------------------------------------- 1 | //go:build !release 2 | 3 | package fixture 4 | 5 | import ( 6 | auth "github.com/YuukanOO/seelf/internal/auth/domain" 7 | "github.com/YuukanOO/seelf/internal/deployment/domain" 8 | "github.com/YuukanOO/seelf/pkg/id" 9 | "github.com/YuukanOO/seelf/pkg/must" 10 | ) 11 | 12 | type ( 13 | registryOption struct { 14 | name string 15 | url domain.Url 16 | uid auth.UserID 17 | } 18 | 19 | RegistryOptionBuilder func(*registryOption) 20 | ) 21 | 22 | func Registry(options ...RegistryOptionBuilder) domain.Registry { 23 | opts := registryOption{ 24 | name: id.New[string](), 25 | url: must.Panic(domain.UrlFrom("http://" + id.New[string]() + ".com")), 26 | uid: id.New[auth.UserID](), 27 | } 28 | 29 | for _, o := range options { 30 | o(&opts) 31 | } 32 | 33 | return must.Panic(domain.NewRegistry(opts.name, domain.NewRegistryUrlRequirement(opts.url, true), opts.uid)) 34 | } 35 | 36 | func WithRegistryName(name string) RegistryOptionBuilder { 37 | return func(o *registryOption) { 38 | o.name = name 39 | } 40 | } 41 | 42 | func WithRegistryCreatedBy(uid auth.UserID) RegistryOptionBuilder { 43 | return func(o *registryOption) { 44 | o.uid = uid 45 | } 46 | } 47 | 48 | func WithUrl(url domain.Url) RegistryOptionBuilder { 49 | return func(o *registryOption) { 50 | o.url = url 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cmd/serve/front/src/components/text-area.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 30 |