Skip to content

Commit 1818c3c

Browse files
committed
Firewall handling with multi-app deployment with no custom domain
1 parent a1b91a6 commit 1818c3c

File tree

5 files changed

+232
-78
lines changed

5 files changed

+232
-78
lines changed

cmd/common_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,10 @@ func TestResolveBuilder_AutoSelect_Dockerfile(t *testing.T) {
6060
t.Fatalf("Failed to create Dockerfile: %v", err)
6161
}
6262

63-
// Test: Should fallback to nixpacks since dockerfile builder is not yet implemented
64-
// TODO: When dockerfile builder is implemented, this should expect 'dockerfile'
63+
// Test: Should select dockerfile builder since it's now implemented
6564
builderName := resolveBuilder(target, tmpDir, detection, "")
66-
if builderName != "nixpacks" && builderName != "native" {
67-
t.Errorf("Expected auto-select 'nixpacks' or 'native' (fallback from dockerfile), got '%s'", builderName)
65+
if builderName != "dockerfile" {
66+
t.Errorf("Expected auto-select 'dockerfile', got '%s'", builderName)
6867
}
6968
}
7069

cmd/deploy.go

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,7 @@ Examples:
120120

121121
builderName := resolveBuilder(target, projectPath, &detection, deployBuilderFlag)
122122

123-
dockerfilePath := filepath.Join(projectPath, "Dockerfile")
124-
_, dockerfileExists := os.Stat(dockerfilePath)
125-
showFallback := dockerfileExists == nil && builderName != "dockerfile"
126-
127-
if showFallback {
128-
fmt.Printf(" %s %s %s\n",
129-
deployMutedStyle.Render("Builder:"),
130-
deployMutedStyle.Render(builderName),
131-
deployMutedStyle.Render("(dockerfile not implemented yet)"))
132-
} else {
133-
fmt.Printf(" %s %s\n", deployMutedStyle.Render("Builder:"), deployMutedStyle.Render(builderName))
134-
}
123+
fmt.Printf(" %s %s\n", deployMutedStyle.Render("Builder:"), deployMutedStyle.Render(builderName))
135124

