Skip to content

Commit e0ea61c

Browse files
committed
fix: project icons not loading when used with yaml/env aliases
1 parent ada97ad commit e0ea61c

File tree

4 files changed

+137
-6
lines changed

4 files changed

+137
-6
lines changed

backend/internal/services/project_service.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,13 @@ func (s *ProjectService) GetProjectServices(ctx context.Context, projectID strin
561561
return []ProjectServiceInfo{}, fmt.Errorf("no compose file found in project directory: %s", projectFromDb.Path)
562562
}
563563

564-
meta, metaErr := projects.ParseArcaneComposeMetadata(ctx, composeFileFullPath)
564+
projectsDirectory, projectsDirErr := s.getProjectsDirectoryInternal(ctx)
565+
if projectsDirErr != nil {
566+
slog.WarnContext(ctx, "failed to resolve projects directory for Arcane compose metadata", "path", composeFileFullPath, "error", projectsDirErr)
567+
}
568+
autoInjectEnv := s.settingsService.GetBoolSetting(ctx, "autoInjectEnv", false)
569+
570+
meta, metaErr := projects.ParseArcaneComposeMetadata(ctx, composeFileFullPath, projectsDirectory, autoInjectEnv)
565571
if metaErr != nil {
566572
slog.WarnContext(ctx, "failed to parse Arcane compose metadata", "path", composeFileFullPath, "error", metaErr)
567573
}
@@ -3096,7 +3102,13 @@ func (s *ProjectService) getProjectMetadataForProject(ctx context.Context, p mod
30963102
return projects.ArcaneComposeMetadata{ServiceIcons: map[string]string{}}
30973103
}
30983104

3099-
meta, err := projects.ParseArcaneComposeMetadata(ctx, composeFile)
3105+
projectsDirectory, projectsDirErr := s.getProjectsDirectoryInternal(ctx)
3106+
if projectsDirErr != nil {
3107+
slog.WarnContext(ctx, "failed to resolve projects directory for Arcane compose metadata", "path", composeFile, "error", projectsDirErr)
3108+
}
3109+
autoInjectEnv := s.settingsService.GetBoolSetting(ctx, "autoInjectEnv", false)
3110+
3111+
meta, err := projects.ParseArcaneComposeMetadata(ctx, composeFile, projectsDirectory, autoInjectEnv)
31003112
if err != nil {
31013113
slog.WarnContext(ctx, "failed to parse Arcane compose metadata", "path", composeFile, "error", err)
31023114
return projects.ArcaneComposeMetadata{ServiceIcons: map[string]string{}}

backend/internal/services/project_service_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,6 +1786,54 @@ func TestProjectService_SyncProjectsFromFileSystem_DiscoversNestedProjectsAndRel
17861786
assert.Equal(t, "project2", items[1].DirName)
17871787
}
17881788

