|
@@ -5,7 +5,6 @@ import (
|
|
|
"bytes"
|
|
|
"context"
|
|
|
"crypto/rand"
|
|
|
- "encoding/base64"
|
|
|
"encoding/json"
|
|
|
"fmt"
|
|
|
"io"
|
|
@@ -15,77 +14,44 @@ import (
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
- "git.linuxforward.com/byop/byop-engine/clients"
|
|
|
+ "git.linuxforward.com/byop/byop-engine/analyzer"
|
|
|
"git.linuxforward.com/byop/byop-engine/dbstore"
|
|
|
"git.linuxforward.com/byop/byop-engine/models"
|
|
|
"github.com/sirupsen/logrus"
|
|
|
-
|
|
|
- "github.com/docker/docker/api/types"
|
|
|
- "github.com/docker/docker/api/types/container"
|
|
|
- "github.com/docker/docker/api/types/filters"
|
|
|
- "github.com/docker/docker/api/types/image"
|
|
|
- "github.com/docker/docker/api/types/registry"
|
|
|
- docker "github.com/docker/docker/client"
|
|
|
)
|
|
|
|
|
|
// PreviewService defines the interface for preview services
|
|
|
type PreviewService interface {
|
|
|
- CreatePreview(ctx context.Context, appId int) (*models.Preview, error)
|
|
|
- DeletePreview(ctx context.Context, appID int) error
|
|
|
- StopPreview(ctx context.Context, previewID int) error
|
|
|
- Close(ctx context.Context) // Updated signature to include context
|
|
|
+ CreatePreview(ctx context.Context, appId uint) (*models.Preview, error)
|
|
|
+ DeletePreview(ctx context.Context, appID uint) error
|
|
|
+ StopPreview(ctx context.Context, previewID uint) error
|
|
|
+ Close(ctx context.Context)
|
|
|
}
|
|
|
|
|
|
// PreviewCommon contains shared functionality for preview services
|
|
|
type PreviewCommon struct {
|
|
|
- store *dbstore.SQLiteStore
|
|
|
- entry *logrus.Entry
|
|
|
- dockerClient *docker.Client
|
|
|
- registryClient clients.RegistryClient
|
|
|
- registryURL string
|
|
|
- registryUser string
|
|
|
- registryPass string
|
|
|
+ store *dbstore.SQLiteStore
|
|
|
+ entry *logrus.Entry
|
|
|
+ registryURL string
|
|
|
+ registryUser string
|
|
|
+ registryPass string
|
|
|
}
|
|
|
|
|
|
// NewPreviewCommon creates a new PreviewCommon instance
|
|
|
-func NewPreviewCommon(store *dbstore.SQLiteStore, registryClient clients.RegistryClient, registryURL, registryUser, registryPass string) *PreviewCommon {
|
|
|
- dockerClient, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithAPIVersionNegotiation())
|
|
|
- if err != nil {
|
|
|
- logrus.WithError(err).Fatal("Failed to create Docker client")
|
|
|
- }
|
|
|
-
|
|
|
+func NewPreviewCommon(store *dbstore.SQLiteStore, registryURL, registryUser, registryPass string) *PreviewCommon {
|
|
|
return &PreviewCommon{
|
|
|
- store: store,
|
|
|
- entry: logrus.WithField("service", "PreviewCommon"),
|
|
|
- dockerClient: dockerClient,
|
|
|
- registryClient: registryClient,
|
|
|
- registryURL: registryURL,
|
|
|
- registryUser: registryUser,
|
|
|
- registryPass: registryPass,
|
|
|
+ store: store,
|
|
|
+ entry: logrus.WithField("service", "PreviewCommon"),
|
|
|
+ registryURL: registryURL,
|
|
|
+ registryUser: registryUser,
|
|
|
+ registryPass: registryPass,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// Close cleans up the Docker client connection
|
|
|
+// Close cleans up resources
|
|
|
func (pc *PreviewCommon) Close() {
|
|
|
- // Clean up BYOP preview images
|
|
|
- pc.CleanupPreviewImages(context.Background())
|
|
|
-
|
|
|
// Clean up preview database state
|
|
|
pc.CleanupPreviewState(context.Background())
|
|
|
-
|
|
|
- // Close the Docker client connection
|
|
|
- if pc.dockerClient != nil {
|
|
|
- if err := pc.dockerClient.Close(); err != nil {
|
|
|
- pc.entry.WithError(err).Error("Failed to close Docker client")
|
|
|
- } else {
|
|
|
- pc.entry.Info("Docker client connection closed")
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// GetDockerClient returns the Docker client
|
|
|
-func (pc *PreviewCommon) GetDockerClient() *docker.Client {
|
|
|
- return pc.dockerClient
|
|
|
}
|
|
|
|
|
|
// GetStore returns the database store
|
|
@@ -136,11 +102,32 @@ func (pc *PreviewCommon) CloneRepository(ctx context.Context, repoURL, branch, t
|
|
|
}
|
|
|
|
|
|
// CreateBuildContext creates a tar archive of the build context
|
|
|
-func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir string) (io.ReadCloser, error) {
|
|
|
+func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir string, component *models.Component) (io.ReadCloser, error) {
|
|
|
var buf bytes.Buffer
|
|
|
tw := tar.NewWriter(&buf)
|
|
|
defer tw.Close()
|
|
|
|
|
|
+ // For docker-compose components, adjust the context directory to use the build context
|
|
|
+ effectiveContextDir := contextDir
|
|
|
+ relativeDockerfilePath := "Dockerfile"
|
|
|
+
|
|
|
+ if component != nil && component.SourceType == "docker-compose" && component.BuildContext != "" {
|
|
|
+ // Resolve the build context directory
|
|
|
+ effectiveContextDir = filepath.Join(contextDir, component.BuildContext)
|
|
|
+
|
|
|
+ // Set Dockerfile path relative to the build context
|
|
|
+ if component.DockerfilePath != "" {
|
|
|
+ relativeDockerfilePath = component.DockerfilePath
|
|
|
+ }
|
|
|
+
|
|
|
+ pc.entry.WithFields(logrus.Fields{
|
|
|
+ "component_id": component.ID,
|
|
|
+ "build_context": component.BuildContext,
|
|
|
+ "dockerfile_path": relativeDockerfilePath,
|
|
|
+ "resolved_context_dir": effectiveContextDir,
|
|
|
+ }).Info("Using docker-compose build context for component")
|
|
|
+ }
|
|
|
+
|
|
|
// Common ignore patterns for Git repositories
|
|
|
ignorePatterns := []string{
|
|
|
".git",
|
|
@@ -192,13 +179,13 @@ func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir stri
|
|
|
"documentation",
|
|
|
}
|
|
|
|
|
|
- err := filepath.Walk(contextDir, func(file string, fi os.FileInfo, err error) error {
|
|
|
+ err := filepath.Walk(effectiveContextDir, func(file string, fi os.FileInfo, err error) error {
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
// Get relative path
|
|
|
- relPath, err := filepath.Rel(contextDir, file)
|
|
|
+ relPath, err := filepath.Rel(effectiveContextDir, file)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
@@ -279,208 +266,59 @@ func (pc *PreviewCommon) CreateBuildContext(ctx context.Context, contextDir stri
|
|
|
return io.NopCloser(&buf), nil
|
|
|
}
|
|
|
|
|
|
-// createDockerIgnore creates a .dockerignore file in the specified directory
|
|
|
-func (pc *PreviewCommon) createDockerIgnore(ctx context.Context, contextDir string) {
|
|
|
- dockerignoreContent := `# Auto-generated by BYOP Engine
|
|
|
-.git
|
|
|
-.gitignore
|
|
|
-node_modules
|
|
|
-.next
|
|
|
-dist
|
|
|
-build
|
|
|
-target
|
|
|
-__pycache__
|
|
|
-*.pyc
|
|
|
-.DS_Store
|
|
|
-Thumbs.db
|
|
|
-*.log
|
|
|
-*.tmp
|
|
|
-*.swp
|
|
|
-.env
|
|
|
-.vscode
|
|
|
-.idea
|
|
|
-playwright
|
|
|
-cypress
|
|
|
-coverage
|
|
|
-test
|
|
|
-tests
|
|
|
-__tests__
|
|
|
-snapshots
|
|
|
-*.test.js
|
|
|
-*.spec.js
|
|
|
-*.test.ts
|
|
|
-*.spec.ts
|
|
|
-*.png
|
|
|
-*.jpg
|
|
|
-*.jpeg
|
|
|
-*.gif
|
|
|
-*.bmp
|
|
|
-*.svg
|
|
|
-*.ico
|
|
|
-*.zip
|
|
|
-*.tar.gz
|
|
|
-*.tar
|
|
|
-*.gz
|
|
|
-README.md
|
|
|
-readme.md
|
|
|
-CHANGELOG.md
|
|
|
-LICENSE
|
|
|
-CONTRIBUTING.md
|
|
|
-*.md
|
|
|
-docs
|
|
|
-documentation
|
|
|
-`
|
|
|
-
|
|
|
- dockerignorePath := filepath.Join(contextDir, ".dockerignore")
|
|
|
- if err := os.WriteFile(dockerignorePath, []byte(dockerignoreContent), 0644); err != nil {
|
|
|
- pc.entry.WithField("path", dockerignorePath).Warnf("Failed to create .dockerignore file: %v", err)
|
|
|
- } else {
|
|
|
- pc.entry.WithField("path", dockerignorePath).Debug("Created .dockerignore file")
|
|
|
- }
|
|
|
+// fileExists checks if a file exists
|
|
|
+func fileExists(path string) bool {
|
|
|
+ _, err := os.Stat(path)
|
|
|
+ return err == nil
|
|
|
}
|
|
|
|
|
|
-// validateAndFixDockerfile checks for unsupported Dockerfile syntax and fixes common issues
|
|
|
-func (pc *PreviewCommon) validateAndFixDockerfile(ctx context.Context, contextDir string) error {
|
|
|
- dockerfilePath := filepath.Join(contextDir, "Dockerfile")
|
|
|
-
|
|
|
- // Check if Dockerfile exists
|
|
|
- if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
|
|
|
- return fmt.Errorf("dockerfile not found in repository")
|
|
|
- }
|
|
|
-
|
|
|
- // Read the Dockerfile
|
|
|
- content, err := os.ReadFile(dockerfilePath)
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("failed to read Dockerfile: %v", err)
|
|
|
- }
|
|
|
-
|
|
|
- originalContent := string(content)
|
|
|
- modifiedContent := originalContent
|
|
|
- modified := false
|
|
|
-
|
|
|
- // Fix common issues
|
|
|
- lines := strings.Split(originalContent, "\n")
|
|
|
- var fixedLines []string
|
|
|
-
|
|
|
- for i, line := range lines {
|
|
|
- trimmedLine := strings.TrimSpace(line)
|
|
|
-
|
|
|
- // Check for unsupported --exclude flag in COPY or ADD commands
|
|
|
- if strings.HasPrefix(trimmedLine, "COPY") || strings.HasPrefix(trimmedLine, "ADD") {
|
|
|
- if strings.Contains(trimmedLine, "--exclude") {
|
|
|
- pc.entry.WithField("line", i+1).Warn("Found unsupported --exclude flag in Dockerfile, removing it")
|
|
|
- // Remove --exclude flag and its arguments
|
|
|
- parts := strings.Fields(trimmedLine)
|
|
|
- var cleanedParts []string
|
|
|
- skipNext := false
|
|
|
-
|
|
|
- for _, part := range parts {
|
|
|
- if skipNext {
|
|
|
- skipNext = false
|
|
|
- continue
|
|
|
- }
|
|
|
- if strings.HasPrefix(part, "--exclude") {
|
|
|
- if strings.Contains(part, "=") {
|
|
|
- // --exclude=pattern format
|
|
|
- continue
|
|
|
- } else {
|
|
|
- // --exclude pattern format
|
|
|
- skipNext = true
|
|
|
- continue
|
|
|
- }
|
|
|
- }
|
|
|
- cleanedParts = append(cleanedParts, part)
|
|
|
- }
|
|
|
-
|
|
|
- fixedLine := strings.Join(cleanedParts, " ")
|
|
|
- fixedLines = append(fixedLines, fixedLine)
|
|
|
- modified = true
|
|
|
- pc.entry.WithField("original", trimmedLine).WithField("fixed", fixedLine).Info("Fixed Dockerfile line")
|
|
|
- } else {
|
|
|
- fixedLines = append(fixedLines, line)
|
|
|
- }
|
|
|
- } else {
|
|
|
- fixedLines = append(fixedLines, line)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Write back the fixed Dockerfile if modified
|
|
|
- if modified {
|
|
|
- modifiedContent = strings.Join(fixedLines, "\n")
|
|
|
- if err := os.WriteFile(dockerfilePath, []byte(modifiedContent), 0644); err != nil {
|
|
|
- return fmt.Errorf("failed to write fixed Dockerfile: %v", err)
|
|
|
- }
|
|
|
- pc.entry.WithField("path", dockerfilePath).Info("Fixed Dockerfile syntax issues")
|
|
|
- }
|
|
|
-
|
|
|
- return nil
|
|
|
-}
|
|
|
-
|
|
|
-// isDockerfilePresent checks if a Dockerfile exists in the repository directory
|
|
|
-func (pc *PreviewCommon) isDockerfilePresent(tempDir string) (bool, error) {
|
|
|
- // Check for common Dockerfile names
|
|
|
- dockerfileNames := []string{"Dockerfile", "dockerfile", "Dockerfile.prod", "Dockerfile.production"}
|
|
|
-
|
|
|
- for _, name := range dockerfileNames {
|
|
|
- dockerfilePath := filepath.Join(tempDir, name)
|
|
|
- if _, err := os.Stat(dockerfilePath); err == nil {
|
|
|
- pc.entry.WithField("dockerfile_path", dockerfilePath).Debug("Found Dockerfile")
|
|
|
- return true, nil
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- pc.entry.WithField("temp_dir", tempDir).Debug("No Dockerfile found")
|
|
|
- return false, nil
|
|
|
-}
|
|
|
-
|
|
|
-// BuildComponentImages builds Docker images for components
|
|
|
-// It first checks for pre-built images in the registry before rebuilding from source
|
|
|
+// BuildComponentImages builds Docker images for components using shell commands
|
|
|
+// This simplified version uses docker build commands directly instead of Docker API
|
|
|
func (pc *PreviewCommon) BuildComponentImages(ctx context.Context, components []models.Component) ([]string, string, error) {
|
|
|
var imageNames []string
|
|
|
var allLogs strings.Builder
|
|
|
|
|
|
for _, component := range components {
|
|
|
- pc.entry.WithField("component_id", component.ID).WithField("status", component.Status).Info("Processing component for preview")
|
|
|
+ pc.entry.WithFields(logrus.Fields{
|
|
|
+ "component_id": component.ID,
|
|
|
+ "status": component.Status,
|
|
|
+ "source_type": component.SourceType,
|
|
|
+ "build_context": component.BuildContext,
|
|
|
+ "dockerfile_path": component.DockerfilePath,
|
|
|
+ "service_name": component.ServiceName,
|
|
|
+ }).Info("Processing component for preview")
|
|
|
+
|
|
|
+ allLogs.WriteString(fmt.Sprintf("Processing component %d (%s) - SourceType: %s, BuildContext: %s, DockerfilePath: %s\n",
|
|
|
+ component.ID, component.Name, component.SourceType, component.BuildContext, component.DockerfilePath))
|
|
|
|
|
|
// Generate local image name for preview
|
|
|
imageName := fmt.Sprintf("byop-preview-%s:%d", component.Name, component.ID)
|
|
|
|
|
|
- // Check if component has pre-built image information
|
|
|
- if component.CurrentImageURI != "" && component.CurrentImageTag != "" {
|
|
|
- pc.entry.WithField("component_id", component.ID).WithField("image_uri", component.CurrentImageURI).Info("Component has pre-built image, checking registry")
|
|
|
- allLogs.WriteString(fmt.Sprintf("Component %d has pre-built image %s, checking availability\n", component.ID, component.CurrentImageURI))
|
|
|
-
|
|
|
- // Check if the pre-built image exists in the registry
|
|
|
- if pc.registryClient != nil && pc.registryURL != "" {
|
|
|
- exists, err := pc.registryClient.CheckImageExists(ctx, component.CurrentImageURI, pc.registryURL, pc.registryUser, pc.registryPass)
|
|
|
- if err != nil {
|
|
|
- pc.entry.WithField("component_id", component.ID).WithError(err).Warn("Failed to check if pre-built image exists, falling back to rebuild")
|
|
|
- allLogs.WriteString(fmt.Sprintf("Failed to check registry image for component %d: %v, rebuilding from source\n", component.ID, err))
|
|
|
- } else if exists {
|
|
|
- // Pull the pre-built image from registry to local Docker
|
|
|
- if err := pc.pullPreBuiltImage(ctx, component.CurrentImageURI, imageName); err != nil {
|
|
|
- pc.entry.WithField("component_id", component.ID).WithError(err).Warn("Failed to pull pre-built image, falling back to rebuild")
|
|
|
- allLogs.WriteString(fmt.Sprintf("Failed to pull pre-built image for component %d: %v, rebuilding from source\n", component.ID, err))
|
|
|
- } else {
|
|
|
- pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Successfully used pre-built image")
|
|
|
- allLogs.WriteString(fmt.Sprintf("Successfully pulled and tagged pre-built image for component %d as %s\n", component.ID, imageName))
|
|
|
- imageNames = append(imageNames, imageName)
|
|
|
- continue // Skip to next component
|
|
|
- }
|
|
|
- } else {
|
|
|
- pc.entry.WithField("component_id", component.ID).Info("Pre-built image not found in registry, rebuilding from source")
|
|
|
- allLogs.WriteString(fmt.Sprintf("Pre-built image for component %d not found in registry, rebuilding from source\n", component.ID))
|
|
|
- }
|
|
|
- } else {
|
|
|
- pc.entry.WithField("component_id", component.ID).Warn("Registry client not configured, cannot check pre-built images")
|
|
|
- allLogs.WriteString(fmt.Sprintf("Registry not configured for component %d, rebuilding from source\n", component.ID))
|
|
|
- }
|
|
|
+ // Check if the local preview image already exists using shell command
|
|
|
+ pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Checking if local preview image exists")
|
|
|
+ checkCmd := exec.CommandContext(ctx, "docker", "image", "inspect", imageName)
|
|
|
+ if err := checkCmd.Run(); err == nil {
|
|
|
+ pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Local preview image already exists, skipping build")
|
|
|
+ allLogs.WriteString(fmt.Sprintf("Component %d already has local preview image %s, skipping build\n", component.ID, imageName))
|
|
|
+ imageNames = append(imageNames, imageName)
|
|
|
+ continue // Skip to next component
|
|
|
} else {
|
|
|
- pc.entry.WithField("component_id", component.ID).Info("Component has no pre-built image information, building from source")
|
|
|
- allLogs.WriteString(fmt.Sprintf("Component %d has no pre-built image, building from source\n", component.ID))
|
|
|
+ pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).WithError(err).Info("Local preview image not found, will build from source")
|
|
|
+ }
|
|
|
+
|
|
|
+ // For docker-compose components with public images, use them directly
|
|
|
+ if component.SourceType == "docker-compose" && component.CurrentImageURI != "" &&
|
|
|
+ strings.Contains(component.CurrentImageURI, ":") &&
|
|
|
+ !strings.Contains(component.CurrentImageURI, "host.docker.internal") &&
|
|
|
+ !strings.Contains(component.CurrentImageURI, pc.registryURL) {
|
|
|
+ // This is likely a public image (mysql:5.7, postgres:13, etc.)
|
|
|
+ pc.entry.WithField("component_id", component.ID).WithField("image_uri", component.CurrentImageURI).Info("Using public image directly for preview")
|
|
|
+ allLogs.WriteString(fmt.Sprintf("Component %d using public image %s directly\n", component.ID, component.CurrentImageURI))
|
|
|
+ imageNames = append(imageNames, component.CurrentImageURI)
|
|
|
+ continue // Skip to next component
|
|
|
}
|
|
|
|
|
|
- // Fallback: Build from source code
|
|
|
+ // Build from source code
|
|
|
pc.entry.WithField("component_id", component.ID).Info("Building Docker image from source")
|
|
|
allLogs.WriteString(fmt.Sprintf("Building component %d from source\n", component.ID))
|
|
|
|
|
@@ -494,104 +332,60 @@ func (pc *PreviewCommon) BuildComponentImages(ctx context.Context, components []
|
|
|
return nil, allLogs.String(), err
|
|
|
}
|
|
|
|
|
|
- // Special handling for components with existing Dockerfiles (status "valid")
|
|
|
- if component.Status == "valid" {
|
|
|
- pc.entry.WithField("component_id", component.ID).Info("Component has existing Dockerfile, building directly")
|
|
|
- allLogs.WriteString(fmt.Sprintf("Component %d has existing Dockerfile, building directly\n", component.ID))
|
|
|
-
|
|
|
- // For components with existing Dockerfiles, just use the Dockerfile as-is
|
|
|
- // No need to validate/fix or create .dockerignore since they should work as-is
|
|
|
- } else {
|
|
|
- // For components without existing Dockerfiles (generated via LLB), apply fixes
|
|
|
- pc.entry.WithField("component_id", component.ID).Info("Component using generated Dockerfile, applying fixes")
|
|
|
-
|
|
|
- // Create .dockerignore file to exclude unnecessary files
|
|
|
- pc.createDockerIgnore(ctx, tempDir)
|
|
|
+ // Determine build context and dockerfile path
|
|
|
+ buildContext := tempDir
|
|
|
+ dockerfilePath := "Dockerfile"
|
|
|
|
|
|
- // Check and fix Dockerfile if needed
|
|
|
- if err := pc.validateAndFixDockerfile(ctx, tempDir); err != nil {
|
|
|
- allLogs.WriteString(fmt.Sprintf("Failed to validate Dockerfile for component %d: %v\n", component.ID, err))
|
|
|
- return nil, allLogs.String(), err
|
|
|
+ if component.SourceType == "docker-compose" && component.BuildContext != "" {
|
|
|
+ buildContext = filepath.Join(tempDir, component.BuildContext)
|
|
|
+ if component.DockerfilePath != "" {
|
|
|
+ dockerfilePath = component.DockerfilePath
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Create a tar archive of the build context
|
|
|
- pc.entry.WithField("component_id", component.ID).Info("Creating build context tar archive")
|
|
|
- tarReader, err := pc.CreateBuildContext(ctx, tempDir)
|
|
|
- if err != nil {
|
|
|
- errMsg := fmt.Sprintf("Failed to create build context for %s: %v", imageName, err)
|
|
|
- pc.entry.WithField("component_id", component.ID).Error(errMsg)
|
|
|
- allLogs.WriteString(errMsg + "\n")
|
|
|
- return nil, allLogs.String(), err
|
|
|
- }
|
|
|
- defer tarReader.Close()
|
|
|
-
|
|
|
- pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Starting Docker image build")
|
|
|
- buildResponse, err := pc.dockerClient.ImageBuild(ctx, tarReader, types.ImageBuildOptions{
|
|
|
- Tags: []string{imageName},
|
|
|
- Dockerfile: "Dockerfile",
|
|
|
- Remove: true,
|
|
|
- ForceRemove: true,
|
|
|
- })
|
|
|
- if err != nil {
|
|
|
- errMsg := fmt.Sprintf("Failed to start build for %s: %v", imageName, err)
|
|
|
- pc.entry.WithField("component_id", component.ID).Error(errMsg)
|
|
|
- allLogs.WriteString(errMsg + "\n")
|
|
|
- return nil, allLogs.String(), err
|
|
|
- }
|
|
|
- defer buildResponse.Body.Close()
|
|
|
-
|
|
|
- // Read and parse build output properly
|
|
|
- buildOutput, err := io.ReadAll(buildResponse.Body)
|
|
|
- if err != nil {
|
|
|
- allLogs.WriteString(fmt.Sprintf("Failed to read build output for %s: %v\n", imageName, err))
|
|
|
- return nil, allLogs.String(), err
|
|
|
- }
|
|
|
+ // Check if we need to generate a Dockerfile
|
|
|
+ fullDockerfilePath := filepath.Join(buildContext, dockerfilePath)
|
|
|
+ if component.Status != "valid" || !fileExists(fullDockerfilePath) {
|
|
|
+ pc.entry.WithField("component_id", component.ID).Info("Generating Dockerfile for component")
|
|
|
+ allLogs.WriteString(fmt.Sprintf("Generating Dockerfile for component %d\n", component.ID))
|
|
|
|
|
|
- buildOutputStr := string(buildOutput)
|
|
|
- allLogs.WriteString(fmt.Sprintf("Building %s:\n%s\n", imageName, buildOutputStr))
|
|
|
-
|
|
|
- // Check for Docker build errors in JSON output
|
|
|
- buildSuccess := false
|
|
|
- buildErrorFound := false
|
|
|
-
|
|
|
- // Parse each line of JSON output
|
|
|
- lines := strings.Split(buildOutputStr, "\n")
|
|
|
- for _, line := range lines {
|
|
|
- line = strings.TrimSpace(line)
|
|
|
- if line == "" {
|
|
|
- continue
|
|
|
+ // Use analyzer to generate Dockerfile
|
|
|
+ stack, err := analyzer.AnalyzeCode(tempDir)
|
|
|
+ if err != nil {
|
|
|
+ allLogs.WriteString(fmt.Sprintf("Failed to analyze code for %s: %v\n", component.Name, err))
|
|
|
+ return nil, allLogs.String(), err
|
|
|
}
|
|
|
|
|
|
- // Look for success indicators
|
|
|
- if strings.Contains(line, `"stream":"Successfully built`) ||
|
|
|
- strings.Contains(line, `"stream":"Successfully tagged`) {
|
|
|
- buildSuccess = true
|
|
|
+ dockerfileContent, err := stack.GenerateDockerfile(tempDir)
|
|
|
+ if err != nil {
|
|
|
+ allLogs.WriteString(fmt.Sprintf("Failed to generate Dockerfile for %s: %v\n", component.Name, err))
|
|
|
+ return nil, allLogs.String(), err
|
|
|
}
|
|
|
|
|
|
- // Look for error indicators
|
|
|
- if strings.Contains(line, `"error"`) ||
|
|
|
- strings.Contains(line, `"errorDetail"`) ||
|
|
|
- strings.Contains(line, `"stream":"ERROR`) ||
|
|
|
- strings.Contains(line, `"stream":"The command"`) && strings.Contains(line, "returned a non-zero code") {
|
|
|
- buildErrorFound = true
|
|
|
- allLogs.WriteString(fmt.Sprintf("Build error detected in line: %s\n", line))
|
|
|
+ // Write Dockerfile (no Traefik labels here - they'll be added at runtime)
|
|
|
+ if err := os.WriteFile(fullDockerfilePath, []byte(dockerfileContent), 0644); err != nil {
|
|
|
+ allLogs.WriteString(fmt.Sprintf("Failed to write Dockerfile for %s: %v\n", component.Name, err))
|
|
|
+ return nil, allLogs.String(), err
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if buildErrorFound {
|
|
|
- allLogs.WriteString(fmt.Sprintf("Build failed for %s: errors found in build output\n", imageName))
|
|
|
- return nil, allLogs.String(), fmt.Errorf("docker build failed for %s: check build logs", imageName)
|
|
|
- }
|
|
|
+ // Build the image using docker build command
|
|
|
+ pc.entry.WithField("component_id", component.ID).WithField("image_name", imageName).Info("Building Docker image")
|
|
|
+ allLogs.WriteString(fmt.Sprintf("Building Docker image for component %d: %s\n", component.ID, imageName))
|
|
|
|
|
|
- if !buildSuccess {
|
|
|
- allLogs.WriteString(fmt.Sprintf("Build failed for %s: no success indicators found in build output\n", imageName))
|
|
|
- return nil, allLogs.String(), fmt.Errorf("docker build failed for %s: build did not complete successfully", imageName)
|
|
|
- }
|
|
|
+ buildCmd := exec.CommandContext(ctx, "docker", "build", "-t", imageName, "-f", dockerfilePath, buildContext)
|
|
|
+ output, err := buildCmd.CombinedOutput()
|
|
|
+ buildOutputStr := string(output)
|
|
|
+ allLogs.WriteString(fmt.Sprintf("Build output for %s:\n%s\n", imageName, buildOutputStr))
|
|
|
|
|
|
- // Verify the image exists and is properly tagged
|
|
|
- _, err = pc.dockerClient.ImageInspect(ctx, imageName)
|
|
|
if err != nil {
|
|
|
+ allLogs.WriteString(fmt.Sprintf("Failed to build %s: %v\n", imageName, err))
|
|
|
+ return nil, allLogs.String(), fmt.Errorf("failed to build image %s: %v", imageName, err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Verify the image was built successfully
|
|
|
+ verifyCmd := exec.CommandContext(ctx, "docker", "image", "inspect", imageName)
|
|
|
+ if err := verifyCmd.Run(); err != nil {
|
|
|
allLogs.WriteString(fmt.Sprintf("Build verification failed for %s: image not found after build - %v\n", imageName, err))
|
|
|
return nil, allLogs.String(), fmt.Errorf("failed to build image %s: image not found after build", imageName)
|
|
|
}
|
|
@@ -603,61 +397,19 @@ func (pc *PreviewCommon) BuildComponentImages(ctx context.Context, components []
|
|
|
return imageNames, allLogs.String(), nil
|
|
|
}
|
|
|
|
|
|
-// pullPreBuiltImage pulls a pre-built image from the registry and tags it for local use
|
|
|
-func (pc *PreviewCommon) pullPreBuiltImage(ctx context.Context, registryImageURI, localImageName string) error {
|
|
|
- pc.entry.WithField("registry_image", registryImageURI).WithField("local_image", localImageName).Info("Pulling pre-built image from registry")
|
|
|
-
|
|
|
- // Pull the image from registry
|
|
|
- pullOptions := image.PullOptions{}
|
|
|
-
|
|
|
- // Add authentication if registry credentials are configured
|
|
|
- if pc.registryUser != "" && pc.registryPass != "" {
|
|
|
- authConfig := registry.AuthConfig{
|
|
|
- Username: pc.registryUser,
|
|
|
- Password: pc.registryPass,
|
|
|
- }
|
|
|
- encodedJSON, err := json.Marshal(authConfig)
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("failed to encode registry auth: %w", err)
|
|
|
- }
|
|
|
- pullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
|
|
|
- }
|
|
|
-
|
|
|
- reader, err := pc.dockerClient.ImagePull(ctx, registryImageURI, pullOptions)
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("failed to pull image %s: %w", registryImageURI, err)
|
|
|
- }
|
|
|
- defer reader.Close()
|
|
|
-
|
|
|
- // Read the pull output (similar to build output)
|
|
|
- pullOutput, err := io.ReadAll(reader)
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("failed to read pull output: %w", err)
|
|
|
- }
|
|
|
-
|
|
|
- pc.entry.WithField("pull_output", string(pullOutput)).Debug("Image pull completed")
|
|
|
-
|
|
|
- // Tag the pulled image with the local preview tag
|
|
|
- err = pc.dockerClient.ImageTag(ctx, registryImageURI, localImageName)
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("failed to tag image %s as %s: %w", registryImageURI, localImageName, err)
|
|
|
- }
|
|
|
-
|
|
|
- // Verify the image is now available locally
|
|
|
- _, err = pc.dockerClient.ImageInspect(ctx, localImageName)
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("failed to verify locally tagged image %s: %w", localImageName, err)
|
|
|
- }
|
|
|
-
|
|
|
- pc.entry.WithField("local_image", localImageName).Info("Successfully pulled and tagged pre-built image")
|
|
|
- return nil
|
|
|
-}
|
|
|
-
|
|
|
// GetAppComponents retrieves components for an app
|
|
|
func (pc *PreviewCommon) GetAppComponents(ctx context.Context, app *models.App) ([]models.Component, error) {
|
|
|
var components []models.Component
|
|
|
|
|
|
- for _, componentID := range app.Components {
|
|
|
+ // Parse the JSON string of component IDs
|
|
|
+ var componentIDs []uint
|
|
|
+ if app.Components != "" {
|
|
|
+ if err := json.Unmarshal([]byte(app.Components), &componentIDs); err != nil {
|
|
|
+ return nil, fmt.Errorf("failed to parse component IDs from app %d: %v", app.ID, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, componentID := range componentIDs {
|
|
|
component, err := pc.store.GetComponentByID(ctx, componentID)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
@@ -671,39 +423,34 @@ func (pc *PreviewCommon) GetAppComponents(ctx context.Context, app *models.App)
|
|
|
return components, nil
|
|
|
}
|
|
|
|
|
|
-// CleanupPreviewImages cleans up BYOP preview Docker images
|
|
|
+// CleanupPreviewImages cleans up BYOP preview Docker images using shell commands
|
|
|
func (pc *PreviewCommon) CleanupPreviewImages(ctx context.Context) {
|
|
|
pc.entry.Info("Cleaning up BYOP preview images...")
|
|
|
|
|
|
- images, err := pc.dockerClient.ImageList(ctx, image.ListOptions{All: true})
|
|
|
+ // List all images and filter for byop-preview
|
|
|
+ cmd := exec.CommandContext(ctx, "docker", "images", "--format", "{{.Repository}}:{{.Tag}}")
|
|
|
+ output, err := cmd.Output()
|
|
|
if err != nil {
|
|
|
pc.entry.WithError(err).Error("Failed to list images for cleanup")
|
|
|
return
|
|
|
}
|
|
|
|
|
|
removedCount := 0
|
|
|
- for _, img := range images {
|
|
|
- // Check if image name contains "byop-preview"
|
|
|
- isPreviewImage := false
|
|
|
- for _, tag := range img.RepoTags {
|
|
|
- if strings.Contains(tag, "byop-preview") {
|
|
|
- isPreviewImage = true
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if !isPreviewImage {
|
|
|
+ lines := strings.Split(string(output), "\n")
|
|
|
+ for _, line := range lines {
|
|
|
+ line = strings.TrimSpace(line)
|
|
|
+ if line == "" {
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
- // Remove the image
|
|
|
- if _, err := pc.dockerClient.ImageRemove(ctx, img.ID, image.RemoveOptions{
|
|
|
- Force: true,
|
|
|
- PruneChildren: true,
|
|
|
- }); err != nil {
|
|
|
- pc.entry.WithError(err).WithField("image_id", img.ID).Warn("Failed to remove preview image")
|
|
|
- } else {
|
|
|
- removedCount++
|
|
|
+ if strings.Contains(line, "byop-preview") {
|
|
|
+ // Remove the image
|
|
|
+ rmCmd := exec.CommandContext(ctx, "docker", "rmi", line, "--force")
|
|
|
+ if err := rmCmd.Run(); err != nil {
|
|
|
+ pc.entry.WithError(err).WithField("image", line).Warn("Failed to remove preview image")
|
|
|
+ } else {
|
|
|
+ removedCount++
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -712,146 +459,44 @@ func (pc *PreviewCommon) CleanupPreviewImages(ctx context.Context) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// CleanupByAppID cleans up all BYOP preview containers and images for a specific app ID
|
|
|
-func (pc *PreviewCommon) CleanupByAppID(ctx context.Context, appID int) {
|
|
|
- pc.entry.WithField("app_id", appID).Info("Cleaning up BYOP preview containers...")
|
|
|
+// CleanupAllPreviewContainers cleans up all BYOP preview containers using shell commands
|
|
|
+func (pc *PreviewCommon) CleanupAllPreviewContainers(ctx context.Context) {
|
|
|
+ pc.entry.Info("Cleaning up all BYOP preview containers...")
|
|
|
|
|
|
- // List all containers
|
|
|
- containers, err := pc.dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
|
|
+ // List all containers and filter for byop-preview
|
|
|
+ cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--format", "{{.Names}}")
|
|
|
+ output, err := cmd.Output()
|
|
|
if err != nil {
|
|
|
pc.entry.WithError(err).Error("Failed to list containers for cleanup")
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- for _, ctn := range containers {
|
|
|
- isPreviewContainer := false
|
|
|
- containerName := ""
|
|
|
-
|
|
|
- // Check if the container is a BYOP preview container
|
|
|
- for key, value := range ctn.Labels {
|
|
|
- if key == "byop.preview" && value == "true" {
|
|
|
- isPreviewContainer = true
|
|
|
- if len(ctn.Names) > 0 {
|
|
|
- containerName = ctn.Names[0]
|
|
|
- }
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if !isPreviewContainer {
|
|
|
+ removedCount := 0
|
|
|
+ lines := strings.Split(string(output), "\n")
|
|
|
+ for _, line := range lines {
|
|
|
+ line = strings.TrimSpace(line)
|
|
|
+ if line == "" {
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
- if ctn.Labels["byop.app.id"] != fmt.Sprintf("%d", appID) {
|
|
|
- continue // Only clean up containers for the specified app ID
|
|
|
- }
|
|
|
-
|
|
|
- pc.entry.WithField("container_id", ctn.ID).WithField("container_name", containerName).Info("Removing BYOP preview container")
|
|
|
-
|
|
|
- // Remove the container
|
|
|
- if err := pc.dockerClient.ContainerRemove(ctx, ctn.ID, container.RemoveOptions{
|
|
|
- Force: true,
|
|
|
- }); err != nil {
|
|
|
- pc.entry.WithError(err).WithField("container_id", ctn.ID).Warn("Failed to remove preview container")
|
|
|
- } else {
|
|
|
- pc.entry.WithField("container_id", ctn.ID).Info("Successfully removed preview container")
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// CleanupAllPreviewContainers cleans up all BYOP preview containers
|
|
|
-func (pc *PreviewCommon) CleanupAllPreviewContainers(ctx context.Context) {
|
|
|
- pc.entry.Info("Cleaning up all BYOP preview containers...")
|
|
|
-
|
|
|
- // Get all containers with filters for BYOP preview containers
|
|
|
- containers, err := pc.dockerClient.ContainerList(ctx, container.ListOptions{
|
|
|
- All: true, // Include stopped containers too
|
|
|
- Filters: filters.NewArgs(
|
|
|
- filters.Arg("label", "byop.preview=true"),
|
|
|
- ),
|
|
|
- })
|
|
|
- if err != nil {
|
|
|
- pc.entry.WithError(err).Error("Failed to list BYOP preview containers")
|
|
|
- // Fallback to name-based filtering if labels don't work
|
|
|
- pc.cleanupByName(ctx)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if len(containers) == 0 {
|
|
|
- pc.entry.Info("No BYOP preview containers found to cleanup")
|
|
|
- } else {
|
|
|
- pc.entry.WithField("container_count", len(containers)).Info("Found BYOP preview containers to cleanup")
|
|
|
- }
|
|
|
-
|
|
|
- // Remove BYOP preview containers
|
|
|
- for _, ctn := range containers {
|
|
|
- containerName := "unknown"
|
|
|
- if len(ctn.Names) > 0 {
|
|
|
- containerName = strings.TrimPrefix(ctn.Names[0], "/")
|
|
|
- }
|
|
|
+ if strings.Contains(line, "byop-preview") || strings.Contains(line, "preview") {
|
|
|
+ pc.entry.WithField("container_name", line).Info("Removing BYOP preview container")
|
|
|
|
|
|
- pc.entry.WithField("container_id", ctn.ID).WithField("container_name", containerName).Info("Removing BYOP preview container")
|
|
|
+ // Stop and remove container
|
|
|
+ stopCmd := exec.CommandContext(ctx, "docker", "stop", line)
|
|
|
+ stopCmd.Run() // Ignore errors for stop
|
|
|
|
|
|
- // Stop container first if it's running
|
|
|
- if ctn.State == "running" {
|
|
|
- if err := pc.dockerClient.ContainerStop(ctx, ctn.ID, container.StopOptions{}); err != nil {
|
|
|
- pc.entry.WithError(err).WithField("container_id", ctn.ID).Warn("Failed to stop container, will force remove")
|
|
|
+ rmCmd := exec.CommandContext(ctx, "docker", "rm", "-f", line)
|
|
|
+ if err := rmCmd.Run(); err != nil {
|
|
|
+ pc.entry.WithError(err).WithField("container_name", line).Error("Failed to remove container")
|
|
|
+ } else {
|
|
|
+ removedCount++
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- // Remove container
|
|
|
- if err := pc.dockerClient.ContainerRemove(ctx, ctn.ID, container.RemoveOptions{
|
|
|
- Force: true,
|
|
|
- RemoveVolumes: true,
|
|
|
- }); err != nil {
|
|
|
- pc.entry.WithError(err).WithField("container_id", ctn.ID).Error("Failed to remove BYOP preview container")
|
|
|
- } else {
|
|
|
- pc.entry.WithField("container_id", ctn.ID).WithField("container_name", containerName).Info("Successfully removed BYOP preview container")
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// Fallback method to cleanup containers by name pattern
|
|
|
-func (pc *PreviewCommon) cleanupByName(ctx context.Context) {
|
|
|
- pc.entry.Info("Using fallback name-based container cleanup")
|
|
|
-
|
|
|
- containers, err := pc.dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
|
|
- if err != nil {
|
|
|
- pc.entry.WithError(err).Error("Failed to list containers for name-based cleanup")
|
|
|
- return
|
|
|
}
|
|
|
|
|
|
- for _, ctn := range containers {
|
|
|
- // Check if any container name contains "byop-preview"
|
|
|
- isPreviewContainer := false
|
|
|
- containerName := "unknown"
|
|
|
-
|
|
|
- for _, name := range ctn.Names {
|
|
|
- cleanName := strings.TrimPrefix(name, "/")
|
|
|
- if strings.Contains(cleanName, "byop-preview") || strings.Contains(cleanName, "preview") {
|
|
|
- isPreviewContainer = true
|
|
|
- containerName = cleanName
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if !isPreviewContainer {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- pc.entry.WithField("container_id", ctn.ID).WithField("container_name", containerName).Info("Removing BYOP preview container (name-based)")
|
|
|
-
|
|
|
- // Stop and remove
|
|
|
- if ctn.State == "running" {
|
|
|
- pc.dockerClient.ContainerStop(ctx, ctn.ID, container.StopOptions{})
|
|
|
- }
|
|
|
-
|
|
|
- if err := pc.dockerClient.ContainerRemove(ctx, ctn.ID, container.RemoveOptions{
|
|
|
- Force: true,
|
|
|
- RemoveVolumes: true,
|
|
|
- }); err != nil {
|
|
|
- pc.entry.WithError(err).WithField("container_id", ctn.ID).Error("Failed to remove container")
|
|
|
- }
|
|
|
+ if removedCount > 0 {
|
|
|
+ pc.entry.WithField("removed_containers", removedCount).Info("Cleaned up BYOP preview containers")
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -898,7 +543,7 @@ func (pc *PreviewCommon) CleanupPreviewState(ctx context.Context) {
|
|
|
}
|
|
|
|
|
|
// GetPreviewImageNames reconstructs the Docker image names used for a preview
|
|
|
-func (pc *PreviewCommon) GetPreviewImageNames(appID int) ([]string, error) {
|
|
|
+func (pc *PreviewCommon) GetPreviewImageNames(appID uint) ([]string, error) {
|
|
|
// Get app details
|
|
|
app, err := pc.store.GetAppByID(context.Background(), appID)
|
|
|
if err != nil {
|
|
@@ -921,8 +566,8 @@ func (pc *PreviewCommon) GetPreviewImageNames(appID int) ([]string, error) {
|
|
|
return imageNames, nil
|
|
|
}
|
|
|
|
|
|
-// CleanupPreviewImagesForApp cleans up Docker images for a specific app (works for both local and remote)
|
|
|
-func (pc *PreviewCommon) CleanupPreviewImagesForApp(ctx context.Context, appID int, isRemote bool, ipAddress string) error {
|
|
|
+// CleanupPreviewImagesForApp cleans up Docker images for a specific app using shell commands
|
|
|
+func (pc *PreviewCommon) CleanupPreviewImagesForApp(ctx context.Context, appID uint, isRemote bool, ipAddress string) error {
|
|
|
imageNames, err := pc.GetPreviewImageNames(appID)
|
|
|
if err != nil {
|
|
|
pc.entry.WithField("app_id", appID).WithError(err).Warn("Failed to get preview image names for cleanup")
|
|
@@ -936,16 +581,14 @@ func (pc *PreviewCommon) CleanupPreviewImagesForApp(ctx context.Context, appID i
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// cleanupLocalDockerImages removes specific Docker images locally
|
|
|
+// cleanupLocalDockerImages removes specific Docker images locally using shell commands
|
|
|
func (pc *PreviewCommon) cleanupLocalDockerImages(ctx context.Context, imageNames []string) error {
|
|
|
pc.entry.WithField("image_count", len(imageNames)).Info("Cleaning up specific Docker images locally")
|
|
|
|
|
|
for _, imageName := range imageNames {
|
|
|
- // Remove the image locally using Docker client
|
|
|
- if _, err := pc.dockerClient.ImageRemove(ctx, imageName, image.RemoveOptions{
|
|
|
- Force: true,
|
|
|
- PruneChildren: true,
|
|
|
- }); err != nil {
|
|
|
+ // Remove the image locally using docker rmi command
|
|
|
+ cmd := exec.CommandContext(ctx, "docker", "rmi", imageName, "--force")
|
|
|
+ if err := cmd.Run(); err != nil {
|
|
|
// Log warning but don't fail the cleanup - image might already be removed or in use
|
|
|
pc.entry.WithField("image_name", imageName).WithError(err).Warn("Failed to remove Docker image locally (this may be normal)")
|
|
|
} else {
|
|
@@ -1007,20 +650,259 @@ func (pc *PreviewCommon) executeSSHCommand(ctx context.Context, ipAddress, comma
|
|
|
}
|
|
|
|
|
|
// Database helper methods
|
|
|
-func (pc *PreviewCommon) UpdatePreviewStatus(ctx context.Context, previewID int, status, errorMsg string) {
|
|
|
+func (pc *PreviewCommon) UpdatePreviewStatus(ctx context.Context, previewID uint, status, errorMsg string) {
|
|
|
if err := pc.store.UpdatePreviewStatus(ctx, previewID, status, errorMsg); err != nil {
|
|
|
pc.entry.WithField("preview_id", previewID).Errorf("Failed to update preview status: %v", err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (pc *PreviewCommon) UpdatePreviewBuildLogs(ctx context.Context, previewID int, logs string) {
|
|
|
+func (pc *PreviewCommon) UpdatePreviewBuildLogs(ctx context.Context, previewID uint, logs string) {
|
|
|
if err := pc.store.UpdatePreviewBuildLogs(ctx, previewID, logs); err != nil {
|
|
|
pc.entry.WithField("preview_id", previewID).Errorf("Failed to update preview build logs: %v", err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (pc *PreviewCommon) UpdatePreviewDeployLogs(ctx context.Context, previewID int, logs string) {
|
|
|
+func (pc *PreviewCommon) UpdatePreviewDeployLogs(ctx context.Context, previewID uint, logs string) {
|
|
|
if err := pc.store.UpdatePreviewDeployLogs(ctx, previewID, logs); err != nil {
|
|
|
pc.entry.WithField("preview_id", previewID).Errorf("Failed to update preview deploy logs: %v", err)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// CleanupByAppID cleans up all BYOP preview containers and images for a specific app ID using shell commands
|
|
|
+func (pc *PreviewCommon) CleanupByAppID(ctx context.Context, appID uint) {
|
|
|
+ pc.entry.WithField("app_id", appID).Info("Cleaning up BYOP preview containers...")
|
|
|
+
|
|
|
+ // List all containers and filter for byop-preview containers with the specific app ID
|
|
|
+ // We'll use the app ID in the container naming pattern
|
|
|
+ appPattern := fmt.Sprintf("byop-preview-app-%d", appID)
|
|
|
+
|
|
|
+ cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--format", "{{.Names}}")
|
|
|
+ output, err := cmd.Output()
|
|
|
+ if err != nil {
|
|
|
+ pc.entry.WithError(err).Error("Failed to list containers for cleanup")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ removedCount := 0
|
|
|
+ lines := strings.Split(string(output), "\n")
|
|
|
+ for _, line := range lines {
|
|
|
+ line = strings.TrimSpace(line)
|
|
|
+ if line == "" {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if this container is for the specific app
|
|
|
+ if strings.Contains(line, appPattern) || (strings.Contains(line, "byop-preview") && strings.Contains(line, fmt.Sprintf("-%d-", appID))) {
|
|
|
+ pc.entry.WithField("container_name", line).WithField("app_id", appID).Info("Removing BYOP preview container for app")
|
|
|
+
|
|
|
+ // Stop and remove container
|
|
|
+ stopCmd := exec.CommandContext(ctx, "docker", "stop", line)
|
|
|
+ stopCmd.Run() // Ignore errors for stop
|
|
|
+
|
|
|
+ rmCmd := exec.CommandContext(ctx, "docker", "rm", "-f", line)
|
|
|
+ if err := rmCmd.Run(); err != nil {
|
|
|
+ pc.entry.WithError(err).WithField("container_name", line).WithField("app_id", appID).Error("Failed to remove container")
|
|
|
+ } else {
|
|
|
+ removedCount++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if removedCount > 0 {
|
|
|
+ pc.entry.WithField("removed_containers", removedCount).WithField("app_id", appID).Info("Cleaned up BYOP preview containers for app")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// AddTraefikLabelsToDockerfile adds Traefik routing labels to a Dockerfile
|
|
|
+func (pc *PreviewCommon) AddTraefikLabelsToDockerfile(dockerfileContent, appName string, appID uint, port string) string {
|
|
|
+ if port == "" {
|
|
|
+ port = "3000" // Default port
|
|
|
+ }
|
|
|
+
|
|
|
+ // Generate the preview subdomain
|
|
|
+ previewID := pc.GeneratePreviewID()
|
|
|
+ subdomain := fmt.Sprintf("%s-%d-%s", appName, appID, previewID)
|
|
|
+
|
|
|
+ // Traefik labels to add
|
|
|
+ traefikLabels := []string{
|
|
|
+ `LABEL traefik.enable="true"`,
|
|
|
+ fmt.Sprintf(`LABEL traefik.http.routers.%s.rule="Host(%s.preview.byop.dev)"`, subdomain, "`"+subdomain+"`"),
|
|
|
+ fmt.Sprintf(`LABEL traefik.http.routers.%s.entrypoints="websecure"`, subdomain),
|
|
|
+ fmt.Sprintf(`LABEL traefik.http.routers.%s.tls.certresolver="letsencrypt"`, subdomain),
|
|
|
+ fmt.Sprintf(`LABEL traefik.http.services.%s.loadbalancer.server.port="%s"`, subdomain, port),
|
|
|
+ `LABEL traefik.docker.network="traefik"`,
|
|
|
+ `LABEL byop.preview="true"`,
|
|
|
+ fmt.Sprintf(`LABEL byop.app.id="%d"`, appID),
|
|
|
+ fmt.Sprintf(`LABEL byop.app.name="%s"`, appName),
|
|
|
+ fmt.Sprintf(`LABEL byop.preview.id="%s"`, previewID),
|
|
|
+ }
|
|
|
+
|
|
|
+ // Parse the Dockerfile content
|
|
|
+ lines := strings.Split(dockerfileContent, "\n")
|
|
|
+ var modifiedLines []string
|
|
|
+
|
|
|
+ // Find the last non-empty, non-comment line to insert labels before any final CMD/ENTRYPOINT
|
|
|
+ lastInstructionIndex := -1
|
|
|
+ for i := len(lines) - 1; i >= 0; i-- {
|
|
|
+ line := strings.TrimSpace(lines[i])
|
|
|
+ if line != "" && !strings.HasPrefix(line, "#") {
|
|
|
+ if strings.HasPrefix(strings.ToUpper(line), "CMD") ||
|
|
|
+ strings.HasPrefix(strings.ToUpper(line), "ENTRYPOINT") {
|
|
|
+ lastInstructionIndex = i
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // If we found a CMD/ENTRYPOINT, insert labels before it
|
|
|
+ if lastInstructionIndex != -1 {
|
|
|
+ modifiedLines = append(modifiedLines, lines[:lastInstructionIndex]...)
|
|
|
+ modifiedLines = append(modifiedLines, "")
|
|
|
+ modifiedLines = append(modifiedLines, "# Traefik labels for BYOP preview routing")
|
|
|
+ modifiedLines = append(modifiedLines, traefikLabels...)
|
|
|
+ modifiedLines = append(modifiedLines, "")
|
|
|
+ modifiedLines = append(modifiedLines, lines[lastInstructionIndex:]...)
|
|
|
+ } else {
|
|
|
+ // No CMD/ENTRYPOINT found, just append labels at the end
|
|
|
+ modifiedLines = append(modifiedLines, lines...)
|
|
|
+ modifiedLines = append(modifiedLines, "")
|
|
|
+ modifiedLines = append(modifiedLines, "# Traefik labels for BYOP preview routing")
|
|
|
+ modifiedLines = append(modifiedLines, traefikLabels...)
|
|
|
+ }
|
|
|
+
|
|
|
+ return strings.Join(modifiedLines, "\n")
|
|
|
+}
|
|
|
+
|
|
|
+// DetectPortFromDockerfile attempts to detect the exposed port from a Dockerfile
|
|
|
+func (pc *PreviewCommon) DetectPortFromDockerfile(dockerfileContent string) string {
|
|
|
+ lines := strings.Split(dockerfileContent, "\n")
|
|
|
+
|
|
|
+ for _, line := range lines {
|
|
|
+ line = strings.TrimSpace(strings.ToUpper(line))
|
|
|
+ if strings.HasPrefix(line, "EXPOSE ") {
|
|
|
+ // Extract port number
|
|
|
+ parts := strings.Fields(line)
|
|
|
+ if len(parts) >= 2 {
|
|
|
+ port := strings.Split(parts[1], "/")[0] // Handle cases like "3000/tcp"
|
|
|
+ return port
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Common default ports based on technology detection
|
|
|
+ content := strings.ToLower(dockerfileContent)
|
|
|
+ if strings.Contains(content, "node") || strings.Contains(content, "npm") {
|
|
|
+ return "3000"
|
|
|
+ } else if strings.Contains(content, "python") || strings.Contains(content, "flask") {
|
|
|
+ return "5000"
|
|
|
+ } else if strings.Contains(content, "django") {
|
|
|
+ return "8000"
|
|
|
+ } else if strings.Contains(content, "nginx") {
|
|
|
+ return "80"
|
|
|
+ }
|
|
|
+
|
|
|
+ return "3000" // Default fallback
|
|
|
+}
|
|
|
+
|
|
|
+// GenerateTraefikComposeOverride creates a docker-compose override for Traefik routing
|
|
|
+// This is used during preview deployment to add routing labels to pre-built images
|
|
|
+func (pc *PreviewCommon) GenerateTraefikComposeOverride(serviceName, appName string, appID uint, port string, previewID string) string {
|
|
|
+ if port == "" {
|
|
|
+ port = "3000"
|
|
|
+ }
|
|
|
+
|
|
|
+ if previewID == "" {
|
|
|
+ previewID = pc.GeneratePreviewID()
|
|
|
+ }
|
|
|
+
|
|
|
+ subdomain := fmt.Sprintf("%s-%d-%s", appName, appID, previewID)
|
|
|
+
|
|
|
+ override := fmt.Sprintf(`version: '3.8'
|
|
|
+services:
|
|
|
+ %s:
|
|
|
+ labels:
|
|
|
+ - traefik.enable=true
|
|
|
+ - traefik.http.routers.%s.rule=Host(%s%s.preview.byop.dev%s)
|
|
|
+ - traefik.http.routers.%s.entrypoints=websecure
|
|
|
+ - traefik.http.routers.%s.tls.certresolver=letsencrypt
|
|
|
+ - traefik.http.services.%s.loadbalancer.server.port=%s
|
|
|
+ - traefik.docker.network=traefik
|
|
|
+ - byop.preview=true
|
|
|
+ - byop.app.id=%d
|
|
|
+ - byop.app.name=%s
|
|
|
+ - byop.preview.id=%s
|
|
|
+ networks:
|
|
|
+ - traefik
|
|
|
+ - default
|
|
|
+
|
|
|
+networks:
|
|
|
+ traefik:
|
|
|
+ external: true
|
|
|
+`, serviceName, subdomain, "`", subdomain, "`", subdomain, subdomain, subdomain, port, appID, appName, previewID)
|
|
|
+
|
|
|
+ return override
|
|
|
+}
|
|
|
+
|
|
|
+// GeneratePreviewComposeFile creates a complete docker-compose file for preview deployment
|
|
|
+// This uses pre-built component images and adds Traefik routing
|
|
|
+func (pc *PreviewCommon) GeneratePreviewComposeFile(app *models.App, components []models.Component, imageNames []string, previewID string) (string, error) {
|
|
|
+ if len(components) != len(imageNames) {
|
|
|
+ return "", fmt.Errorf("component count (%d) doesn't match image count (%d)", len(components), len(imageNames))
|
|
|
+ }
|
|
|
+
|
|
|
+ var services []string
|
|
|
+
|
|
|
+ for i, component := range components {
|
|
|
+ imageName := imageNames[i]
|
|
|
+ serviceName := component.ServiceName
|
|
|
+ if serviceName == "" {
|
|
|
+ serviceName = component.Name
|
|
|
+ }
|
|
|
+
|
|
|
+ // Detect port from component or use default
|
|
|
+ port := "3000" // Default port
|
|
|
+
|
|
|
+ // Try to detect port from the built image if available
|
|
|
+ // For now, we'll use a default since we don't store port info in Component model
|
|
|
+
|
|
|
+ // Generate subdomain for this component
|
|
|
+ subdomain := fmt.Sprintf("%s-%d-%s", app.Name, app.ID, previewID)
|
|
|
+ if len(components) > 1 {
|
|
|
+ subdomain = fmt.Sprintf("%s-%s-%d-%s", app.Name, component.Name, app.ID, previewID)
|
|
|
+ }
|
|
|
+
|
|
|
+ serviceConfig := fmt.Sprintf(` %s:
|
|
|
+ image: %s
|
|
|
+ labels:
|
|
|
+ - traefik.enable=true
|
|
|
+ - traefik.http.routers.%s.rule=Host(%s%s.preview.byop.dev%s)
|
|
|
+ - traefik.http.routers.%s.entrypoints=websecure
|
|
|
+ - traefik.http.routers.%s.tls.certresolver=letsencrypt
|
|
|
+ - traefik.http.services.%s.loadbalancer.server.port=%s
|
|
|
+ - traefik.docker.network=traefik
|
|
|
+ - byop.preview=true
|
|
|
+ - byop.app.id=%d
|
|
|
+ - byop.app.name=%s
|
|
|
+ - byop.preview.id=%s
|
|
|
+ - byop.component.id=%d
|
|
|
+ - byop.component.name=%s
|
|
|
+ networks:
|
|
|
+ - traefik
|
|
|
+ - default
|
|
|
+ restart: unless-stopped`,
|
|
|
+ serviceName, imageName, subdomain, "`", subdomain, "`", subdomain, subdomain, subdomain, port, app.ID, app.Name, previewID, component.ID, component.Name)
|
|
|
+
|
|
|
+ services = append(services, serviceConfig)
|
|
|
+ }
|
|
|
+
|
|
|
+ composeFile := fmt.Sprintf(`version: '3.8'
|
|
|
+services:
|
|
|
+%s
|
|
|
+
|
|
|
+networks:
|
|
|
+ traefik:
|
|
|
+ external: true
|
|
|
+`, strings.Join(services, "\n\n"))
|
|
|
+
|
|
|
+ return composeFile, nil
|
|
|
+}
|