mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
Merge pull request #25 from Dvorinka/feat/db-public-port
feat: public_port for managed databases — opt-in 0.0.0.0 bind
This commit is contained in:
16 files changed
+235
-45
No files matched your search
@@ -52,7 +52,8 @@ type DatabaseService struct {
|
|||||||
Region string `json:"region" db:"region"`
|
Region string `json:"region" db:"region"`
|
||||||
BackupSchedule string `json:"backup_schedule,omitempty"`
|
BackupSchedule string `json:"backup_schedule,omitempty"`
|
||||||
NextBackupAt *time.Time `json:"next_backup_at,omitempty"`
|
NextBackupAt *time.Time `json:"next_backup_at,omitempty"`
|
||||||
Provider string `json:"provider"` // managed | external
|
Provider string `json:"provider"` // managed | external
|
||||||
|
PublicPort bool `json:"public_port" db:"public_port"`
|
||||||
External *DatabaseExternalRef `json:"external,omitempty"` // connection target for external DBs
|
External *DatabaseExternalRef `json:"external,omitempty"` // connection target for external DBs
|
||||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
@@ -117,10 +118,11 @@ type DatabaseSettings struct {
|
|||||||
|
|
||||||
// DatabaseCreateRequest represents a request to create a new database
|
// DatabaseCreateRequest represents a request to create a new database
|
||||||
type DatabaseCreateRequest struct {
|
type DatabaseCreateRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Type string `json:"type" binding:"required"`
|
Type string `json:"type" binding:"required"`
|
||||||
Plan string `json:"plan" binding:"required,oneof=hobby starter standard business"`
|
Plan string `json:"plan" binding:"required,oneof=hobby starter standard business"`
|
||||||
Region string `json:"region" binding:"required"`
|
Region string `json:"region" binding:"required"`
|
||||||
|
PublicPort bool `json:"public_port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseUpdateRequest represents a request to update a database
|
// DatabaseUpdateRequest represents a request to update a database
|
||||||
@@ -128,6 +130,7 @@ type DatabaseUpdateRequest struct {
|
|||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Plan string `json:"plan,omitempty"`
|
Plan string `json:"plan,omitempty"`
|
||||||
BackupSchedule *string `json:"backup_schedule,omitempty"`
|
BackupSchedule *string `json:"backup_schedule,omitempty"`
|
||||||
|
PublicPort *bool `json:"public_port,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseActionRequest represents a request to perform database actions
|
// DatabaseActionRequest represents a request to perform database actions
|
||||||
@@ -291,10 +294,11 @@ func (h *DatabaseHandler) CreateDatabase(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
databaseID, dbName, dbType, err := h.createManagedDatabase(c.Request.Context(), userID, managedDatabaseCreateRequest{
|
databaseID, dbName, dbType, err := h.createManagedDatabase(c.Request.Context(), userID, managedDatabaseCreateRequest{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
Plan: req.Plan,
|
Plan: req.Plan,
|
||||||
Region: req.Region,
|
Region: req.Region,
|
||||||
|
PublicPort: req.PublicPort,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
@@ -322,6 +326,7 @@ type managedDatabaseCreateRequest struct {
|
|||||||
Type string
|
Type string
|
||||||
Plan string
|
Plan string
|
||||||
Region string
|
Region string
|
||||||
|
PublicPort bool
|
||||||
RuntimeVariables map[string]string
|
RuntimeVariables map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,16 +370,17 @@ func (h *DatabaseHandler) createManagedDatabase(ctx context.Context, userID stri
|
|||||||
version := h.getDefaultVersion(dbType)
|
version := h.getDefaultVersion(dbType)
|
||||||
|
|
||||||
err = h.queries.CreateDatabaseService(ctx, sqlcdb.CreateDatabaseServiceParams{
|
err = h.queries.CreateDatabaseService(ctx, sqlcdb.CreateDatabaseServiceParams{
|
||||||
ID: databaseID,
|
ID: databaseID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: dbType,
|
Type: dbType,
|
||||||
Status: "building",
|
Status: "building",
|
||||||
Version: version,
|
Version: version,
|
||||||
Plan: plan,
|
Plan: plan,
|
||||||
Region: region,
|
Region: region,
|
||||||
CreatedAt: sql.NullTime{Time: now, Valid: true},
|
PublicPort: req.PublicPort,
|
||||||
UpdatedAt: sql.NullTime{Time: now, Valid: true},
|
CreatedAt: sql.NullTime{Time: now, Valid: true},
|
||||||
|
UpdatedAt: sql.NullTime{Time: now, Valid: true},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", fmt.Errorf("failed to create database service: %w", err)
|
return "", "", "", fmt.Errorf("failed to create database service: %w", err)
|
||||||
@@ -411,11 +417,40 @@ func (h *DatabaseHandler) UpdateDatabase(c *gin.Context) {
|
|||||||
req.Name = strings.TrimSpace(req.Name)
|
req.Name = strings.TrimSpace(req.Name)
|
||||||
req.Plan = strings.TrimSpace(req.Plan)
|
req.Plan = strings.TrimSpace(req.Plan)
|
||||||
|
|
||||||
if req.Name == "" && req.Plan == "" && req.BackupSchedule == nil {
|
if req.Name == "" && req.Plan == "" && req.BackupSchedule == nil && req.PublicPort == nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No fields to update"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "No fields to update"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.PublicPort != nil {
|
||||||
|
row, err := h.queries.GetDatabaseServiceByIDAndUser(c.Request.Context(), sqlcdb.GetDatabaseServiceByIDAndUserParams{
|
||||||
|
ID: databaseID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found", "code": "NOT_FOUND"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if row.Provider == "external" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "public_port applies to managed databases — external databases are already reachable", "code": "VALIDATION"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.queries.SetDatabaseServicePublicPortByIDAndUser(c.Request.Context(), sqlcdb.SetDatabaseServicePublicPortByIDAndUserParams{
|
||||||
|
PublicPort: *req.PublicPort,
|
||||||
|
UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||||||
|
ID: databaseID,
|
||||||
|
UserID: userID,
|
||||||
|
}); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update public port"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Port bindings are baked into the container — recreate it so the
|
||||||
|
// new bind scope takes effect. The data volume survives.
|
||||||
|
if *req.PublicPort != row.PublicPort {
|
||||||
|
go h.reprovisionManagedDatabase(databaseID, row.Name, row.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if req.BackupSchedule != nil {
|
if req.BackupSchedule != nil {
|
||||||
var schedule sql.NullString
|
var schedule sql.NullString
|
||||||
var nextBackup sql.NullTime
|
var nextBackup sql.NullTime
|
||||||
@@ -1108,6 +1143,7 @@ func mapDatabaseServiceRow(row sqlcdb.DatabaseService) DatabaseService {
|
|||||||
BackupSchedule: databaseNullString(row.BackupSchedule),
|
BackupSchedule: databaseNullString(row.BackupSchedule),
|
||||||
NextBackupAt: databaseNullTimePtr(row.NextBackupAt),
|
NextBackupAt: databaseNullTimePtr(row.NextBackupAt),
|
||||||
Provider: row.Provider,
|
Provider: row.Provider,
|
||||||
|
PublicPort: row.PublicPort,
|
||||||
ConnectionURL: databaseNullString(row.ConnectionUrl),
|
ConnectionURL: databaseNullString(row.ConnectionUrl),
|
||||||
CreatedAt: databaseNullTime(row.CreatedAt),
|
CreatedAt: databaseNullTime(row.CreatedAt),
|
||||||
UpdatedAt: databaseNullTime(row.UpdatedAt),
|
UpdatedAt: databaseNullTime(row.UpdatedAt),
|
||||||
@@ -1288,6 +1324,17 @@ func (h *DatabaseHandler) provisionDatabase(databaseID, databaseName, dbType str
|
|||||||
h.provisionDatabaseWithVariables(databaseID, databaseName, dbType, nil)
|
h.provisionDatabaseWithVariables(databaseID, databaseName, dbType, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reprovisionManagedDatabase recreates a managed container so changed
|
||||||
|
// runtime settings (public_port) take effect. The data volume persists.
|
||||||
|
func (h *DatabaseHandler) reprovisionManagedDatabase(databaseID, databaseName, dbType string) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
if h.dockerClient != nil {
|
||||||
|
_ = h.dockerClient.RemoveContainer(ctx, managedDatabaseContainerName(databaseID), true)
|
||||||
|
}
|
||||||
|
h.provisionDatabase(databaseID, databaseName, dbType)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *DatabaseHandler) provisionDatabaseWithVariables(databaseID, databaseName, dbType string, runtimeVariables map[string]string) {
|
func (h *DatabaseHandler) provisionDatabaseWithVariables(databaseID, databaseName, dbType string, runtimeVariables map[string]string) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1344,8 +1391,12 @@ func (h *DatabaseHandler) provisionDatabaseRuntime(ctx context.Context, database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hostIP := "127.0.0.1"
|
||||||
|
if row, err := h.queries.GetDatabaseServiceByID(ctx, databaseID); err == nil && row.PublicPort {
|
||||||
|
hostIP = "0.0.0.0"
|
||||||
|
}
|
||||||
portBindings := nat.PortMap{
|
portBindings := nat.PortMap{
|
||||||
plan.Port: []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: ""}},
|
plan.Port: []nat.PortBinding{{HostIP: hostIP, HostPort: ""}},
|
||||||
}
|
}
|
||||||
|
|
||||||
exposedPorts := nat.PortSet{plan.Port: struct{}{}}
|
exposedPorts := nat.PortSet{plan.Port: struct{}{}}
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ var dbCreateCmd = &cobra.Command{
|
|||||||
body[key] = v
|
body[key] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v, _ := cmd.Flags().GetBool("public"); v {
|
||||||
|
body["public_port"] = true
|
||||||
|
}
|
||||||
data, err := c.Do("POST", "/databases", body)
|
data, err := c.Do("POST", "/databases", body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -88,6 +91,39 @@ var dbCreateCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dbUpdateCmd = &cobra.Command{
|
||||||
|
Use: "update <id>",
|
||||||
|
Short: "Update a database (--public/--public=false toggles the exposed host port)",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
c, err := client()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body := map[string]interface{}{}
|
||||||
|
if cmd.Flags().Changed("public") {
|
||||||
|
v, _ := cmd.Flags().GetBool("public")
|
||||||
|
body["public_port"] = v
|
||||||
|
}
|
||||||
|
if v, _ := cmd.Flags().GetString("name"); v != "" {
|
||||||
|
body["name"] = v
|
||||||
|
}
|
||||||
|
if cmd.Flags().Changed("backup-schedule") {
|
||||||
|
v, _ := cmd.Flags().GetString("backup-schedule")
|
||||||
|
body["backup_schedule"] = v
|
||||||
|
}
|
||||||
|
if len(body) == 0 {
|
||||||
|
return &APIError{Message: "nothing to update — pass --public/--private-style flags, --name, or --backup-schedule", ExitCode: ExitError}
|
||||||
|
}
|
||||||
|
data, err := c.Do("PUT", "/databases/"+args[0], body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
PrintRaw(data)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var dbDeleteCmd = &cobra.Command{
|
var dbDeleteCmd = &cobra.Command{
|
||||||
Use: "delete <id>",
|
Use: "delete <id>",
|
||||||
Short: "Delete a database and its data",
|
Short: "Delete a database and its data",
|
||||||
@@ -278,6 +314,10 @@ func init() {
|
|||||||
dbCreateCmd.Flags().String("type", "postgres", "postgres|mysql|mariadb|mongodb|redis|dragonfly|clickhouse")
|
dbCreateCmd.Flags().String("type", "postgres", "postgres|mysql|mariadb|mongodb|redis|dragonfly|clickhouse")
|
||||||
dbCreateCmd.Flags().String("version", "", "engine version tag")
|
dbCreateCmd.Flags().String("version", "", "engine version tag")
|
||||||
dbCreateCmd.Flags().String("storage-size", "", "volume size (e.g. 10Gi)")
|
dbCreateCmd.Flags().String("storage-size", "", "volume size (e.g. 10Gi)")
|
||||||
|
dbCreateCmd.Flags().Bool("public", false, "bind the database port on all interfaces (default: localhost only)")
|
||||||
|
dbUpdateCmd.Flags().Bool("public", false, "bind the database port on all interfaces")
|
||||||
|
dbUpdateCmd.Flags().String("name", "", "rename the database")
|
||||||
|
dbUpdateCmd.Flags().String("backup-schedule", "", "cron expression for automatic backups (empty clears)")
|
||||||
for _, cmd := range []*cobra.Command{dbRegisterCmd, dbTestConnCmd} {
|
for _, cmd := range []*cobra.Command{dbRegisterCmd, dbTestConnCmd} {
|
||||||
cmd.Flags().String("type", "postgres", "postgres|mysql|mariadb|mongodb|redis|dragonfly|clickhouse")
|
cmd.Flags().String("type", "postgres", "postgres|mysql|mariadb|mongodb|redis|dragonfly|clickhouse")
|
||||||
cmd.Flags().String("host", "", "database host")
|
cmd.Flags().String("host", "", "database host")
|
||||||
@@ -287,5 +327,5 @@ func init() {
|
|||||||
cmd.Flags().String("password", "", "database password (stored encrypted on register)")
|
cmd.Flags().String("password", "", "database password (stored encrypted on register)")
|
||||||
cmd.Flags().Bool("ssl", false, "require TLS")
|
cmd.Flags().Bool("ssl", false, "require TLS")
|
||||||
}
|
}
|
||||||
DatabasesCmd.AddCommand(dbListCmd, dbGetCmd, dbCreateCmd, dbRegisterCmd, dbTestConnCmd, dbDeleteCmd, dbActionCmd, dbBackupCmd, dbRestoreCmd, dbDownloadCmd)
|
DatabasesCmd.AddCommand(dbListCmd, dbGetCmd, dbCreateCmd, dbUpdateCmd, dbRegisterCmd, dbTestConnCmd, dbDeleteCmd, dbActionCmd, dbBackupCmd, dbRestoreCmd, dbDownloadCmd)
|
||||||
}
|
}
|
||||||
@@ -55,21 +55,22 @@ func (q *Queries) CreateDatabaseBackup(ctx context.Context, arg CreateDatabaseBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createDatabaseService = `-- name: CreateDatabaseService :exec
|
const createDatabaseService = `-- name: CreateDatabaseService :exec
|
||||||
INSERT INTO database_services (id, user_id, name, type, status, version, plan, region, created_at, updated_at)
|
INSERT INTO database_services (id, user_id, name, type, status, version, plan, region, public_port, created_at, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateDatabaseServiceParams struct {
|
type CreateDatabaseServiceParams struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Plan string `json:"plan"`
|
Plan string `json:"plan"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
CreatedAt sql.NullTime `json:"created_at"`
|
PublicPort bool `json:"public_port"`
|
||||||
UpdatedAt sql.NullTime `json:"updated_at"`
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
|
UpdatedAt sql.NullTime `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateDatabaseService(ctx context.Context, arg CreateDatabaseServiceParams) error {
|
func (q *Queries) CreateDatabaseService(ctx context.Context, arg CreateDatabaseServiceParams) error {
|
||||||
@@ -82,6 +83,7 @@ func (q *Queries) CreateDatabaseService(ctx context.Context, arg CreateDatabaseS
|
|||||||
arg.Version,
|
arg.Version,
|
||||||
arg.Plan,
|
arg.Plan,
|
||||||
arg.Region,
|
arg.Region,
|
||||||
|
arg.PublicPort,
|
||||||
arg.CreatedAt,
|
arg.CreatedAt,
|
||||||
arg.UpdatedAt,
|
arg.UpdatedAt,
|
||||||
)
|
)
|
||||||
@@ -196,7 +198,7 @@ func (q *Queries) GetDatabaseBackupByIDAndDatabaseAndUser(ctx context.Context, a
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getDatabaseServiceByID = `-- name: GetDatabaseServiceByID :one
|
const getDatabaseServiceByID = `-- name: GetDatabaseServiceByID :one
|
||||||
SELECT database_services.id, database_services.user_id, database_services.name, database_services.type, database_services.status, database_services.version, database_services.plan, database_services.region, database_services.connection_url, database_services.backup_schedule, database_services.next_backup_at, database_services.provider, database_services.external_host, database_services.external_port, database_services.external_name, database_services.external_username, database_services.external_password, database_services.external_ssl, database_services.created_at, database_services.updated_at
|
SELECT database_services.id, database_services.user_id, database_services.name, database_services.type, database_services.status, database_services.version, database_services.plan, database_services.region, database_services.connection_url, database_services.backup_schedule, database_services.next_backup_at, database_services.provider, database_services.external_host, database_services.external_port, database_services.external_name, database_services.external_username, database_services.external_password, database_services.external_ssl, database_services.public_port, database_services.created_at, database_services.updated_at
|
||||||
FROM database_services
|
FROM database_services
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
@@ -223,6 +225,7 @@ func (q *Queries) GetDatabaseServiceByID(ctx context.Context, id string) (Databa
|
|||||||
&i.ExternalUsername,
|
&i.ExternalUsername,
|
||||||
&i.ExternalPassword,
|
&i.ExternalPassword,
|
||||||
&i.ExternalSsl,
|
&i.ExternalSsl,
|
||||||
|
&i.PublicPort,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
@@ -230,7 +233,7 @@ func (q *Queries) GetDatabaseServiceByID(ctx context.Context, id string) (Databa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getDatabaseServiceByIDAndUser = `-- name: GetDatabaseServiceByIDAndUser :one
|
const getDatabaseServiceByIDAndUser = `-- name: GetDatabaseServiceByIDAndUser :one
|
||||||
SELECT database_services.id, database_services.user_id, database_services.name, database_services.type, database_services.status, database_services.version, database_services.plan, database_services.region, database_services.connection_url, database_services.backup_schedule, database_services.next_backup_at, database_services.provider, database_services.external_host, database_services.external_port, database_services.external_name, database_services.external_username, database_services.external_password, database_services.external_ssl, database_services.created_at, database_services.updated_at
|
SELECT database_services.id, database_services.user_id, database_services.name, database_services.type, database_services.status, database_services.version, database_services.plan, database_services.region, database_services.connection_url, database_services.backup_schedule, database_services.next_backup_at, database_services.provider, database_services.external_host, database_services.external_port, database_services.external_name, database_services.external_username, database_services.external_password, database_services.external_ssl, database_services.public_port, database_services.created_at, database_services.updated_at
|
||||||
FROM database_services
|
FROM database_services
|
||||||
WHERE id = $1 AND user_id = $2
|
WHERE id = $1 AND user_id = $2
|
||||||
`
|
`
|
||||||
@@ -262,6 +265,7 @@ func (q *Queries) GetDatabaseServiceByIDAndUser(ctx context.Context, arg GetData
|
|||||||
&i.ExternalUsername,
|
&i.ExternalUsername,
|
||||||
&i.ExternalPassword,
|
&i.ExternalPassword,
|
||||||
&i.ExternalSsl,
|
&i.ExternalSsl,
|
||||||
|
&i.PublicPort,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
@@ -282,7 +286,7 @@ func (q *Queries) GetDatabaseServiceOwnerID(ctx context.Context, id string) (str
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listAllDatabaseServices = `-- name: ListAllDatabaseServices :many
|
const listAllDatabaseServices = `-- name: ListAllDatabaseServices :many
|
||||||
SELECT database_services.id, database_services.user_id, database_services.name, database_services.type, database_services.status, database_services.version, database_services.plan, database_services.region, database_services.connection_url, database_services.backup_schedule, database_services.next_backup_at, database_services.provider, database_services.external_host, database_services.external_port, database_services.external_name, database_services.external_username, database_services.external_password, database_services.external_ssl, database_services.created_at, database_services.updated_at
|
SELECT database_services.id, database_services.user_id, database_services.name, database_services.type, database_services.status, database_services.version, database_services.plan, database_services.region, database_services.connection_url, database_services.backup_schedule, database_services.next_backup_at, database_services.provider, database_services.external_host, database_services.external_port, database_services.external_name, database_services.external_username, database_services.external_password, database_services.external_ssl, database_services.public_port, database_services.created_at, database_services.updated_at
|
||||||
FROM database_services
|
FROM database_services
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
@@ -315,6 +319,7 @@ func (q *Queries) ListAllDatabaseServices(ctx context.Context) ([]DatabaseServic
|
|||||||
&i.ExternalUsername,
|
&i.ExternalUsername,
|
||||||
&i.ExternalPassword,
|
&i.ExternalPassword,
|
||||||
&i.ExternalSsl,
|
&i.ExternalSsl,
|
||||||
|
&i.PublicPort,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -378,7 +383,7 @@ func (q *Queries) ListDatabaseBackupsByDatabaseAndUser(ctx context.Context, arg
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listDatabaseServicesByUser = `-- name: ListDatabaseServicesByUser :many
|
const listDatabaseServicesByUser = `-- name: ListDatabaseServicesByUser :many
|
||||||
SELECT database_services.id, database_services.user_id, database_services.name, database_services.type, database_services.status, database_services.version, database_services.plan, database_services.region, database_services.connection_url, database_services.backup_schedule, database_services.next_backup_at, database_services.provider, database_services.external_host, database_services.external_port, database_services.external_name, database_services.external_username, database_services.external_password, database_services.external_ssl, database_services.created_at, database_services.updated_at
|
SELECT database_services.id, database_services.user_id, database_services.name, database_services.type, database_services.status, database_services.version, database_services.plan, database_services.region, database_services.connection_url, database_services.backup_schedule, database_services.next_backup_at, database_services.provider, database_services.external_host, database_services.external_port, database_services.external_name, database_services.external_username, database_services.external_password, database_services.external_ssl, database_services.public_port, database_services.created_at, database_services.updated_at
|
||||||
FROM database_services
|
FROM database_services
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
@@ -412,6 +417,7 @@ func (q *Queries) ListDatabaseServicesByUser(ctx context.Context, userID string)
|
|||||||
&i.ExternalUsername,
|
&i.ExternalUsername,
|
||||||
&i.ExternalPassword,
|
&i.ExternalPassword,
|
||||||
&i.ExternalSsl,
|
&i.ExternalSsl,
|
||||||
|
&i.PublicPort,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -527,6 +533,29 @@ func (q *Queries) SetDatabaseNextBackupAt(ctx context.Context, arg SetDatabaseNe
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setDatabaseServicePublicPortByIDAndUser = `-- name: SetDatabaseServicePublicPortByIDAndUser :exec
|
||||||
|
UPDATE database_services
|
||||||
|
SET public_port = $1, updated_at = $2
|
||||||
|
WHERE id = $3 AND user_id = $4
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetDatabaseServicePublicPortByIDAndUserParams struct {
|
||||||
|
PublicPort bool `json:"public_port"`
|
||||||
|
UpdatedAt sql.NullTime `json:"updated_at"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetDatabaseServicePublicPortByIDAndUser(ctx context.Context, arg SetDatabaseServicePublicPortByIDAndUserParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, setDatabaseServicePublicPortByIDAndUser,
|
||||||
|
arg.PublicPort,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
arg.ID,
|
||||||
|
arg.UserID,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const setDatabaseServiceStatusAndConnectionByID = `-- name: SetDatabaseServiceStatusAndConnectionByID :exec
|
const setDatabaseServiceStatusAndConnectionByID = `-- name: SetDatabaseServiceStatusAndConnectionByID :exec
|
||||||
UPDATE database_services
|
UPDATE database_services
|
||||||
SET status = $1, connection_url = $2, updated_at = $3
|
SET status = $1, connection_url = $2, updated_at = $3
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ type DatabaseService struct {
|
|||||||
ExternalUsername sql.NullString `json:"external_username"`
|
ExternalUsername sql.NullString `json:"external_username"`
|
||||||
ExternalPassword string `json:"external_password"`
|
ExternalPassword string `json:"external_password"`
|
||||||
ExternalSsl bool `json:"external_ssl"`
|
ExternalSsl bool `json:"external_ssl"`
|
||||||
|
PublicPort bool `json:"public_port"`
|
||||||
CreatedAt sql.NullTime `json:"created_at"`
|
CreatedAt sql.NullTime `json:"created_at"`
|
||||||
UpdatedAt sql.NullTime `json:"updated_at"`
|
UpdatedAt sql.NullTime `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ type Querier interface {
|
|||||||
SetDatabaseBackupScheduleByIDAndUser(ctx context.Context, arg SetDatabaseBackupScheduleByIDAndUserParams) error
|
SetDatabaseBackupScheduleByIDAndUser(ctx context.Context, arg SetDatabaseBackupScheduleByIDAndUserParams) error
|
||||||
SetDatabaseBackupStatusByID(ctx context.Context, arg SetDatabaseBackupStatusByIDParams) error
|
SetDatabaseBackupStatusByID(ctx context.Context, arg SetDatabaseBackupStatusByIDParams) error
|
||||||
SetDatabaseNextBackupAt(ctx context.Context, arg SetDatabaseNextBackupAtParams) error
|
SetDatabaseNextBackupAt(ctx context.Context, arg SetDatabaseNextBackupAtParams) error
|
||||||
|
SetDatabaseServicePublicPortByIDAndUser(ctx context.Context, arg SetDatabaseServicePublicPortByIDAndUserParams) error
|
||||||
SetDatabaseServiceStatusAndConnectionByID(ctx context.Context, arg SetDatabaseServiceStatusAndConnectionByIDParams) error
|
SetDatabaseServiceStatusAndConnectionByID(ctx context.Context, arg SetDatabaseServiceStatusAndConnectionByIDParams) error
|
||||||
SetDatabaseServiceStatusByID(ctx context.Context, arg SetDatabaseServiceStatusByIDParams) error
|
SetDatabaseServiceStatusByID(ctx context.Context, arg SetDatabaseServiceStatusByIDParams) error
|
||||||
SetDatabaseServiceStatusByIDAndUser(ctx context.Context, arg SetDatabaseServiceStatusByIDAndUserParams) error
|
SetDatabaseServiceStatusByIDAndUser(ctx context.Context, arg SetDatabaseServiceStatusByIDAndUserParams) error
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ func init() {
|
|||||||
// Databases
|
// Databases
|
||||||
register(spec{"containr_databases_list", "List managed databases", "GET", "/databases", nil, nil, false})
|
register(spec{"containr_databases_list", "List managed databases", "GET", "/databases", nil, nil, false})
|
||||||
register(spec{"containr_databases_get", "Get a database (connection URL included)", "GET", "/databases/{id}", []string{"id"}, nil, false})
|
register(spec{"containr_databases_get", "Get a database (connection URL included)", "GET", "/databases/{id}", []string{"id"}, nil, false})
|
||||||
register(spec{"containr_databases_create", "Provision a managed database. Args: name, type (postgres|mysql|mariadb|mongodb|redis|dragonfly|clickhouse).", "POST", "/databases", nil, nil, false})
|
register(spec{"containr_databases_create", "Provision a managed database. Args: name, type (postgres|mysql|mariadb|mongodb|redis|dragonfly|clickhouse), public_port (bind on all interfaces, default localhost-only).", "POST", "/databases", nil, nil, false})
|
||||||
|
register(spec{"containr_databases_update", "Update a database. Args: id, name, backup_schedule (empty clears), public_port — toggling recreates the container so the new bind scope applies.", "PUT", "/databases/{id}", []string{"id"}, nil, false})
|
||||||
register(spec{"containr_databases_register_external", "Register a database hosted elsewhere — probed first, password stored encrypted. Args: name (required), type (required), host (required), port, database, username, password, ssl.", "POST", "/databases/register-external", nil, []string{"name", "type", "host"}, false})
|
register(spec{"containr_databases_register_external", "Register a database hosted elsewhere — probed first, password stored encrypted. Args: name (required), type (required), host (required), port, database, username, password, ssl.", "POST", "/databases/register-external", nil, []string{"name", "type", "host"}, false})
|
||||||
register(spec{"containr_databases_test_connection", "Probe a DB connection ad-hoc. Args: type, host (required), port, database, username, password, ssl.", "POST", "/databases/test-connection", nil, []string{"type", "host"}, false})
|
register(spec{"containr_databases_test_connection", "Probe a DB connection ad-hoc. Args: type, host (required), port, database, username, password, ssl.", "POST", "/databases/test-connection", nil, []string{"type", "host"}, false})
|
||||||
register(spec{"containr_databases_test_connection_by_id", "Re-probe a registered external database with its stored credentials. Args: id.", "POST", "/databases/{id}/test-connection", []string{"id"}, nil, false})
|
register(spec{"containr_databases_test_connection_by_id", "Re-probe a registered external database with its stored credentials. Args: id.", "POST", "/databases/{id}/test-connection", []string{"id"}, nil, false})
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE database_services
|
||||||
|
ADD COLUMN IF NOT EXISTS public_port BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE database_services
|
||||||
|
DROP COLUMN IF EXISTS public_port;
|
||||||
|
-- +goose StatementEnd
|
||||||
@@ -35,8 +35,13 @@ FROM database_services
|
|||||||
WHERE user_id = sqlc.arg(user_id) AND LOWER(name) = LOWER(sqlc.arg(name));
|
WHERE user_id = sqlc.arg(user_id) AND LOWER(name) = LOWER(sqlc.arg(name));
|
||||||
|
|
||||||
-- name: CreateDatabaseService :exec
|
-- name: CreateDatabaseService :exec
|
||||||
INSERT INTO database_services (id, user_id, name, type, status, version, plan, region, created_at, updated_at)
|
INSERT INTO database_services (id, user_id, name, type, status, version, plan, region, public_port, created_at, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);
|
||||||
|
|
||||||
|
-- name: SetDatabaseServicePublicPortByIDAndUser :exec
|
||||||
|
UPDATE database_services
|
||||||
|
SET public_port = $1, updated_at = $2
|
||||||
|
WHERE id = $3 AND user_id = $4;
|
||||||
|
|
||||||
-- name: UpdateDatabaseServiceNameAndPlanByIDAndUser :exec
|
-- name: UpdateDatabaseServiceNameAndPlanByIDAndUser :exec
|
||||||
UPDATE database_services
|
UPDATE database_services
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ CREATE TABLE database_services (
|
|||||||
external_username VARCHAR(255),
|
external_username VARCHAR(255),
|
||||||
external_password TEXT NOT NULL DEFAULT '',
|
external_password TEXT NOT NULL DEFAULT '',
|
||||||
external_ssl BOOLEAN NOT NULL DEFAULT false,
|
external_ssl BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
public_port BOOLEAN NOT NULL DEFAULT false,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
restoreDatabaseBackup,
|
restoreDatabaseBackup,
|
||||||
createManagedDatabase,
|
createManagedDatabase,
|
||||||
registerExternalDatabase,
|
registerExternalDatabase,
|
||||||
|
updateDatabase,
|
||||||
testDatabaseConnection,
|
testDatabaseConnection,
|
||||||
testDatabaseConnectionByID,
|
testDatabaseConnectionByID,
|
||||||
type ExternalDatabaseInput,
|
type ExternalDatabaseInput,
|
||||||
@@ -1906,7 +1907,7 @@ export function DatabasesPage() {
|
|||||||
const [bindDb, setBindDb] = useState<DatabaseEntity | null>(null);
|
const [bindDb, setBindDb] = useState<DatabaseEntity | null>(null);
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [createMode, setCreateMode] = useState<'managed' | 'external'>('managed');
|
const [createMode, setCreateMode] = useState<'managed' | 'external'>('managed');
|
||||||
const [createForm, setCreateForm] = useState({ name: '', type: 'postgresql', plan: 'hobby', region: 'local' });
|
const [createForm, setCreateForm] = useState({ name: '', type: 'postgresql', plan: 'hobby', region: 'local', public: false });
|
||||||
const [externalForm, setExternalForm] = useState({ name: '', type: 'postgresql', host: '', port: '', database: '', username: '', password: '', ssl: false });
|
const [externalForm, setExternalForm] = useState({ name: '', type: 'postgresql', host: '', port: '', database: '', username: '', password: '', ssl: false });
|
||||||
const [externalProbe, setExternalProbe] = useState<{ ok: boolean; latency_ms?: number; error?: string } | null>(null);
|
const [externalProbe, setExternalProbe] = useState<{ ok: boolean; latency_ms?: number; error?: string } | null>(null);
|
||||||
const [externalTestResult, setExternalTestResult] = useState<Record<string, { ok: boolean; latency_ms?: number; error?: string }>>({});
|
const [externalTestResult, setExternalTestResult] = useState<Record<string, { ok: boolean; latency_ms?: number; error?: string }>>({});
|
||||||
@@ -1926,10 +1927,11 @@ export function DatabasesPage() {
|
|||||||
type: createForm.type as CreateDatabaseInput['type'],
|
type: createForm.type as CreateDatabaseInput['type'],
|
||||||
plan: createForm.plan as CreateDatabaseInput['plan'],
|
plan: createForm.plan as CreateDatabaseInput['plan'],
|
||||||
region: createForm.region.trim() || 'local',
|
region: createForm.region.trim() || 'local',
|
||||||
|
public_port: createForm.public,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setCreateOpen(false);
|
setCreateOpen(false);
|
||||||
setCreateForm({ name: '', type: 'postgresql', plan: 'hobby', region: 'local' });
|
setCreateForm({ name: '', type: 'postgresql', plan: 'hobby', region: 'local', public: false });
|
||||||
setCreateError(null);
|
setCreateError(null);
|
||||||
invalidate();
|
invalidate();
|
||||||
},
|
},
|
||||||
@@ -1965,6 +1967,11 @@ export function DatabasesPage() {
|
|||||||
onSuccess: (result) => setExternalProbe(result),
|
onSuccess: (result) => setExternalProbe(result),
|
||||||
onError: (error) => setExternalProbe({ ok: false, error: error instanceof Error ? error.message : 'probe failed' }),
|
onError: (error) => setExternalProbe({ ok: false, error: error instanceof Error ? error.message : 'probe failed' }),
|
||||||
});
|
});
|
||||||
|
const publicPortMutation = useMutation({
|
||||||
|
mutationFn: ({ id, publicPort }: { id: string; publicPort: boolean }) =>
|
||||||
|
updateDatabase(id, { public_port: publicPort }),
|
||||||
|
onSuccess: () => window.setTimeout(invalidate, 1500),
|
||||||
|
});
|
||||||
const testByIdMutation = useMutation({
|
const testByIdMutation = useMutation({
|
||||||
mutationFn: (id: string) => testDatabaseConnectionByID(id),
|
mutationFn: (id: string) => testDatabaseConnectionByID(id),
|
||||||
onSuccess: (result, id) => setExternalTestResult((prev) => ({ ...prev, [id]: result })),
|
onSuccess: (result, id) => setExternalTestResult((prev) => ({ ...prev, [id]: result })),
|
||||||
@@ -2132,6 +2139,13 @@ export function DatabasesPage() {
|
|||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-[var(--radius-md)] border border-[var(--border-subtle)] text-xs font-medium text-[var(--text-secondary)] hover:border-[var(--border-default)] disabled:opacity-40 transition-colors"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-[var(--radius-md)] border border-[var(--border-subtle)] text-xs font-medium text-[var(--text-secondary)] hover:border-[var(--border-default)] disabled:opacity-40 transition-colors"
|
||||||
>
|
>
|
||||||
<RefreshCw size={11} /> Restart
|
<RefreshCw size={11} /> Restart
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { if (window.confirm(`${db.public_port ? 'Stop exposing' : 'Expose'} ${db.name} on all interfaces? The container will be recreated; data persists.`)) publicPortMutation.mutate({ id: db.id ?? '', publicPort: !db.public_port }) }}
|
||||||
|
disabled={publicPortMutation.isPending}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-[var(--radius-md)] border text-xs font-medium transition-colors disabled:opacity-40 ${db.public_port ? 'border-[var(--warning)] text-[var(--warning)]' : 'border-[var(--border-subtle)] text-[var(--text-secondary)] hover:border-[var(--border-default)]'}`}
|
||||||
|
>
|
||||||
|
<Link2 size={11} /> {db.public_port ? 'Public ✓' : 'Local only'}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -2368,6 +2382,7 @@ export function DatabasesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Type</label>
|
<label className="mb-1.5 block text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Type</label>
|
||||||
@@ -2403,6 +2418,16 @@ export function DatabasesPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<label className="flex items-center gap-2 text-xs text-[var(--text-secondary)]">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={createForm.public}
|
||||||
|
onChange={(e) => setCreateForm((f) => ({ ...f, public: e.target.checked }))}
|
||||||
|
className="accent-[var(--accent-primary)]"
|
||||||
|
/>
|
||||||
|
Expose port on all interfaces (default: localhost only)
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{createError ? (
|
{createError ? (
|
||||||
<div className="rounded-[var(--radius-md)] bg-[var(--error-soft)] px-3.5 py-2.5 text-xs text-[var(--error)]">{createError}</div>
|
<div className="rounded-[var(--radius-md)] bg-[var(--error-soft)] px-3.5 py-2.5 text-xs text-[var(--error)]">{createError}</div>
|
||||||
|
|||||||
@@ -9185,6 +9185,8 @@ export interface components {
|
|||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
provider?: "managed" | "external";
|
provider?: "managed" | "external";
|
||||||
|
/** @description managed only — bind the database port on all interfaces instead of 127.0.0.1; toggling recreates the container */
|
||||||
|
public_port?: boolean;
|
||||||
external?: components["schemas"]["DatabaseExternal"];
|
external?: components["schemas"]["DatabaseExternal"];
|
||||||
/**
|
/**
|
||||||
* Format: date-time
|
* Format: date-time
|
||||||
@@ -9220,6 +9222,8 @@ export interface components {
|
|||||||
plan: "hobby" | "starter" | "standard" | "business";
|
plan: "hobby" | "starter" | "standard" | "business";
|
||||||
/** @description Database region */
|
/** @description Database region */
|
||||||
region: string;
|
region: string;
|
||||||
|
/** @description Bind the database port on all interfaces (default 127.0.0.1) */
|
||||||
|
public_port?: boolean;
|
||||||
};
|
};
|
||||||
DatabaseExternal: {
|
DatabaseExternal: {
|
||||||
host?: string;
|
host?: string;
|
||||||
@@ -9253,6 +9257,8 @@ export interface components {
|
|||||||
plan?: "hobby" | "starter" | "standard" | "business";
|
plan?: "hobby" | "starter" | "standard" | "business";
|
||||||
/** @description Cron expression for automatic backups; empty string clears the schedule */
|
/** @description Cron expression for automatic backups; empty string clears the schedule */
|
||||||
backup_schedule?: string | null;
|
backup_schedule?: string | null;
|
||||||
|
/** @description Toggle the published port between 127.0.0.1 and all interfaces; recreates the managed container */
|
||||||
|
public_port?: boolean;
|
||||||
};
|
};
|
||||||
DatabaseActionRequest: {
|
DatabaseActionRequest: {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const bundledDocs: BundledDoc[] = [
|
|||||||
"path": "guides/CLI.md",
|
"path": "guides/CLI.md",
|
||||||
"section": "guides",
|
"section": "guides",
|
||||||
"title": "Containr CLI",
|
"title": "Containr CLI",
|
||||||
"markdown": "# Containr CLI\n\nThe CLI controls every part of a Containr instance — the same API surface\nthe web UI uses. Build it with `scripts/build-cli.sh` or\n`go build ./cmd/cli` from `app/backend`.\n\n## Authentication\n\nCreate a personal access token in the web UI under\n**Settings → Personal Access Tokens** (or `containr tokens create`), then:\n\n```bash\ncontainr auth login cnp_... # verify + store\ncontainr auth login cnp_... --url https://containr.example.com\ncontainr auth login cnp_... --name production # named profile\ncontainr auth status # show active profile + identity\ncontainr auth logout\n```\n\nResolution order for every command: `--token`/`--api-url` flags >\n`CONTAINR_TOKEN`/`CONTAINR_API_URL` env > active profile >\n`http://localhost:8080/api/v1`. The active profile is `--profile` >\n`CONTAINR_PROFILE` > `current_profile` in `~/.containr.yaml`.\n\n## Output and scripting\n\n- `--json` prints the raw API response — safe to pipe and parse.\n- `--yes` / `-y` skips confirmation on destructive commands (delete,\n revoke, restore, failover). Non-interactive shells refuse destructive\n commands without it.\n- Exit codes are stable: `0` ok, `1` error, `2` auth/permission,\n `3` not found, `4` validation, `5` unreachable.\n\n## Commands\n\n```\nauth login|logout|status token auth and profiles\ntokens list|create|revoke personal access tokens\nprojects list|get|create|delete\nservices list|get|create|update|delete|start|stop|restart|redeploy\nservices clone|move duplicate config or re-parent a service\nservices domains list|add|remove|default|check multi-domain + DNS preflight\nregistries list|add|update|remove private-image pull credentials\nvolumes list|delete docker volume inventory + cleanup (admin)\ndeploy <service-id> trigger a deployment (--no-cache, --commit-hash, --branch)\ndeployments list|get|logs|rollback|cancel\nlogs <service-id> [--follow] runtime logs (SSE follow)\nexec <service-id> -- <cmd> one-off container command\nvariables list|set|unset env vars (secrets masked; --redeploy applies them)\ndatabases list|get|create|delete|action|backup|restore|download-backup\ndatabases register <name> register an external database (--type --host --port --database --username --password --ssl; probed before storing)\ndatabases test-connection probe a connection — by id, or ad-hoc flags\ncron list|get|create|delete|trigger|executions\ntemplates list|get|create|delete|deploy\nnodes list|get|delete|tokens node agents + onboarding tokens (admin)\nscaling status|policies|services|scale\nha status|alerts|health|policies|enable|disable|failover\nsecurity scan|vulnerabilities|metrics\ngateway services|keys|analytics (admin)\nadmin overview|users|settings|audit-logs\nnotifications list|read\nup deploy the current directory\n```\n\n## containr up\n\nRun inside a git checkout: uses `remote.origin.url` + the current branch\nas the build source, creates a service named after the directory when none\nexists, then triggers a deployment.\n\n```bash\ncontainr up --project <id>\ncontainr up --project <id> --image ghcr.io/example/app:latest # image, no build\n```\n\nSet `project_id` on the active profile to omit `--project` (stored in\n`~/.containr.yaml` under `profiles.<name>.project_id`).\n\n## Service volumes\n\n`services update` manages mounts; flags replace the whole mount list:\n\n```bash\ncontainr services update <id> --volume data:/data --bind /srv/cfg:/etc/app:ro\ncontainr services update <id> --clear-volumes\n```\n\n`type` is `volume` (docker named volume, auto-created) or `bind` (host\npath). `target` must be absolute. Mounts apply on the next deploy or\nredeploy — `containr deploy <id>` to apply immediately.\n\n`volumes list` shows the node's docker volume inventory with in-use\nflags; `volumes delete <name>` removes an unused volume (admin scope).\n\n## Service domains and access\n\nA service can carry multiple hostnames; the default is mirrored to\n`services.domain`:\n\n```bash\ncontainr services domains add <id> app.example.com --default\ncontainr services domains check <id> # DNS preflight per domain\ncontainr services domains remove <id> <domain-id>\n```\n\nDNS check compares each domain to the expected target — set\n`app_settings.public_ip` (or `PUBLIC_IP` env) so `wrong-target` is\nmeaningful; without it any resolvable domain reports `ok`.\n\nAccess gates apply via Traefik middlewares (no extra proxy):\n\n```bash\ncontainr services update <id> --maintenance on\ncontainr services update <id> --basic-auth admin:secret --basic-auth ops:pw2\n```\n\nMaintenance mode redirects all traffic to `<BASE_URL>/api/v1/maintenance`\n— set `app_settings.base_url` or `BASE_URL`; without it the service is\ntaken offline with empty responses. Basic-auth credentials are stored\nbcrypt-hashed (htpasswd) and gate eLine truncated
|
"markdown": "# Containr CLI\n\nThe CLI controls every part of a Containr instance — the same API surface\nthe web UI uses. Build it with `scripts/build-cli.sh` or\n`go build ./cmd/cli` from `app/backend`.\n\n## Authentication\n\nCreate a personal access token in the web UI under\n**Settings → Personal Access Tokens** (or `containr tokens create`), then:\n\n```bash\ncontainr auth login cnp_... # verify + store\ncontainr auth login cnp_... --url https://containr.example.com\ncontainr auth login cnp_... --name production # named profile\ncontainr auth status # show active profile + identity\ncontainr auth logout\n```\n\nResolution order for every command: `--token`/`--api-url` flags >\n`CONTAINR_TOKEN`/`CONTAINR_API_URL` env > active profile >\n`http://localhost:8080/api/v1`. The active profile is `--profile` >\n`CONTAINR_PROFILE` > `current_profile` in `~/.containr.yaml`.\n\n## Output and scripting\n\n- `--json` prints the raw API response — safe to pipe and parse.\n- `--yes` / `-y` skips confirmation on destructive commands (delete,\n revoke, restore, failover). Non-interactive shells refuse destructive\n commands without it.\n- Exit codes are stable: `0` ok, `1` error, `2` auth/permission,\n `3` not found, `4` validation, `5` unreachable.\n\n## Commands\n\n```\nauth login|logout|status token auth and profiles\ntokens list|create|revoke personal access tokens\nprojects list|get|create|delete\nservices list|get|create|update|delete|start|stop|restart|redeploy\nservices clone|move duplicate config or re-parent a service\nservices domains list|add|remove|default|check multi-domain + DNS preflight\nregistries list|add|update|remove private-image pull credentials\nvolumes list|delete docker volume inventory + cleanup (admin)\ndeploy <service-id> trigger a deployment (--no-cache, --commit-hash, --branch)\ndeployments list|get|logs|rollback|cancel\nlogs <service-id> [--follow] runtime logs (SSE follow)\nexec <service-id> -- <cmd> one-off container command\nvariables list|set|unset env vars (secrets masked; --redeploy applies them)\ndatabases list|get|create|update|delete|action|backup|restore|download-backup\n create --public exposes the port on all interfaces;\n update <id> --public/--public=false toggles it (container recreates, data persists)\ndatabases register <name> register an external database (--type --host --port --database --username --password --ssl; probed before storing)\ndatabases test-connection probe a connection — by id, or ad-hoc flags\ncron list|get|create|delete|trigger|executions\ntemplates list|get|create|delete|deploy\nnodes list|get|delete|tokens node agents + onboarding tokens (admin)\nscaling status|policies|services|scale\nha status|alerts|health|policies|enable|disable|failover\nsecurity scan|vulnerabilities|metrics\ngateway services|keys|analytics (admin)\nadmin overview|users|settings|audit-logs\nnotifications list|read\nup deploy the current directory\n```\n\n## containr up\n\nRun inside a git checkout: uses `remote.origin.url` + the current branch\nas the build source, creates a service named after the directory when none\nexists, then triggers a deployment.\n\n```bash\ncontainr up --project <id>\ncontainr up --project <id> --image ghcr.io/example/app:latest # image, no build\n```\n\nSet `project_id` on the active profile to omit `--project` (stored in\n`~/.containr.yaml` under `profiles.<name>.project_id`).\n\n## Service volumes\n\n`services update` manages mounts; flags replace the whole mount list:\n\n```bash\ncontainr services update <id> --volume data:/data --bind /srv/cfg:/etc/app:ro\ncontainr services update <id> --clear-volumes\n```\n\n`type` is `volume` (docker named volume, auto-created) or `bind` (host\npath). `target` must be absolute. Mounts apply on the next deploy or\nredeploy — `containr deploy <id>` to apply immediately.\n\n`volumes list` shows the node's docker volume inventory with in-use\nflags; `volumes delete <name>` removes an unused volume (admin scope).\n\n## Service domains and access\n\nA service can carry multiple hostnames; the default is mirrored to\n`services.domain`:\n\n```bash\ncontainr services domains add <id> app.example.com --default\ncontainr services domains check <id> # DNS preflight per domain\ncontainr services domains remove <id> <domain-id>\n```\n\nDNS check compares each domain to the expected target — set\n`app_settings.public_ip` (or `PUBLIC_IP` env) so `wrong-target` is\nmeaningful; without it any resolvable domain reports `ok`.\n\nAccess gates apply via Traefik middlewares (no extra proxy):\n\n```bash\ncontainr services update <id> --maintenance on\ncontainr services update <id> --basic-auth admin:secret --basic-auth ops:pw2\n```\n\nMaintenance mode redirects all traffic to `<BASE
Line truncated
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "guides/CLOUDFLARE_SETUP.md",
|
"path": "guides/CLOUDFLARE_SETUP.md",
|
||||||
|
|||||||
@@ -1708,6 +1708,13 @@ export async function getDatabase(id: string): Promise<DatabaseEntity> {
|
|||||||
return requestJson<DatabaseEntity>(`/databases/${encodeURIComponent(id)}`);
|
return requestJson<DatabaseEntity>(`/databases/${encodeURIComponent(id)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateDatabase(id: string, input: components['schemas']['UpdateDatabaseRequest']): Promise<void> {
|
||||||
|
await requestJson(`/databases/${encodeURIComponent(id)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function databaseAction(id: string, action: 'start' | 'stop' | 'restart'): Promise<void> {
|
export async function databaseAction(id: string, action: 'start' | 'stop' | 'restart'): Promise<void> {
|
||||||
await requestJson(`/databases/${encodeURIComponent(id)}/action`, {
|
await requestJson(`/databases/${encodeURIComponent(id)}/action`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -6391,6 +6391,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
enum: [managed, external]
|
enum: [managed, external]
|
||||||
description: managed = Containr-provisioned container; external = registered connection to a database hosted elsewhere
|
description: managed = Containr-provisioned container; external = registered connection to a database hosted elsewhere
|
||||||
|
public_port:
|
||||||
|
type: boolean
|
||||||
|
description: managed only — bind the database port on all interfaces instead of 127.0.0.1; toggling recreates the container
|
||||||
external:
|
external:
|
||||||
$ref: '#/components/schemas/DatabaseExternal'
|
$ref: '#/components/schemas/DatabaseExternal'
|
||||||
next_backup_at:
|
next_backup_at:
|
||||||
@@ -6434,6 +6437,9 @@ components:
|
|||||||
region:
|
region:
|
||||||
type: string
|
type: string
|
||||||
description: Database region
|
description: Database region
|
||||||
|
public_port:
|
||||||
|
type: boolean
|
||||||
|
description: Bind the database port on all interfaces (default 127.0.0.1)
|
||||||
|
|
||||||
DatabaseExternal:
|
DatabaseExternal:
|
||||||
type: object
|
type: object
|
||||||
@@ -6492,6 +6498,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Cron expression for automatic backups; empty string clears the schedule
|
description: Cron expression for automatic backups; empty string clears the schedule
|
||||||
|
public_port:
|
||||||
|
type: boolean
|
||||||
|
description: Toggle the published port between 127.0.0.1 and all interfaces; recreates the managed container
|
||||||
|
|
||||||
DatabaseActionRequest:
|
DatabaseActionRequest:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
+3
-1
@@ -48,7 +48,9 @@ logs <service-id> [--follow] runtime logs (SSE follow)
|
|||||||
exec <service-id> -- <cmd> one-off container command
|
exec <service-id> -- <cmd> one-off container command
|
||||||
variables list|set|unset env vars (secrets masked; --redeploy applies them)
|
variables list|set|unset env vars (secrets masked; --redeploy applies them)
|
||||||
variables shared list|set|unset project shared vars — referenced as ${{shared.KEY}}
|
variables shared list|set|unset project shared vars — referenced as ${{shared.KEY}}
|
||||||
databases list|get|create|delete|action|backup|restore|download-backup
|
databases list|get|create|update|delete|action|backup|restore|download-backup
|
||||||
|
create --public exposes the port on all interfaces;
|
||||||
|
update <id> --public/--public=false toggles it (container recreates, data persists)
|
||||||
databases register <name> register an external database (--type --host --port --database --username --password --ssl; probed before storing)
|
databases register <name> register an external database (--type --host --port --database --username --password --ssl; probed before storing)
|
||||||
databases test-connection probe a connection — by id, or ad-hoc flags
|
databases test-connection probe a connection — by id, or ad-hoc flags
|
||||||
cron list|get|create|delete|trigger|executions
|
cron list|get|create|delete|trigger|executions
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Base URL: `https://<host>/api/v1`. OpenAPI: `docs/api/openapi.yaml`.
|
|||||||
- **Clone & move**: `POST /services/{id}/clone` `{name?, project_id?, environment?}` copies config, volumes, domains, access gates, and variables (secret ciphertext verbatim; clone starts stopped). `POST /services/{id}/move` `{project_id}` re-parents a service — both require ownership of source and target; name conflicts → `CONFLICT`. Cloned domains are verbatim copies: the same hostname on two services makes routing ambiguous.
|
- **Clone & move**: `POST /services/{id}/clone` `{name?, project_id?, environment?}` copies config, volumes, domains, access gates, and variables (secret ciphertext verbatim; clone starts stopped). `POST /services/{id}/move` `{project_id}` re-parents a service — both require ownership of source and target; name conflicts → `CONFLICT`. Cloned domains are verbatim copies: the same hostname on two services makes routing ambiguous.
|
||||||
- **Registries**: `GET|POST /registries`, `PUT|DELETE /registries/{id}` — owner-scoped private-image pull credentials, encrypted at rest, never returned (`has_password` signals presence). `host` matched against the image reference on image deploys (`docker.io` for Docker Hub; scheme/case normalized). Empty `password` on update keeps the stored credential.
|
- **Registries**: `GET|POST /registries`, `PUT|DELETE /registries/{id}` — owner-scoped private-image pull credentials, encrypted at rest, never returned (`has_password` signals presence). `host` matched against the image reference on image deploys (`docker.io` for Docker Hub; scheme/case normalized). Empty `password` on update keeps the stored credential.
|
||||||
- **External databases**: `POST /databases/register-external` (probes first; `{name,type,host,port,database,username,password,ssl}`), `POST /databases/test-connection` (ad-hoc probe → `{ok,latency_ms,error}`), `POST /databases/{id}/test-connection` (re-probe with stored credentials). Rows carry `provider:"external"` + `external{host,port,database,username,has_password,ssl}`; passwords encrypted at rest, never returned. Lifecycle actions and backups reject external rows — no container exists.
|
- **External databases**: `POST /databases/register-external` (probes first; `{name,type,host,port,database,username,password,ssl}`), `POST /databases/test-connection` (ad-hoc probe → `{ok,latency_ms,error}`), `POST /databases/{id}/test-connection` (re-probe with stored credentials). Rows carry `provider:"external"` + `external{host,port,database,username,has_password,ssl}`; passwords encrypted at rest, never returned. Lifecycle actions and backups reject external rows — no container exists.
|
||||||
|
- **Public DB port**: managed databases bind `127.0.0.1` by default; `public_port:true` on create or `PUT /databases/{id} {public_port}` binds `0.0.0.0`. Toggling recreates the container — the data volume persists.
|
||||||
- **Identifiers**: all ids are UUIDs. Tokens: `cnp_` user PATs, `cagt_` agent onboarding tokens.
|
- **Identifiers**: all ids are UUIDs. Tokens: `cnp_` user PATs, `cagt_` agent onboarding tokens.
|
||||||
|
|
||||||
## Core objects
|
## Core objects
|
||||||
|
|||||||
Reference in new issue
Block a user