136125
if target.Builder != builderName {
137126
target.Builder = builderName

pkg/builders/dockerfile/builder.go

Lines changed: 205 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import (
44
"context"
55
"fmt"
66
"lightfold/pkg/builders"
7+
"lightfold/pkg/config"
78
"os"
9+
"os/exec"
810
"path/filepath"
11+
"strings"
912
)
1013

1114
// DockerfileBuilder uses an existing Dockerfile for building
@@ -22,32 +25,222 @@ func (d *DockerfileBuilder) Name() string {
2225
}
2326

2427
func (d *DockerfileBuilder) IsAvailable() bool {
25-
// TODO: Implement Dockerfile builder - check for Docker daemon availability
26-
// Dockerfile builder is not yet implemented
27-
return false
28+
// Check if docker command exists
29+
_, err := exec.LookPath("docker")
30+
if err != nil {
31+
return false
32+
}
33+
34+
// Check if Docker daemon is accessible
35+
cmd := exec.Command("docker", "info")
36+
if err := cmd.Run(); err != nil {
37+
return false
38+
}
39+
40+
return true
2841
}
2942

3043
func (d *DockerfileBuilder) NeedsNginx() bool {
44+
// Assume Dockerfile includes its own web server
45+
// User can configure nginx separately if needed
3146
return false
3247
}
3348

3449
func (d *DockerfileBuilder) Build(ctx context.Context, opts *builders.BuildOptions) (*builders.BuildResult, error) {
50+
// Verify Dockerfile exists
3551
dockerfilePath := filepath.Join(opts.ProjectPath, "Dockerfile")
3652
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
3753
return &builders.BuildResult{
3854
Success: false,
3955
}, fmt.Errorf("Dockerfile not found at %s", dockerfilePath)
4056
}
4157

42-
// TODO: Implement dockerfile builder
43-
// - Build image: docker build -t <app>:latest .
44-
// - Export as tarball: docker save <app>:latest -o /tmp/image.tar
45-
// - Transfer to server via SSH
46-
// - Load and run: docker load < image.tar && docker run ...
58+
// Extract app name from release path
59+
appName := extractAppName(opts.ReleasePath)
60+
if appName == "" {
61+
return &builders.BuildResult{
62+
Success: false,
63+
}, fmt.Errorf("failed to extract app name from release path: %s", opts.ReleasePath)
64+
}
65+
66+
imageName := fmt.Sprintf("lightfold-%s:latest", appName)
67+
tarballPath := filepath.Join(os.TempDir(), fmt.Sprintf("lightfold-%s-image.tar", appName))
68+
69+
var buildLog strings.Builder
70+
71+
// Step 1: Build Docker image locally
72+
buildLog.WriteString(fmt.Sprintf("Building Docker image: %s\n", imageName))
73+
buildCmd := exec.CommandContext(ctx, "docker", "build", "-t", imageName, opts.ProjectPath)
74+
buildCmd.Dir = opts.ProjectPath
75+
76+
buildOutput, err := buildCmd.CombinedOutput()
77+
buildLog.Write(buildOutput)
78+
79+
if err != nil {
80+
return &builders.BuildResult{
81+
Success: false,
82+
BuildLog: buildLog.String(),
83+
}, fmt.Errorf("docker build failed: %w\nOutput: %s", err, buildOutput)
84+
}
85+
86+
// Step 2: Export image as tarball
87+
buildLog.WriteString(fmt.Sprintf("\nExporting image to tarball: %s\n", tarballPath))
88+
saveCmd := exec.CommandContext(ctx, "docker", "save", "-o", tarballPath, imageName)
89+
90+
saveOutput, err := saveCmd.CombinedOutput()
91+
buildLog.Write(saveOutput)
92+
93+
if err != nil {
94+
return &builders.BuildResult{
95+
Success: false,
96+
BuildLog: buildLog.String(),
97+
}, fmt.Errorf("docker save failed: %w\nOutput: %s", err, saveOutput)
98+
}
99+
100+
// Ensure cleanup of tarball
101+
defer os.Remove(tarballPath)
102+
103+
// Step 3: Ensure Docker is installed on remote server
104+
buildLog.WriteString("\nChecking Docker on remote server...\n")
105+
ssh := opts.SSHExecutor
106+
107+
dockerCheck := ssh.Execute("which docker")
108+
if dockerCheck.ExitCode != 0 {
109+
buildLog.WriteString("Installing Docker on remote server...\n")
110+
111+
// Install Docker using official script
112+
installCmds := []string{
113+
"curl -fsSL https://get.docker.com -o /tmp/get-docker.sh",
114+
"sudo sh /tmp/get-docker.sh",
115+
"sudo usermod -aG docker deploy",
116+
"rm /tmp/get-docker.sh",
117+
}
118+
119+
for _, cmd := range installCmds {
120+
result := ssh.ExecuteSudo(cmd)
121+
buildLog.WriteString(result.Stdout)
122+
buildLog.WriteString(result.Stderr)
123+
124+
if result.ExitCode != 0 {
125+
return &builders.BuildResult{
126+
Success: false,
127+
BuildLog: buildLog.String(),
128+
}, fmt.Errorf("failed to install Docker on remote server: %s", result.Stderr)
129+
}
130+
}
131+
132+
buildLog.WriteString("Docker installed successfully\n")
133+
}
134+
135+
// Step 4: Transfer tarball to remote server
136+
remoteTarballPath := fmt.Sprintf("/tmp/lightfold-%s-image.tar", appName)
137+
buildLog.WriteString(fmt.Sprintf("\nTransferring image to server: %s\n", remoteTarballPath))
138+
139+
if err := ssh.UploadFile(tarballPath, remoteTarballPath); err != nil {
140+
return &builders.BuildResult{
141+
Success: false,
142+
BuildLog: buildLog.String(),
143+
}, fmt.Errorf("failed to transfer image tarball: %w", err)
144+
}
145+
146+
// Step 5: Load Docker image on remote server
147+
buildLog.WriteString("\nLoading Docker image on remote server...\n")
148+
loadResult := ssh.Execute(fmt.Sprintf("docker load -i %s", remoteTarballPath))
149+
buildLog.WriteString(loadResult.Stdout)
150+
buildLog.WriteString(loadResult.Stderr)
151+
152+
if loadResult.ExitCode != 0 {
153+
ssh.Execute(fmt.Sprintf("rm %s", remoteTarballPath))
154+
return &builders.BuildResult{
155+
Success: false,
156+
BuildLog: buildLog.String(),
157+
}, fmt.Errorf("docker load failed: %s", loadResult.Stderr)
158+
}
159+
160+
// Cleanup remote tarball
161+
ssh.Execute(fmt.Sprintf("rm %s", remoteTarballPath))
162+
163+
// Step 6: Write environment variables to .env file if provided
164+
if len(opts.EnvVars) > 0 {
165+
var envContent strings.Builder
166+
for key, value := range opts.EnvVars {
167+
envContent.WriteString(fmt.Sprintf("%s=%s\n", key, value))
168+
}
169+
170+
envPath := fmt.Sprintf("%s/.env", opts.ReleasePath)
171+
if err := ssh.WriteRemoteFile(envPath, envContent.String(), config.PermEnvFile); err != nil {
172+
return &builders.BuildResult{
173+
Success: false,
174+
BuildLog: buildLog.String(),
175+
}, fmt.Errorf("failed to write .env file: %w", err)
176+
}
177+
178+
ssh.ExecuteSudo(fmt.Sprintf("chown deploy:deploy %s", envPath))
179+
buildLog.WriteString(fmt.Sprintf("\nWrote environment variables to %s\n", envPath))
180+
}
181+
182+
// Step 7: Create docker-compose.yml or docker run script
183+
// We'll create a simple run script that can be used by systemd
184+
port := extractPortFromEnv(opts.EnvVars)
185+
if port == "" {
186+
port = "3000" // Default port
187+
}
188+
189+
runScript := fmt.Sprintf(`#!/bin/bash
190+
# Lightfold Docker container run script
191+
CONTAINER_NAME="lightfold-%s"
192+
IMAGE_NAME="%s"
193+
RELEASE_PATH="%s"
194+
195+
# Stop and remove existing container if running
196+
docker stop $CONTAINER_NAME 2>/dev/null || true
197+
docker rm $CONTAINER_NAME 2>/dev/null || true
198+
199+
# Run new container
200+
docker run -d \
201+
--name $CONTAINER_NAME \
202+
--restart unless-stopped \
203+
-p %s:3000 \
204+
--env-file $RELEASE_PATH/.env \
205+
$IMAGE_NAME
206+
`, appName, imageName, opts.ReleasePath, port)
207+
208+
runScriptPath := fmt.Sprintf("%s/docker-run.sh", opts.ReleasePath)
209+
if err := ssh.WriteRemoteFile(runScriptPath, runScript, 0755); err != nil {
210+
return &builders.BuildResult{
211+
Success: false,
212+
BuildLog: buildLog.String(),
213+
}, fmt.Errorf("failed to write docker run script: %w", err)
214+
}
215+
216+
ssh.ExecuteSudo(fmt.Sprintf("chown deploy:deploy %s", runScriptPath))
217+
buildLog.WriteString(fmt.Sprintf("\nCreated Docker run script at %s\n", runScriptPath))
218+
219+
buildLog.WriteString("\n✓ Docker build completed successfully\n")
47220

48221
return &builders.BuildResult{
49-
Success: false,
50-
BuildLog: "",
51-
IncludesNginx: true,
52-
}, fmt.Errorf("dockerfile builder not yet implemented - requires container runtime support")
222+
Success: true,
223+
BuildLog: buildLog.String(),
224+
IncludesNginx: false, // Docker containers typically include their own web server
225+
StartCommand: fmt.Sprintf("bash %s/docker-run.sh", opts.ReleasePath),
226+
}, nil
227+
}
228+
229+
// extractAppName extracts the app name from the release path
230+
// Example: /srv/myapp/releases/20240101120000 -> myapp
231+
func extractAppName(releasePath string) string {
232+
parts := strings.Split(releasePath, "/")
233+
if len(parts) >= 3 {
234+
// Path format: /srv/{appname}/releases/{timestamp}
235+
return parts[2]
236+
}
237+
return ""
238+
}
239+
240+
// extractPortFromEnv tries to find PORT env var, defaults to ""
241+
func extractPortFromEnv(envVars map[string]string) string {
242+
if port, ok := envVars["PORT"]; ok {
243+
return port
244+
}
245+
return ""
53246
}