1789+
func TestProjectService_ListProjects_LoadsProjectIconFromGlobalEnvInIncludedMetadata(t *testing.T) {
1790+
db := setupProjectTestDB(t)
1791+
ctx := context.Background()
1792+
1793+
settingsService, err := NewSettingsService(ctx, db)
1794+
require.NoError(t, err)
1795+
1796+
projectsRoot := t.TempDir()
1797+
projectPath := filepath.Join(projectsRoot, "demo")
1798+
require.NoError(t, os.MkdirAll(projectPath, 0o755))
1799+
1800+
require.NoError(t, os.WriteFile(
1801+
filepath.Join(projectsRoot, projects.GlobalEnvFileName),
1802+
[]byte("ICON_CDN_URL=https://cdn.jsdelivr.net/gh/selfhst/icons@main\n"),
1803+
0o600,
1804+
))
1805+
1806+
require.NoError(t, os.WriteFile(filepath.Join(projectPath, "compose.yaml"), []byte(`include:
1807+
- metadata.yaml
1808+
services:
1809+
watchtower:
1810+
image: nickfedor/watchtower:latest
1811+
`), 0o600))
1812+
1813+
require.NoError(t, os.WriteFile(filepath.Join(projectPath, "metadata.yaml"), []byte(`x-watchtower-icon: &watchtower-icon "${ICON_CDN_URL:+${ICON_CDN_URL}/svg/watchtower.svg}"
1814+
x-arcane:
1815+
icon: *watchtower-icon
1816+
services:
1817+
watchtower:
1818+
labels:
1819+
com.getarcaneapp.arcane.icon: *watchtower-icon
1820+
`), 0o600))
1821+
1822+
require.NoError(t, settingsService.SetStringSetting(ctx, "projectsDirectory", projectsRoot))
1823+
1824+
svc := NewProjectService(db, settingsService, nil, nil, nil, nil, config.Load())
1825+
require.NoError(t, svc.SyncProjectsFromFileSystem(ctx))
1826+
1827+
items, page, err := svc.ListProjects(ctx, pagination.QueryParams{
1828+
SortParams: pagination.SortParams{Sort: "path", Order: pagination.SortAsc},
1829+
PaginationParams: pagination.PaginationParams{Limit: -1},
1830+
})
1831+
require.NoError(t, err)
1832+
require.EqualValues(t, 1, page.TotalItems)
1833+
require.Len(t, items, 1)
1834+
assert.Equal(t, "https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/watchtower.svg", items[0].IconURL)
1835+
}
1836+
17891837
func TestProjectService_CountProjectFolders_RecursivelyCountsNestedProjects(t *testing.T) {
17901838
db := setupProjectTestDB(t)
17911839
ctx := context.Background()

backend/pkg/projects/custom_labels.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,24 @@ type ArcaneComposeMetadata struct {
3636
}
3737

3838
// ParseArcaneComposeMetadata reads a Docker Compose file and extracts Arcane-specific metadata.
39-
func ParseArcaneComposeMetadata(ctx context.Context, composeFilePath string) (ArcaneComposeMetadata, error) {
39+
// When projectsDirectory is set, Arcane's project env loading is used so .env.global is available.
40+
func ParseArcaneComposeMetadata(ctx context.Context, composeFilePath, projectsDirectory string, autoInjectEnv bool) (ArcaneComposeMetadata, error) {
41+
if composeFilePath == "" {
42+
return ArcaneComposeMetadata{ServiceIcons: map[string]string{}}, nil
43+
}
44+
4045
workdir := filepath.Dir(composeFilePath)
41-
envMap := loadComposeEnvironment(workdir)
46+
if strings.TrimSpace(projectsDirectory) == "" {
47+
envMap := loadComposeEnvironment(workdir)
48+
return ParseArcaneComposeMetadataWithEnv(ctx, composeFilePath, envMap)
49+
}
50+
51+
envLoader := NewEnvLoader(projectsDirectory, workdir, autoInjectEnv)
52+
envMap, _, err := envLoader.LoadEnvironment(ctx)
53+
if err != nil {
54+
return ArcaneComposeMetadata{ServiceIcons: map[string]string{}}, fmt.Errorf("load project environment: %w", err)
55+
}
56+
4257
return ParseArcaneComposeMetadataWithEnv(ctx, composeFilePath, envMap)
4358
}
4459

backend/pkg/projects/custom_labels_test.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ x-arcane:
2828
composePath := filepath.Join(tempDir, "compose.yaml")
2929
require.NoError(t, os.WriteFile(composePath, []byte(composeContent), 0o600))
3030

31-
meta, err := ParseArcaneComposeMetadata(context.Background(), composePath)
31+
meta, err := ParseArcaneComposeMetadata(context.Background(), composePath, tempDir, false)
3232
require.NoError(t, err)
3333
require.Equal(t, "https://cdn.jsdelivr.net/gh/homarr-labs/webp/raspberry-pi.webp", meta.ProjectIconURL)
3434
require.Equal(t, []string{"https://www.example.com"}, meta.ProjectURLS)
@@ -53,8 +53,64 @@ services:
5353
`
5454
require.NoError(t, os.WriteFile(filepath.Join(tempDir, "meta.yaml"), []byte(metaContent), 0o600))
5555

56-
meta, err := ParseArcaneComposeMetadata(context.Background(), composePath)
56+
meta, err := ParseArcaneComposeMetadata(context.Background(), composePath, tempDir, false)
5757
require.NoError(t, err)
5858
require.Equal(t, "https://example.com/icon.png", meta.ProjectIconURL)
5959
require.Equal(t, []string{"https://example.com/docs"}, meta.ProjectURLS)
6060
}
61+
62+
func TestParseArcaneComposeMetadata_LoadsGlobalEnvForIncludedMetadata(t *testing.T) {
63+
projectsRoot := t.TempDir()
64+
projectDir := filepath.Join(projectsRoot, "demo")
65+
require.NoError(t, os.MkdirAll(projectDir, 0o755))
66+
67+
require.NoError(t, os.WriteFile(
68+
filepath.Join(projectsRoot, GlobalEnvFileName),
69+
[]byte("ICON_CDN_URL=https://cdn.jsdelivr.net/gh/selfhst/icons@main\n"),
70+
0o600,
71+
))
72+
73+
require.NoError(t, os.WriteFile(filepath.Join(projectDir, "compose.yaml"), []byte(`include:
74+
- metadata.yaml
75+
services:
76+
watchtower:
77+
image: nickfedor/watchtower:latest
78+
`), 0o600))
79+
80+
require.NoError(t, os.WriteFile(filepath.Join(projectDir, "metadata.yaml"), []byte(`x-watchtower-icon: &watchtower-icon "${ICON_CDN_URL:+${ICON_CDN_URL}/svg/watchtower.svg}"
81+
x-arcane:
82+
icon: *watchtower-icon
83+
`), 0o600))
84+
85+
meta, err := ParseArcaneComposeMetadata(context.Background(), filepath.Join(projectDir, "compose.yaml"), projectsRoot, false)
86+
require.NoError(t, err)
87+
require.Equal(t, "https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/watchtower.svg", meta.ProjectIconURL)
88+
}
89+
90+
func TestParseArcaneComposeMetadata_LoadsGlobalEnvForNestedProjects(t *testing.T) {
91+
projectsRoot := t.TempDir()
92+
projectDir := filepath.Join(projectsRoot, "group", "demo")
93+
require.NoError(t, os.MkdirAll(projectDir, 0o755))
94+
95+
require.NoError(t, os.WriteFile(
96+
filepath.Join(projectsRoot, GlobalEnvFileName),
97+
[]byte("ICON_CDN_URL=https://cdn.jsdelivr.net/gh/selfhst/icons@main\n"),
98+
0o600,
99+
))
100+
101+
require.NoError(t, os.WriteFile(filepath.Join(projectDir, "compose.yaml"), []byte(`include:
102+
- metadata.yaml
103+
services:
104+
watchtower:
105+
image: nickfedor/watchtower:latest
106+
`), 0o600))
107+
108+
require.NoError(t, os.WriteFile(filepath.Join(projectDir, "metadata.yaml"), []byte(`x-watchtower-icon: &watchtower-icon "${ICON_CDN_URL:+${ICON_CDN_URL}/svg/watchtower.svg}"
109+
x-arcane:
110+
icon: *watchtower-icon
111+
`), 0o600))
112+
113+
meta, err := ParseArcaneComposeMetadata(context.Background(), filepath.Join(projectDir, "compose.yaml"), projectsRoot, false)
114+
require.NoError(t, err)
115+
require.Equal(t, "https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/watchtower.svg", meta.ProjectIconURL)
116+
}

0 commit comments

Comments
 (0)