Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/web/ui/ui.go
4093 views
1
// Package ui exposes utilities to get a Handler for the Grafana Agent Flow UI.
2
package ui
3
4
import (
5
"bytes"
6
"fmt"
7
"io"
8
"io/fs"
9
"net/http"
10
"path"
11
"strings"
12
"sync"
13
"text/template"
14
"time"
15
16
"github.com/gorilla/mux"
17
"github.com/prometheus/common/server"
18
)
19
20
// RegisterRoutes registers routes to the provided mux.Router for serving the
21
// Grafana Agent Flow UI. The UI will be served relative to pathPrefix. If no
22
// pathPrefix is specified, the UI will be served at root.
23
//
24
// By default, the UI is retrieved from the ./web/ui/build directory relative
25
// to working directory, assuming that the Agent is run from the repo root.
26
// However, if the builtinassets Go tag is present, the built UI will be
27
// embedded into the binary; run go generate -tags builtinassets for this
28
// package to generate the assets to embed.
29
//
30
// RegisterRoutes catches all requests from pathPrefix and so should only be
31
// invoked after all other routes have been registered.
32
//
33
// RegisterRoutes is not intended for public use and will only work properly
34
// when called from github.com/grafana/agent.
35
func RegisterRoutes(pathPrefix string, router *mux.Router) {
36
if !strings.HasSuffix(pathPrefix, "/") {
37
pathPrefix = pathPrefix + "/"
38
}
39
40
publicPath := path.Join(pathPrefix, "public")
41
42
renderer := &templateRenderer{
43
pathPrefix: strings.TrimSuffix(pathPrefix, "/"),
44
inner: Assets(),
45
46
contentCache: make(map[string]string),
47
contentCacheTime: make(map[string]time.Time),
48
}
49
50
router.PathPrefix(publicPath).Handler(http.StripPrefix(publicPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
51
server.StaticFileServer(renderer).ServeHTTP(w, r)
52
})))
53
54
router.HandleFunc(strings.TrimSuffix(pathPrefix, "/"), func(w http.ResponseWriter, r *http.Request) {
55
// Redirect to form with /
56
http.Redirect(w, r, pathPrefix, http.StatusFound)
57
})
58
router.PathPrefix(pathPrefix).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
59
r.URL.Path = "/"
60
server.StaticFileServer(renderer).ServeHTTP(w, r)
61
})
62
}
63
64
// templateRenderer wraps around an inner fs.FS and will use html/template to
65
// render files it serves. Files will be cached after rendering to save on CPU
66
// time between repeated requests.
67
//
68
// The templateRenderer is used to inject runtime variables into the statically
69
// built UI, such as the base URL path where the UI is exposed.
70
type templateRenderer struct {
71
pathPrefix string
72
inner http.FileSystem
73
74
cacheMut sync.RWMutex
75
contentCache map[string]string
76
contentCacheTime map[string]time.Time
77
}
78
79
var _ http.FileSystem = (*templateRenderer)(nil)
80
81
func (tr *templateRenderer) Open(name string) (http.File, error) {
82
// First, open the inner file.
83
f, err := tr.inner.Open(name)
84
if err != nil {
85
return nil, err
86
}
87
// Get the modification time of the file. This will be used to determine if
88
// our cache is stale.
89
fi, err := f.Stat()
90
if err != nil {
91
_ = f.Close()
92
return nil, err
93
}
94
95
// Return the underlying file if we got a directory. Otherwise, we're going
96
// to create our own synthetic file.
97
//
98
// When we create a synthetic file, we close the original file, f, on
99
// return. Otherwise, we leave f open on return so the caller can read and
100
// close it.
101
if fi.IsDir() {
102
return f, nil
103
}
104
defer f.Close()
105
106
// Return the existing cache entry if one exists.
107
if ent, ok := tr.getCacheEntry(name, fi, true); ok {
108
return ent, nil
109
}
110
111
tr.cacheMut.Lock()
112
defer tr.cacheMut.Unlock()
113
114
// Check to see if another goroutine happened to cache the file while we were
115
// waiting for the lock.
116
if ent, ok := tr.getCacheEntry(name, fi, false); ok {
117
return ent, nil
118
}
119
120
rawBytes, err := io.ReadAll(f)
121
if err != nil {
122
return nil, fmt.Errorf("reading file for template processing: %w", err)
123
}
124
tmpl, err := template.New(name).Delims("{{!", "!}}").Parse(string(rawBytes))
125
if err != nil {
126
return nil, fmt.Errorf("parsing template: %w", err)
127
}
128
129
// Render the file as an html/template.
130
var buf bytes.Buffer
131
if err := tmpl.Execute(&buf, struct{ PublicURL string }{PublicURL: tr.pathPrefix}); err != nil {
132
return nil, fmt.Errorf("rendering template: %w", err)
133
}
134
135
tr.contentCache[name] = buf.String()
136
tr.contentCacheTime[name] = fi.ModTime()
137
138
return &readerFile{
139
Reader: bytes.NewReader(buf.Bytes()),
140
fi: &infoWithSize{
141
FileInfo: fi,
142
size: int64(buf.Len()),
143
},
144
}, nil
145
}
146
147
func (tr *templateRenderer) getCacheEntry(name string, fi fs.FileInfo, lock bool) (f http.File, ok bool) {
148
if lock {
149
tr.cacheMut.RLock()
150
defer tr.cacheMut.RUnlock()
151
}
152
153
content, ok := tr.contentCache[name]
154
if !ok {
155
return nil, false
156
}
157
158
// Before returning, make sure that fi isn't newer than our cache time.
159
if fi.ModTime().After(tr.contentCacheTime[name]) {
160
// The file has changed since we cached it. It needs to be re-cached. This
161
// is only common to happen during development, but would rarely happen in
162
// production where the files are static.
163
return nil, false
164
}
165
166
return &readerFile{
167
Reader: strings.NewReader(content),
168
fi: &infoWithSize{
169
FileInfo: fi,
170
size: int64(len(content)),
171
},
172
}, true
173
}
174
175
type readerFile struct {
176
io.Reader
177
fi fs.FileInfo
178
}
179
180
var _ http.File = (*readerFile)(nil)
181
182
func (rf *readerFile) Stat() (fs.FileInfo, error) { return rf.fi, nil }
183
184
func (rf *readerFile) Close() error {
185
// no-op: nothing to close
186
return nil
187
}
188
189
// http.Filesystem expects that io.Seeker and Readdir is implemented for all
190
// http.File implementations.
191
//
192
// These don't need to do anything; http also contains an adapter for fs.FS to
193
// http.FileSystem (http.FS) where these two methods can be a no-op.
194
195
func (rf *readerFile) Seek(offset int64, whence int) (int64, error) {
196
return 0, fmt.Errorf("Seek not implemented")
197
}
198
199
func (rf *readerFile) Readdir(count int) ([]fs.FileInfo, error) {
200
return nil, fmt.Errorf("Readdir not implemented")
201
}
202
203
type infoWithSize struct {
204
fs.FileInfo
205
size int64
206
}
207
208
func (iws *infoWithSize) Size() int64 { return iws.size }
209
210