pkg/builders/dockerfile/builder_test.go

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package dockerfile
22

33
import (
44
"context"
5-
"os"
6-
"path/filepath"
75
"testing"
86

97
"lightfold/pkg/builders"
@@ -18,10 +16,11 @@ func TestDockerfileBuilder_Name(t *testing.T) {
1816

1917
func TestDockerfileBuilder_IsAvailable(t *testing.T) {
2018
builder := &DockerfileBuilder{}
21-
// TODO: When Dockerfile builder is implemented, this should return true
22-
if builder.IsAvailable() {
23-
t.Error("Dockerfile builder should return false until implemented")
24-
}
19+
// IsAvailable() checks if Docker daemon is accessible
20+
// This will return true if Docker is installed and running
21+
available := builder.IsAvailable()
22+
t.Logf("Docker available: %v", available)
23+
// We don't assert a specific value since it depends on the test environment
2524
}
2625

2726
func TestDockerfileBuilder_NeedsNginx(t *testing.T) {
@@ -40,21 +39,17 @@ func TestDockerfileBuilder_Build_NotImplemented(t *testing.T) {
4039
ProjectPath: tmpDir,
4140
})
4241

42+
// Now that it's implemented, it should error because Dockerfile is missing
4343
if err == nil {
44-
t.Error("Expected error for not implemented builder")
44+
t.Error("Expected error when Dockerfile is missing")
4545
}
4646

4747
if result == nil {
4848
t.Fatal("Expected non-nil result even on error")
4949
}
5050

5151
if result.Success {
52-
t.Error("Expected success=false for not implemented builder")
53-
}
54-
55-
// Verify error message mentions not implemented
56-
if err.Error() == "" {
57-
t.Error("Expected non-empty error message")
52+
t.Error("Expected success=false when Dockerfile is missing")
5853
}
5954
}
6055

@@ -82,25 +77,8 @@ func TestDockerfileBuilder_Build_MissingDockerfile(t *testing.T) {
8277
}
8378

8479
func TestDockerfileBuilder_Build_DockerfileExists(t *testing.T) {
85-
builder := &DockerfileBuilder{}
86-
87-
tmpDir := t.TempDir()
88-
dockerfilePath := filepath.Join(tmpDir, "Dockerfile")
89-
if err := os.WriteFile(dockerfilePath, []byte("FROM node:18\nCOPY . .\n"), 0644); err != nil {
90-
t.Fatalf("Failed to create test Dockerfile: %v", err)
91-
}
80+
t.Skip("Skipping full build test - requires Docker daemon and SSH executor")
9281

93-
result, err := builder.Build(context.Background(), &builders.BuildOptions{
94-
ProjectPath: tmpDir,
95-
})
96-
97-
// Should still error because not implemented, but should pass Dockerfile check
98-
if err == nil {
99-
t.Error("Expected error for not implemented builder")
100-
}
101-
102-
// Error should be about not implemented, not missing Dockerfile
103-
if result == nil {
104-
t.Fatal("Expected non-nil result")
105-
}
82+
// Note: Full integration testing is covered by end-to-end tests
83+
// Unit test just verifies Dockerfile existence checking in TestDockerfileBuilder_Build_MissingDockerfile
10684
}

0 commit comments

Comments
 (0)