Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions autoship-server/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,40 @@ package main

import (
"log"
"os"

"github.com/Ashmit-Kumar/Auto-Ship/autoship-server/internal/api"
"github.com/Ashmit-Kumar/Auto-Ship/autoship-server/internal/cloud"
"github.com/Ashmit-Kumar/Auto-Ship/autoship-server/internal/config"
"github.com/Ashmit-Kumar/Auto-Ship/autoship-server/internal/db"
"github.com/Ashmit-Kumar/Auto-Ship/autoship-server/internal/utils"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/joho/godotenv"
)

func main() {
if err := godotenv.Load(); err != nil {
log.Println("No .env file found, using environment variables")
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
}

if err := cloud.Init(os.Getenv("CLOUD_PROVIDER")); err != nil {
if err := cloud.Init(cfg.CloudProvider); err != nil {
log.Fatalf("Failed to initialize cloud provider: %v", err)
}
if err := utils.LoadEnv(); err != nil {
log.Fatalf("Error loading JWT environment variables: %v", err)
}

mongoURI := os.Getenv("MONGO_URI")
if mongoURI == "" {
log.Fatal("MONGO_URI is not set")
}
db.SetMongoURI(mongoURI)
db.SetMongoURI(cfg.MongoURI)
db.Connect()
defer db.Disconnect()

port := os.Getenv("PORT")
if port == "" {
port = "3000"
if err := db.EnsurePortsIndex(cfg.MongoCollection); err != nil {
log.Fatalf("Failed to ensure ports unique index: %v", err)
}

app := fiber.New()
app.Use(cors.New())
api.RegisterRoutes(app)

log.Printf("🚀 Server running on http://localhost:%s", port)
log.Fatal(app.Listen(":" + port))
log.Printf("🚀 Server running on http://localhost:%s", cfg.ServerPort)
log.Fatal(app.Listen(":" + cfg.ServerPort))
}
10 changes: 10 additions & 0 deletions autoship-server/internal/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import (
)

// Signup handler
//
// FUTURE: no input validation — empty email or empty password parses
// successfully and creates an unusable user (bcrypt happily hashes "").
// Add explicit non-empty + email-format checks before the existing-user
// lookup.
func Signup(c *fiber.Ctx) error {
var user models.User
if err := c.BodyParser(&user); err != nil {
Expand Down Expand Up @@ -123,6 +128,9 @@ func GitHubCallback(c *fiber.Ctx) error {
// Extract email (or fallback)
email, ok := userInfo["email"].(string)
if !ok || email == "" {
// FUTURE: panics if userInfo["login"] is nil or non-string. Fiber
// catches it as 500 but the safe form is:
// if login, ok := userInfo["login"].(string); ok { email = login + "@github.com" }
email = userInfo["login"].(string) + "@github.com"
}

Expand Down Expand Up @@ -162,6 +170,8 @@ func GitHubCallback(c *fiber.Ctx) error {
// })

// Redirect to frontend dashboard with token
// FUTURE: hardcoded — make this a FRONTEND_URL env var once the server
// is deployed somewhere with a non-localhost frontend host.
redirectURL := "http://localhost:3000/dashboard?token=" + token
return c.Redirect(redirectURL, fiber.StatusFound)
}
146 changes: 146 additions & 0 deletions autoship-server/internal/api/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package api

import (
"fmt"
"log"
"os"
"time"

"github.com/Ashmit-Kumar/Auto-Ship/autoship-server/internal/cloud"
"github.com/Ashmit-Kumar/Auto-Ship/autoship-server/internal/db"
"github.com/Ashmit-Kumar/Auto-Ship/autoship-server/internal/models"
"github.com/Ashmit-Kumar/Auto-Ship/autoship-server/internal/services"
"github.com/Ashmit-Kumar/Auto-Ship/autoship-server/internal/utils"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)

// maxConcurrentDeploys caps how many builds run at the same time. Docker
// build + temp container probing is heavy; without a cap, a burst of requests
// can OOM the host.
const maxConcurrentDeploys = 8

// deploySlots is a counting semaphore — acquire by sending, release by
// receiving. Excess deploys block on send until a slot frees up.
var deploySlots = make(chan struct{}, maxConcurrentDeploys)

type deployResult struct {
url string
projectType string
containerPort int
hostPort int
containerName string
}

// runDeployment executes the build/deploy pipeline for a project that has
// already been persisted in the pending state. Runs in a detached goroutine
// from HandleRepoSubmit so HTTP-request cancellation does NOT abort the
// build mid-way (which would otherwise leave half-running containers).
//
// Status transitions: pending -> deploying -> (succeeded | failed). Failure
// detail lands in Project.DeployError so the client polling /projects/:id
// can surface it without needing log access.
func runDeployment(id primitive.ObjectID, username, repoURL, repoName, envContent, startCommand string) {
defer func() {
if r := recover(); r != nil {
log.Printf("[deploy %s] panic: %v", id.Hex(), r)
_ = db.UpdateProjectByID(id, bson.M{
"status": models.StatusFailed,
"deploy_error": fmt.Sprintf("panic: %v", r),
})
}
}()

deploySlots <- struct{}{}
defer func() { <-deploySlots }()

if err := db.UpdateProjectByID(id, bson.M{"status": models.StatusDeploying}); err != nil {
log.Printf("[deploy %s] failed to mark deploying: %v", id.Hex(), err)
// keep going — a transient Mongo blip shouldn't abort the build
}

result, err := executeDeploy(username, repoURL, repoName, envContent, startCommand)
if err != nil {
log.Printf("[deploy %s] failed: %v", id.Hex(), err)
_ = db.UpdateProjectByID(id, bson.M{
"status": models.StatusFailed,
"deploy_error": err.Error(),
})
return
}

if err := db.UpdateProjectByID(id, bson.M{
"status": models.StatusSucceeded,
"project_type": result.projectType,
"hosted_url": result.url,
"container_port": result.containerPort,
"host_port": result.hostPort,
"container_name": result.containerName,
}); err != nil {
log.Printf("[deploy %s] succeeded but failed to persist result: %v", id.Hex(), err)
return
}
log.Printf("[deploy %s] succeeded (%s)", id.Hex(), result.url)
}

func executeDeploy(username, repoURL, repoName, envContent, startCommand string) (deployResult, error) {
path, err := services.CloneRepository(repoURL, username, repoName)
if err != nil {
return deployResult{}, fmt.Errorf("clone failed: %w", err)
}
defer os.RemoveAll(path)

projectType := services.DetectProjectType(path)
if projectType == "unknown" {
return deployResult{}, fmt.Errorf("unknown project type; no recognised entry file in repo")
}

if projectType == "static" {
keyPrefix := fmt.Sprintf("%s/%s", username, repoName)
url, err := cloud.Get().UploadStaticSite(path, keyPrefix)
if err != nil {
return deployResult{}, fmt.Errorf("static upload: %w", err)
}
return deployResult{url: url, projectType: projectType}, nil
}

// Dynamic: build + run container locally, then hand off to autoship-scripts
// (via the file-based queue) for DNS/nginx/SSL.
containerPort, hostPort, containerName, err := services.FullPipeline(username, path, envContent, startCommand)
if err != nil {
return deployResult{}, fmt.Errorf("container pipeline: %w", err)
}

subdomain := utils.GenerateSubdomain(repoName, os.Getenv("DOMAIN"))
reqID := utils.GenerateRandomID()
deployReq := map[string]interface{}{
"id": reqID,
"subdomain": subdomain,
"projectType": projectType,
"port": hostPort,
"status": "pending",
}
if err := utils.AppendJSONToFile("/var/lib/autoship/deploy/deploy-requests.json", deployReq); err != nil {
return deployResult{}, fmt.Errorf("queue deploy request: %w", err)
}

resp, err := utils.WaitForResponse("/var/lib/autoship/deploy/deploy-responses.json", reqID, 60*time.Second)
if err != nil {
return deployResult{}, fmt.Errorf("wait for autoship-scripts: %w", err)
}
if status, _ := resp["status"].(string); status != "success" {
return deployResult{}, fmt.Errorf("autoship-scripts reported failure: %v", resp["error"])
}

url, _ := resp["url"].(string)
if url == "" {
url = fmt.Sprintf("https://%s", subdomain)
}
return deployResult{
url: url,
projectType: projectType,
containerPort: containerPort,
hostPort: hostPort,
containerName: containerName,
}, nil
}
Loading