unprovision
This commit is contained in:
parent
22c2f68c2a
commit
1e53bba6e7
6 changed files with 148 additions and 0 deletions
|
|
@ -46,6 +46,8 @@ func RunCLI(args []string) error {
|
|||
switch args[0] {
|
||||
case "provision":
|
||||
return runCLIProvision(args[1:])
|
||||
case "unprovision":
|
||||
return runCLIUnprovision(args[1:])
|
||||
case "login":
|
||||
return runCLILogin(args[1:])
|
||||
case "help", "--help", "-h":
|
||||
|
|
@ -121,6 +123,44 @@ func runCLIProvision(args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runCLIUnprovision(args []string) error {
|
||||
state, _ := loadCLIState()
|
||||
fs := flag.NewFlagSet("unprovision", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
|
||||
var apiURL, name string
|
||||
fs.StringVar(&apiURL, "api-url", firstNonEmpty(state.APIURL, getenv("BOX_API_URL", "http://127.0.0.1:8080")), "box server API base URL")
|
||||
fs.StringVar(&name, "name", "", "project name (required)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
apiURL = strings.TrimRight(strings.TrimSpace(apiURL), "/")
|
||||
name = strings.TrimSpace(name)
|
||||
if apiURL == "" || name == "" {
|
||||
return errors.New("required flags: --api-url, --name")
|
||||
}
|
||||
state.APIURL = apiURL
|
||||
c := cliClient{baseURL: apiURL, state: &state}
|
||||
if err := c.ensureAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := saveCLIState(state); err != nil {
|
||||
return err
|
||||
}
|
||||
existing, err := c.findProjectBySlug(slugify(name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing == nil {
|
||||
return errors.New("project not found")
|
||||
}
|
||||
if err := c.postEmpty(fmt.Sprintf("/api/projects/%d/unprovision", existing.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("project unprovisioned id=%d name=%s slug=%s\n", existing.ID, existing.Name, existing.Slug)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCLILogin(args []string) error {
|
||||
state, _ := loadCLIState()
|
||||
fs := flag.NewFlagSet("login", flag.ContinueOnError)
|
||||
|
|
@ -448,6 +488,28 @@ func (c *cliClient) postJSON(path string, payload any, out any) error {
|
|||
return json.Unmarshal(body, out)
|
||||
}
|
||||
|
||||
func (c *cliClient) postEmpty(path string) error {
|
||||
if err := c.ensureAccessToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, c.baseURL+path, bytes.NewReader([]byte("{}")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.state.AccessToken)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("api %s failed status=%d body=%s", path, resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printCLIProject(action string, p Project) {
|
||||
fmt.Printf("project %s id=%d name=%s slug=%s\n", action, p.ID, p.Name, p.Slug)
|
||||
fmt.Printf("workspace=%s unix_user=%s\n", p.Workspace, p.UnixUser)
|
||||
|
|
@ -533,9 +595,11 @@ func cliUsageText() string {
|
|||
go run . # server
|
||||
go run ./cmd/boxctl login --api-url https://box.example.com --issuer https://auth.example.com/application/o/box --client-id box-cli [--workspace <name>] [--unix-user <unix_user>]
|
||||
go run ./cmd/boxctl provision --name <project_name> [--repo-url <url>] [--description <text>] [--private=true|false]
|
||||
go run ./cmd/boxctl unprovision --name <project_name>
|
||||
|
||||
Commands:
|
||||
login OAuth browser login (PKCE) and save tokens/defaults in ~/.config/boxctl/config.json
|
||||
provision Upsert project via API using bearer token
|
||||
unprovision Tear down project service/db/route and remove project record
|
||||
`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,32 @@ END $$;`, sqlQuoteLiteral(p.DBUser), sqlQuoteIdent(p.DBUser), sqlQuoteLiteral(p.
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *App) dropProjectDatabase(ctx context.Context, p Project) error {
|
||||
if !a.cfg.DBProvisionEnabled {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(p.DBName) == "" || strings.TrimSpace(p.DBUser) == "" {
|
||||
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)
|
||||
}
|
||||
// Terminate active sessions so DROP DATABASE succeeds.
|
||||
_, _ = adminDB.ExecContext(ctx, `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, p.DBName)
|
||||
if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`DROP DATABASE IF EXISTS %s`, sqlQuoteIdent(p.DBName))); err != nil {
|
||||
return fmt.Errorf("drop database failed: %w", err)
|
||||
}
|
||||
if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`DROP ROLE IF EXISTS %s`, sqlQuoteIdent(p.DBUser))); err != nil {
|
||||
return fmt.Errorf("drop role failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) projectDatabaseURL(p Project) (string, error) {
|
||||
if p.DBName == "" || p.DBUser == "" || p.DBPassword == "" {
|
||||
return "", nil
|
||||
|
|
|
|||
|
|
@ -211,6 +211,26 @@ func (a *App) reprovisionProject(ctx context.Context, userID string, projectID i
|
|||
return p, nil
|
||||
}
|
||||
|
||||
func (a *App) unprovisionProject(ctx context.Context, userID string, projectID int64) error {
|
||||
p, err := a.getProject(ctx, userID, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.removeSystemdForProject(p); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.dropProjectDatabase(ctx, p); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := a.db.ExecContext(ctx, `DELETE FROM projects WHERE id=$1 AND user_id=$2`, p.ID, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.syncCaddyConfigFragment(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -143,6 +143,8 @@ func (a *App) handleProjectSubroutes(w http.ResponseWriter, r *http.Request, use
|
|||
a.handleServiceRegenerate(w, r, user, projectID)
|
||||
case "reprovision":
|
||||
a.handleProjectReprovision(w, r, user, projectID)
|
||||
case "unprovision":
|
||||
a.handleProjectUnprovision(w, r, user, projectID)
|
||||
case "status":
|
||||
a.handleProjectStatus(w, r, user, projectID)
|
||||
case "logs":
|
||||
|
|
@ -165,6 +167,18 @@ func (a *App) handleProjectReprovision(w http.ResponseWriter, r *http.Request, u
|
|||
writeJSON(w, http.StatusOK, p)
|
||||
}
|
||||
|
||||
func (a *App) handleProjectUnprovision(w http.ResponseWriter, r *http.Request, user User, projectID int64) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := a.unprovisionProject(r.Context(), user.Username, projectID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "unprovisioned"})
|
||||
}
|
||||
|
||||
func (a *App) handleProjectEnv(w http.ResponseWriter, r *http.Request, user User, projectID int64) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
|
|
|
|||
|
|
@ -257,3 +257,20 @@ func (a *App) getServiceStatus(p Project) (ServiceStatus, error) {
|
|||
func (a *App) getServiceLogs(p Project, lines int) (string, error) {
|
||||
return runJournalctlUser(p.UnixUser, "-u", p.ServiceName, "--no-pager", "-n", strconv.Itoa(lines), "-r", "-o", "short-iso")
|
||||
}
|
||||
|
||||
func (a *App) removeSystemdForProject(p Project) error {
|
||||
homeDir, err := homeForUnixUser(p.UnixUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
servicePath := filepath.Join(homeDir, ".config", "systemd", "user", p.ServiceName)
|
||||
projectDir, err := a.projectDirFor(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = runSystemctlUser(p.UnixUser, "disable", "--now", p.ServiceName)
|
||||
_ = os.Remove(servicePath)
|
||||
_ = runSystemctlUser(p.UnixUser, "daemon-reload")
|
||||
_ = os.Remove(filepath.Join(projectDir, "app.sock"))
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue