Files
Tomas Dvorak 355a97bab4 overhaul
2026-04-14 18:04:48 +02:00

241 lines
7.2 KiB
Go

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)
}