Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Allow overriding of built in app host templates #4678

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion cli/azd/pkg/apphost/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ func AspireDashboardUrl(
}

func init() {

var combinedTemplates fs.FS

// get the current working directory
if wd, err := os.Getwd(); err == nil {
// create a new overlay filesystem that combines the local filesystem with the embedded resources
combinedTemplates = OverlayFS(os.DirFS(wd), resources.AppHostTemplates)
} else {
// default back to embedded resources
combinedTemplates = resources.AppHostTemplates
}

tmpl, err := template.New("templates").
Option("missingkey=error").
Funcs(
Expand Down Expand Up @@ -96,7 +108,7 @@ func init() {
},
},
).
ParseFS(resources.AppHostTemplates, "apphost/templates/*")
ParseFS(combinedTemplates, "apphost/templates/*")
if err != nil {
panic("failed to parse generator templates: " + err.Error())
}
Expand Down
64 changes: 64 additions & 0 deletions cli/azd/pkg/apphost/overlayfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package apphost

import "io/fs"

// overlayFS implements fs.FS and merges local + embedded
type overlayFS struct {
local fs.FS
embedded fs.FS
}

func OverlayFS(local, embedded fs.FS) fs.FS {

return &overlayFS{
local: local,
embedded: embedded,
}
}

// overlayFS is a filesystem that combines a local filesystem with an embedded filesystem.
// When opening a file, it first tries to open it from the local filesystem
// if that fails, it falls back to the embedded filesystem.
func (o overlayFS) Open(name string) (fs.File, error) {
// 1. Attempt to open from local
if f, err := o.local.Open(name); err == nil {
return f, nil
}

// 2. Otherwise, fall back to the embedded filesystem
return o.embedded.Open(name)
}

func (o overlayFS) ReadDir(name string) ([]fs.DirEntry, error) {
// 1. Read the directory from embedded
embeddedEntries, err := fs.ReadDir(o.embedded, name)
if err != nil {
// If embedded doesn’t have this directory,
// we consider it a real error
return nil, err
}

// 2. Try local
localEntries, localErr := fs.ReadDir(o.local, name)
if localErr != nil {
// Local folder doesn't exist? That’s okay.
// Return the embedded entries only
return embeddedEntries, nil
}

// Build a map of embedded files for quick lookups
embedMap := make(map[string]int, len(embeddedEntries))
for i, e := range embeddedEntries {
embedMap[e.Name()] = i
}

// 3. Override any embedded entries with local
for _, le := range localEntries {
if idx, found := embedMap[le.Name()]; found {
// If local has the same file name as embed, override the embedded entry
embeddedEntries[idx] = le
}
}

return embeddedEntries, nil
}
136 changes: 136 additions & 0 deletions cli/azd/pkg/apphost/overlayfs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package apphost

import (
"io"
"io/fs"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

// In-memory mock FS for testing
type mockFS map[string][]byte

func (m mockFS) Open(name string) (fs.File, error) {
data, ok := m[name]
if !ok {
return nil, fs.ErrNotExist
}
return &mockFile{data: data, name: name}, nil
}

func (m mockFS) ReadDir(name string) ([]fs.DirEntry, error) {
// We'll consider everything in the top directory (no subdirs)
// to keep this example simple
if name != "." {
return nil, fs.ErrNotExist
}

var entries []fs.DirEntry
for path := range m {
entries = append(entries, mockDirEntry{path})
}
if len(entries) == 0 {
return nil, fs.ErrNotExist
}

return entries, nil
}

type mockFile struct {
data []byte
name string
off int
}

func (f *mockFile) Stat() (fs.FileInfo, error) { return mockFileInfo{f.name, int64(len(f.data))}, nil }
func (f *mockFile) Close() error { return nil }
func (f *mockFile) Read(p []byte) (int, error) {
if f.off >= len(f.data) {
return 0, io.EOF
}
n := copy(p, f.data[f.off:])
f.off += n
return n, nil
}

type mockFileInfo struct {
name string
size int64
}

func (fi mockFileInfo) Name() string { return fi.name }
func (fi mockFileInfo) Size() int64 { return fi.size }
func (fi mockFileInfo) Mode() fs.FileMode { return 0 }
func (fi mockFileInfo) ModTime() time.Time { return time.Time{} }
func (fi mockFileInfo) IsDir() bool { return false }
func (fi mockFileInfo) Sys() any { return nil }

type mockDirEntry struct {
name string
}

func (de mockDirEntry) Name() string { return de.name }
func (de mockDirEntry) IsDir() bool { return false }
func (de mockDirEntry) Type() fs.FileMode { return 0 }
func (de mockDirEntry) Info() (fs.FileInfo, error) { return mockFileInfo{de.name, 0}, nil }

// ----------------------------------------------------------------
// The Actual Test
// ----------------------------------------------------------------

func TestOverlayFS(t *testing.T) {
// 1) embeddedFS has foo.txt & bar.txt
embeddedFS := mockFS{
"foo.txt": []byte("EMBEDDED: foo"),
"bar.txt": []byte("EMBEDDED: bar"),
}

// 2) localFS overrides foo.txt and has localOnly.txt
localFS := mockFS{
"foo.txt": []byte("LOCAL OVERRIDE: foo"),
"localOnly.txt": []byte("LOCAL: this file not in embedded"),
}

// 3) Build combined
combined := OverlayFS(localFS, embeddedFS)

t.Run("local file is returned when file exists", func(t *testing.T) {
file, err := combined.Open("foo.txt")

if err != nil {
t.Fatalf("Open(foo.txt) error: %v", err)
}

content, _ := io.ReadAll(file)
actual := string(content)

assert.Contains(t, actual, "LOCAL OVERRIDE: foo")
})

t.Run("embedded file is returned when only embedded file exists", func(t *testing.T) {
file, err := combined.Open("bar.txt")

if err != nil {
t.Fatalf("Open(bar.txt) error: %v", err)
}

content, _ := io.ReadAll(file)
actual := string(content)

assert.Contains(t, actual, "EMBEDDED: bar")
})

t.Run("read directory returns only the files that exist in the primary file system", func(t *testing.T) {
entries, err := fs.ReadDir(combined, ".")
if err != nil {
t.Fatalf("ReadDir(.) error: %v", err)
}
assert.Len(t, entries, 2)
assert.Contains(t, entries, mockDirEntry{"foo.txt"})
assert.Contains(t, entries, mockDirEntry{"bar.txt"})

})

}