Skip to content

Commit e96e652

Browse files
authored
Merge pull request #7 from flatrun/feat/improve-autodeployments
feat: improve deployment automation and file management
2 parents 4fceb8e + c99c9fd commit e96e652

8 files changed

Lines changed: 208 additions & 178 deletions

File tree

config.example.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ certbot:
2525
container_name: certbot
2626
email: your-email@example.com
2727
staging: true
28+
# Optional: Override default paths (relative to deployments_path)
29+
# certs_path: /deployments/nginx/certs/live
30+
# webroot_path: /deployments/nginx/html
2831

2932
logging:
3033
level: info

internal/api/server.go

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package api
33
import (
44
"context"
55
"fmt"
6-
"io"
76
"net/http"
87
"os"
98
"path/filepath"
@@ -1270,17 +1269,8 @@ func corsMiddleware(allowedOrigins []string) gin.HandlerFunc {
12701269
func (s *Server) listDeploymentFiles(c *gin.Context) {
12711270
name := c.Param("name")
12721271
path := c.DefaultQuery("path", "/")
1273-
root := c.Query("root") == "true"
1274-
1275-
var filesList []files.FileInfo
1276-
var err error
1277-
1278-
if root {
1279-
filesList, err = s.filesManager.ListAllFiles(name, path)
1280-
} else {
1281-
filesList, err = s.filesManager.ListFiles(name, path)
1282-
}
12831272

1273+
filesList, err := s.filesManager.ListFiles(name, path)
12841274
if err != nil {
12851275
c.JSON(http.StatusInternalServerError, gin.H{
12861276
"error": err.Error(),
@@ -1291,14 +1281,12 @@ func (s *Server) listDeploymentFiles(c *gin.Context) {
12911281
c.JSON(http.StatusOK, gin.H{
12921282
"files": filesList,
12931283
"path": path,
1294-
"root": root,
12951284
})
12961285
}
12971286

12981287
func (s *Server) getDeploymentFile(c *gin.Context) {
12991288
name := c.Param("name")
13001289
path := c.Param("path")
1301-
root := c.Query("root") == "true"
13021290

13031291
if c.Query("info") == "true" {
13041292
info, err := s.filesManager.GetFileInfo(name, path)
@@ -1313,13 +1301,7 @@ func (s *Server) getDeploymentFile(c *gin.Context) {
13131301
}
13141302

13151303
if c.Query("list") == "true" {
1316-
var filesList []files.FileInfo
1317-
var err error
1318-
if root {
1319-
filesList, err = s.filesManager.ListAllFiles(name, path)
1320-
} else {
1321-
filesList, err = s.filesManager.ListFiles(name, path)
1322-
}
1304+
filesList, err := s.filesManager.ListFiles(name, path)
13231305
if err != nil {
13241306
c.JSON(http.StatusInternalServerError, gin.H{
13251307
"error": err.Error(),
@@ -1329,21 +1311,11 @@ func (s *Server) getDeploymentFile(c *gin.Context) {
13291311
c.JSON(http.StatusOK, gin.H{
13301312
"files": filesList,
13311313
"path": path,
1332-
"root": root,
13331314
})
13341315
return
13351316
}
13361317

1337-
var file io.ReadCloser
1338-
var info *files.FileInfo
1339-
var err error
1340-
1341-
if root {
1342-
file, info, err = s.filesManager.ReadAllFile(name, path)
1343-
} else {
1344-
file, info, err = s.filesManager.ReadFile(name, path)
1345-
}
1346-
1318+
file, info, err := s.filesManager.ReadFile(name, path)
13471319
if err != nil {
13481320
c.JSON(http.StatusNotFound, gin.H{
13491321
"error": err.Error(),

internal/docker/compose.go

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package docker
33
import (
44
"bytes"
55
"fmt"
6+
"os"
67
"os/exec"
78
"strings"
9+
10+
"gopkg.in/yaml.v3"
811
)
912

1013
type ComposeExecutor struct {
@@ -58,10 +61,66 @@ func (c *ComposeExecutor) Pull(deploymentPath string) (string, error) {
5861

5962
func (c *ComposeExecutor) getProjectName(deploymentPath string) string {
6063
parts := strings.Split(strings.TrimSuffix(deploymentPath, "/"), "/")
61-
if len(parts) > 0 {
62-
return "flatrun-" + parts[len(parts)-1]
64+
if len(parts) == 0 {
65+
return "flatrun"
66+
}
67+
dirName := parts[len(parts)-1]
68+
69+
// First, try to read name from compose file
70+
if name := c.readComposeProjectName(deploymentPath); name != "" {
71+
return name
72+
}
73+
74+
// Fallback: detect existing project from running containers
75+
if name := c.detectExistingProject(dirName); name != "" {
76+
return name
77+
}
78+
79+
// Default to directory name for compatibility
80+
return dirName
81+
}
82+
83+
// readComposeProjectName reads the 'name:' attribute from the compose file
84+
func (c *ComposeExecutor) readComposeProjectName(deploymentPath string) string {
85+
composeFiles := []string{
86+
"docker-compose.yml",
87+
"docker-compose.yaml",
88+
"compose.yml",
89+
"compose.yaml",
6390
}
64-
return "flatrun"
91+
92+
for _, filename := range composeFiles {
93+
path := deploymentPath + "/" + filename
94+
data, err := os.ReadFile(path)
95+
if err != nil {
96+
continue
97+
}
98+
99+
var compose struct {
100+
Name string `yaml:"name"`
101+
}
102+
if err := yaml.Unmarshal(data, &compose); err == nil && compose.Name != "" {
103+
return compose.Name
104+
}
105+
}
106+
return ""
107+
}
108+
109+
// detectExistingProject checks if containers exist with common project name patterns
110+
func (c *ComposeExecutor) detectExistingProject(dirName string) string {
111+
candidates := []string{
112+
dirName,
113+
"flatrun-" + dirName,
114+
}
115+
116+
for _, candidate := range candidates {
117+
cmd := exec.Command("docker", "compose", "-p", candidate, "ps", "-q")
118+
output, err := cmd.Output()
119+
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
120+
return candidate
121+
}
122+
}
123+
return ""
65124
}
66125

67126
func (c *ComposeExecutor) runCompose(deploymentPath string, args ...string) (string, error) {

internal/docker/discovery.go

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strconv"
8+
"strings"
79
"time"
810

911
"github.com/flatrun/agent/pkg/models"
@@ -102,9 +104,14 @@ func (d *Discovery) GetDeployment(name string) (*models.Deployment, error) {
102104
}
103105

104106
metadataPath := filepath.Join(dirPath, "service.yml")
105-
if metadata, err := d.loadMetadata(metadataPath); err == nil {
106-
deployment.Metadata = metadata
107+
metadata, err := d.loadMetadata(metadataPath)
108+
if err != nil {
109+
metadata = d.generateMetadataFromCompose(composePath, name)
110+
if metadata != nil {
111+
_ = d.SaveMetadata(name, metadata)
112+
}
107113
}
114+
deployment.Metadata = metadata
108115

109116
if services, err := d.parseComposeServices(composePath); err == nil {
110117
deployment.Services = services
@@ -203,10 +210,38 @@ func (d *Discovery) CreateDeployment(name string, composeContent string) error {
203210
return err
204211
}
205212

213+
// Ensure compose file has a name attribute for project identification
214+
composeContent = d.ensureComposeName(name, composeContent)
215+
206216
composePath := filepath.Join(dirPath, "docker-compose.yml")
207217
return os.WriteFile(composePath, []byte(composeContent), 0644)
208218
}
209219

220+
// ensureComposeName adds or updates the name attribute in a compose file
221+
func (d *Discovery) ensureComposeName(name string, content string) string {
222+
var compose map[string]interface{}
223+
if err := yaml.Unmarshal([]byte(content), &compose); err != nil {
224+
// If parsing fails, prepend name manually
225+
return fmt.Sprintf("name: %s\n%s", name, content)
226+
}
227+
228+
// Check if name already exists
229+
if _, exists := compose["name"]; exists {
230+
return content
231+
}
232+
233+
// Add name attribute
234+
compose["name"] = name
235+
236+
// Re-marshal with name included
237+
data, err := yaml.Marshal(compose)
238+
if err != nil {
239+
return fmt.Sprintf("name: %s\n%s", name, content)
240+
}
241+
242+
return string(data)
243+
}
244+
210245
func (d *Discovery) DeleteDeployment(name string) error {
211246
dirPath := filepath.Join(d.basePath, name)
212247
return os.RemoveAll(dirPath)
@@ -266,3 +301,68 @@ func (d *Discovery) DeleteMetadata(name string) error {
266301

267302
return os.Remove(metadataPath)
268303
}
304+
305+
func (d *Discovery) generateMetadataFromCompose(composePath, name string) *models.ServiceMetadata {
306+
data, err := os.ReadFile(composePath)
307+
if err != nil {
308+
return nil
309+
}
310+
311+
var compose composeFile
312+
if err := yaml.Unmarshal(data, &compose); err != nil {
313+
return nil
314+
}
315+
316+
metadata := &models.ServiceMetadata{
317+
Name: name,
318+
Type: "",
319+
Networking: models.NetworkingConfig{
320+
Expose: false,
321+
Protocol: "http",
322+
ProxyType: "http",
323+
},
324+
SSL: models.SSLConfig{
325+
Enabled: false,
326+
AutoCert: false,
327+
},
328+
HealthCheck: models.HealthCheckConfig{
329+
Path: "/",
330+
Interval: "30s",
331+
},
332+
}
333+
334+
for _, svc := range compose.Services {
335+
if len(svc.Ports) > 0 {
336+
portStr := d.parsePort(svc.Ports[0])
337+
if portStr != "" {
338+
port := d.extractContainerPort(portStr)
339+
if port > 0 {
340+
metadata.Networking.ContainerPort = port
341+
}
342+
}
343+
break
344+
}
345+
}
346+
347+
return metadata
348+
}
349+
350+
func (d *Discovery) extractContainerPort(portStr string) int {
351+
parts := strings.Split(portStr, ":")
352+
var portPart string
353+
if len(parts) == 2 {
354+
portPart = parts[1]
355+
} else if len(parts) == 1 {
356+
portPart = parts[0]
357+
} else {
358+
return 0
359+
}
360+
361+
portPart = strings.Split(portPart, "/")[0]
362+
363+
port, err := strconv.Atoi(portPart)
364+
if err != nil {
365+
return 0
366+
}
367+
return port
368+
}

0 commit comments

Comments
 (0)