db provisioning
This commit is contained in:
parent
af8bbf536f
commit
1293ef678e
8 changed files with 151 additions and 38 deletions
|
|
@ -1,4 +1,6 @@
|
||||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/box?sslmode=disable
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/box?sslmode=disable
|
||||||
|
DB_PROVISION_ENABLED=true
|
||||||
|
DB_PROVISION_ADMIN_URL=
|
||||||
LISTEN_ADDR=:8080
|
LISTEN_ADDR=:8080
|
||||||
UNIX_SOCKET_PATH=
|
UNIX_SOCKET_PATH=
|
||||||
DEV_MODE=true
|
DEV_MODE=true
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ Important:
|
||||||
- `DEV_MODE=false` expects auth headers from your reverse proxy:
|
- `DEV_MODE=false` expects auth headers from your reverse proxy:
|
||||||
- `X-authentik-username`
|
- `X-authentik-username`
|
||||||
- `X-authentik-email`
|
- `X-authentik-email`
|
||||||
|
- `DB_PROVISION_ENABLED=true` provisions a dedicated PostgreSQL role/database per app.
|
||||||
|
- `DB_PROVISION_ADMIN_URL` is the admin DSN used for provisioning (defaults to `DATABASE_URL`).
|
||||||
- `FORGEJO_BASE_URL` + `FORGEJO_TOKEN` enable repository creation.
|
- `FORGEJO_BASE_URL` + `FORGEJO_TOKEN` enable repository creation.
|
||||||
- `FORGEJO_ORG` switches creation from personal repos to org repos.
|
- `FORGEJO_ORG` switches creation from personal repos to org repos.
|
||||||
- `UNIX_SOCKET_PATH` enables unix socket listener in addition to `LISTEN_ADDR`.
|
- `UNIX_SOCKET_PATH` enables unix socket listener in addition to `LISTEN_ADDR`.
|
||||||
|
|
@ -112,5 +114,6 @@ Then it attempts as that unix user:
|
||||||
The generated `.env` is synced from the project env-var configuration and always includes:
|
The generated `.env` is synced from the project env-var configuration and always includes:
|
||||||
- `LISTEN_NETWORK=unix`
|
- `LISTEN_NETWORK=unix`
|
||||||
- `LISTEN_ADDRESS=<unix-user-home>/projects/<app>/app.sock`
|
- `LISTEN_ADDRESS=<unix-user-home>/projects/<app>/app.sock`
|
||||||
|
- `DATABASE_URL=postgres://<app_db_user>:<generated_password>@.../<app_db_name>`
|
||||||
|
|
||||||
If user systemd is not available in the runtime environment, project creation still succeeds and unit files are still generated.
|
If user systemd is not available in the runtime environment, project creation still succeeds and unit files are still generated.
|
||||||
|
|
|
||||||
|
|
@ -9,26 +9,31 @@ import (
|
||||||
|
|
||||||
func loadConfig() (Config, error) {
|
func loadConfig() (Config, error) {
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
DatabaseURL: getenv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/box?sslmode=disable"),
|
DatabaseURL: getenv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/box?sslmode=disable"),
|
||||||
ListenAddr: getenv("LISTEN_ADDR", ":8080"),
|
ListenAddr: getenv("LISTEN_ADDR", ":8080"),
|
||||||
UnixSocketPath: getenv("UNIX_SOCKET_PATH", ""),
|
UnixSocketPath: getenv("UNIX_SOCKET_PATH", ""),
|
||||||
DevMode: getenvBool("DEV_MODE", true),
|
DevMode: getenvBool("DEV_MODE", true),
|
||||||
DemoUser: getenv("DEV_DEMO_USER", "demo"),
|
DemoUser: getenv("DEV_DEMO_USER", "demo"),
|
||||||
DemoEmail: getenv("DEV_DEMO_EMAIL", "demo@example.local"),
|
DemoEmail: getenv("DEV_DEMO_EMAIL", "demo@example.local"),
|
||||||
AuthHeaderUser: getenv("AUTH_HEADER_USER", "X-authentik-username"),
|
AuthHeaderUser: getenv("AUTH_HEADER_USER", "X-authentik-username"),
|
||||||
AuthHeaderEmail: getenv("AUTH_HEADER_EMAIL", "X-authentik-email"),
|
AuthHeaderEmail: getenv("AUTH_HEADER_EMAIL", "X-authentik-email"),
|
||||||
ForgejoBaseURL: strings.TrimRight(getenv("FORGEJO_BASE_URL", ""), "/"),
|
ForgejoBaseURL: strings.TrimRight(getenv("FORGEJO_BASE_URL", ""), "/"),
|
||||||
ForgejoToken: getenv("FORGEJO_TOKEN", ""),
|
ForgejoToken: getenv("FORGEJO_TOKEN", ""),
|
||||||
ForgejoOrg: getenv("FORGEJO_ORG", ""),
|
ForgejoOrg: getenv("FORGEJO_ORG", ""),
|
||||||
CaddyAdminURL: strings.TrimRight(getenv("CADDY_ADMIN_URL", "http://localhost:2019"), "/"),
|
DBProvisionAdminURL: getenv("DB_PROVISION_ADMIN_URL", ""),
|
||||||
CaddyServerID: getenv("CADDY_SERVER_ID", "srv0"),
|
DBProvisionEnabled: getenvBool("DB_PROVISION_ENABLED", true),
|
||||||
CaddyDomainSuffix: getenv("CADDY_DOMAIN_SUFFIX", ""),
|
CaddyAdminURL: strings.TrimRight(getenv("CADDY_ADMIN_URL", "http://localhost:2019"), "/"),
|
||||||
CaddyRouteEnabled: getenvBool("CADDY_ROUTE_ENABLED", false),
|
CaddyServerID: getenv("CADDY_SERVER_ID", "srv0"),
|
||||||
WebhookBaseURL: strings.TrimRight(getenv("WEBHOOK_BASE_URL", ""), "/"),
|
CaddyDomainSuffix: getenv("CADDY_DOMAIN_SUFFIX", ""),
|
||||||
|
CaddyRouteEnabled: getenvBool("CADDY_ROUTE_ENABLED", false),
|
||||||
|
WebhookBaseURL: strings.TrimRight(getenv("WEBHOOK_BASE_URL", ""), "/"),
|
||||||
}
|
}
|
||||||
if cfg.DatabaseURL == "" {
|
if cfg.DatabaseURL == "" {
|
||||||
return cfg, errors.New("DATABASE_URL is required")
|
return cfg, errors.New("DATABASE_URL is required")
|
||||||
}
|
}
|
||||||
|
if cfg.DBProvisionAdminURL == "" {
|
||||||
|
cfg.DBProvisionAdminURL = cfg.DatabaseURL
|
||||||
|
}
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ func (a *App) migrate(ctx context.Context) error {
|
||||||
deploy_token TEXT NOT NULL DEFAULT '',
|
deploy_token TEXT NOT NULL DEFAULT '',
|
||||||
provision_state TEXT NOT NULL DEFAULT 'provisioned',
|
provision_state TEXT NOT NULL DEFAULT 'provisioned',
|
||||||
provision_error TEXT NOT NULL DEFAULT '',
|
provision_error TEXT NOT NULL DEFAULT '',
|
||||||
|
db_name TEXT NOT NULL DEFAULT '',
|
||||||
|
db_user TEXT NOT NULL DEFAULT '',
|
||||||
|
db_password TEXT NOT NULL DEFAULT '',
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
UNIQUE (user_id, slug)
|
UNIQUE (user_id, slug)
|
||||||
);`,
|
);`,
|
||||||
|
|
@ -51,6 +54,9 @@ func (a *App) migrate(ctx context.Context) error {
|
||||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS repo_private BOOLEAN NOT NULL DEFAULT true;`,
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS repo_private BOOLEAN NOT NULL DEFAULT true;`,
|
||||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS provision_state TEXT NOT NULL DEFAULT 'provisioned';`,
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS provision_state TEXT NOT NULL DEFAULT 'provisioned';`,
|
||||||
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS provision_error TEXT NOT NULL DEFAULT '';`,
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS provision_error TEXT NOT NULL DEFAULT '';`,
|
||||||
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS db_name TEXT NOT NULL DEFAULT '';`,
|
||||||
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS db_user TEXT NOT NULL DEFAULT '';`,
|
||||||
|
`ALTER TABLE projects ADD COLUMN IF NOT EXISTS db_password TEXT NOT NULL DEFAULT '';`,
|
||||||
}
|
}
|
||||||
for _, q := range alter {
|
for _, q := range alter {
|
||||||
if _, err := a.db.ExecContext(ctx, q); err != nil {
|
if _, err := a.db.ExecContext(ctx, q); err != nil {
|
||||||
|
|
|
||||||
83
internal/app/db_provisioning.go
Normal file
83
internal/app/db_provisioning.go
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *App) ensureProjectDatabase(ctx context.Context, p *Project) error {
|
||||||
|
if !a.cfg.DBProvisionEnabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
adminDB, err := sql.Open("pgx", a.cfg.DBProvisionAdminURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("db provisioning connect failed: %w", err)
|
||||||
|
}
|
||||||
|
defer adminDB.Close()
|
||||||
|
if err := adminDB.PingContext(ctx); err != nil {
|
||||||
|
return fmt.Errorf("db provisioning ping failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.DBName == "" {
|
||||||
|
p.DBName = fmt.Sprintf("app_%d", p.ID)
|
||||||
|
}
|
||||||
|
if p.DBUser == "" {
|
||||||
|
p.DBUser = fmt.Sprintf("appu_%d", p.ID)
|
||||||
|
}
|
||||||
|
if p.DBPassword == "" {
|
||||||
|
pw, err := newDeployToken()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.DBPassword = pw
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %s) THEN
|
||||||
|
CREATE ROLE %s LOGIN PASSWORD %s;
|
||||||
|
ELSE
|
||||||
|
ALTER ROLE %s LOGIN PASSWORD %s;
|
||||||
|
END IF;
|
||||||
|
END $$;`, sqlQuoteLiteral(p.DBUser), sqlQuoteIdent(p.DBUser), sqlQuoteLiteral(p.DBPassword), sqlQuoteIdent(p.DBUser), sqlQuoteLiteral(p.DBPassword))); err != nil {
|
||||||
|
return fmt.Errorf("create/alter role failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = %s) THEN
|
||||||
|
CREATE DATABASE %s OWNER %s;
|
||||||
|
END IF;
|
||||||
|
END $$;`, sqlQuoteLiteral(p.DBName), sqlQuoteIdent(p.DBName), sqlQuoteIdent(p.DBUser))); err != nil {
|
||||||
|
return fmt.Errorf("create database failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := a.db.ExecContext(ctx, `UPDATE projects SET db_name=$1, db_user=$2, db_password=$3 WHERE id=$4`, p.DBName, p.DBUser, p.DBPassword, p.ID); err != nil {
|
||||||
|
return fmt.Errorf("persist db credentials failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) projectDatabaseURL(p Project) (string, error) {
|
||||||
|
if p.DBName == "" || p.DBUser == "" || p.DBPassword == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
base, err := url.Parse(a.cfg.DBProvisionAdminURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
base.User = url.UserPassword(p.DBUser, p.DBPassword)
|
||||||
|
base.Path = "/" + p.DBName
|
||||||
|
return base.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlQuoteIdent(s string) string {
|
||||||
|
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlQuoteLiteral(s string) string {
|
||||||
|
return `'` + strings.ReplaceAll(s, `'`, `''`) + `'`
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) listProjects(ctx context.Context, userID string) ([]Project, error) {
|
func (a *App) listProjects(ctx context.Context, userID string) ([]Project, error) {
|
||||||
rows, err := a.db.QueryContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.repo_private, p.service_name, p.route_host, p.target_port, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.created_at
|
rows, err := a.db.QueryContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.repo_private, p.service_name, p.route_host, p.target_port, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.db_name, p.db_user, p.db_password, p.created_at
|
||||||
FROM projects p
|
FROM projects p
|
||||||
JOIN workspaces w ON w.id = p.workspace_id
|
JOIN workspaces w ON w.id = p.workspace_id
|
||||||
WHERE p.user_id=$1 ORDER BY p.created_at DESC`, userID)
|
WHERE p.user_id=$1 ORDER BY p.created_at DESC`, userID)
|
||||||
|
|
@ -20,7 +20,7 @@ func (a *App) listProjects(ctx context.Context, userID string) ([]Project, error
|
||||||
var out []Project
|
var out []Project
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var p Project
|
var p Project
|
||||||
if err := rows.Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt); err != nil {
|
if err := rows.Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.DBName, &p.DBUser, &p.DBPassword, &p.CreatedAt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
|
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
|
||||||
|
|
@ -54,9 +54,9 @@ func (a *App) createProject(ctx context.Context, user User, workspaceID int64, n
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
err = tx.QueryRowContext(ctx, `INSERT INTO projects (user_id, workspace_id, name, slug, description, repo_url, repo_private, service_name, route_host, target_port, deploy_token, provision_state, provision_error) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,'pending','') RETURNING id, user_id, name, slug, description, repo_url, repo_private, service_name, route_host, target_port, workspace_id, deploy_token, provision_state, provision_error, created_at`,
|
err = tx.QueryRowContext(ctx, `INSERT INTO projects (user_id, workspace_id, name, slug, description, repo_url, repo_private, service_name, route_host, target_port, deploy_token, provision_state, provision_error) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,'pending','') RETURNING id, user_id, name, slug, description, repo_url, repo_private, service_name, route_host, target_port, workspace_id, deploy_token, provision_state, provision_error, db_name, db_user, db_password, created_at`,
|
||||||
user.Username, workspaceID, name, slug, description, repoURL, private, serviceName, routeHost, targetPort, deployToken,
|
user.Username, workspaceID, name, slug, description, repoURL, private, serviceName, routeHost, targetPort, deployToken,
|
||||||
).Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt)
|
).Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.DBName, &p.DBUser, &p.DBPassword, &p.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
return Project{}, errors.New("project with this name already exists")
|
return Project{}, errors.New("project with this name already exists")
|
||||||
|
|
@ -74,6 +74,12 @@ func (a *App) createProject(ctx context.Context, user User, workspaceID int64, n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) provisionProject(ctx context.Context, p *Project) error {
|
func (a *App) provisionProject(ctx context.Context, p *Project) error {
|
||||||
|
if err := a.ensureProjectDatabase(ctx, p); err != nil {
|
||||||
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
||||||
|
p.ProvisionState = "failed"
|
||||||
|
p.ProvisionError = err.Error()
|
||||||
|
return err
|
||||||
|
}
|
||||||
if p.RepoURL == "" {
|
if p.RepoURL == "" {
|
||||||
if a.cfg.ForgejoBaseURL == "" || a.cfg.ForgejoToken == "" {
|
if a.cfg.ForgejoBaseURL == "" || a.cfg.ForgejoToken == "" {
|
||||||
err := errors.New("repo_url is empty and Forgejo is not configured; set FORGEJO_BASE_URL/FORGEJO_TOKEN or provide repo_url")
|
err := errors.New("repo_url is empty and Forgejo is not configured; set FORGEJO_BASE_URL/FORGEJO_TOKEN or provide repo_url")
|
||||||
|
|
@ -136,9 +142,9 @@ func (a *App) provisionProject(ctx context.Context, p *Project) error {
|
||||||
|
|
||||||
func (a *App) getProject(ctx context.Context, userID string, projectID int64) (Project, error) {
|
func (a *App) getProject(ctx context.Context, userID string, projectID int64) (Project, error) {
|
||||||
var p Project
|
var p Project
|
||||||
err := a.db.QueryRowContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.repo_private, p.service_name, p.route_host, p.target_port, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.created_at
|
err := a.db.QueryRowContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.repo_private, p.service_name, p.route_host, p.target_port, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.provision_state, p.provision_error, p.db_name, p.db_user, p.db_password, p.created_at
|
||||||
FROM projects p JOIN workspaces w ON w.id=p.workspace_id WHERE p.user_id=$1 AND p.id=$2`, userID, projectID).
|
FROM projects p JOIN workspaces w ON w.id=p.workspace_id WHERE p.user_id=$1 AND p.id=$2`, userID, projectID).
|
||||||
Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt)
|
Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.RepoPrivate, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.DBName, &p.DBUser, &p.DBPassword, &p.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return Project{}, errors.New("project not found")
|
return Project{}, errors.New("project not found")
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@ func (a *App) renderSystemdForProject(ctx context.Context, p Project) error {
|
||||||
for _, v := range envVars {
|
for _, v := range envVars {
|
||||||
values[v.Key] = v.Value
|
values[v.Key] = v.Value
|
||||||
}
|
}
|
||||||
|
if dbURL, err := a.projectDatabaseURL(p); err == nil && dbURL != "" {
|
||||||
|
values["DATABASE_URL"] = dbURL
|
||||||
|
}
|
||||||
values["LISTEN_NETWORK"] = "unix"
|
values["LISTEN_NETWORK"] = "unix"
|
||||||
values["LISTEN_ADDRESS"] = socketPath
|
values["LISTEN_ADDRESS"] = socketPath
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,24 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DatabaseURL string
|
DatabaseURL string
|
||||||
ListenAddr string
|
ListenAddr string
|
||||||
UnixSocketPath string
|
UnixSocketPath string
|
||||||
DevMode bool
|
DevMode bool
|
||||||
DemoUser string
|
DemoUser string
|
||||||
DemoEmail string
|
DemoEmail string
|
||||||
AuthHeaderUser string
|
AuthHeaderUser string
|
||||||
AuthHeaderEmail string
|
AuthHeaderEmail string
|
||||||
ForgejoBaseURL string
|
ForgejoBaseURL string
|
||||||
ForgejoToken string
|
ForgejoToken string
|
||||||
ForgejoOrg string
|
ForgejoOrg string
|
||||||
CaddyAdminURL string
|
DBProvisionAdminURL string
|
||||||
CaddyServerID string
|
DBProvisionEnabled bool
|
||||||
CaddyDomainSuffix string
|
CaddyAdminURL string
|
||||||
CaddyRouteEnabled bool
|
CaddyServerID string
|
||||||
WebhookBaseURL string
|
CaddyDomainSuffix string
|
||||||
|
CaddyRouteEnabled bool
|
||||||
|
WebhookBaseURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
|
|
@ -52,6 +54,9 @@ type Project struct {
|
||||||
DeployToken string `json:"-"`
|
DeployToken string `json:"-"`
|
||||||
ProvisionState string `json:"provision_state"`
|
ProvisionState string `json:"provision_state"`
|
||||||
ProvisionError string `json:"provision_error,omitempty"`
|
ProvisionError string `json:"provision_error,omitempty"`
|
||||||
|
DBName string `json:"db_name,omitempty"`
|
||||||
|
DBUser string `json:"db_user,omitempty"`
|
||||||
|
DBPassword string `json:"-"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue