182 lines
6.8 KiB
Go
182 lines
6.8 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
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.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
|
|
FROM projects p
|
|
JOIN workspaces w ON w.id = p.workspace_id
|
|
WHERE p.user_id=$1 ORDER BY p.created_at DESC`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Project
|
|
for rows.Next() {
|
|
var p Project
|
|
if err := rows.Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
|
|
out = append(out, p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (a *App) createProject(ctx context.Context, user User, workspaceID int64, name, description string, private bool, existingRepoURL string, targetPort int) (Project, error) {
|
|
slug := slugify(name)
|
|
if slug == "" {
|
|
return Project{}, errors.New("project name must include letters or numbers")
|
|
}
|
|
workspace, err := a.getWorkspace(ctx, user.Username, workspaceID)
|
|
if err != nil {
|
|
return Project{}, err
|
|
}
|
|
|
|
repoURL := existingRepoURL
|
|
serviceName := fmt.Sprintf("projectmgr-%s-%s.service", slugify(user.Username), slug)
|
|
routeHost := a.buildRouteHost(slug, user.Username)
|
|
deployToken, err := newDeployToken()
|
|
if err != nil {
|
|
return Project{}, err
|
|
}
|
|
|
|
var p Project
|
|
tx, err := a.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return Project{}, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
err = tx.QueryRowContext(ctx, `INSERT INTO projects (user_id, workspace_id, name, slug, description, repo_url, service_name, route_host, target_port, deploy_token, provision_state, provision_error) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'pending','') RETURNING id, user_id, name, slug, description, repo_url, service_name, route_host, target_port, workspace_id, deploy_token, provision_state, provision_error, created_at`,
|
|
user.Username, workspaceID, name, slug, description, repoURL, serviceName, routeHost, targetPort, deployToken,
|
|
).Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "duplicate key") {
|
|
return Project{}, errors.New("project with this name already exists")
|
|
}
|
|
return Project{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return Project{}, err
|
|
}
|
|
|
|
p.Workspace = workspace.Name
|
|
p.UnixUser = workspace.UnixUser
|
|
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
|
|
|
|
if p.RepoURL == "" {
|
|
repoURL, err = a.createForgejoRepo(ctx, slug, description, private)
|
|
if err != nil {
|
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
|
p.ProvisionState = "failed"
|
|
p.ProvisionError = err.Error()
|
|
return p, err
|
|
}
|
|
p.RepoURL = repoURL
|
|
if _, err := a.db.ExecContext(ctx, `UPDATE projects SET repo_url=$1 WHERE id=$2`, repoURL, p.ID); err != nil {
|
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
|
p.ProvisionState = "failed"
|
|
p.ProvisionError = err.Error()
|
|
return p, err
|
|
}
|
|
}
|
|
if err := a.ensureForgejoDeployWebhook(ctx, p); err != nil {
|
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
|
p.ProvisionState = "failed"
|
|
p.ProvisionError = err.Error()
|
|
return p, err
|
|
}
|
|
|
|
if err := a.renderSystemdForProject(ctx, p); err != nil {
|
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
|
p.ProvisionState = "failed"
|
|
p.ProvisionError = err.Error()
|
|
return p, err
|
|
}
|
|
if err := a.ensureCaddyRoute(ctx, p); err != nil {
|
|
_ = a.updateProvisioningState(ctx, p.ID, "failed", err.Error())
|
|
p.ProvisionState = "failed"
|
|
p.ProvisionError = err.Error()
|
|
return p, err
|
|
}
|
|
if err := a.updateProvisioningState(ctx, p.ID, "provisioned", ""); err != nil {
|
|
p.ProvisionState = "failed"
|
|
p.ProvisionError = err.Error()
|
|
return p, err
|
|
}
|
|
p.ProvisionState = "provisioned"
|
|
p.ProvisionError = ""
|
|
return p, nil
|
|
}
|
|
|
|
func (a *App) getProject(ctx context.Context, userID string, projectID int64) (Project, error) {
|
|
var p Project
|
|
err := a.db.QueryRowContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, 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
|
|
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.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.ProvisionState, &p.ProvisionError, &p.CreatedAt)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return Project{}, errors.New("project not found")
|
|
}
|
|
return Project{}, err
|
|
}
|
|
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
|
|
return p, nil
|
|
}
|
|
|
|
func (a *App) updateProvisioningState(ctx context.Context, projectID int64, state, errText string) error {
|
|
_, err := a.db.ExecContext(ctx, `UPDATE projects SET provision_state=$1, provision_error=$2 WHERE id=$3`, state, errText, projectID)
|
|
return err
|
|
}
|
|
|
|
func (a *App) listEnvVars(ctx context.Context, userID string, projectID int64) ([]EnvVar, error) {
|
|
if _, err := a.getProject(ctx, userID, projectID); err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := a.db.QueryContext(ctx, `SELECT id, project_id, key, value FROM project_env_vars WHERE project_id=$1 ORDER BY key`, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []EnvVar
|
|
for rows.Next() {
|
|
var v EnvVar
|
|
if err := rows.Scan(&v.ID, &v.ProjectID, &v.Key, &v.Value); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, v)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (a *App) upsertEnvVar(ctx context.Context, userID string, projectID int64, key, value string) error {
|
|
p, err := a.getProject(ctx, userID, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = a.db.ExecContext(ctx, `INSERT INTO project_env_vars (project_id, key, value) VALUES ($1,$2,$3)
|
|
ON CONFLICT(project_id, key) DO UPDATE SET value = EXCLUDED.value`, projectID, key, value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return a.renderSystemdForProject(ctx, p)
|
|
}
|
|
|
|
func (a *App) deleteEnvVar(ctx context.Context, userID string, projectID int64, key string) error {
|
|
p, err := a.getProject(ctx, userID, projectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = a.db.ExecContext(ctx, `DELETE FROM project_env_vars WHERE project_id=$1 AND key=$2`, projectID, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return a.renderSystemdForProject(ctx, p)
|
|
}
|