mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-04 04:22:57 +00:00
310 lines
8.5 KiB
Go
310 lines
8.5 KiB
Go
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()
|
|
}
|