639 lines
17 KiB
Go

package deployments
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/DeBrosOfficial/network/pkg/auth"
"github.com/spf13/cobra"
)
// DeployCmd is the root deploy command
var DeployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy applications",
Long: "Deploy static sites, Next.js apps, Go backends, and Node.js backends",
}
// DeployStaticCmd deploys a static site
var DeployStaticCmd = &cobra.Command{
Use: "static <source_path>",
Short: "Deploy a static site (React, Vue, etc.)",
Args: cobra.ExactArgs(1),
RunE: deployStatic,
}
// DeployNextJSCmd deploys a Next.js application
var DeployNextJSCmd = &cobra.Command{
Use: "nextjs <source_path>",
Short: "Deploy a Next.js application",
Args: cobra.ExactArgs(1),
RunE: deployNextJS,
}
// DeployGoCmd deploys a Go backend
var DeployGoCmd = &cobra.Command{
Use: "go <source_path>",
Short: "Deploy a Go backend",
Args: cobra.ExactArgs(1),
RunE: deployGo,
}
// DeployNodeJSCmd deploys a Node.js backend
var DeployNodeJSCmd = &cobra.Command{
Use: "nodejs <source_path>",
Short: "Deploy a Node.js backend",
Args: cobra.ExactArgs(1),
RunE: deployNodeJS,
}
var (
deployName string
deploySubdomain string
deploySSR bool
deployUpdate bool
)
func init() {
DeployStaticCmd.Flags().StringVar(&deployName, "name", "", "Deployment name (required)")
DeployStaticCmd.Flags().StringVar(&deploySubdomain, "subdomain", "", "Custom subdomain")
DeployStaticCmd.Flags().BoolVar(&deployUpdate, "update", false, "Update existing deployment")
DeployStaticCmd.MarkFlagRequired("name")
DeployNextJSCmd.Flags().StringVar(&deployName, "name", "", "Deployment name (required)")
DeployNextJSCmd.Flags().StringVar(&deploySubdomain, "subdomain", "", "Custom subdomain")
DeployNextJSCmd.Flags().BoolVar(&deploySSR, "ssr", false, "Deploy with SSR (server-side rendering)")
DeployNextJSCmd.Flags().BoolVar(&deployUpdate, "update", false, "Update existing deployment")
DeployNextJSCmd.MarkFlagRequired("name")
DeployGoCmd.Flags().StringVar(&deployName, "name", "", "Deployment name (required)")
DeployGoCmd.Flags().StringVar(&deploySubdomain, "subdomain", "", "Custom subdomain")
DeployGoCmd.Flags().BoolVar(&deployUpdate, "update", false, "Update existing deployment")
DeployGoCmd.MarkFlagRequired("name")
DeployNodeJSCmd.Flags().StringVar(&deployName, "name", "", "Deployment name (required)")
DeployNodeJSCmd.Flags().StringVar(&deploySubdomain, "subdomain", "", "Custom subdomain")
DeployNodeJSCmd.Flags().BoolVar(&deployUpdate, "update", false, "Update existing deployment")
DeployNodeJSCmd.MarkFlagRequired("name")
DeployCmd.AddCommand(DeployStaticCmd)
DeployCmd.AddCommand(DeployNextJSCmd)
DeployCmd.AddCommand(DeployGoCmd)
DeployCmd.AddCommand(DeployNodeJSCmd)
}
func deployStatic(cmd *cobra.Command, args []string) error {
sourcePath := args[0]
// Warn if source looks like it needs building
if _, err := os.Stat(filepath.Join(sourcePath, "package.json")); err == nil {
if _, err := os.Stat(filepath.Join(sourcePath, "index.html")); os.IsNotExist(err) {
fmt.Printf("⚠️ Warning: %s has package.json but no index.html. You may need to build first.\n", sourcePath)
fmt.Printf(" Try: cd %s && npm run build, then deploy the output directory (e.g. dist/ or out/)\n\n", sourcePath)
}
}
fmt.Printf("📦 Creating tarball from %s...\n", sourcePath)
tarball, err := createTarball(sourcePath)
if err != nil {
return fmt.Errorf("failed to create tarball: %w", err)
}
defer os.Remove(tarball)
fmt.Printf("☁️ Uploading to Orama Network...\n")
endpoint := "/v1/deployments/static/upload"
if deployUpdate {
endpoint = "/v1/deployments/static/update"
}
resp, err := uploadDeployment(endpoint, tarball, map[string]string{
"name": deployName,
"subdomain": deploySubdomain,
})
if err != nil {
return err
}
fmt.Printf("\n✅ Deployment successful!\n\n")
printDeploymentInfo(resp)
return nil
}
func deployNextJS(cmd *cobra.Command, args []string) error {
sourcePath, err := filepath.Abs(args[0])
if err != nil {
return fmt.Errorf("failed to resolve path: %w", err)
}
// Verify it's a Next.js project
if _, err := os.Stat(filepath.Join(sourcePath, "package.json")); os.IsNotExist(err) {
return fmt.Errorf("no package.json found in %s", sourcePath)
}
// Step 1: Install dependencies if needed
if _, err := os.Stat(filepath.Join(sourcePath, "node_modules")); os.IsNotExist(err) {
fmt.Printf("📦 Installing dependencies...\n")
if err := runBuildCommand(sourcePath, "npm", "install"); err != nil {
return fmt.Errorf("npm install failed: %w", err)
}
}
// Step 2: Build
fmt.Printf("🔨 Building Next.js application...\n")
if err := runBuildCommand(sourcePath, "npm", "run", "build"); err != nil {
return fmt.Errorf("build failed: %w", err)
}
var tarball string
if deploySSR {
// SSR: tarball the standalone output
standalonePath := filepath.Join(sourcePath, ".next", "standalone")
if _, err := os.Stat(standalonePath); os.IsNotExist(err) {
return fmt.Errorf(".next/standalone/ not found. Ensure next.config.js has output: 'standalone'")
}
// Copy static assets into standalone
staticSrc := filepath.Join(sourcePath, ".next", "static")
staticDst := filepath.Join(standalonePath, ".next", "static")
if _, err := os.Stat(staticSrc); err == nil {
if err := copyDir(staticSrc, staticDst); err != nil {
return fmt.Errorf("failed to copy static assets: %w", err)
}
}
// Copy public directory if it exists
publicSrc := filepath.Join(sourcePath, "public")
publicDst := filepath.Join(standalonePath, "public")
if _, err := os.Stat(publicSrc); err == nil {
if err := copyDir(publicSrc, publicDst); err != nil {
return fmt.Errorf("failed to copy public directory: %w", err)
}
}
fmt.Printf("📦 Creating tarball from standalone output...\n")
tarball, err = createTarballAll(standalonePath)
} else {
// Static export: tarball the out/ directory
outPath := filepath.Join(sourcePath, "out")
if _, err := os.Stat(outPath); os.IsNotExist(err) {
return fmt.Errorf("out/ directory not found. For static export, ensure next.config.js has output: 'export'")
}
fmt.Printf("📦 Creating tarball from static export...\n")
tarball, err = createTarball(outPath)
}
if err != nil {
return fmt.Errorf("failed to create tarball: %w", err)
}
defer os.Remove(tarball)
fmt.Printf("☁️ Uploading to Orama Network...\n")
endpoint := "/v1/deployments/nextjs/upload"
if deployUpdate {
endpoint = "/v1/deployments/nextjs/update"
}
resp, err := uploadDeployment(endpoint, tarball, map[string]string{
"name": deployName,
"subdomain": deploySubdomain,
"ssr": fmt.Sprintf("%t", deploySSR),
})
if err != nil {
return err
}
fmt.Printf("\n✅ Deployment successful!\n\n")
printDeploymentInfo(resp)
if deploySSR {
fmt.Printf("⚠️ Note: SSR deployment may take a minute to start. Check status with: orama deployments get %s\n", deployName)
}
return nil
}
func deployGo(cmd *cobra.Command, args []string) error {
sourcePath, err := filepath.Abs(args[0])
if err != nil {
return fmt.Errorf("failed to resolve path: %w", err)
}
// Verify it's a Go project
if _, err := os.Stat(filepath.Join(sourcePath, "go.mod")); os.IsNotExist(err) {
return fmt.Errorf("no go.mod found in %s", sourcePath)
}
// Cross-compile for Linux amd64 (production VPS target)
fmt.Printf("🔨 Building Go binary (linux/amd64)...\n")
buildCmd := exec.Command("go", "build", "-o", "app", ".")
buildCmd.Dir = sourcePath
buildCmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=0")
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
if err := buildCmd.Run(); err != nil {
return fmt.Errorf("go build failed: %w", err)
}
defer os.Remove(filepath.Join(sourcePath, "app")) // Clean up after tarball
fmt.Printf("📦 Creating tarball...\n")
tarball, err := createTarballFiles(sourcePath, []string{"app"})
if err != nil {
return fmt.Errorf("failed to create tarball: %w", err)
}
defer os.Remove(tarball)
fmt.Printf("☁️ Uploading to Orama Network...\n")
endpoint := "/v1/deployments/go/upload"
if deployUpdate {
endpoint = "/v1/deployments/go/update"
}
resp, err := uploadDeployment(endpoint, tarball, map[string]string{
"name": deployName,
"subdomain": deploySubdomain,
})
if err != nil {
return err
}
fmt.Printf("\n✅ Deployment successful!\n\n")
printDeploymentInfo(resp)
return nil
}
func deployNodeJS(cmd *cobra.Command, args []string) error {
sourcePath, err := filepath.Abs(args[0])
if err != nil {
return fmt.Errorf("failed to resolve path: %w", err)
}
// Verify it's a Node.js project
if _, err := os.Stat(filepath.Join(sourcePath, "package.json")); os.IsNotExist(err) {
return fmt.Errorf("no package.json found in %s", sourcePath)
}
// Install dependencies if needed
if _, err := os.Stat(filepath.Join(sourcePath, "node_modules")); os.IsNotExist(err) {
fmt.Printf("📦 Installing dependencies...\n")
if err := runBuildCommand(sourcePath, "npm", "install", "--production"); err != nil {
return fmt.Errorf("npm install failed: %w", err)
}
}
// Run build script if it exists
if hasBuildScript(sourcePath) {
fmt.Printf("🔨 Building...\n")
if err := runBuildCommand(sourcePath, "npm", "run", "build"); err != nil {
return fmt.Errorf("build failed: %w", err)
}
}
fmt.Printf("📦 Creating tarball...\n")
tarball, err := createTarball(sourcePath)
if err != nil {
return fmt.Errorf("failed to create tarball: %w", err)
}
defer os.Remove(tarball)
fmt.Printf("☁️ Uploading to Orama Network...\n")
endpoint := "/v1/deployments/nodejs/upload"
if deployUpdate {
endpoint = "/v1/deployments/nodejs/update"
}
resp, err := uploadDeployment(endpoint, tarball, map[string]string{
"name": deployName,
"subdomain": deploySubdomain,
})
if err != nil {
return err
}
fmt.Printf("\n✅ Deployment successful!\n\n")
printDeploymentInfo(resp)
return nil
}
// runBuildCommand runs a command in the given directory with stdout/stderr streaming
func runBuildCommand(dir string, name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// hasBuildScript checks if package.json has a "build" script
func hasBuildScript(dir string) bool {
data, err := os.ReadFile(filepath.Join(dir, "package.json"))
if err != nil {
return false
}
var pkg map[string]interface{}
if err := json.Unmarshal(data, &pkg); err != nil {
return false
}
scripts, ok := pkg["scripts"].(map[string]interface{})
if !ok {
return false
}
_, ok = scripts["build"]
return ok
}
// copyDir recursively copies a directory
func copyDir(src, dst string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
dstPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(dstPath, data, info.Mode())
})
}
// createTarballFiles creates a tarball containing only specific files from a directory
func createTarballFiles(baseDir string, files []string) (string, error) {
tmpFile, err := os.CreateTemp("", "orama-deploy-*.tar.gz")
if err != nil {
return "", err
}
defer tmpFile.Close()
gzWriter := gzip.NewWriter(tmpFile)
defer gzWriter.Close()
tarWriter := tar.NewWriter(gzWriter)
defer tarWriter.Close()
for _, f := range files {
fullPath := filepath.Join(baseDir, f)
info, err := os.Stat(fullPath)
if err != nil {
return "", fmt.Errorf("file %s not found: %w", f, err)
}
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return "", err
}
header.Name = f
if err := tarWriter.WriteHeader(header); err != nil {
return "", err
}
if !info.IsDir() {
file, err := os.Open(fullPath)
if err != nil {
return "", err
}
_, err = io.Copy(tarWriter, file)
file.Close()
if err != nil {
return "", err
}
}
}
return tmpFile.Name(), nil
}
func createTarball(sourcePath string) (string, error) {
return createTarballWithOptions(sourcePath, true)
}
// createTarballAll creates a tarball including node_modules and hidden dirs (for standalone output)
func createTarballAll(sourcePath string) (string, error) {
return createTarballWithOptions(sourcePath, false)
}
func createTarballWithOptions(sourcePath string, skipNodeModules bool) (string, error) {
// Create temp file
tmpFile, err := os.CreateTemp("", "orama-deploy-*.tar.gz")
if err != nil {
return "", err
}
defer tmpFile.Close()
// Create gzip writer
gzWriter := gzip.NewWriter(tmpFile)
defer gzWriter.Close()
// Create tar writer
tarWriter := tar.NewWriter(gzWriter)
defer tarWriter.Close()
// Walk directory and add files
err = filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip hidden files and node_modules (unless disabled)
if skipNodeModules {
if strings.HasPrefix(info.Name(), ".") && info.Name() != "." {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if info.Name() == "node_modules" {
return filepath.SkipDir
}
}
// Create tar header
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
// Update header name to be relative to source
relPath, err := filepath.Rel(sourcePath, path)
if err != nil {
return err
}
header.Name = relPath
// Write header
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
// Write file content if not a directory
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(tarWriter, file)
return err
}
return nil
})
return tmpFile.Name(), err
}
func uploadDeployment(endpoint, tarballPath string, formData map[string]string) (map[string]interface{}, error) {
// Open tarball
file, err := os.Open(tarballPath)
if err != nil {
return nil, err
}
defer file.Close()
// Create multipart request
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Add form fields
for key, value := range formData {
writer.WriteField(key, value)
}
// Add file
part, err := writer.CreateFormFile("tarball", filepath.Base(tarballPath))
if err != nil {
return nil, err
}
_, err = io.Copy(part, file)
if err != nil {
return nil, err
}
writer.Close()
// Get API URL from config
apiURL := getAPIURL()
url := apiURL + endpoint
// Create request
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
// Add auth header
token, err := getAuthToken()
if err != nil {
return nil, fmt.Errorf("authentication required: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
// Send request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Read response
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("deployment failed: %s", string(respBody))
}
// Parse response
var result map[string]interface{}
err = json.Unmarshal(respBody, &result)
if err != nil {
return nil, err
}
return result, nil
}
func printDeploymentInfo(resp map[string]interface{}) {
fmt.Printf("Name: %s\n", resp["name"])
fmt.Printf("Type: %s\n", resp["type"])
fmt.Printf("Status: %s\n", resp["status"])
fmt.Printf("Version: %v\n", resp["version"])
if contentCID, ok := resp["content_cid"]; ok && contentCID != "" {
fmt.Printf("Content CID: %s\n", contentCID)
}
if urls, ok := resp["urls"].([]interface{}); ok && len(urls) > 0 {
fmt.Printf("\nURLs:\n")
for _, url := range urls {
fmt.Printf(" • %s\n", url)
}
}
}
func getAPIURL() string {
// Check environment variable first
if url := os.Getenv("ORAMA_API_URL"); url != "" {
return url
}
// Get from active environment config
return auth.GetDefaultGatewayURL()
}
func getAuthToken() (string, error) {
// Check environment variable first
if token := os.Getenv("ORAMA_TOKEN"); token != "" {
return token, nil
}
// Try to get from enhanced credentials store
store, err := auth.LoadEnhancedCredentials()
if err != nil {
return "", fmt.Errorf("failed to load credentials: %w", err)
}
gatewayURL := auth.GetDefaultGatewayURL()
creds := store.GetDefaultCredential(gatewayURL)
if creds == nil {
return "", fmt.Errorf("no credentials found for %s. Run 'orama auth login' to authenticate", gatewayURL)
}
if !creds.IsValid() {
return "", fmt.Errorf("credentials expired for %s. Run 'orama auth login' to re-authenticate", gatewayURL)
}
return creds.APIKey, nil
}