mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
Git-sourced deploys never materialized a checkout: sourcePath was build_path resolved against the server cwd, so builds consumed the backend's own directory. Remote placement could only run registry-pulled images. - internal/source: per-deployment checkout (init/fetch/checkout) at branch or commit; GIT_ASKPASS keeps tokens out of argv, .git/config, and the build context; dumb-transport and SHA-refusal fallbacks; build_path validated inside the checkout. - BuildManager.PackageContext: produces the same tar context Build ships to the daemon — generated Dockerfile at tar root, .git excluded. Fixes nixpacks passing the Dockerfile path as content. - Agent artifact endpoint: GET /api/agents/artifacts/:id streams build contexts and docker-save tarballs behind agent-token auth; hex-validated ids, per-deployment cleanup, boot-time purge. - Agent commands: build_image (docker build -, --build-arg values via process env, not argv) and load_image (docker load for containr-* rollback tags no registry serves). - Deployment flow: remote git builds package context → artifact → node docker-builds the deployment tag; remote rollbacks of local tags export via SaveImage → artifact → node docker-loads. Verified end-to-end: agent-pinned git service built on the node (build_image → create_container → deployed) and rollback shipped a docker-save tarball (load_image → deployed). Artifact auth returns 401/404 correctly; workspaces and blobs are removed after deploy. Authored By: TDvorak <[email protected]>
317 lines
9.9 KiB
Go
317 lines
9.9 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"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
|
|
staticBuilder *StaticBuilder
|
|
dockerClient *docker.Client
|
|
cacheManager *CacheManager
|
|
parallelBuilder *ParallelBuilder
|
|
workDir string
|
|
}
|
|
|
|
type BuildType string
|
|
|
|
const (
|
|
BuildTypeRailpack BuildType = "railpack"
|
|
BuildTypeNixpacks BuildType = "nixpacks"
|
|
BuildTypeDockerfile BuildType = "dockerfile"
|
|
BuildTypeStatic BuildType = "static"
|
|
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),
|
|
staticBuilder: NewStaticBuilder(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 BuildTypeStatic:
|
|
return bm.staticBuilder.Build(ctx, req)
|
|
case BuildTypePrebuilt:
|
|
return bm.buildPrebuilt(ctx, req)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported build type: %s", req.BuildType)
|
|
}
|
|
}
|
|
|
|
// PackageContext produces the same build-context tar Build would hand the
|
|
// local daemon, without building it. Node agents download the tar and run
|
|
// docker build themselves — the Dockerfile lands at the tar root.
|
|
func (bm *BuildManager) PackageContext(ctx context.Context, req *types.BuildRequest) (io.ReadCloser, error) {
|
|
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)
|
|
}
|
|
|
|
var dockerfileContent string
|
|
switch BuildType(req.BuildType) {
|
|
case BuildTypeDockerfile:
|
|
name, err := bm.dockerfileBuilder.DetectDockerfile(ctx, req.SourcePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to detect Dockerfile: %w", err)
|
|
}
|
|
path := filepath.Join(req.SourcePath, name)
|
|
if optimized, err := bm.dockerfileBuilder.OptimizeDockerfile(ctx, path); err == nil {
|
|
path = optimized
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read Dockerfile: %w", err)
|
|
}
|
|
dockerfileContent = string(data)
|
|
case BuildTypeRailpack:
|
|
if err := bm.railpackBuilder.DetectRailpack(ctx, req.SourcePath); err != nil {
|
|
return nil, fmt.Errorf("Railpack cannot build this project: %w", err)
|
|
}
|
|
content, err := bm.railpackBuilder.generateDockerfile(ctx, req.SourcePath, &RailpackConfig{
|
|
BuildCmd: req.BuildCommand,
|
|
StartCmd: req.StartCommand,
|
|
Env: req.Environment,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate Dockerfile with Railpack: %w", err)
|
|
}
|
|
dockerfileContent = content
|
|
case BuildTypeNixpacks:
|
|
plan, err := bm.nixpacksBuilder.GeneratePlan(ctx, req.SourcePath, &NixpacksConfig{
|
|
BuildCmd: req.BuildCommand,
|
|
StartCmd: req.StartCommand,
|
|
Env: req.Environment,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate build plan: %w", err)
|
|
}
|
|
path, err := bm.nixpacksBuilder.generateDockerfile(plan)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate Dockerfile: %w", err)
|
|
}
|
|
defer os.Remove(path)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read generated Dockerfile: %w", err)
|
|
}
|
|
dockerfileContent = string(data)
|
|
case BuildTypeStatic:
|
|
dockerfileContent = staticDockerfile
|
|
default:
|
|
return nil, fmt.Errorf("unsupported build type for remote packaging: %s", req.BuildType)
|
|
}
|
|
|
|
return createBuildContext(req.SourcePath, dockerfileContent)
|
|
}
|
|
|
|
// 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 — must drain the pull stream or the daemon
|
|
// aborts it and the tag below races a missing image.
|
|
auth := registry.AuthConfig{Username: req.PullUsername, Password: req.PullPassword}
|
|
if err := bm.dockerClient.PullImageWait(ctx, req.PrebuiltImage, auth); 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)
|
|
}
|