mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
fix: un-ignore internal/build package
The build/ rule shadowed app/backend/internal/build; the !internal/build/ negation only matched the repo root. Package was never committed, breaking CI backend tests and the Docker image build. Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
7178eb54f2
commit
2e7da73b1c
10 files changed
+3314
-1
No files matched your search
+1
-1
@@ -13,7 +13,7 @@ build/
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
# Keep internal build system code
|
# Keep internal build system code
|
||||||
!internal/build/
|
!**/internal/build/
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Railpack Integration
|
||||||
|
|
||||||
|
This directory contains the build system for Containr, with Railpack as the primary build method.
|
||||||
|
|
||||||
|
## Build Types
|
||||||
|
|
||||||
|
### Railpack (Primary)
|
||||||
|
- **Detection**: Automatically detects supported frameworks (Node.js, Python, Go, Rust, Java, Ruby, PHP, Static)
|
||||||
|
- **API**: Uses https://api.railpack.app/v1/ for Dockerfile generation
|
||||||
|
- **Fallback**: Falls back to Nixpacks if Railpack cannot build the project
|
||||||
|
|
||||||
|
### Nixpacks (Secondary)
|
||||||
|
- **Detection**: Used as fallback when Railpack detection fails
|
||||||
|
- **Local**: Runs locally without external API calls
|
||||||
|
|
||||||
|
### Dockerfile
|
||||||
|
- **Detection**: Checks for existing Dockerfile in repository
|
||||||
|
- **Priority**: Highest priority if Dockerfile exists
|
||||||
|
|
||||||
|
### Prebuilt
|
||||||
|
- **Usage**: For pre-built container images
|
||||||
|
- **Configuration**: Specified in service configuration
|
||||||
|
|
||||||
|
## Supported Frameworks
|
||||||
|
|
||||||
|
Railpack supports the following frameworks:
|
||||||
|
- **Node.js** - package.json detection
|
||||||
|
- **Python** - requirements.txt detection
|
||||||
|
- **Go** - go.mod detection
|
||||||
|
- **Rust** - Cargo.toml detection
|
||||||
|
- **Java** - pom.xml or build.gradle detection
|
||||||
|
- **Ruby** - Gemfile detection
|
||||||
|
- **PHP** - composer.json detection
|
||||||
|
- **Static** - Static site detection
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create build manager
|
||||||
|
buildManager := build.NewBuildManager("/tmp/builds", dockerClient)
|
||||||
|
|
||||||
|
// Build a project
|
||||||
|
response, err := buildManager.Build(ctx, &types.BuildRequest{
|
||||||
|
SourcePath: "/path/to/project",
|
||||||
|
ImageName: "my-app",
|
||||||
|
ImageTag: "latest",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
- **Generate Dockerfile**: POST https://api.railpack.app/v1/generate
|
||||||
|
- **Build Plan**: POST https://api.railpack.app/v1/plan
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Railpack can be configured with:
|
||||||
|
- Custom build commands
|
||||||
|
- Custom start commands
|
||||||
|
- Environment variables
|
||||||
|
- Build root directory
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- https://railpack.com/
|
||||||
|
- https://railpack.com/getting-started
|
||||||
|
- https://github.com/railwayapp/railpack
|
||||||
@@ -0,0 +1,489 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"containr/internal/docker"
|
||||||
|
"containr/internal/types"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CacheManager handles build caching operations
|
||||||
|
type CacheManager struct {
|
||||||
|
cacheDir string
|
||||||
|
dockerClient *docker.Client
|
||||||
|
remoteCache *RemoteCacheConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheEntry represents a cached layer entry
|
||||||
|
type CacheEntry struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
ImageName string `json:"image_name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Metadata map[string]string `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteCacheConfig defines remote cache configuration
|
||||||
|
type RemoteCacheConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Registry string `json:"registry"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Insecure bool `json:"insecure"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheKey represents a build cache key
|
||||||
|
type CacheKey struct {
|
||||||
|
Runtime string `json:"runtime"`
|
||||||
|
Files map[string]string `json:"files"` // file path -> hash
|
||||||
|
EnvVars map[string]string `json:"env_vars"` // env var name -> hash
|
||||||
|
BuildCmd string `json:"build_cmd"`
|
||||||
|
Packages []string `json:"packages"`
|
||||||
|
BuildArgs map[string]string `json:"build_args"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCacheManager(cacheDir string, dockerClient *docker.Client) *CacheManager {
|
||||||
|
return &CacheManager{
|
||||||
|
cacheDir: cacheDir,
|
||||||
|
dockerClient: dockerClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRemoteCache configures remote cache settings
|
||||||
|
func (cm *CacheManager) SetRemoteCache(config *RemoteCacheConfig) {
|
||||||
|
cm.remoteCache = config
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCacheKey creates a unique cache key for the build context
|
||||||
|
func (cm *CacheManager) GenerateCacheKey(ctx context.Context, req *types.BuildRequest) (*CacheKey, error) {
|
||||||
|
cacheKey := &CacheKey{
|
||||||
|
Runtime: req.BuildType,
|
||||||
|
Files: make(map[string]string),
|
||||||
|
EnvVars: make(map[string]string),
|
||||||
|
BuildCmd: req.BuildCommand,
|
||||||
|
Packages: []string{},
|
||||||
|
BuildArgs: req.Environment,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash important files
|
||||||
|
importantFiles := []string{
|
||||||
|
"go.mod", "go.sum",
|
||||||
|
"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
||||||
|
"requirements.txt", "setup.py", "pyproject.toml", "Pipfile",
|
||||||
|
"Cargo.toml", "Cargo.lock",
|
||||||
|
"Gemfile", "Gemfile.lock",
|
||||||
|
"composer.json", "composer.lock",
|
||||||
|
"deno.json", "deno.jsonc",
|
||||||
|
"bun.lockb",
|
||||||
|
"Dockerfile",
|
||||||
|
".dockerignore",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range importantFiles {
|
||||||
|
filePath := filepath.Join(req.SourcePath, file)
|
||||||
|
if hash, err := cm.hashFile(filePath); err == nil {
|
||||||
|
cacheKey.Files[file] = hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash environment variables (only non-sensitive ones)
|
||||||
|
for key, value := range req.Environment {
|
||||||
|
if !cm.isSensitiveEnv(key) {
|
||||||
|
hash := sha256.Sum256([]byte(value))
|
||||||
|
cacheKey.EnvVars[key] = hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract package dependencies for different runtimes
|
||||||
|
if packages, err := cm.extractPackages(ctx, req); err == nil {
|
||||||
|
cacheKey.Packages = packages
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashFile calculates SHA256 hash of a file
|
||||||
|
func (cm *CacheManager) hashFile(filePath string) (string, error) {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
if _, err := io.Copy(hash, file); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSensitiveEnv checks if an environment variable is sensitive
|
||||||
|
func (cm *CacheManager) isSensitiveEnv(key string) bool {
|
||||||
|
sensitiveKeys := []string{
|
||||||
|
"PASSWORD", "SECRET", "KEY", "TOKEN", "API", "AUTH",
|
||||||
|
"DATABASE_URL", "REDIS_URL", "PRIVATE",
|
||||||
|
}
|
||||||
|
|
||||||
|
keyUpper := strings.ToUpper(key)
|
||||||
|
for _, sensitive := range sensitiveKeys {
|
||||||
|
if strings.Contains(keyUpper, sensitive) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPackages extracts package dependencies from lock files
|
||||||
|
func (cm *CacheManager) extractPackages(ctx context.Context, req *types.BuildRequest) ([]string, error) {
|
||||||
|
var packages []string
|
||||||
|
|
||||||
|
// Go modules
|
||||||
|
if goMod := filepath.Join(req.SourcePath, "go.mod"); cm.fileExists(goMod) {
|
||||||
|
if content, err := os.ReadFile(goMod); err == nil {
|
||||||
|
packages = cm.extractGoPackages(string(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node.js packages
|
||||||
|
if packageJson := filepath.Join(req.SourcePath, "package.json"); cm.fileExists(packageJson) {
|
||||||
|
if content, err := os.ReadFile(packageJson); err == nil {
|
||||||
|
packages = append(packages, cm.extractNodePackages(string(content))...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Python requirements
|
||||||
|
if reqTxt := filepath.Join(req.SourcePath, "requirements.txt"); cm.fileExists(reqTxt) {
|
||||||
|
if content, err := os.ReadFile(reqTxt); err == nil {
|
||||||
|
packages = append(packages, cm.extractPythonPackages(string(content))...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *CacheManager) fileExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *CacheManager) extractGoPackages(content string) []string {
|
||||||
|
var packages []string
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "\t") && strings.Contains(line, " ") {
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) > 0 {
|
||||||
|
packages = append(packages, parts[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *CacheManager) extractNodePackages(content string) []string {
|
||||||
|
var packages []string
|
||||||
|
var packageJson struct {
|
||||||
|
Dependencies map[string]string `json:"dependencies"`
|
||||||
|
DevDependencies map[string]string `json:"devDependencies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(content), &packageJson); err == nil {
|
||||||
|
for name := range packageJson.Dependencies {
|
||||||
|
packages = append(packages, name)
|
||||||
|
}
|
||||||
|
for name := range packageJson.DevDependencies {
|
||||||
|
packages = append(packages, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *CacheManager) extractPythonPackages(content string) []string {
|
||||||
|
var packages []string
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" && !strings.HasPrefix(line, "#") && !strings.HasPrefix(line, "-") {
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) > 0 {
|
||||||
|
// Remove version specifiers
|
||||||
|
pkgName := parts[0]
|
||||||
|
if idx := strings.IndexAny(pkgName, "=<>!"); idx != -1 {
|
||||||
|
pkgName = pkgName[:idx]
|
||||||
|
}
|
||||||
|
packages = append(packages, pkgName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCacheKeyHash generates a hash from the cache key
|
||||||
|
func (cm *CacheManager) GetCacheKeyHash(cacheKey *CacheKey) string {
|
||||||
|
data, _ := json.Marshal(cacheKey)
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupCache checks if a cached image exists for the given cache key
|
||||||
|
func (cm *CacheManager) LookupCache(ctx context.Context, cacheKey *CacheKey) (*CacheEntry, error) {
|
||||||
|
keyHash := cm.GetCacheKeyHash(cacheKey)
|
||||||
|
|
||||||
|
// Check local cache first
|
||||||
|
if entry, err := cm.lookupLocalCache(ctx, keyHash); err == nil {
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check remote cache if enabled
|
||||||
|
if cm.remoteCache != nil && cm.remoteCache.Enabled {
|
||||||
|
if entry, err := cm.lookupRemoteCache(ctx, keyHash); err == nil {
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("cache entry not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupLocalCache checks local cache storage
|
||||||
|
func (cm *CacheManager) lookupLocalCache(ctx context.Context, keyHash string) (*CacheEntry, error) {
|
||||||
|
cacheFile := filepath.Join(cm.cacheDir, keyHash+".json")
|
||||||
|
|
||||||
|
data, err := os.ReadFile(cacheFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry CacheEntry
|
||||||
|
if err := json.Unmarshal(data, &entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the image still exists in Docker
|
||||||
|
if _, err := cm.dockerClient.GetImageInfo(ctx, entry.ImageName); err != nil {
|
||||||
|
// Remove stale cache entry
|
||||||
|
os.Remove(cacheFile)
|
||||||
|
return nil, fmt.Errorf("cached image not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupRemoteCache checks remote cache registry
|
||||||
|
func (cm *CacheManager) lookupRemoteCache(ctx context.Context, keyHash string) (*CacheEntry, error) {
|
||||||
|
if cm.remoteCache == nil {
|
||||||
|
return nil, fmt.Errorf("remote cache not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteImage := fmt.Sprintf("%s/%s/cache:%s", cm.remoteCache.Registry, cm.remoteCache.Namespace, keyHash)
|
||||||
|
|
||||||
|
// Try to pull the cached image
|
||||||
|
auth := registry.AuthConfig{}
|
||||||
|
_, err := cm.dockerClient.PullImage(ctx, remoteImage, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("remote cache entry not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image info
|
||||||
|
imageInfo, err := cm.dockerClient.GetImageInfo(ctx, remoteImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create local cache entry
|
||||||
|
entry := &CacheEntry{
|
||||||
|
Key: keyHash,
|
||||||
|
ImageName: remoteImage,
|
||||||
|
Size: imageInfo.Size,
|
||||||
|
Created: time.Now(),
|
||||||
|
Hash: keyHash,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"source": "remote",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to local cache for faster future access
|
||||||
|
cm.saveLocalCache(ctx, entry)
|
||||||
|
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreCache stores a build result in cache
|
||||||
|
func (cm *CacheManager) StoreCache(ctx context.Context, cacheKey *CacheKey, imageName string, metadata map[string]string) error {
|
||||||
|
keyHash := cm.GetCacheKeyHash(cacheKey)
|
||||||
|
|
||||||
|
// Get image info
|
||||||
|
imageInfo, err := cm.dockerClient.GetImageInfo(ctx, imageName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get image info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &CacheEntry{
|
||||||
|
Key: keyHash,
|
||||||
|
ImageName: imageName,
|
||||||
|
Size: imageInfo.Size,
|
||||||
|
Created: time.Now(),
|
||||||
|
Hash: keyHash,
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in local cache
|
||||||
|
if err := cm.saveLocalCache(ctx, entry); err != nil {
|
||||||
|
return fmt.Errorf("failed to save local cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in remote cache if enabled
|
||||||
|
if cm.remoteCache != nil && cm.remoteCache.Enabled {
|
||||||
|
if err := cm.saveRemoteCache(ctx, entry); err != nil {
|
||||||
|
// Log error but don't fail the build
|
||||||
|
fmt.Printf("Warning: failed to save to remote cache: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveLocalCache saves cache entry to local storage
|
||||||
|
func (cm *CacheManager) saveLocalCache(ctx context.Context, entry *CacheEntry) error {
|
||||||
|
if err := os.MkdirAll(cm.cacheDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheFile := filepath.Join(cm.cacheDir, entry.Key+".json")
|
||||||
|
data, err := json.MarshalIndent(entry, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(cacheFile, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveRemoteCache pushes cache entry to remote registry
|
||||||
|
func (cm *CacheManager) saveRemoteCache(ctx context.Context, entry *CacheEntry) error {
|
||||||
|
if cm.remoteCache == nil {
|
||||||
|
return fmt.Errorf("remote cache not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteImage := fmt.Sprintf("%s/%s/cache:%s", cm.remoteCache.Registry, cm.remoteCache.Namespace, entry.Key)
|
||||||
|
|
||||||
|
// Tag and push to remote registry
|
||||||
|
if err := cm.dockerClient.TagImage(ctx, entry.ImageName, remoteImage); err != nil {
|
||||||
|
return fmt.Errorf("failed to tag image for remote cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cm.dockerClient.PushImage(ctx, remoteImage, cm.remoteCache.Registry); err != nil {
|
||||||
|
return fmt.Errorf("failed to push to remote cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupCache removes old cache entries
|
||||||
|
func (cm *CacheManager) CleanupCache(ctx context.Context, maxAge time.Duration) error {
|
||||||
|
entries, err := os.ReadDir(cm.cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cutoff := time.Now().Add(-maxAge)
|
||||||
|
removed := 0
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheFile := filepath.Join(cm.cacheDir, entry.Name())
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.ModTime().Before(cutoff) {
|
||||||
|
// Read cache entry to remove associated image if needed
|
||||||
|
if data, err := os.ReadFile(cacheFile); err == nil {
|
||||||
|
var cacheEntry CacheEntry
|
||||||
|
if json.Unmarshal(data, &cacheEntry) == nil {
|
||||||
|
// Optionally remove Docker image (commented out for safety)
|
||||||
|
// cm.dockerClient.RemoveImage(ctx, cacheEntry.ImageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Remove(cacheFile)
|
||||||
|
removed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Cleaned up %d cache entries\n", removed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCacheStats returns cache statistics
|
||||||
|
func (cm *CacheManager) GetCacheStats(ctx context.Context) (map[string]interface{}, error) {
|
||||||
|
entries, err := os.ReadDir(cm.cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := map[string]interface{}{
|
||||||
|
"total_entries": 0,
|
||||||
|
"total_size": int64(0),
|
||||||
|
"oldest_entry": nil,
|
||||||
|
"newest_entry": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldest, newest time.Time
|
||||||
|
count := 0
|
||||||
|
totalSize := int64(0)
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheFile := filepath.Join(cm.cacheDir, entry.Name())
|
||||||
|
data, err := os.ReadFile(cacheFile)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheEntry CacheEntry
|
||||||
|
if json.Unmarshal(data, &cacheEntry) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
|
totalSize += cacheEntry.Size
|
||||||
|
|
||||||
|
if oldest.IsZero() || cacheEntry.Created.Before(oldest) {
|
||||||
|
oldest = cacheEntry.Created
|
||||||
|
}
|
||||||
|
if newest.IsZero() || cacheEntry.Created.After(newest) {
|
||||||
|
newest = cacheEntry.Created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["total_entries"] = count
|
||||||
|
stats["total_size"] = totalSize
|
||||||
|
if !oldest.IsZero() {
|
||||||
|
stats["oldest_entry"] = oldest
|
||||||
|
}
|
||||||
|
if !newest.IsZero() {
|
||||||
|
stats["newest_entry"] = newest
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,520 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomRuntimeManager handles custom runtime and buildpack support
|
||||||
|
type CustomRuntimeManager struct {
|
||||||
|
workDir string
|
||||||
|
buildpacksDir string
|
||||||
|
runtimes map[string]*CustomRuntime
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomRuntime represents a custom runtime configuration
|
||||||
|
type CustomRuntime struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Type string `json:"type"` // "buildpack", "dockerfile", "native"
|
||||||
|
Description string `json:"description"`
|
||||||
|
Buildpack *BuildpackConfig `json:"buildpack,omitempty"`
|
||||||
|
Dockerfile *CustomDockerfileConfig `json:"dockerfile,omitempty"`
|
||||||
|
Native *NativeConfig `json:"native,omitempty"`
|
||||||
|
Dependencies []string `json:"dependencies"`
|
||||||
|
Environment map[string]string `json:"environment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildpackConfig represents buildpack configuration
|
||||||
|
type BuildpackConfig struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Buildpacks []string `json:"buildpacks"`
|
||||||
|
Groups []BuildpackGroup `json:"groups"`
|
||||||
|
Order []BuildpackOrder `json:"order"`
|
||||||
|
Environment map[string]string `json:"environment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildpackGroup represents a group of buildpacks
|
||||||
|
type BuildpackGroup struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Home string `json:"home"`
|
||||||
|
Order []string `json:"order"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildpackOrder represents buildpack execution order
|
||||||
|
type BuildpackOrder struct {
|
||||||
|
Group []string `json:"group"`
|
||||||
|
Lifecycle string `json:"lifecycle"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomDockerfileConfig represents custom Dockerfile configuration (to avoid conflict)
|
||||||
|
type CustomDockerfileConfig struct {
|
||||||
|
Template string `json:"template"`
|
||||||
|
BaseImage string `json:"base_image"`
|
||||||
|
BuildArgs map[string]string `json:"build_args"`
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
StartCmd string `json:"start_cmd"`
|
||||||
|
HealthCheck *HealthCheck `json:"health_check,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NativeConfig represents native build configuration
|
||||||
|
type NativeConfig struct {
|
||||||
|
Compiler string `json:"compiler"`
|
||||||
|
BuildCmd string `json:"build_cmd"`
|
||||||
|
StartCmd string `json:"start_cmd"`
|
||||||
|
Extensions []string `json:"extensions"`
|
||||||
|
Environment map[string]string `json:"environment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck represents health check configuration
|
||||||
|
type HealthCheck struct {
|
||||||
|
Test []string `json:"test"`
|
||||||
|
Interval string `json:"interval"`
|
||||||
|
Timeout string `json:"timeout"`
|
||||||
|
Retries int `json:"retries"`
|
||||||
|
StartPeriod string `json:"start_period"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCustomRuntimeManager(workDir string) *CustomRuntimeManager {
|
||||||
|
return &CustomRuntimeManager{
|
||||||
|
workDir: workDir,
|
||||||
|
buildpacksDir: filepath.Join(workDir, "buildpacks"),
|
||||||
|
runtimes: make(map[string]*CustomRuntime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCustomRuntime loads a custom runtime from configuration
|
||||||
|
func (crm *CustomRuntimeManager) LoadCustomRuntime(ctx context.Context, configPath string) (*CustomRuntime, error) {
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read runtime config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var runtime CustomRuntime
|
||||||
|
if err := json.Unmarshal(data, &runtime); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse runtime config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate runtime configuration
|
||||||
|
if err := crm.validateRuntime(&runtime); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid runtime configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store runtime
|
||||||
|
crm.runtimes[runtime.Name] = &runtime
|
||||||
|
|
||||||
|
return &runtime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateRuntime validates a custom runtime configuration
|
||||||
|
func (crm *CustomRuntimeManager) validateRuntime(runtime *CustomRuntime) error {
|
||||||
|
if runtime.Name == "" {
|
||||||
|
return fmt.Errorf("runtime name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.Version == "" {
|
||||||
|
return fmt.Errorf("runtime version is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.Type {
|
||||||
|
case "buildpack":
|
||||||
|
if runtime.Buildpack == nil {
|
||||||
|
return fmt.Errorf("buildpack configuration is required for buildpack type")
|
||||||
|
}
|
||||||
|
if runtime.Buildpack.ID == "" {
|
||||||
|
return fmt.Errorf("buildpack ID is required")
|
||||||
|
}
|
||||||
|
case "dockerfile":
|
||||||
|
if runtime.Dockerfile == nil {
|
||||||
|
return fmt.Errorf("dockerfile configuration is required for dockerfile type")
|
||||||
|
}
|
||||||
|
if runtime.Dockerfile.BaseImage == "" {
|
||||||
|
return fmt.Errorf("base image is required for dockerfile type")
|
||||||
|
}
|
||||||
|
case "native":
|
||||||
|
if runtime.Native == nil {
|
||||||
|
return fmt.Errorf("native configuration is required for native type")
|
||||||
|
}
|
||||||
|
if runtime.Native.Compiler == "" {
|
||||||
|
return fmt.Errorf("compiler is required for native type")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported runtime type: %s", runtime.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectCustomRuntime detects if a project uses a custom runtime
|
||||||
|
func (crm *CustomRuntimeManager) DetectCustomRuntime(ctx context.Context, projectPath string) (*CustomRuntime, error) {
|
||||||
|
// Check for runtime configuration file
|
||||||
|
runtimeFiles := []string{
|
||||||
|
"runtime.json",
|
||||||
|
".runtime.json",
|
||||||
|
"containr.json",
|
||||||
|
".containr.json",
|
||||||
|
"buildpack.yml",
|
||||||
|
".buildpack.yml",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range runtimeFiles {
|
||||||
|
configPath := filepath.Join(projectPath, file)
|
||||||
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
|
runtime, err := crm.LoadCustomRuntime(ctx, configPath)
|
||||||
|
if err != nil {
|
||||||
|
continue // Try next file
|
||||||
|
}
|
||||||
|
return runtime, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for known custom runtime indicators
|
||||||
|
if runtime := crm.detectKnownRuntimes(projectPath); runtime != nil {
|
||||||
|
return runtime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no custom runtime detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectKnownRuntimes detects known custom runtimes from project structure
|
||||||
|
func (crm *CustomRuntimeManager) detectKnownRuntimes(projectPath string) *CustomRuntime {
|
||||||
|
// Detect Elixir/Phoenix
|
||||||
|
if crm.fileExists(filepath.Join(projectPath, "mix.exs")) {
|
||||||
|
return &CustomRuntime{
|
||||||
|
Name: "elixir",
|
||||||
|
Version: "1.15",
|
||||||
|
Type: "dockerfile",
|
||||||
|
Dockerfile: &CustomDockerfileConfig{
|
||||||
|
BaseImage: "elixir:1.15-alpine",
|
||||||
|
BuildArgs: map[string]string{
|
||||||
|
"MIX_ENV": "prod",
|
||||||
|
},
|
||||||
|
StartCmd: "mix phx.server",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect Dart
|
||||||
|
if crm.fileExists(filepath.Join(projectPath, "pubspec.yaml")) {
|
||||||
|
return &CustomRuntime{
|
||||||
|
Name: "dart",
|
||||||
|
Version: "3.0",
|
||||||
|
Type: "dockerfile",
|
||||||
|
Dockerfile: &CustomDockerfileConfig{
|
||||||
|
BaseImage: "dart:3.0-sdk",
|
||||||
|
BuildArgs: map[string]string{
|
||||||
|
"BUILD_ENV": "production",
|
||||||
|
},
|
||||||
|
StartCmd: "dart run bin/server.dart",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect Swift
|
||||||
|
if crm.fileExists(filepath.Join(projectPath, "Package.swift")) {
|
||||||
|
return &CustomRuntime{
|
||||||
|
Name: "swift",
|
||||||
|
Version: "5.9",
|
||||||
|
Type: "dockerfile",
|
||||||
|
Dockerfile: &CustomDockerfileConfig{
|
||||||
|
BaseImage: "swift:5.9",
|
||||||
|
BuildArgs: map[string]string{
|
||||||
|
"BUILD_CONFIGURATION": "release",
|
||||||
|
},
|
||||||
|
StartCmd: ".build/release/MyApp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect Kotlin
|
||||||
|
if crm.fileExists(filepath.Join(projectPath, "build.gradle.kts")) || crm.fileExists(filepath.Join(projectPath, "build.gradle")) {
|
||||||
|
return &CustomRuntime{
|
||||||
|
Name: "kotlin",
|
||||||
|
Version: "1.9",
|
||||||
|
Type: "dockerfile",
|
||||||
|
Dockerfile: &CustomDockerfileConfig{
|
||||||
|
BaseImage: "openjdk:17-jdk-slim",
|
||||||
|
BuildArgs: map[string]string{
|
||||||
|
"KOTLIN_VERSION": "1.9.0",
|
||||||
|
},
|
||||||
|
StartCmd: "java -jar app.jar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildWithCustomRuntime builds a project using custom runtime
|
||||||
|
func (crm *CustomRuntimeManager) BuildWithCustomRuntime(ctx context.Context, runtime *CustomRuntime, projectPath string, imageName string) error {
|
||||||
|
switch runtime.Type {
|
||||||
|
case "buildpack":
|
||||||
|
return crm.buildWithBuildpack(ctx, runtime, projectPath, imageName)
|
||||||
|
case "dockerfile":
|
||||||
|
return crm.buildWithDockerfile(ctx, runtime, projectPath, imageName)
|
||||||
|
case "native":
|
||||||
|
return crm.buildWithNative(ctx, runtime, projectPath, imageName)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported runtime type: %s", runtime.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWithBuildpack builds using buildpacks
|
||||||
|
func (crm *CustomRuntimeManager) buildWithBuildpack(ctx context.Context, runtime *CustomRuntime, projectPath string, imageName string) error {
|
||||||
|
// This would integrate with buildpack tools like pack or cnb
|
||||||
|
// For now, generate a Dockerfile based on buildpack configuration
|
||||||
|
|
||||||
|
dockerfile := crm.generateBuildpackDockerfile(runtime)
|
||||||
|
dockerfilePath := filepath.Join(crm.workDir, "Dockerfile.buildpack")
|
||||||
|
|
||||||
|
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write buildpack Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build using the generated Dockerfile
|
||||||
|
// This would use the docker client to build the image
|
||||||
|
fmt.Printf("Building with buildpack runtime: %s\n", runtime.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWithDockerfile builds using custom Dockerfile template
|
||||||
|
func (crm *CustomRuntimeManager) buildWithDockerfile(ctx context.Context, runtime *CustomRuntime, projectPath string, imageName string) error {
|
||||||
|
dockerfile := crm.generateCustomDockerfile(runtime)
|
||||||
|
dockerfilePath := filepath.Join(crm.workDir, "Dockerfile.custom")
|
||||||
|
|
||||||
|
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write custom Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build using the generated Dockerfile
|
||||||
|
fmt.Printf("Building with custom Dockerfile runtime: %s\n", runtime.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWithNative builds using native compilation
|
||||||
|
func (crm *CustomRuntimeManager) buildWithNative(ctx context.Context, runtime *CustomRuntime, projectPath string, imageName string) error {
|
||||||
|
// Generate a Dockerfile for native compilation
|
||||||
|
dockerfile := crm.generateNativeDockerfile(runtime)
|
||||||
|
dockerfilePath := filepath.Join(crm.workDir, "Dockerfile.native")
|
||||||
|
|
||||||
|
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write native Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build using the generated Dockerfile
|
||||||
|
fmt.Printf("Building with native runtime: %s\n", runtime.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateBuildpackDockerfile generates a Dockerfile for buildpack-based builds
|
||||||
|
func (crm *CustomRuntimeManager) generateBuildpackDockerfile(runtime *CustomRuntime) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString(fmt.Sprintf("# Buildpack-based runtime: %s\n", runtime.Name))
|
||||||
|
builder.WriteString(fmt.Sprintf("FROM %s as builder\n", runtime.Dockerfile.BaseImage))
|
||||||
|
builder.WriteString("WORKDIR /app\n")
|
||||||
|
builder.WriteString("COPY . .\n")
|
||||||
|
|
||||||
|
// Add buildpack-specific instructions
|
||||||
|
if runtime.Buildpack != nil {
|
||||||
|
for _, buildpack := range runtime.Buildpack.Buildpacks {
|
||||||
|
builder.WriteString(fmt.Sprintf("RUN echo 'Installing buildpack: %s'\n", buildpack))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
if len(runtime.Environment) > 0 {
|
||||||
|
builder.WriteString("\n# Environment variables\n")
|
||||||
|
for k, v := range runtime.Environment {
|
||||||
|
builder.WriteString(fmt.Sprintf("ENV %s=%s\n", k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n# Production stage\n")
|
||||||
|
builder.WriteString("FROM scratch\n")
|
||||||
|
builder.WriteString("COPY --from=builder /app /app\n")
|
||||||
|
builder.WriteString("WORKDIR /app\n")
|
||||||
|
|
||||||
|
if runtime.Native != nil && runtime.Native.StartCmd != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("CMD [\"%s\"]\n", runtime.Native.StartCmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCustomDockerfile generates a custom Dockerfile from template
|
||||||
|
func (crm *CustomRuntimeManager) generateCustomDockerfile(runtime *CustomRuntime) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString(fmt.Sprintf("# Custom runtime: %s\n", runtime.Name))
|
||||||
|
builder.WriteString(fmt.Sprintf("FROM %s\n", runtime.Dockerfile.BaseImage))
|
||||||
|
builder.WriteString("WORKDIR /app\n")
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
if len(runtime.Environment) > 0 {
|
||||||
|
builder.WriteString("\n# Environment variables\n")
|
||||||
|
for k, v := range runtime.Environment {
|
||||||
|
builder.WriteString(fmt.Sprintf("ENV %s=%s\n", k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build arguments
|
||||||
|
if len(runtime.Dockerfile.BuildArgs) > 0 {
|
||||||
|
builder.WriteString("\n# Build arguments\n")
|
||||||
|
for k, v := range runtime.Dockerfile.BuildArgs {
|
||||||
|
builder.WriteString(fmt.Sprintf("ARG %s=%s\n", k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
if len(runtime.Dockerfile.Labels) > 0 {
|
||||||
|
builder.WriteString("\n# Labels\n")
|
||||||
|
for k, v := range runtime.Dockerfile.Labels {
|
||||||
|
builder.WriteString(fmt.Sprintf("LABEL %s=%s\n", k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n# Copy source code\n")
|
||||||
|
builder.WriteString("COPY . .\n")
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
if runtime.Dockerfile.HealthCheck != nil {
|
||||||
|
builder.WriteString("\n# Health check\n")
|
||||||
|
healthCheck := runtime.Dockerfile.HealthCheck
|
||||||
|
builder.WriteString(fmt.Sprintf("HEALTHCHECK --interval=%s --timeout=%s --retries=%d --start-period=%s \\\n",
|
||||||
|
healthCheck.Interval, healthCheck.Timeout, healthCheck.Retries, healthCheck.StartPeriod))
|
||||||
|
builder.WriteString(" CMD ")
|
||||||
|
for i, test := range healthCheck.Test {
|
||||||
|
if i > 0 {
|
||||||
|
builder.WriteString(" ")
|
||||||
|
}
|
||||||
|
builder.WriteString(fmt.Sprintf("\"%s\"", test))
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default command
|
||||||
|
if runtime.Native != nil && runtime.Native.StartCmd != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("\nCMD [\"%s\"]\n", runtime.Native.StartCmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateNativeDockerfile generates a Dockerfile for native compilation
|
||||||
|
func (crm *CustomRuntimeManager) generateNativeDockerfile(runtime *CustomRuntime) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString(fmt.Sprintf("# Native runtime: %s\n", runtime.Name))
|
||||||
|
builder.WriteString(fmt.Sprintf("FROM %s as builder\n", runtime.Dockerfile.BaseImage))
|
||||||
|
builder.WriteString("WORKDIR /app\n")
|
||||||
|
|
||||||
|
// Install compiler and dependencies
|
||||||
|
if runtime.Native != nil {
|
||||||
|
builder.WriteString(fmt.Sprintf("RUN echo 'Installing %s compiler'\n", runtime.Native.Compiler))
|
||||||
|
|
||||||
|
for _, dep := range runtime.Dependencies {
|
||||||
|
builder.WriteString(fmt.Sprintf("RUN echo 'Installing dependency: %s'\n", dep))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n# Copy source code\n")
|
||||||
|
builder.WriteString("COPY . .\n")
|
||||||
|
|
||||||
|
// Build command
|
||||||
|
if runtime.Native != nil && runtime.Native.BuildCmd != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("\nRUN %s\n", runtime.Native.BuildCmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n# Production stage\n")
|
||||||
|
builder.WriteString("FROM alpine:latest\n")
|
||||||
|
builder.WriteString("WORKDIR /app\n")
|
||||||
|
|
||||||
|
// Copy compiled binary
|
||||||
|
if runtime.Native != nil {
|
||||||
|
builder.WriteString("COPY --from=builder /app/app .\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start command
|
||||||
|
if runtime.Native != nil && runtime.Native.StartCmd != "" {
|
||||||
|
builder.WriteString(fmt.Sprintf("CMD [\"%s\"]\n", runtime.Native.StartCmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileExists checks if a file exists
|
||||||
|
func (crm *CustomRuntimeManager) fileExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstalledRuntimes returns all installed custom runtimes
|
||||||
|
func (crm *CustomRuntimeManager) GetInstalledRuntimes() map[string]*CustomRuntime {
|
||||||
|
return crm.runtimes
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallRuntime installs a custom runtime from a URL or local path
|
||||||
|
func (crm *CustomRuntimeManager) InstallRuntime(ctx context.Context, source string) error {
|
||||||
|
// This would download and install a runtime from a URL
|
||||||
|
// For now, just load from local path
|
||||||
|
runtime, err := crm.LoadCustomRuntime(ctx, source)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to install runtime: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Installed custom runtime: %s %s\n", runtime.Name, runtime.Version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptimizeDependencies optimizes and caches dependencies for faster builds
|
||||||
|
func (crm *CustomRuntimeManager) OptimizeDependencies(ctx context.Context, runtime *CustomRuntime, projectPath string) error {
|
||||||
|
switch runtime.Type {
|
||||||
|
case "buildpack":
|
||||||
|
return crm.optimizeBuildpackDeps(ctx, runtime, projectPath)
|
||||||
|
case "dockerfile":
|
||||||
|
return crm.optimizeDockerfileDeps(ctx, runtime, projectPath)
|
||||||
|
case "native":
|
||||||
|
return crm.optimizeNativeDeps(ctx, runtime, projectPath)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported runtime type: %s", runtime.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// optimizeBuildpackDeps optimizes buildpack dependencies
|
||||||
|
func (crm *CustomRuntimeManager) optimizeBuildpackDeps(ctx context.Context, runtime *CustomRuntime, projectPath string) error {
|
||||||
|
// Create dependency cache layers
|
||||||
|
fmt.Printf("Optimizing buildpack dependencies for %s\n", runtime.Name)
|
||||||
|
|
||||||
|
// This would create cached layers for common dependencies
|
||||||
|
// Implementation depends on the specific buildpack system being used
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// optimizeDockerfileDeps optimizes Dockerfile dependencies
|
||||||
|
func (crm *CustomRuntimeManager) optimizeDockerfileDeps(ctx context.Context, runtime *CustomRuntime, projectPath string) error {
|
||||||
|
// Create optimized Dockerfile with dependency caching
|
||||||
|
fmt.Printf("Optimizing Dockerfile dependencies for %s\n", runtime.Name)
|
||||||
|
|
||||||
|
// This would reorganize the Dockerfile to optimize layer caching
|
||||||
|
// Common pattern: copy dependency files first, install deps, then copy source
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// optimizeNativeDeps optimizes native compilation dependencies
|
||||||
|
func (crm *CustomRuntimeManager) optimizeNativeDeps(ctx context.Context, runtime *CustomRuntime, projectPath string) error {
|
||||||
|
// Create dependency cache for native builds
|
||||||
|
fmt.Printf("Optimizing native dependencies for %s\n", runtime.Name)
|
||||||
|
|
||||||
|
// This would cache compiled dependencies and object files
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"containr/internal/docker"
|
||||||
|
"containr/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DockerfileBuilder struct {
|
||||||
|
workDir string
|
||||||
|
dockerClient *docker.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerfileConfig struct {
|
||||||
|
DockerfilePath string `json:"dockerfile_path"`
|
||||||
|
ContextPath string `json:"context_path"`
|
||||||
|
BuildArgs map[string]string `json:"build_args"`
|
||||||
|
Target string `json:"target,omitempty"`
|
||||||
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDockerfileBuilder(workDir string, dockerClient *docker.Client) *DockerfileBuilder {
|
||||||
|
return &DockerfileBuilder{
|
||||||
|
workDir: workDir,
|
||||||
|
dockerClient: dockerClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectDockerfile checks if a Dockerfile exists in the repository
|
||||||
|
func (d *DockerfileBuilder) DetectDockerfile(ctx context.Context, repoPath string) (string, error) {
|
||||||
|
dockerfiles := []string{
|
||||||
|
"Dockerfile",
|
||||||
|
"Dockerfile.prod",
|
||||||
|
"Dockerfile.production",
|
||||||
|
"Dockerfile.build",
|
||||||
|
"dockerfile",
|
||||||
|
"dockerfile.prod",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dockerfile := range dockerfiles {
|
||||||
|
fullPath := filepath.Join(repoPath, dockerfile)
|
||||||
|
if _, err := os.Stat(fullPath); err == nil {
|
||||||
|
return dockerfile, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("no Dockerfile found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDockerfile validates the Dockerfile syntax and structure
|
||||||
|
func (d *DockerfileBuilder) ValidateDockerfile(ctx context.Context, dockerfilePath string) error {
|
||||||
|
// Check if file exists
|
||||||
|
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("Dockerfile does not exist: %s", dockerfilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and validate basic structure
|
||||||
|
content, err := os.ReadFile(dockerfilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerfileContent := string(content)
|
||||||
|
lines := strings.Split(dockerfileContent, "\n")
|
||||||
|
|
||||||
|
hasFrom := false
|
||||||
|
hasExposeOrCmd := false
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "FROM") {
|
||||||
|
hasFrom = true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "EXPOSE") || strings.HasPrefix(line, "CMD") || strings.HasPrefix(line, "ENTRYPOINT") {
|
||||||
|
hasExposeOrCmd = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasFrom {
|
||||||
|
return fmt.Errorf("Dockerfile must have a FROM instruction")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasExposeOrCmd {
|
||||||
|
return fmt.Errorf("Dockerfile should have EXPOSE, CMD, or ENTRYPOINT instruction")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptimizeDockerfile applies common optimizations to the Dockerfile
|
||||||
|
func (d *DockerfileBuilder) OptimizeDockerfile(ctx context.Context, dockerfilePath string) (string, error) {
|
||||||
|
content, err := os.ReadFile(dockerfilePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
optimized := d.optimizeDockerfileContent(string(content))
|
||||||
|
|
||||||
|
optimizedPath := filepath.Join(d.workDir, "Dockerfile.optimized")
|
||||||
|
err = os.WriteFile(optimizedPath, []byte(optimized), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write optimized Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return optimizedPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DockerfileBuilder) optimizeDockerfileContent(content string) string {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
var optimized []string
|
||||||
|
|
||||||
|
// Track if we've already added common optimizations
|
||||||
|
hasMultiStage := false
|
||||||
|
hasSecurityOptimizations := false
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
// Skip empty lines and comments
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||||
|
optimized = append(optimized, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for multi-stage builds
|
||||||
|
if strings.HasPrefix(trimmed, "FROM") && strings.Contains(trimmed, " AS ") {
|
||||||
|
hasMultiStage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for build cache optimizations
|
||||||
|
if strings.Contains(trimmed, "RUN") && strings.Contains(trimmed, "npm") {
|
||||||
|
hasMultiStage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
optimized = append(optimized, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add optimizations if not present
|
||||||
|
if !hasMultiStage {
|
||||||
|
// Insert multi-stage build suggestion at the beginning
|
||||||
|
optimized = append([]string{
|
||||||
|
"# Multi-stage build for smaller production image",
|
||||||
|
"FROM node:20-alpine AS builder",
|
||||||
|
"WORKDIR /app",
|
||||||
|
"COPY package*.json ./",
|
||||||
|
"RUN npm ci --only=production",
|
||||||
|
"COPY . .",
|
||||||
|
"RUN npm run build",
|
||||||
|
"",
|
||||||
|
"# Production stage",
|
||||||
|
"FROM node:20-alpine AS production",
|
||||||
|
"WORKDIR /app",
|
||||||
|
"COPY --from=builder /app/dist ./dist",
|
||||||
|
"COPY --from=builder /app/node_modules ./node_modules",
|
||||||
|
"EXPOSE 8080",
|
||||||
|
"CMD [\"npm\", \"start\"]",
|
||||||
|
}, optimized...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasSecurityOptimizations {
|
||||||
|
optimized = append(optimized, "", "# Security optimizations")
|
||||||
|
optimized = append(optimized, "RUN addgroup -g 1001 -S nodejs")
|
||||||
|
optimized = append(optimized, "RUN adduser -S nodejs -u 1001")
|
||||||
|
optimized = append(optimized, "USER nodejs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(optimized, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build builds the container image using Dockerfile
|
||||||
|
func (d *DockerfileBuilder) Build(ctx context.Context, req *types.BuildRequest) (*types.BuildResponse, error) {
|
||||||
|
// Detect Dockerfile
|
||||||
|
dockerfileName, err := d.DetectDockerfile(ctx, req.SourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to detect Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerfilePath := filepath.Join(req.SourcePath, dockerfileName)
|
||||||
|
|
||||||
|
// Validate Dockerfile
|
||||||
|
err = d.ValidateDockerfile(ctx, dockerfilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize Dockerfile (optional)
|
||||||
|
optimizedDockerfile, err := d.OptimizeDockerfile(ctx, dockerfilePath)
|
||||||
|
if err != nil {
|
||||||
|
// If optimization fails, use original
|
||||||
|
optimizedDockerfile = dockerfilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare build args
|
||||||
|
buildArgs := make(map[string]*string)
|
||||||
|
for k, v := range req.Environment {
|
||||||
|
buildArgs[k] = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default build args
|
||||||
|
buildArgs["BUILDKIT_INLINE_CACHE"] = strPtr("1")
|
||||||
|
buildArgs["TARGETPLATFORM"] = strPtr("linux/amd64")
|
||||||
|
|
||||||
|
// Build Docker image
|
||||||
|
imageName := fmt.Sprintf("%s:%s", req.ImageName, req.ImageTag)
|
||||||
|
|
||||||
|
// Create build context tar
|
||||||
|
buildCtx, err := createBuildContext(req.SourcePath, optimizedDockerfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create build context: %w", err)
|
||||||
|
}
|
||||||
|
defer buildCtx.Close()
|
||||||
|
|
||||||
|
buildOptions := docker.BuildOptions{
|
||||||
|
Dockerfile: "Dockerfile",
|
||||||
|
Tags: []string{imageName},
|
||||||
|
BuildArgs: buildArgs,
|
||||||
|
Remove: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.dockerClient.BuildImage(ctx, buildCtx, buildOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build Docker image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image info
|
||||||
|
imageInfo, err := d.dockerClient.GetImageInfo(ctx, imageName)
|
||||||
|
if err != nil {
|
||||||
|
// Continue even if we can't get image info
|
||||||
|
imageInfo = &docker.ImageInfo{Size: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push to registry if specified
|
||||||
|
if req.RegistryURL != "" {
|
||||||
|
fullImageName := fmt.Sprintf("%s/%s:%s", req.RegistryURL, req.ImageName, req.ImageTag)
|
||||||
|
err = d.dockerClient.PushImage(ctx, imageName, req.RegistryURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to push image: %w", err)
|
||||||
|
}
|
||||||
|
imageName = fullImageName
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.BuildResponse{
|
||||||
|
ImageName: imageName,
|
||||||
|
ImageTag: req.ImageTag,
|
||||||
|
Size: imageInfo.Size,
|
||||||
|
Digest: imageInfo.Digest,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateDockerfile generates a basic Dockerfile for common runtimes
|
||||||
|
func (d *DockerfileBuilder) GenerateDockerfile(ctx context.Context, runtime, appPath string) (string, error) {
|
||||||
|
switch runtime {
|
||||||
|
case "go":
|
||||||
|
return d.generateGoDockerfile(appPath), nil
|
||||||
|
case "node":
|
||||||
|
return d.generateNodeDockerfile(appPath), nil
|
||||||
|
case "python":
|
||||||
|
return d.generatePythonDockerfile(appPath), nil
|
||||||
|
case "rust":
|
||||||
|
return d.generateRustDockerfile(appPath), nil
|
||||||
|
case "static":
|
||||||
|
return d.generateStaticDockerfile(appPath), nil
|
||||||
|
default:
|
||||||
|
return d.generateGenericDockerfile(appPath), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DockerfileBuilder) generateGoDockerfile(appPath string) string {
|
||||||
|
return `# Go Dockerfile
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install git (required for some go modules)
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
# Copy go mod files
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
# Copy the binary from builder stage
|
||||||
|
COPY --from=builder /app/main .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Run the binary
|
||||||
|
CMD ["./main"]
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DockerfileBuilder) generateNodeDockerfile(appPath string) string {
|
||||||
|
return `# Node.js Dockerfile
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application (if needed)
|
||||||
|
RUN npm run build || true
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S nodejs
|
||||||
|
RUN adduser -S nodejs -u 1001
|
||||||
|
|
||||||
|
# Copy built application and dependencies
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER nodejs
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["npm", "start"]
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DockerfileBuilder) generatePythonDockerfile(appPath string) string {
|
||||||
|
return `# Python Dockerfile
|
||||||
|
FROM python:3.11-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd --create-home --shell /bin/bash app
|
||||||
|
|
||||||
|
# Copy installed packages
|
||||||
|
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
|
||||||
|
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY --chown=app:app . .
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER app
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["python", "app.py"]
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DockerfileBuilder) generateRustDockerfile(appPath string) string {
|
||||||
|
return `# Rust Dockerfile
|
||||||
|
FROM rust:1.75-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache musl-dev
|
||||||
|
|
||||||
|
# Copy Cargo files
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
|
# Create dummy main.rs to cache dependencies
|
||||||
|
RUN mkdir src && echo "fn main() {}" > src/main.rs
|
||||||
|
RUN cargo build --release
|
||||||
|
RUN rm -rf src
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
# Copy the binary from builder stage
|
||||||
|
COPY --from=builder /app/target/release/app .
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S appgroup
|
||||||
|
RUN adduser -S appuser -u 1001 -G appgroup
|
||||||
|
|
||||||
|
# Change ownership and switch to non-root user
|
||||||
|
RUN chown appuser:appgroup /app/app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Run the binary
|
||||||
|
CMD ["./app"]
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DockerfileBuilder) generateStaticDockerfile(appPath string) string {
|
||||||
|
return `# Static files Dockerfile
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy static files
|
||||||
|
COPY . /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy custom nginx config if exists
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf || true
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DockerfileBuilder) generateGenericDockerfile(appPath string) string {
|
||||||
|
return `# Generic Dockerfile
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install basic runtime
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
# Copy application
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S appgroup
|
||||||
|
RUN adduser -S appuser -u 1001 -G appgroup
|
||||||
|
|
||||||
|
# Change ownership
|
||||||
|
RUN chown -R appuser:appgroup /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Default command - override in your application
|
||||||
|
CMD ["./app"]
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func strPtr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBuildContext(sourcePath, dockerfileContent string) (io.ReadCloser, error) {
|
||||||
|
// Create a temporary directory for the build context
|
||||||
|
tmpDir, err := os.MkdirTemp("", "docker-build-")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Write Dockerfile to temp directory
|
||||||
|
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
|
||||||
|
if err := os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0644); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tar archive of the build context
|
||||||
|
var buf bytes.Buffer
|
||||||
|
tw := tar.NewWriter(&buf)
|
||||||
|
defer tw.Close()
|
||||||
|
|
||||||
|
// Add Dockerfile to tar
|
||||||
|
dockerfileData, err := os.ReadFile(dockerfilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: "Dockerfile",
|
||||||
|
Mode: 0644,
|
||||||
|
Size: int64(len(dockerfileData)),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(dockerfileData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add source files to tar
|
||||||
|
err = filepath.Walk(sourcePath, func(file string, fi os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fi.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(sourcePath, file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if relPath == "Dockerfile" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: relPath,
|
||||||
|
Mode: int64(fi.Mode()),
|
||||||
|
Size: int64(len(data)),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.NopCloser(&buf), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"containr/internal/docker"
|
||||||
|
"containr/internal/types"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuildManager struct {
|
||||||
|
railpackBuilder *RailpackBuilder
|
||||||
|
nixpacksBuilder *NixpacksBuilder
|
||||||
|
dockerfileBuilder *DockerfileBuilder
|
||||||
|
dockerClient *docker.Client
|
||||||
|
cacheManager *CacheManager
|
||||||
|
parallelBuilder *ParallelBuilder
|
||||||
|
workDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BuildTypeRailpack BuildType = "railpack"
|
||||||
|
BuildTypeNixpacks BuildType = "nixpacks"
|
||||||
|
BuildTypeDockerfile BuildType = "dockerfile"
|
||||||
|
BuildTypePrebuilt BuildType = "prebuilt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewBuildManager(workDir string, dockerClient *docker.Client) *BuildManager {
|
||||||
|
cacheManager := NewCacheManager(filepath.Join(workDir, "cache"), dockerClient)
|
||||||
|
parallelBuilder := NewParallelBuilder(nil, cacheManager, 4) // Default 4 workers
|
||||||
|
|
||||||
|
return &BuildManager{
|
||||||
|
railpackBuilder: NewRailpackBuilder(workDir, dockerClient),
|
||||||
|
nixpacksBuilder: NewNixpacksBuilder(workDir, dockerClient),
|
||||||
|
dockerfileBuilder: NewDockerfileBuilder(workDir, dockerClient),
|
||||||
|
dockerClient: dockerClient,
|
||||||
|
cacheManager: cacheManager,
|
||||||
|
parallelBuilder: parallelBuilder,
|
||||||
|
workDir: workDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectBuildType automatically detects the build type based on repository contents
|
||||||
|
func (bm *BuildManager) DetectBuildType(ctx context.Context, repoPath string) (BuildType, error) {
|
||||||
|
// Check for Dockerfile first
|
||||||
|
if _, err := bm.dockerfileBuilder.DetectDockerfile(ctx, repoPath); err == nil {
|
||||||
|
return BuildTypeDockerfile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Railpack can build this project (primary choice)
|
||||||
|
if err := bm.railpackBuilder.DetectRailpack(ctx, repoPath); err == nil {
|
||||||
|
return BuildTypeRailpack, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to Nixpacks as fallback
|
||||||
|
return BuildTypeNixpacks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build executes the build process using the appropriate builder
|
||||||
|
func (bm *BuildManager) Build(ctx context.Context, req *types.BuildRequest) (*types.BuildResponse, error) {
|
||||||
|
// Detect build type if not specified
|
||||||
|
if req.BuildType == "" {
|
||||||
|
detectedType, err := bm.DetectBuildType(ctx, req.SourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to detect build type: %w", err)
|
||||||
|
}
|
||||||
|
req.BuildType = string(detectedType)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch BuildType(req.BuildType) {
|
||||||
|
case BuildTypeRailpack:
|
||||||
|
return bm.railpackBuilder.Build(ctx, req)
|
||||||
|
case BuildTypeNixpacks:
|
||||||
|
return bm.nixpacksBuilder.Build(ctx, req)
|
||||||
|
case BuildTypeDockerfile:
|
||||||
|
return bm.dockerfileBuilder.Build(ctx, req)
|
||||||
|
case BuildTypePrebuilt:
|
||||||
|
return bm.buildPrebuilt(ctx, req)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported build type: %s", req.BuildType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPrebuilt handles prebuilt image deployments
|
||||||
|
func (bm *BuildManager) buildPrebuilt(ctx context.Context, req *types.BuildRequest) (*types.BuildResponse, error) {
|
||||||
|
if req.PrebuiltImage == "" {
|
||||||
|
return nil, fmt.Errorf("prebuilt image not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the prebuilt image
|
||||||
|
auth := registry.AuthConfig{}
|
||||||
|
_, err := bm.dockerClient.PullImage(ctx, req.PrebuiltImage, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to pull prebuilt image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag with our desired name and tag if different
|
||||||
|
if req.ImageName != "" && req.ImageTag != "" {
|
||||||
|
targetImage := fmt.Sprintf("%s:%s", req.ImageName, req.ImageTag)
|
||||||
|
if targetImage != req.PrebuiltImage {
|
||||||
|
err = bm.dockerClient.TagImage(ctx, req.PrebuiltImage, targetImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to tag image: %w", err)
|
||||||
|
}
|
||||||
|
req.PrebuiltImage = targetImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push to registry if specified
|
||||||
|
if req.RegistryURL != "" {
|
||||||
|
err = bm.dockerClient.PushImage(ctx, req.PrebuiltImage, req.RegistryURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to push image: %w", err)
|
||||||
|
}
|
||||||
|
req.PrebuiltImage = fmt.Sprintf("%s/%s", req.RegistryURL, req.PrebuiltImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image info
|
||||||
|
imageInfo, err := bm.dockerClient.GetImageInfo(ctx, req.PrebuiltImage)
|
||||||
|
if err != nil {
|
||||||
|
// Continue even if we can't get image info
|
||||||
|
imageInfo = &docker.ImageInfo{Size: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.BuildResponse{
|
||||||
|
ImageName: req.PrebuiltImage,
|
||||||
|
ImageTag: req.ImageTag,
|
||||||
|
Size: imageInfo.Size,
|
||||||
|
Digest: imageInfo.Digest,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateBuildRequest validates the build request parameters
|
||||||
|
func (bm *BuildManager) ValidateBuildRequest(ctx context.Context, req *types.BuildRequest) error {
|
||||||
|
if req.SourcePath == "" && req.PrebuiltImage == "" {
|
||||||
|
return fmt.Errorf("either source path or prebuilt image must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ImageName == "" {
|
||||||
|
return fmt.Errorf("image name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ImageTag == "" {
|
||||||
|
return fmt.Errorf("image tag is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate source path exists if specified
|
||||||
|
if req.SourcePath != "" {
|
||||||
|
if _, err := filepath.Abs(req.SourcePath); err != nil {
|
||||||
|
return fmt.Errorf("invalid source path: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuildPlan returns the build plan for inspection without building
|
||||||
|
func (bm *BuildManager) GetBuildPlan(ctx context.Context, req *types.BuildRequest) (interface{}, error) {
|
||||||
|
switch BuildType(req.BuildType) {
|
||||||
|
case BuildTypeRailpack:
|
||||||
|
config := &RailpackConfig{
|
||||||
|
BuildCmd: req.BuildCommand,
|
||||||
|
StartCmd: req.StartCommand,
|
||||||
|
Env: req.Environment,
|
||||||
|
}
|
||||||
|
return bm.railpackBuilder.GeneratePlan(ctx, req.SourcePath, config)
|
||||||
|
case BuildTypeNixpacks:
|
||||||
|
config := &NixpacksConfig{
|
||||||
|
BuildCmd: req.BuildCommand,
|
||||||
|
StartCmd: req.StartCommand,
|
||||||
|
Env: req.Environment,
|
||||||
|
}
|
||||||
|
return bm.nixpacksBuilder.GeneratePlan(ctx, req.SourcePath, config)
|
||||||
|
case BuildTypeDockerfile:
|
||||||
|
dockerfile, err := bm.dockerfileBuilder.DetectDockerfile(ctx, req.SourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]string{
|
||||||
|
"dockerfile": dockerfile,
|
||||||
|
"context": req.SourcePath,
|
||||||
|
}, nil
|
||||||
|
case BuildTypePrebuilt:
|
||||||
|
return map[string]string{
|
||||||
|
"image": req.PrebuiltImage,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported build type: %s", req.BuildType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildWithCache attempts to build using cache first
|
||||||
|
func (bm *BuildManager) BuildWithCache(ctx context.Context, req *types.BuildRequest) (*types.BuildResponse, error) {
|
||||||
|
bm.parallelBuilder.buildManager = bm
|
||||||
|
result, err := bm.parallelBuilder.BuildWithCache(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result.Response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildParallel executes multiple builds in parallel
|
||||||
|
func (bm *BuildManager) BuildParallel(ctx context.Context, requests []*types.BuildRequest) ([]*types.BuildResponse, error) {
|
||||||
|
bm.parallelBuilder.buildManager = bm
|
||||||
|
results, err := bm.parallelBuilder.BuildParallel(ctx, requests)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]*types.BuildResponse, len(results))
|
||||||
|
for i, result := range results {
|
||||||
|
if result.Success {
|
||||||
|
responses[i] = result.Response
|
||||||
|
} else {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRemoteCache configures remote cache settings
|
||||||
|
func (bm *BuildManager) SetRemoteCache(config *RemoteCacheConfig) {
|
||||||
|
bm.cacheManager.SetRemoteCache(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCacheStats returns cache statistics
|
||||||
|
func (bm *BuildManager) GetCacheStats(ctx context.Context) (map[string]interface{}, error) {
|
||||||
|
return bm.cacheManager.GetCacheStats(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupCache removes old cache entries
|
||||||
|
func (bm *BuildManager) CleanupCache(ctx context.Context, maxAge time.Duration) error {
|
||||||
|
return bm.cacheManager.CleanupCache(ctx, maxAge)
|
||||||
|
}
|
||||||
@@ -0,0 +1,575 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"containr/internal/docker"
|
||||||
|
"containr/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NixpacksBuilder struct {
|
||||||
|
workDir string
|
||||||
|
dockerClient *docker.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type NixpacksPlan struct {
|
||||||
|
Pkgs []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
} `json:"pkgs"`
|
||||||
|
Build struct {
|
||||||
|
Builder string `json:"builder"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
} `json:"build"`
|
||||||
|
Start struct {
|
||||||
|
Cmd []string `json:"cmd"`
|
||||||
|
Env map[string]string `json:"env"`
|
||||||
|
Dir string `json:"dir"`
|
||||||
|
} `json:"start"`
|
||||||
|
Install struct {
|
||||||
|
Cmd []string `json:"cmd"`
|
||||||
|
} `json:"install"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NixpacksConfig struct {
|
||||||
|
BuildCmd string `json:"build_cmd,omitempty"`
|
||||||
|
StartCmd string `json:"start_cmd,omitempty"`
|
||||||
|
Pkgs []string `json:"pkgs,omitempty"`
|
||||||
|
Env map[string]string `json:"env,omitempty"`
|
||||||
|
InstallCmd string `json:"install_cmd,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNixpacksBuilder(workDir string, dockerClient *docker.Client) *NixpacksBuilder {
|
||||||
|
return &NixpacksBuilder{
|
||||||
|
workDir: workDir,
|
||||||
|
dockerClient: dockerClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectRuntime detects the runtime based on project files
|
||||||
|
func (n *NixpacksBuilder) DetectRuntime(ctx context.Context, repoPath string) (string, error) {
|
||||||
|
detections := map[string][]string{
|
||||||
|
"go": {"go.mod", "go.sum", "main.go"},
|
||||||
|
"node": {"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"},
|
||||||
|
"python": {"requirements.txt", "setup.py", "pyproject.toml", "Pipfile"},
|
||||||
|
"rust": {"Cargo.toml", "Cargo.lock"},
|
||||||
|
"ruby": {"Gemfile", "Gemfile.lock"},
|
||||||
|
"php": {"composer.json", "composer.lock"},
|
||||||
|
"deno": {"deno.json", "deno.jsonc"},
|
||||||
|
"bun": {"bun.lockb", "package.json"},
|
||||||
|
"static": {"index.html", "dist/", "public/"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for runtime, files := range detections {
|
||||||
|
for _, file := range files {
|
||||||
|
fullPath := filepath.Join(repoPath, file)
|
||||||
|
if _, err := os.Stat(fullPath); err == nil {
|
||||||
|
return runtime, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePlan generates a Nixpacks build plan
|
||||||
|
func (n *NixpacksBuilder) GeneratePlan(ctx context.Context, repoPath string, config *NixpacksConfig) (*NixpacksPlan, error) {
|
||||||
|
runtime, err := n.DetectRuntime(ctx, repoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to detect runtime: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := &NixpacksPlan{}
|
||||||
|
|
||||||
|
switch runtime {
|
||||||
|
case "go":
|
||||||
|
plan = n.generateGoPlan(config)
|
||||||
|
case "node":
|
||||||
|
plan = n.generateNodePlan(config)
|
||||||
|
case "python":
|
||||||
|
plan = n.generatePythonPlan(config)
|
||||||
|
case "rust":
|
||||||
|
plan = n.generateRustPlan(config)
|
||||||
|
case "ruby":
|
||||||
|
plan = n.generateRubyPlan(config)
|
||||||
|
case "php":
|
||||||
|
plan = n.generatePHPPlan(config)
|
||||||
|
case "deno":
|
||||||
|
plan = n.generateDenoPlan(config)
|
||||||
|
case "bun":
|
||||||
|
plan = n.generateBunPlan(config)
|
||||||
|
case "static":
|
||||||
|
plan = n.generateStaticPlan(config)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported runtime: %s", runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixpacksBuilder) generateGoPlan(config *NixpacksConfig) *NixpacksPlan {
|
||||||
|
plan := &NixpacksPlan{}
|
||||||
|
|
||||||
|
// Base packages
|
||||||
|
plan.Pkgs = []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
{Name: "go", Version: "1.21", Path: "pkgs.go"},
|
||||||
|
{Name: "cacert", Version: "latest", Path: "pkgs.cacert"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build configuration
|
||||||
|
if config.BuildCmd != "" {
|
||||||
|
plan.Build.Builder = "custom"
|
||||||
|
plan.Build.Args = strings.Fields(config.BuildCmd)
|
||||||
|
} else {
|
||||||
|
plan.Build.Builder = "go"
|
||||||
|
plan.Build.Args = []string{"build", "-o", "app", "."}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start configuration
|
||||||
|
if config.StartCmd != "" {
|
||||||
|
plan.Start.Cmd = strings.Fields(config.StartCmd)
|
||||||
|
} else {
|
||||||
|
plan.Start.Cmd = []string{"./app"}
|
||||||
|
}
|
||||||
|
plan.Start.Dir = "/app"
|
||||||
|
|
||||||
|
// Environment
|
||||||
|
plan.Start.Env = map[string]string{
|
||||||
|
"GOPATH": "/go",
|
||||||
|
"GOCACHE": "/tmp/go-cache",
|
||||||
|
"GOMODCACHE": "/tmp/go-mod-cache",
|
||||||
|
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom environment
|
||||||
|
for k, v := range config.Env {
|
||||||
|
plan.Start.Env[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixpacksBuilder) generateNodePlan(config *NixpacksConfig) *NixpacksPlan {
|
||||||
|
plan := &NixpacksPlan{}
|
||||||
|
|
||||||
|
plan.Pkgs = []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
{Name: "nodejs", Version: "20", Path: "pkgs.nodejs"},
|
||||||
|
{Name: "cacert", Version: "latest", Path: "pkgs.cacert"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BuildCmd != "" {
|
||||||
|
plan.Build.Builder = "custom"
|
||||||
|
plan.Build.Args = strings.Fields(config.BuildCmd)
|
||||||
|
} else {
|
||||||
|
plan.Build.Builder = "npm"
|
||||||
|
plan.Build.Args = []string{"run", "build"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.StartCmd != "" {
|
||||||
|
plan.Start.Cmd = strings.Fields(config.StartCmd)
|
||||||
|
} else {
|
||||||
|
plan.Start.Cmd = []string{"npm", "start"}
|
||||||
|
}
|
||||||
|
plan.Start.Dir = "/app"
|
||||||
|
|
||||||
|
plan.Start.Env = map[string]string{
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range config.Env {
|
||||||
|
plan.Start.Env[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixpacksBuilder) generatePythonPlan(config *NixpacksConfig) *NixpacksPlan {
|
||||||
|
plan := &NixpacksPlan{}
|
||||||
|
|
||||||
|
plan.Pkgs = []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
{Name: "python3", Version: "3.11", Path: "pkgs.python3"},
|
||||||
|
{Name: "cacert", Version: "latest", Path: "pkgs.cacert"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BuildCmd != "" {
|
||||||
|
plan.Build.Builder = "custom"
|
||||||
|
plan.Build.Args = strings.Fields(config.BuildCmd)
|
||||||
|
} else {
|
||||||
|
plan.Build.Builder = "pip"
|
||||||
|
plan.Build.Args = []string{"install", "-r", "requirements.txt"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.StartCmd != "" {
|
||||||
|
plan.Start.Cmd = strings.Fields(config.StartCmd)
|
||||||
|
} else {
|
||||||
|
plan.Start.Cmd = []string{"python", "app.py"}
|
||||||
|
}
|
||||||
|
plan.Start.Dir = "/app"
|
||||||
|
|
||||||
|
plan.Start.Env = map[string]string{
|
||||||
|
"PYTHONPATH": "/app",
|
||||||
|
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range config.Env {
|
||||||
|
plan.Start.Env[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixpacksBuilder) generateRustPlan(config *NixpacksConfig) *NixpacksPlan {
|
||||||
|
plan := &NixpacksPlan{}
|
||||||
|
|
||||||
|
plan.Pkgs = []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
{Name: "rustc", Version: "latest", Path: "pkgs.rustc"},
|
||||||
|
{Name: "cargo", Version: "latest", Path: "pkgs.cargo"},
|
||||||
|
{Name: "cacert", Version: "latest", Path: "pkgs.cacert"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BuildCmd != "" {
|
||||||
|
plan.Build.Builder = "custom"
|
||||||
|
plan.Build.Args = strings.Fields(config.BuildCmd)
|
||||||
|
} else {
|
||||||
|
plan.Build.Builder = "cargo"
|
||||||
|
plan.Build.Args = []string{"build", "--release"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.StartCmd != "" {
|
||||||
|
plan.Start.Cmd = strings.Fields(config.StartCmd)
|
||||||
|
} else {
|
||||||
|
plan.Start.Cmd = []string{"./target/release/app"}
|
||||||
|
}
|
||||||
|
plan.Start.Dir = "/app"
|
||||||
|
|
||||||
|
plan.Start.Env = map[string]string{
|
||||||
|
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range config.Env {
|
||||||
|
plan.Start.Env[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixpacksBuilder) generateRubyPlan(config *NixpacksConfig) *NixpacksPlan {
|
||||||
|
plan := &NixpacksPlan{}
|
||||||
|
|
||||||
|
plan.Pkgs = []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
{Name: "ruby", Version: "3.2", Path: "pkgs.ruby"},
|
||||||
|
{Name: "bundler", Version: "latest", Path: "pkgs.bundler"},
|
||||||
|
{Name: "cacert", Version: "latest", Path: "pkgs.cacert"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BuildCmd != "" {
|
||||||
|
plan.Build.Builder = "custom"
|
||||||
|
plan.Build.Args = strings.Fields(config.BuildCmd)
|
||||||
|
} else {
|
||||||
|
plan.Build.Builder = "bundler"
|
||||||
|
plan.Build.Args = []string{"install"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.StartCmd != "" {
|
||||||
|
plan.Start.Cmd = strings.Fields(config.StartCmd)
|
||||||
|
} else {
|
||||||
|
plan.Start.Cmd = []string{"bundle", "exec", "ruby", "app.rb"}
|
||||||
|
}
|
||||||
|
plan.Start.Dir = "/app"
|
||||||
|
|
||||||
|
plan.Start.Env = map[string]string{
|
||||||
|
"GEM_HOME": "/app/vendor/bundle/ruby/3.2.0",
|
||||||
|
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range config.Env {
|
||||||
|
plan.Start.Env[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixpacksBuilder) generatePHPPlan(config *NixpacksConfig) *NixpacksPlan {
|
||||||
|
plan := &NixpacksPlan{}
|
||||||
|
|
||||||
|
plan.Pkgs = []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
{Name: "php", Version: "8.2", Path: "pkgs.php"},
|
||||||
|
{Name: "composer", Version: "latest", Path: "pkgs.composer"},
|
||||||
|
{Name: "cacert", Version: "latest", Path: "pkgs.cacert"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BuildCmd != "" {
|
||||||
|
plan.Build.Builder = "custom"
|
||||||
|
plan.Build.Args = strings.Fields(config.BuildCmd)
|
||||||
|
} else {
|
||||||
|
plan.Build.Builder = "composer"
|
||||||
|
plan.Build.Args = []string{"install"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.StartCmd != "" {
|
||||||
|
plan.Start.Cmd = strings.Fields(config.StartCmd)
|
||||||
|
} else {
|
||||||
|
plan.Start.Cmd = []string{"php", "-S", "0.0.0.0:8080", "-t", "public"}
|
||||||
|
}
|
||||||
|
plan.Start.Dir = "/app"
|
||||||
|
|
||||||
|
plan.Start.Env = map[string]string{
|
||||||
|
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range config.Env {
|
||||||
|
plan.Start.Env[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixpacksBuilder) generateDenoPlan(config *NixpacksConfig) *NixpacksPlan {
|
||||||
|
plan := &NixpacksPlan{}
|
||||||
|
|
||||||
|
plan.Pkgs = []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
{Name: "deno", Version: "latest", Path: "pkgs.deno"},
|
||||||
|
{Name: "cacert", Version: "latest", Path: "pkgs.cacert"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BuildCmd != "" {
|
||||||
|
plan.Build.Builder = "custom"
|
||||||
|
plan.Build.Args = strings.Fields(config.BuildCmd)
|
||||||
|
} else {
|
||||||
|
plan.Build.Builder = "deno"
|
||||||
|
plan.Build.Args = []string{"cache"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.StartCmd != "" {
|
||||||
|
plan.Start.Cmd = strings.Fields(config.StartCmd)
|
||||||
|
} else {
|
||||||
|
plan.Start.Cmd = []string{"deno", "task", "start"}
|
||||||
|
}
|
||||||
|
plan.Start.Dir = "/app"
|
||||||
|
|
||||||
|
plan.Start.Env = map[string]string{
|
||||||
|
"DENO_DIR": "/deno-dir",
|
||||||
|
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range config.Env {
|
||||||
|
plan.Start.Env[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixpacksBuilder) generateBunPlan(config *NixpacksConfig) *NixpacksPlan {
|
||||||
|
plan := &NixpacksPlan{}
|
||||||
|
|
||||||
|
plan.Pkgs = []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
{Name: "bun", Version: "latest", Path: "pkgs.bun"},
|
||||||
|
{Name: "cacert", Version: "latest", Path: "pkgs.cacert"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BuildCmd != "" {
|
||||||
|
plan.Build.Builder = "custom"
|
||||||
|
plan.Build.Args = strings.Fields(config.BuildCmd)
|
||||||
|
} else {
|
||||||
|
plan.Build.Builder = "bun"
|
||||||
|
plan.Build.Args = []string{"install"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.StartCmd != "" {
|
||||||
|
plan.Start.Cmd = strings.Fields(config.StartCmd)
|
||||||
|
} else {
|
||||||
|
plan.Start.Cmd = []string{"bun", "start"}
|
||||||
|
}
|
||||||
|
plan.Start.Dir = "/app"
|
||||||
|
|
||||||
|
plan.Start.Env = map[string]string{
|
||||||
|
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range config.Env {
|
||||||
|
plan.Start.Env[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixpacksBuilder) generateStaticPlan(config *NixpacksConfig) *NixpacksPlan {
|
||||||
|
plan := &NixpacksPlan{}
|
||||||
|
|
||||||
|
plan.Pkgs = []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}{
|
||||||
|
{Name: "nginx", Version: "latest", Path: "pkgs.nginx"},
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.Build.Builder = "static"
|
||||||
|
plan.Build.Args = []string{}
|
||||||
|
|
||||||
|
if config.StartCmd != "" {
|
||||||
|
plan.Start.Cmd = strings.Fields(config.StartCmd)
|
||||||
|
} else {
|
||||||
|
plan.Start.Cmd = []string{"nginx", "-g", "daemon off;"}
|
||||||
|
}
|
||||||
|
plan.Start.Dir = "/usr/share/nginx/html"
|
||||||
|
|
||||||
|
plan.Start.Env = map[string]string{}
|
||||||
|
|
||||||
|
for k, v := range config.Env {
|
||||||
|
plan.Start.Env[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build builds the container image using Nixpacks
|
||||||
|
func (n *NixpacksBuilder) Build(ctx context.Context, req *types.BuildRequest) (*types.BuildResponse, error) {
|
||||||
|
// Generate build plan
|
||||||
|
config := &NixpacksConfig{
|
||||||
|
BuildCmd: req.BuildCommand,
|
||||||
|
StartCmd: req.StartCommand,
|
||||||
|
Env: req.Environment,
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := n.GeneratePlan(ctx, req.SourcePath, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate build plan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temporary Dockerfile
|
||||||
|
dockerfile, err := n.generateDockerfile(plan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(dockerfile)
|
||||||
|
|
||||||
|
// Build Docker image
|
||||||
|
imageName := fmt.Sprintf("%s:%s", req.ImageName, req.ImageTag)
|
||||||
|
|
||||||
|
// Create build context tar
|
||||||
|
buildCtx, err := createBuildContext(req.SourcePath, dockerfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create build context: %w", err)
|
||||||
|
}
|
||||||
|
defer buildCtx.Close()
|
||||||
|
|
||||||
|
buildOptions := docker.BuildOptions{
|
||||||
|
Dockerfile: "Dockerfile",
|
||||||
|
Tags: []string{imageName},
|
||||||
|
BuildArgs: map[string]*string{
|
||||||
|
"BUILDKIT_INLINE_CACHE": strPtr("1"),
|
||||||
|
},
|
||||||
|
Remove: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = n.dockerClient.BuildImage(ctx, buildCtx, buildOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build Docker image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push to registry if specified
|
||||||
|
if req.RegistryURL != "" {
|
||||||
|
err = n.dockerClient.PushImage(ctx, imageName, req.RegistryURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to push image: %w", err)
|
||||||
|
}
|
||||||
|
imageName = fmt.Sprintf("%s/%s:%s", req.RegistryURL, req.ImageName, req.ImageTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.BuildResponse{
|
||||||
|
ImageName: imageName,
|
||||||
|
ImageTag: req.ImageTag,
|
||||||
|
Size: 0, // TODO: Get actual image size
|
||||||
|
Digest: "", // TODO: Get image digest
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NixpacksBuilder) generateDockerfile(plan *NixpacksPlan) (string, error) {
|
||||||
|
dockerfile := filepath.Join(n.workDir, "Dockerfile")
|
||||||
|
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
// Base image
|
||||||
|
content.WriteString("FROM nixpkgs/nix:latest\n\n")
|
||||||
|
|
||||||
|
// Install packages
|
||||||
|
content.WriteString("# Install packages\n")
|
||||||
|
for _, pkg := range plan.Pkgs {
|
||||||
|
content.WriteString(fmt.Sprintf("RUN nix-env -iA nixpkgs.%s\n", pkg.Name))
|
||||||
|
}
|
||||||
|
content.WriteString("\n")
|
||||||
|
|
||||||
|
// Set working directory
|
||||||
|
content.WriteString("WORKDIR /app\n\n")
|
||||||
|
|
||||||
|
// Copy source code
|
||||||
|
content.WriteString("# Copy source code\n")
|
||||||
|
content.WriteString("COPY . .\n\n")
|
||||||
|
|
||||||
|
// Build step
|
||||||
|
if len(plan.Build.Args) > 0 {
|
||||||
|
content.WriteString("# Build application\n")
|
||||||
|
content.WriteString("RUN " + strings.Join(plan.Build.Args, " ") + "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
if len(plan.Start.Env) > 0 {
|
||||||
|
content.WriteString("# Set environment variables\n")
|
||||||
|
for k, v := range plan.Start.Env {
|
||||||
|
content.WriteString(fmt.Sprintf("ENV %s=%s\n", k, v))
|
||||||
|
}
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start command
|
||||||
|
if len(plan.Start.Cmd) > 0 {
|
||||||
|
content.WriteString("# Start application\n")
|
||||||
|
content.WriteString("CMD [" + strings.Join(plan.Start.Cmd, ", ") + "]\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.WriteFile(dockerfile, []byte(content.String()), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerfile, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"containr/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParallelBuilder handles parallel build execution
|
||||||
|
type ParallelBuilder struct {
|
||||||
|
buildManager *BuildManager
|
||||||
|
cacheManager *CacheManager
|
||||||
|
maxWorkers int
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildJob represents a build job in the queue
|
||||||
|
type BuildJob struct {
|
||||||
|
ID string
|
||||||
|
Request *types.BuildRequest
|
||||||
|
Result chan *BuildResult
|
||||||
|
Priority int
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildResult represents the result of a build job
|
||||||
|
type BuildResult struct {
|
||||||
|
Success bool
|
||||||
|
Response *types.BuildResponse
|
||||||
|
Error error
|
||||||
|
Duration time.Duration
|
||||||
|
CacheHit bool
|
||||||
|
BuildID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkerPool manages a pool of build workers
|
||||||
|
type WorkerPool struct {
|
||||||
|
jobs chan *BuildJob
|
||||||
|
workers []*Worker
|
||||||
|
wg sync.WaitGroup
|
||||||
|
quit chan bool
|
||||||
|
maxWorkers int
|
||||||
|
buildManager *BuildManager
|
||||||
|
cacheManager *CacheManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker represents a build worker
|
||||||
|
type Worker struct {
|
||||||
|
id int
|
||||||
|
jobChan chan *BuildJob
|
||||||
|
quit chan bool
|
||||||
|
buildManager *BuildManager
|
||||||
|
cacheManager *CacheManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParallelBuilder creates a new parallel build manager
|
||||||
|
func NewParallelBuilder(buildManager *BuildManager, cacheManager *CacheManager, maxWorkers int) *ParallelBuilder {
|
||||||
|
return &ParallelBuilder{
|
||||||
|
buildManager: buildManager,
|
||||||
|
cacheManager: cacheManager,
|
||||||
|
maxWorkers: maxWorkers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildWithCache attempts to build using cache first
|
||||||
|
func (pb *ParallelBuilder) BuildWithCache(ctx context.Context, req *types.BuildRequest) (*BuildResult, error) {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
// Generate cache key
|
||||||
|
cacheKey, err := pb.cacheManager.GenerateCacheKey(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return &BuildResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("failed to generate cache key: %w", err),
|
||||||
|
Duration: time.Since(startTime),
|
||||||
|
CacheHit: false,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find cached result
|
||||||
|
if cachedEntry, err := pb.cacheManager.LookupCache(ctx, cacheKey); err == nil {
|
||||||
|
return &BuildResult{
|
||||||
|
Success: true,
|
||||||
|
Response: &types.BuildResponse{
|
||||||
|
ImageName: cachedEntry.ImageName,
|
||||||
|
ImageTag: req.ImageTag,
|
||||||
|
Size: cachedEntry.Size,
|
||||||
|
Digest: cachedEntry.Hash,
|
||||||
|
},
|
||||||
|
Duration: time.Since(startTime),
|
||||||
|
CacheHit: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// No cache hit, proceed with build
|
||||||
|
response, err := pb.buildManager.Build(ctx, req)
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return &BuildResult{
|
||||||
|
Success: false,
|
||||||
|
Error: err,
|
||||||
|
Duration: duration,
|
||||||
|
CacheHit: false,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in cache for future builds
|
||||||
|
metadata := map[string]string{
|
||||||
|
"build_type": req.BuildType,
|
||||||
|
"runtime": cacheKey.Runtime,
|
||||||
|
"build_time": duration.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if storeErr := pb.cacheManager.StoreCache(ctx, cacheKey, response.ImageName, metadata); storeErr != nil {
|
||||||
|
fmt.Printf("Warning: failed to store build cache: %v\n", storeErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BuildResult{
|
||||||
|
Success: true,
|
||||||
|
Response: response,
|
||||||
|
Duration: duration,
|
||||||
|
CacheHit: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWorkerPool creates a new worker pool for parallel builds
|
||||||
|
func NewWorkerPool(maxWorkers int, buildManager *BuildManager, cacheManager *CacheManager) *WorkerPool {
|
||||||
|
return &WorkerPool{
|
||||||
|
jobs: make(chan *BuildJob, 100), // Buffered channel
|
||||||
|
workers: make([]*Worker, maxWorkers),
|
||||||
|
quit: make(chan bool),
|
||||||
|
maxWorkers: maxWorkers,
|
||||||
|
buildManager: buildManager,
|
||||||
|
cacheManager: cacheManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the worker pool
|
||||||
|
func (wp *WorkerPool) Start() {
|
||||||
|
for i := 0; i < wp.maxWorkers; i++ {
|
||||||
|
worker := &Worker{
|
||||||
|
id: i,
|
||||||
|
jobChan: wp.jobs,
|
||||||
|
quit: wp.quit,
|
||||||
|
buildManager: wp.buildManager,
|
||||||
|
cacheManager: wp.cacheManager,
|
||||||
|
}
|
||||||
|
wp.workers[i] = worker
|
||||||
|
wp.wg.Add(1)
|
||||||
|
go worker.start(&wp.wg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the worker pool
|
||||||
|
func (wp *WorkerPool) Stop() {
|
||||||
|
close(wp.quit)
|
||||||
|
wp.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitJob submits a build job to the worker pool
|
||||||
|
func (wp *WorkerPool) SubmitJob(ctx context.Context, job *BuildJob) error {
|
||||||
|
select {
|
||||||
|
case wp.jobs <- job:
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildParallel executes builds in parallel using the worker pool
|
||||||
|
func (pb *ParallelBuilder) BuildParallel(ctx context.Context, requests []*types.BuildRequest) ([]*BuildResult, error) {
|
||||||
|
if len(requests) == 0 {
|
||||||
|
return nil, fmt.Errorf("no build requests provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create worker pool
|
||||||
|
workerPool := NewWorkerPool(pb.maxWorkers, pb.buildManager, pb.cacheManager)
|
||||||
|
workerPool.Start()
|
||||||
|
defer workerPool.Stop()
|
||||||
|
|
||||||
|
// Create jobs
|
||||||
|
jobs := make([]*BuildJob, len(requests))
|
||||||
|
results := make([]*BuildResult, len(requests))
|
||||||
|
|
||||||
|
for i, req := range requests {
|
||||||
|
job := &BuildJob{
|
||||||
|
ID: fmt.Sprintf("build-%d-%d", time.Now().UnixNano(), i),
|
||||||
|
Request: req,
|
||||||
|
Result: make(chan *BuildResult, 1),
|
||||||
|
Priority: 0,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
jobs[i] = job
|
||||||
|
|
||||||
|
// Submit job
|
||||||
|
if err := workerPool.SubmitJob(ctx, job); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to submit job %s: %w", job.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect results
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i, job := range jobs {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(index int, buildJob *BuildJob) {
|
||||||
|
defer wg.Done()
|
||||||
|
select {
|
||||||
|
case result := <-buildJob.Result:
|
||||||
|
results[index] = result
|
||||||
|
case <-ctx.Done():
|
||||||
|
results[index] = &BuildResult{
|
||||||
|
Success: false,
|
||||||
|
Error: ctx.Err(),
|
||||||
|
BuildID: buildJob.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(i, job)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildBatch executes a batch of builds with optimized scheduling
|
||||||
|
func (pb *ParallelBuilder) BuildBatch(ctx context.Context, requests []*types.BuildRequest) (*BatchBuildResult, error) {
|
||||||
|
if len(requests) == 0 {
|
||||||
|
return nil, fmt.Errorf("no build requests provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
// Group builds by cache key potential for optimization
|
||||||
|
cacheGroups := pb.groupByCachePotential(ctx, requests)
|
||||||
|
|
||||||
|
// Execute builds in optimized order
|
||||||
|
var allResults []*BuildResult
|
||||||
|
var totalCacheHits int
|
||||||
|
|
||||||
|
for _, group := range cacheGroups {
|
||||||
|
groupResults, err := pb.BuildParallel(ctx, group.Requests)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allResults = append(allResults, groupResults...)
|
||||||
|
|
||||||
|
// Count cache hits
|
||||||
|
for _, result := range groupResults {
|
||||||
|
if result.CacheHit {
|
||||||
|
totalCacheHits++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map results back to original order
|
||||||
|
resultMap := make(map[string]*BuildResult)
|
||||||
|
for _, result := range allResults {
|
||||||
|
if result.Response != nil {
|
||||||
|
resultMap[result.Response.ImageName] = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create final results array in original order
|
||||||
|
finalResults := make([]*BuildResult, len(requests))
|
||||||
|
for i, req := range requests {
|
||||||
|
imageName := fmt.Sprintf("%s:%s", req.ImageName, req.ImageTag)
|
||||||
|
if result, exists := resultMap[imageName]; exists {
|
||||||
|
finalResults[i] = result
|
||||||
|
} else {
|
||||||
|
finalResults[i] = &BuildResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("build result not found for %s", imageName),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BatchBuildResult{
|
||||||
|
Results: finalResults,
|
||||||
|
TotalBuilds: len(requests),
|
||||||
|
Successful: pb.countSuccessful(finalResults),
|
||||||
|
Failed: pb.countFailed(finalResults),
|
||||||
|
CacheHits: totalCacheHits,
|
||||||
|
TotalDuration: time.Since(startTime),
|
||||||
|
AverageTime: time.Since(startTime) / time.Duration(len(requests)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchBuildResult represents the result of a batch build
|
||||||
|
type BatchBuildResult struct {
|
||||||
|
Results []*BuildResult
|
||||||
|
TotalBuilds int
|
||||||
|
Successful int
|
||||||
|
Failed int
|
||||||
|
CacheHits int
|
||||||
|
TotalDuration time.Duration
|
||||||
|
AverageTime time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheGroup represents a group of builds with similar cache potential
|
||||||
|
type CacheGroup struct {
|
||||||
|
Name string
|
||||||
|
Requests []*types.BuildRequest
|
||||||
|
Priority int
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupByCachePotential groups builds by their cache optimization potential
|
||||||
|
func (pb *ParallelBuilder) groupByCachePotential(ctx context.Context, requests []*types.BuildRequest) []CacheGroup {
|
||||||
|
groups := make(map[string][]*types.BuildRequest)
|
||||||
|
|
||||||
|
for _, req := range requests {
|
||||||
|
cacheKey, err := pb.cacheManager.GenerateCacheKey(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
// Put in uncached group if we can't generate a key
|
||||||
|
groups["uncached"] = append(groups["uncached"], req)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by runtime and similar characteristics
|
||||||
|
groupKey := fmt.Sprintf("%s-%s", cacheKey.Runtime, pb.hashBuildArgs(cacheKey.BuildArgs))
|
||||||
|
groups[groupKey] = append(groups[groupKey], req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to sorted groups with priorities
|
||||||
|
var sortedGroups []CacheGroup
|
||||||
|
for name, reqs := range groups {
|
||||||
|
priority := 0
|
||||||
|
if name == "uncached" {
|
||||||
|
priority = 999 // Lowest priority
|
||||||
|
} else if len(reqs) > 1 {
|
||||||
|
priority = 1 // High priority for groups with multiple builds
|
||||||
|
} else {
|
||||||
|
priority = 2 // Medium priority for single builds
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedGroups = append(sortedGroups, CacheGroup{
|
||||||
|
Name: name,
|
||||||
|
Requests: reqs,
|
||||||
|
Priority: priority,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by priority (lower number = higher priority)
|
||||||
|
for i := 0; i < len(sortedGroups); i++ {
|
||||||
|
for j := i + 1; j < len(sortedGroups); j++ {
|
||||||
|
if sortedGroups[i].Priority > sortedGroups[j].Priority {
|
||||||
|
sortedGroups[i], sortedGroups[j] = sortedGroups[j], sortedGroups[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashBuildArgs creates a hash of build arguments for grouping
|
||||||
|
func (pb *ParallelBuilder) hashBuildArgs(args map[string]string) string {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "no-args"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple hash for grouping - in production, use proper hashing
|
||||||
|
result := ""
|
||||||
|
for k, v := range args {
|
||||||
|
result += k + "=" + v + ";"
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb *ParallelBuilder) countSuccessful(results []*BuildResult) int {
|
||||||
|
count := 0
|
||||||
|
for _, result := range results {
|
||||||
|
if result.Success {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb *ParallelBuilder) countFailed(results []*BuildResult) int {
|
||||||
|
count := 0
|
||||||
|
for _, result := range results {
|
||||||
|
if !result.Success {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker methods
|
||||||
|
|
||||||
|
func (w *Worker) start(wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case job := <-w.jobChan:
|
||||||
|
// Process the job
|
||||||
|
result := w.processJob(job)
|
||||||
|
job.Result <- result
|
||||||
|
case <-w.quit:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) processJob(job *BuildJob) *BuildResult {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Use the parallel builder's cache-aware build method
|
||||||
|
pb := &ParallelBuilder{
|
||||||
|
buildManager: w.buildManager,
|
||||||
|
cacheManager: w.cacheManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := pb.BuildWithCache(ctx, job.Request)
|
||||||
|
if err != nil {
|
||||||
|
return &BuildResult{
|
||||||
|
Success: false,
|
||||||
|
Error: err,
|
||||||
|
BuildID: job.ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.BuildID = job.ID
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"containr/internal/docker"
|
||||||
|
"containr/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RailpackBuilder struct {
|
||||||
|
workDir string
|
||||||
|
dockerClient *docker.Client
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type RailpackConfig struct {
|
||||||
|
BuildCmd string `json:"buildCmd,omitempty"`
|
||||||
|
StartCmd string `json:"startCmd,omitempty"`
|
||||||
|
Env map[string]string `json:"env,omitempty"`
|
||||||
|
Root string `json:"root,omitempty"`
|
||||||
|
Planner string `json:"planner,omitempty"`
|
||||||
|
Variables map[string]string `json:"variables,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RailpackBuildRequest struct {
|
||||||
|
Config RailpackConfig `json:"config"`
|
||||||
|
SourceDir string `json:"sourceDir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RailpackBuildResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Dockerfile string `json:"dockerfile"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RailpackPlan struct {
|
||||||
|
Planner string `json:"planner"`
|
||||||
|
BuildCmd string `json:"buildCmd"`
|
||||||
|
StartCmd string `json:"startCmd"`
|
||||||
|
Env map[string]string `json:"env"`
|
||||||
|
Assets map[string]interface{} `json:"assets"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRailpackBuilder(workDir string, dockerClient *docker.Client) *RailpackBuilder {
|
||||||
|
return &RailpackBuilder{
|
||||||
|
workDir: workDir,
|
||||||
|
dockerClient: dockerClient,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 30 * time.Minute,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectRailpack checks if the project can be built with Railpack
|
||||||
|
func (rb *RailpackBuilder) DetectRailpack(ctx context.Context, sourcePath string) error {
|
||||||
|
// Check for common web framework files that Railpack supports
|
||||||
|
detectionFiles := []string{
|
||||||
|
"package.json", // Node.js
|
||||||
|
"requirements.txt", // Python
|
||||||
|
"go.mod", // Go
|
||||||
|
"Cargo.toml", // Rust
|
||||||
|
"pom.xml", // Java/Maven
|
||||||
|
"build.gradle", // Java/Gradle
|
||||||
|
"Gemfile", // Ruby
|
||||||
|
"composer.json", // PHP
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range detectionFiles {
|
||||||
|
if _, err := os.Stat(filepath.Join(sourcePath, file)); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no supported framework detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePlan creates a build plan using Railpack
|
||||||
|
func (rb *RailpackBuilder) GeneratePlan(ctx context.Context, sourcePath string, config *RailpackConfig) (*RailpackPlan, error) {
|
||||||
|
// Call Railpack API to generate build plan
|
||||||
|
planURL := "https://api.railpack.app/v1/plan"
|
||||||
|
|
||||||
|
req := RailpackBuildRequest{
|
||||||
|
Config: *config,
|
||||||
|
SourceDir: sourcePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
planResp, err := rb.makeRailpackRequest(ctx, "POST", planURL, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate Railpack plan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var plan RailpackPlan
|
||||||
|
if err := json.Unmarshal(planResp, &plan); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse Railpack plan: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build generates a Dockerfile using Railpack and builds the image
|
||||||
|
func (rb *RailpackBuilder) Build(ctx context.Context, req *types.BuildRequest) (*types.BuildResponse, error) {
|
||||||
|
// Detect if Railpack can build this project
|
||||||
|
if err := rb.DetectRailpack(ctx, req.SourcePath); err != nil {
|
||||||
|
return nil, fmt.Errorf("Railpack cannot build this project: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Railpack config
|
||||||
|
config := RailpackConfig{
|
||||||
|
BuildCmd: req.BuildCommand,
|
||||||
|
StartCmd: req.StartCommand,
|
||||||
|
Env: req.Environment,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Dockerfile using Railpack
|
||||||
|
dockerfile, err := rb.generateDockerfile(ctx, req.SourcePath, &config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate Dockerfile with Railpack: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Dockerfile to temporary location
|
||||||
|
dockerfilePath := filepath.Join(rb.workDir, "Dockerfile")
|
||||||
|
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the Docker image
|
||||||
|
imageName := fmt.Sprintf("%s:%s", req.ImageName, req.ImageTag)
|
||||||
|
|
||||||
|
buildCtx := req.SourcePath
|
||||||
|
|
||||||
|
buildArgs := make(map[string]*string)
|
||||||
|
for k, v := range req.Environment {
|
||||||
|
val := v
|
||||||
|
buildArgs[k] = &val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create build context from directory
|
||||||
|
buildContext, err := rb.createBuildContext(buildCtx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create build context: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(buildContext.Name())
|
||||||
|
|
||||||
|
buildOptions := docker.BuildOptions{
|
||||||
|
Dockerfile: "Dockerfile",
|
||||||
|
Tags: []string{imageName},
|
||||||
|
BuildArgs: buildArgs,
|
||||||
|
Labels: req.Labels,
|
||||||
|
Remove: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rb.dockerClient.BuildImage(ctx, buildContext, buildOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image info
|
||||||
|
imageInfo, err := rb.dockerClient.GetImageInfo(ctx, imageName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get image info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push to registry if specified
|
||||||
|
if req.RegistryURL != "" {
|
||||||
|
err = rb.dockerClient.PushImage(ctx, imageName, req.RegistryURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to push image: %w", err)
|
||||||
|
}
|
||||||
|
imageName = fmt.Sprintf("%s/%s", req.RegistryURL, imageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.BuildResponse{
|
||||||
|
ImageName: imageName,
|
||||||
|
ImageTag: req.ImageTag,
|
||||||
|
Size: imageInfo.Size,
|
||||||
|
Digest: imageInfo.Digest,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDockerfile calls Railpack API to generate optimized Dockerfile
|
||||||
|
func (rb *RailpackBuilder) generateDockerfile(ctx context.Context, sourcePath string, config *RailpackConfig) (string, error) {
|
||||||
|
// Use Railpack API to generate Dockerfile
|
||||||
|
generateURL := "https://api.railpack.app/v1/generate"
|
||||||
|
|
||||||
|
req := RailpackBuildRequest{
|
||||||
|
Config: *config,
|
||||||
|
SourceDir: sourcePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := rb.makeRailpackRequest(ctx, "POST", generateURL, req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildResp RailpackBuildResponse
|
||||||
|
if err := json.Unmarshal(resp, &buildResp); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse Railpack response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !buildResp.Success {
|
||||||
|
return "", fmt.Errorf("Railpack generation failed: %s", buildResp.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildResp.Dockerfile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeRailpackRequest makes HTTP requests to Railpack API
|
||||||
|
func (rb *RailpackBuilder) makeRailpackRequest(ctx context.Context, method, url string, body interface{}) ([]byte, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
|
||||||
|
if body != nil {
|
||||||
|
bodyBytes, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||||
|
}
|
||||||
|
bodyReader = strings.NewReader(string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("User-Agent", "containr/1.0")
|
||||||
|
|
||||||
|
resp, err := rb.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, fmt.Errorf("Railpack API error: %d - %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
return respBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSupportedFrameworks returns a list of frameworks supported by Railpack
|
||||||
|
func (rb *RailpackBuilder) GetSupportedFrameworks() []string {
|
||||||
|
return []string{
|
||||||
|
"node.js",
|
||||||
|
"python",
|
||||||
|
"go",
|
||||||
|
"rust",
|
||||||
|
"java",
|
||||||
|
"ruby",
|
||||||
|
"php",
|
||||||
|
"static",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createBuildContext creates a tar archive from the build context directory
|
||||||
|
func (rb *RailpackBuilder) createBuildContext(sourcePath string) (*os.File, error) {
|
||||||
|
// Create a temporary file for the tar archive
|
||||||
|
tmpFile, err := os.CreateTemp("", "build-context-*.tar")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tar archive from source directory
|
||||||
|
cmd := fmt.Sprintf("cd %s && tar -cf %s .", sourcePath, tmpFile.Name())
|
||||||
|
|
||||||
|
// Use tar command to create the build context
|
||||||
|
if err := rb.runCommand(cmd); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tmpFile.Name())
|
||||||
|
return nil, fmt.Errorf("failed to create build context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek back to beginning of file
|
||||||
|
if _, err := tmpFile.Seek(0, 0); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tmpFile.Name())
|
||||||
|
return nil, fmt.Errorf("failed to seek in temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCommand executes a shell command
|
||||||
|
func (rb *RailpackBuilder) runCommand(cmd string) error {
|
||||||
|
parts := strings.Fields(cmd)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return fmt.Errorf("empty command")
|
||||||
|
}
|
||||||
|
|
||||||
|
execCmd := exec.Command(parts[0], parts[1:]...)
|
||||||
|
execCmd.Stdin = os.Stdin
|
||||||
|
execCmd.Stdout = os.Stdout
|
||||||
|
execCmd.Stderr = os.Stderr
|
||||||
|
return execCmd.Run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRailpackBuilder_DetectRailpack(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir, err := os.MkdirTemp("", "railpack-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Test Node.js detection
|
||||||
|
nodeDir := filepath.Join(tempDir, "node-app")
|
||||||
|
if err := os.MkdirAll(nodeDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create node dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create package.json
|
||||||
|
packageJson := `{
|
||||||
|
"name": "test-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
if err := os.WriteFile(filepath.Join(nodeDir, "package.json"), []byte(packageJson), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create package.json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create RailpackBuilder (we'll mock docker client for now)
|
||||||
|
builder := &RailpackBuilder{
|
||||||
|
workDir: tempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test detection
|
||||||
|
err = builder.DetectRailpack(context.Background(), nodeDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected Railpack to detect Node.js app, got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test non-supported directory
|
||||||
|
emptyDir := filepath.Join(tempDir, "empty")
|
||||||
|
if err := os.MkdirAll(emptyDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create empty dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = builder.DetectRailpack(context.Background(), emptyDir)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected Railpack to fail detection on empty directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildManager_DetectBuildType(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir, err := os.MkdirTemp("", "build-manager-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Test Node.js app detection (should prefer Railpack)
|
||||||
|
nodeDir := filepath.Join(tempDir, "node-app")
|
||||||
|
if err := os.MkdirAll(nodeDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create node dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
packageJson := `{"name": "test-app", "version": "1.0.0"}`
|
||||||
|
if err := os.WriteFile(filepath.Join(nodeDir, "package.json"), []byte(packageJson), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create package.json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create build manager (we'll skip docker client for this test)
|
||||||
|
// Note: This would need a mock docker client in a real test
|
||||||
|
t.Skip("BuildManager test requires docker client mock")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRailpackBuilder_GetSupportedFrameworks(t *testing.T) {
|
||||||
|
builder := &RailpackBuilder{}
|
||||||
|
frameworks := builder.GetSupportedFrameworks()
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"node.js",
|
||||||
|
"python",
|
||||||
|
"go",
|
||||||
|
"rust",
|
||||||
|
"java",
|
||||||
|
"ruby",
|
||||||
|
"php",
|
||||||
|
"static",
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(frameworks) != len(expected) {
|
||||||
|
t.Errorf("Expected %d frameworks, got %d", len(expected), len(frameworks))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, framework := range expected {
|
||||||
|
if i >= len(frameworks) || frameworks[i] != framework {
|
||||||
|
t.Errorf("Expected framework %s at index %d, got %s", framework, i, frameworks[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user