mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-04 20:42:58 +00:00
overhaul
This commit is contained in:
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user