Refactor customer → instance package, add dock service, update provision.sh
- Rename internal/customer → internal/instance, customers table → instances - Rename API endpoint /customers → /instances - Add internal/instance package with clean schema (drops email, subscription ID) - Add JSON struct tags, exclude ssh_private_key from responses - Build out crimata-dock C service (libmicrohttpd + libsystemd) - Update provision.sh to install full OS stack from adgundersen/os - Add MIT License Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
17d85202f7
commit
276fddc467
5 changed files with 299 additions and 98 deletions
|
|
@ -11,15 +11,15 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/adgundersen/crimata-infra/internal/compute"
|
||||
"github.com/adgundersen/crimata-infra/internal/customer"
|
||||
"github.com/adgundersen/crimata-infra/internal/dns"
|
||||
"github.com/adgundersen/crimata-infra/internal/export"
|
||||
"github.com/adgundersen/crimata-infra/internal/instance"
|
||||
"github.com/adgundersen/crimata-infra/internal/notify"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
store *customer.Store
|
||||
store *instance.Store
|
||||
compute *compute.Client
|
||||
dns *dns.Client
|
||||
notify *notify.Client
|
||||
|
|
@ -27,7 +27,7 @@ type Handler struct {
|
|||
}
|
||||
|
||||
func NewHandler(
|
||||
store *customer.Store,
|
||||
store *instance.Store,
|
||||
compute *compute.Client,
|
||||
dns *dns.Client,
|
||||
notify *notify.Client,
|
||||
|
|
@ -41,9 +41,9 @@ func (h *Handler) Routes() http.Handler {
|
|||
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
r.Post("/customers", h.createCustomer)
|
||||
r.Get("/customers/{slug}", h.getCustomer)
|
||||
r.Delete("/customers/{slug}", h.deleteCustomer)
|
||||
r.Post("/instances", h.createInstance)
|
||||
r.Get("/instances/{slug}", h.getInstance)
|
||||
r.Delete("/instances/{slug}", h.deleteInstance)
|
||||
return r
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ type createRequest struct {
|
|||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func (h *Handler) createCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) createInstance(w http.ResponseWriter, r *http.Request) {
|
||||
var req createRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
|
|
@ -71,116 +71,113 @@ func (h *Handler) createCustomer(w http.ResponseWriter, r *http.Request) {
|
|||
password := randomHex(12)
|
||||
dbPassword := randomHex(24)
|
||||
|
||||
c := &customer.Customer{
|
||||
StripeCustomerID: req.StripeCustomerID,
|
||||
StripeSubscriptionID: req.StripeSubscriptionID,
|
||||
Email: req.Email,
|
||||
Slug: slug,
|
||||
Status: customer.StatusProvisioning,
|
||||
inst := &instance.Instance{
|
||||
StripeCustomerID: req.StripeCustomerID,
|
||||
Slug: slug,
|
||||
Status: instance.StatusProvisioning,
|
||||
}
|
||||
if err := h.store.Create(c); err != nil {
|
||||
http.Error(w, "failed to create customer record", http.StatusInternalServerError)
|
||||
if err := h.store.Create(inst); err != nil {
|
||||
http.Error(w, "failed to create instance record", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
go h.provision(context.Background(), c, password, dbPassword)
|
||||
go h.provision(context.Background(), inst, req.Email, password, dbPassword)
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
jsonResponse(w, c, http.StatusAccepted)
|
||||
jsonResponse(w, inst, http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (h *Handler) getCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) getInstance(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
c, err := h.store.GetBySlug(slug)
|
||||
if err != nil || c == nil {
|
||||
inst, err := h.store.GetBySlug(slug)
|
||||
if err != nil || inst == nil {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
jsonResponse(w, c, http.StatusOK)
|
||||
jsonResponse(w, inst, http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handler) deleteCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) deleteInstance(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
c, err := h.store.GetBySlug(slug)
|
||||
if err != nil || c == nil {
|
||||
inst, err := h.store.GetBySlug(slug)
|
||||
if err != nil || inst == nil {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
go h.deprovision(context.Background(), c)
|
||||
go h.deprovision(context.Background(), inst)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// ── Provisioning ──────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) provision(ctx context.Context, c *customer.Customer, password, dbPassword string) {
|
||||
func (h *Handler) provision(ctx context.Context, inst *instance.Instance, email, password, dbPassword string) {
|
||||
// 1. Launch EC2
|
||||
instance, err := h.compute.Launch(ctx, c.Slug)
|
||||
ec2, err := h.compute.Launch(ctx, inst.Slug)
|
||||
if err != nil {
|
||||
fmt.Printf("provision: launch failed for %s: %v\n", c.Email, err)
|
||||
h.store.UpdateStatus(c.ID, customer.StatusFailed)
|
||||
fmt.Printf("provision: launch failed for %s: %v\n", inst.Slug, err)
|
||||
h.store.UpdateStatus(inst.ID, instance.StatusFailed)
|
||||
return
|
||||
}
|
||||
h.store.UpdateEC2(c.ID, instance.InstanceID, instance.PublicIP)
|
||||
h.store.UpdateSSHKey(c.ID, instance.SSHPrivateKey)
|
||||
h.store.UpdateEC2(inst.ID, ec2.InstanceID, ec2.PublicIP)
|
||||
h.store.UpdateSSHKey(inst.ID, ec2.SSHPrivateKey)
|
||||
|
||||
// 2. Wait for instance to be ready
|
||||
if err := h.compute.WaitUntilReady(ctx, instance.InstanceID, instance.PublicIP); err != nil {
|
||||
fmt.Printf("provision: wait failed for %s: %v\n", c.Email, err)
|
||||
h.store.UpdateStatus(c.ID, customer.StatusFailed)
|
||||
if err := h.compute.WaitUntilReady(ctx, ec2.InstanceID, ec2.PublicIP); err != nil {
|
||||
fmt.Printf("provision: wait failed for %s: %v\n", inst.Slug, err)
|
||||
h.store.UpdateStatus(inst.ID, instance.StatusFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Run provisioning script via SSH
|
||||
if err := h.compute.Provision(ctx, instance.PublicIP, instance.SSHPrivateKey, c.Slug, password, dbPassword); err != nil {
|
||||
fmt.Printf("provision: script failed for %s: %v\n", c.Email, err)
|
||||
h.store.UpdateStatus(c.ID, customer.StatusFailed)
|
||||
if err := h.compute.Provision(ctx, ec2.PublicIP, ec2.SSHPrivateKey, inst.Slug, password, dbPassword); err != nil {
|
||||
fmt.Printf("provision: script failed for %s: %v\n", inst.Slug, err)
|
||||
h.store.UpdateStatus(inst.ID, instance.StatusFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Create Route53 record
|
||||
if err := h.dns.CreateRecord(ctx, c.Slug, instance.PublicIP); err != nil {
|
||||
fmt.Printf("provision: dns failed for %s: %v\n", c.Email, err)
|
||||
if err := h.dns.CreateRecord(ctx, inst.Slug, ec2.PublicIP); err != nil {
|
||||
fmt.Printf("provision: dns failed for %s: %v\n", inst.Slug, err)
|
||||
}
|
||||
|
||||
// 5. Send welcome email
|
||||
if err := h.notify.SendWelcome(ctx, c.Email, c.Slug, password); err != nil {
|
||||
fmt.Printf("provision: email failed for %s: %v\n", c.Email, err)
|
||||
if err := h.notify.SendWelcome(ctx, email, inst.Slug, password); err != nil {
|
||||
fmt.Printf("provision: email failed for %s: %v\n", inst.Slug, err)
|
||||
}
|
||||
|
||||
h.store.UpdateStatus(c.ID, customer.StatusActive)
|
||||
fmt.Printf("provision: %s is live at %s.crimata.com\n", c.Email, c.Slug)
|
||||
h.store.UpdateStatus(inst.ID, instance.StatusActive)
|
||||
fmt.Printf("provision: %s is live at %s.crimata.com\n", email, inst.Slug)
|
||||
}
|
||||
|
||||
func (h *Handler) deprovision(ctx context.Context, c *customer.Customer) {
|
||||
h.store.UpdateStatus(c.ID, customer.StatusCancelled)
|
||||
func (h *Handler) deprovision(ctx context.Context, inst *instance.Instance) {
|
||||
h.store.UpdateStatus(inst.ID, instance.StatusCancelled)
|
||||
|
||||
// 1. Export data and email download link
|
||||
downloadURL, err := h.export.Export(ctx, c.EC2InstanceID, c.Slug)
|
||||
downloadURL, err := h.export.Export(ctx, inst.EC2InstanceID, inst.Slug)
|
||||
if err != nil {
|
||||
fmt.Printf("deprovision: export failed for %s: %v\n", c.Email, err)
|
||||
fmt.Printf("deprovision: export failed for %s: %v\n", inst.Slug, err)
|
||||
} else {
|
||||
h.notify.SendDataExport(ctx, c.Email, downloadURL)
|
||||
h.notify.SendDataExport(ctx, inst.StripeCustomerID, downloadURL)
|
||||
}
|
||||
|
||||
// 2. Terminate EC2
|
||||
if err := h.compute.Terminate(ctx, c.EC2InstanceID); err != nil {
|
||||
fmt.Printf("deprovision: terminate failed for %s: %v\n", c.Email, err)
|
||||
if err := h.compute.Terminate(ctx, inst.EC2InstanceID); err != nil {
|
||||
fmt.Printf("deprovision: terminate failed for %s: %v\n", inst.Slug, err)
|
||||
}
|
||||
|
||||
// 3. Remove Route53 record
|
||||
if err := h.dns.DeleteRecord(ctx, c.Slug, c.EC2PublicIP); err != nil {
|
||||
fmt.Printf("deprovision: dns delete failed for %s: %v\n", c.Email, err)
|
||||
if err := h.dns.DeleteRecord(ctx, inst.Slug, inst.EC2PublicIP); err != nil {
|
||||
fmt.Printf("deprovision: dns delete failed for %s: %v\n", inst.Slug, err)
|
||||
}
|
||||
|
||||
fmt.Printf("deprovision: %s cleaned up\n", c.Email)
|
||||
fmt.Printf("deprovision: %s cleaned up\n", inst.Slug)
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
var slugRe = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
func uniqueSlug(store *customer.Store, email string) string {
|
||||
func uniqueSlug(store *instance.Store, email string) string {
|
||||
base := strings.Trim(slugRe.ReplaceAllString(
|
||||
strings.ToLower(strings.Split(email, "@")[0]), "-"), "-")
|
||||
if len(base) > 20 {
|
||||
|
|
|
|||
|
|
@ -1,66 +1,147 @@
|
|||
#!/bin/bash
|
||||
# provision.sh — v0.1 bootstrap: installs nginx and serves a welcome page
|
||||
# provision.sh — Crimata OS bootstrap
|
||||
# Usage: provision.sh <slug> <password> <db_password>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SLUG=$1
|
||||
PASSWORD=$2
|
||||
DB_PASSWORD=$3
|
||||
OS_REPO="https://github.com/adgundersen/os"
|
||||
|
||||
log() { echo "[crimata] $1"; }
|
||||
|
||||
# ── 1. System dependencies ─────────────────────────────────────────────────────
|
||||
log "Installing nginx..."
|
||||
# ── 1. System dependencies ──────────────────────────────────────────────────
|
||||
log "Installing system dependencies..."
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq nginx
|
||||
apt-get install -y -qq \
|
||||
nginx \
|
||||
postgresql postgresql-contrib \
|
||||
gcc make \
|
||||
libmicrohttpd-dev \
|
||||
libpam-dev \
|
||||
libsystemd-dev \
|
||||
nodejs npm \
|
||||
git
|
||||
|
||||
# ── 2. Welcome page ────────────────────────────────────────────────────────────
|
||||
log "Creating welcome page..."
|
||||
mkdir -p /var/www/crimata
|
||||
# ── 2. Linux user (PAM auth uses real OS users) ─────────────────────────────
|
||||
log "Creating user $SLUG..."
|
||||
useradd --create-home --shell /bin/bash "$SLUG" || true
|
||||
echo "$SLUG:$PASSWORD" | chpasswd
|
||||
|
||||
cat > /var/www/crimata/index.html << EOF
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crimata — $SLUG</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #fff;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
h1 { font-size: 2rem; font-weight: 600; }
|
||||
p { color: #888; font-size: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome, $SLUG.</h1>
|
||||
<p>Your Crimata hub is ready.</p>
|
||||
</body>
|
||||
</html>
|
||||
# ── 3. Fetch OS source ──────────────────────────────────────────────────────
|
||||
log "Fetching OS..."
|
||||
git clone --depth 1 "$OS_REPO" /tmp/crimata-os
|
||||
|
||||
# ── 4. Build and install core binaries ─────────────────────────────────────
|
||||
log "Building crimata-auth..."
|
||||
mkdir -p /opt/crimata/bin
|
||||
make -C /tmp/crimata-os/auth OUT=/opt/crimata/bin/crimata-auth
|
||||
|
||||
log "Building crimata-dock..."
|
||||
make -C /tmp/crimata-os/dock OUT=/opt/crimata/bin/crimata-dock
|
||||
|
||||
# ── 5. Install web desktop UI ───────────────────────────────────────────────
|
||||
log "Installing UI..."
|
||||
mkdir -p /opt/crimata/ui
|
||||
cp -r /tmp/crimata-os/ui/* /opt/crimata/ui/
|
||||
|
||||
# ── 6. Install apps ─────────────────────────────────────────────────────────
|
||||
log "Installing crimata-contacts..."
|
||||
mkdir -p /usr/lib/crimata-contacts
|
||||
cp -r /tmp/crimata-os/apps/contacts/* /usr/lib/crimata-contacts/
|
||||
cd /usr/lib/crimata-contacts
|
||||
npm install --silent
|
||||
npm run build
|
||||
|
||||
# ── 7. Postgres setup ───────────────────────────────────────────────────────
|
||||
log "Configuring Postgres..."
|
||||
systemctl enable postgresql
|
||||
systemctl start postgresql
|
||||
|
||||
su -c "psql -tc \"SELECT 1 FROM pg_roles WHERE rolname='crimata'\" | grep -q 1 || \
|
||||
psql -c \"CREATE USER crimata WITH PASSWORD '$DB_PASSWORD';\"" postgres
|
||||
|
||||
su -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='crimata_contacts'\" | grep -q 1 || \
|
||||
psql -c \"CREATE DATABASE crimata_contacts OWNER crimata;\"" postgres
|
||||
|
||||
DATABASE_URL="postgresql://crimata:$DB_PASSWORD@localhost/crimata_contacts" \
|
||||
node /usr/lib/crimata-contacts/dist/migrate.js
|
||||
|
||||
# ── 8. systemd units ────────────────────────────────────────────────────────
|
||||
log "Installing systemd units..."
|
||||
|
||||
cat > /etc/systemd/system/crimata-auth.service << EOF
|
||||
[Unit]
|
||||
Description=Crimata Auth
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/opt/crimata/bin/crimata-auth
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# ── 3. Nginx config ────────────────────────────────────────────────────────────
|
||||
cat > /etc/systemd/system/crimata-dock.service << EOF
|
||||
[Unit]
|
||||
Description=Crimata Dock
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/opt/crimata/bin/crimata-dock
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
cat > /etc/systemd/system/crimata-contacts.service << EOF
|
||||
[Unit]
|
||||
Description=Crimata Contacts
|
||||
After=network.target postgresql.service
|
||||
|
||||
[Service]
|
||||
ExecStart=node /usr/lib/crimata-contacts/dist/index.js
|
||||
Restart=always
|
||||
Environment=PORT=3001
|
||||
Environment=DATABASE_URL=postgresql://crimata:$DB_PASSWORD@localhost/crimata_contacts
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable crimata-auth crimata-dock crimata-contacts
|
||||
systemctl start crimata-auth crimata-dock crimata-contacts
|
||||
|
||||
# ── 9. Nginx ────────────────────────────────────────────────────────────────
|
||||
log "Configuring nginx..."
|
||||
|
||||
cat > /etc/nginx/sites-available/crimata << EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name $SLUG.crimata.com;
|
||||
|
||||
root /var/www/crimata;
|
||||
root /opt/crimata/ui;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
|
||||
location /auth {
|
||||
proxy_pass http://localhost:7700;
|
||||
}
|
||||
|
||||
location /dock/ {
|
||||
proxy_pass http://localhost:7701/;
|
||||
}
|
||||
|
||||
location /apps/contacts/ {
|
||||
proxy_pass http://localhost:3001/contacts/;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
|
|
@ -70,4 +151,7 @@ nginx -t
|
|||
systemctl enable nginx
|
||||
systemctl restart nginx
|
||||
|
||||
log "Provisioning complete. $SLUG.crimata.com is ready."
|
||||
# ── 10. Cleanup ─────────────────────────────────────────────────────────────
|
||||
rm -rf /tmp/crimata-os
|
||||
|
||||
log "Done. $SLUG.crimata.com is live."
|
||||
|
|
|
|||
113
internal/instance/instance.go
Normal file
113
internal/instance/instance.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package instance
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusProvisioning Status = "provisioning"
|
||||
StatusActive Status = "active"
|
||||
StatusFailed Status = "failed"
|
||||
StatusCancelled Status = "cancelled"
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
ID int64 `json:"id"`
|
||||
StripeCustomerID string `json:"stripe_customer_id"`
|
||||
Slug string `json:"slug"`
|
||||
EC2InstanceID string `json:"ec2_instance_id"`
|
||||
EC2PublicIP string `json:"ec2_public_ip"`
|
||||
SSHPrivateKey string `json:"-"`
|
||||
Status Status `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewStore(db *sql.DB) *Store {
|
||||
return &Store{db: db}
|
||||
}
|
||||
|
||||
func (s *Store) Migrate() error {
|
||||
_, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS instances (
|
||||
id SERIAL PRIMARY KEY,
|
||||
stripe_customer_id TEXT UNIQUE NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
ec2_instance_id TEXT NOT NULL DEFAULT '',
|
||||
ec2_public_ip TEXT NOT NULL DEFAULT '',
|
||||
ssh_private_key TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'provisioning',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) Create(inst *Instance) error {
|
||||
return s.db.QueryRow(`
|
||||
INSERT INTO instances (stripe_customer_id, slug, status)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, created_at`,
|
||||
inst.StripeCustomerID, inst.Slug, inst.Status,
|
||||
).Scan(&inst.ID, &inst.CreatedAt)
|
||||
}
|
||||
|
||||
func (s *Store) GetByStripeID(stripeCustomerID string) (*Instance, error) {
|
||||
inst := &Instance{}
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id, stripe_customer_id, slug, ec2_instance_id, ec2_public_ip,
|
||||
ssh_private_key, status, created_at
|
||||
FROM instances WHERE stripe_customer_id = $1`,
|
||||
stripeCustomerID,
|
||||
).Scan(
|
||||
&inst.ID, &inst.StripeCustomerID, &inst.Slug,
|
||||
&inst.EC2InstanceID, &inst.EC2PublicIP,
|
||||
&inst.SSHPrivateKey, &inst.Status, &inst.CreatedAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return inst, err
|
||||
}
|
||||
|
||||
func (s *Store) GetBySlug(slug string) (*Instance, error) {
|
||||
inst := &Instance{}
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id, stripe_customer_id, slug, ec2_instance_id, ec2_public_ip,
|
||||
ssh_private_key, status, created_at
|
||||
FROM instances WHERE slug = $1`,
|
||||
slug,
|
||||
).Scan(
|
||||
&inst.ID, &inst.StripeCustomerID, &inst.Slug,
|
||||
&inst.EC2InstanceID, &inst.EC2PublicIP,
|
||||
&inst.SSHPrivateKey, &inst.Status, &inst.CreatedAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return inst, err
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSSHKey(id int64, privateKey string) error {
|
||||
_, err := s.db.Exec(`UPDATE instances SET ssh_private_key = $1 WHERE id = $2`, privateKey, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) UpdateEC2(id int64, instanceID, publicIP string) error {
|
||||
_, err := s.db.Exec(
|
||||
`UPDATE instances SET ec2_instance_id = $1, ec2_public_ip = $2 WHERE id = $3`,
|
||||
instanceID, publicIP, id,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) UpdateStatus(id int64, status Status) error {
|
||||
_, err := s.db.Exec(`UPDATE instances SET status = $1 WHERE id = $2`, status, id)
|
||||
return err
|
||||
}
|
||||
Loading…
Reference in a new issue