Repository navigation
Expand file tree
/
Copy pathhost.go
More file actions
346 lines (304 loc) · 11.4 KB
/
Copy pathhost.go
File metadata and controls
346 lines (304 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package sdk
import (
"context"
"encoding/json"
"errors"
"maps"
"sync"
"sync/atomic"
"github.com/nginxui/plugin-sdk-go/jsonrpc"
"github.com/nginxui/plugin-sdk-go/protocol"
)
// ErrHostNotReady is returned by host.* calls made before the host sent
// plugin.initialized.
var ErrHostNotReady = errors.New("sdk: host calls are not allowed before plugin.initialized")
// Info describes the running plugin process and the host behind it.
type Info struct {
// PluginID comes from NGINX_UI_PLUGIN_ID.
PluginID string
// APIVersion comes from NGINX_UI_PLUGIN_API_VERSION.
APIVersion int
// DataDir comes from NGINX_UI_PLUGIN_DATA_DIR and is the only writable
// directory the plugin owns.
DataDir string
// HostVersion comes from NGINX_UI_VERSION and is refined by the handshake.
HostVersion string
// Host is the handshake payload, empty until plugin.initialize arrives.
Host protocol.HostInfo
// Permissions are the manifest permissions the host granted.
Permissions []string
}
// Host is the plugin's client for the host.* side of the protocol.
type Host struct {
conn *jsonrpc.Conn
ready atomic.Bool
mu sync.RWMutex
settings map[string]any
info Info
}
var currentHost atomic.Pointer[Host]
// CurrentHost returns the host client of the running plugin, or nil when the
// process is not serving. It is usable for logging from the moment Run starts.
func CurrentHost() *Host { return currentHost.Load() }
type hostContextKey struct{}
// WithHost attaches a host client to ctx.
func WithHost(ctx context.Context, h *Host) context.Context {
return context.WithValue(ctx, hostContextKey{}, h)
}
// HostFromContext returns the host client carried by ctx, falling back to the
// process wide client so handlers always get a usable value.
func HostFromContext(ctx context.Context) *Host {
if h, ok := ctx.Value(hostContextKey{}).(*Host); ok && h != nil {
return h
}
return CurrentHost()
}
func newHost(conn *jsonrpc.Conn, info Info) *Host {
return &Host{conn: conn, info: info, settings: map[string]any{}}
}
// Ready reports whether host.* calls are allowed yet.
func (h *Host) Ready() bool { return h != nil && h.ready.Load() }
// Info returns a snapshot of the plugin and host identity.
func (h *Host) Info() Info {
h.mu.RLock()
defer h.mu.RUnlock()
out := h.info
out.Permissions = append([]string(nil), h.info.Permissions...)
return out
}
// Settings returns a copy of the latest settings the host pushed through
// plugin.initialize or plugin.configure.
func (h *Host) Settings() map[string]any {
h.mu.RLock()
defer h.mu.RUnlock()
return maps.Clone(h.settings)
}
func (h *Host) setSettings(s map[string]any) {
h.mu.Lock()
defer h.mu.Unlock()
if s == nil {
s = map[string]any{}
}
h.settings = maps.Clone(s)
}
func (h *Host) setHandshake(p protocol.InitializeParams) {
h.mu.Lock()
defer h.mu.Unlock()
h.info.Host = p.Host
if p.Host.Version != "" {
h.info.HostVersion = p.Host.Version
}
h.info.Permissions = append([]string(nil), p.Permissions...)
h.settings = maps.Clone(p.Settings)
if h.settings == nil {
h.settings = map[string]any{}
}
}
func (h *Host) call(ctx context.Context, method string, params, result any) error {
if h == nil || h.conn == nil {
return ErrHostNotReady
}
if !h.ready.Load() {
return ErrHostNotReady
}
return h.conn.Call(ctx, method, params, result)
}
// Log sends one line to the host log. It is a notification, so it never blocks
// on a reply; the SDK logger falls back to stderr when it fails.
func (h *Host) Log(level, msg string, fields map[string]any) error {
if h == nil || h.conn == nil {
return ErrHostNotReady
}
if !h.ready.Load() {
return ErrHostNotReady
}
return h.conn.Notify(context.Background(), protocol.MethodHostLog, protocol.HostLogParams{
Level: level,
Message: msg,
Fields: fields,
})
}
// kvGetResult mirrors protocol.HostKVGetResult with a decodable value.
type kvGetResult struct {
Value json.RawMessage `json:"value"`
Found bool `json:"found"`
}
// KVGet reads a key from the plugin's private store. out may be nil.
func (h *Host) KVGet(ctx context.Context, key string, out any) (bool, error) {
var res kvGetResult
if err := h.call(ctx, protocol.MethodHostKVGet, protocol.HostKVGetParams{Key: key}, &res); err != nil {
return false, err
}
if !res.Found || out == nil || len(res.Value) == 0 {
return res.Found, nil
}
if err := json.Unmarshal(res.Value, out); err != nil {
return true, err
}
return true, nil
}
// KVSet stores a JSON value, up to 64 KiB encoded.
func (h *Host) KVSet(ctx context.Context, key string, value any) error {
return h.call(ctx, protocol.MethodHostKVSet, protocol.HostKVSetParams{Key: key, Value: value}, nil)
}
// KVDelete removes a key.
func (h *Host) KVDelete(ctx context.Context, key string) error {
return h.call(ctx, protocol.MethodHostKVDelete, protocol.HostKVGetParams{Key: key}, nil)
}
// KVList lists the keys under a prefix.
func (h *Host) KVList(ctx context.Context, prefix string) ([]string, error) {
var res protocol.HostKVListResult
if err := h.call(ctx, protocol.MethodHostKVList, protocol.HostKVListParams{Prefix: prefix}, &res); err != nil {
return nil, err
}
return res.Keys, nil
}
// SettingsGet re-reads the plugin settings from the host.
func (h *Host) SettingsGet(ctx context.Context) (map[string]any, error) {
var res protocol.HostSettingsGetResult
if err := h.call(ctx, protocol.MethodHostSettingsGet, nil, &res); err != nil {
return nil, err
}
h.setSettings(res.Settings)
return res.Settings, nil
}
// Locale returns the host UI locale.
func (h *Host) Locale(ctx context.Context) (string, error) {
var res protocol.HostI18nLocaleResult
if err := h.call(ctx, protocol.MethodHostI18nLocale, nil, &res); err != nil {
return "", err
}
return res.Locale, nil
}
// CredentialsGet resolves a stored credential the plugin was granted access to.
func (h *Host) CredentialsGet(ctx context.Context, kind, id string) (protocol.HostCredentialsGetResult, error) {
var res protocol.HostCredentialsGetResult
err := h.call(ctx, protocol.MethodHostCredentialsGet, protocol.HostCredentialsGetParams{Kind: kind, ID: id}, &res)
return res, err
}
// CronRegister asks the host to call method on a schedule.
func (h *Host) CronRegister(ctx context.Context, id, schedule, method string) error {
return h.call(ctx, protocol.MethodHostCronRegister, protocol.HostCronRegisterParams{
ID: id,
Schedule: schedule,
Method: method,
}, nil)
}
// CronUnregister drops a previously registered schedule.
func (h *Host) CronUnregister(ctx context.Context, id string) error {
return h.call(ctx, protocol.MethodHostCronUnregister, protocol.HostCronUnregisterParams{ID: id}, nil)
}
// Notify raises a user facing notification in the nginx-ui UI.
func (h *Host) Notify(ctx context.Context, level, title, content string, details any) error {
return h.call(ctx, protocol.MethodHostNotify, protocol.HostNotifyParams{
Level: level,
Title: title,
Content: content,
Details: details,
}, nil)
}
// MetricsSnapshot reads the host analytics snapshot into out.
func (h *Host) MetricsSnapshot(ctx context.Context, out any) error {
var res struct {
Snapshot json.RawMessage `json:"snapshot"`
}
if err := h.call(ctx, protocol.MethodHostMetricsSnapshot, nil, &res); err != nil {
return err
}
if out == nil || len(res.Snapshot) == 0 {
return nil
}
return json.Unmarshal(res.Snapshot, out)
}
// LogsList returns the nginx log files the host allows the plugin to read.
// Rotated files are not listed; read them next to the listed path. It needs
// the log.files permission.
func (h *Host) LogsList(ctx context.Context) ([]protocol.HostLogFile, error) {
var res protocol.HostLogsListResult
if err := h.call(ctx, protocol.MethodHostLogsList, nil, &res); err != nil {
return nil, err
}
return res.Logs, nil
}
// ActivitySet shows or clears one entry of the host processing indicator.
// label is an English source string the host translates, the browser bundle
// supplies the translations. Entries are cleared when the plugin stops.
func (h *Host) ActivitySet(ctx context.Context, key, label string, active bool) error {
return h.call(ctx, protocol.MethodHostActivitySet, protocol.HostActivitySetParams{
Key: key,
Label: label,
Active: active,
}, nil)
}
// Activity shows an indicator entry and returns the function that clears it.
// The clear call ignores a canceled ctx so it still reaches the host.
func (h *Host) Activity(ctx context.Context, key, label string) (stop func(), err error) {
if err = h.ActivitySet(ctx, key, label, true); err != nil {
return func() {}, err
}
return func() {
_ = h.ActivitySet(context.WithoutCancel(ctx), key, label, false)
}, nil
}
// NginxSnippetPut writes one nginx configuration snippet of the plugin. The
// host tests the whole configuration and reloads nginx, and puts the previous
// snippet back when nginx rejects the new one, which is reported as an
// invalid params error. include is the directive a person adds where the
// snippet should apply. It needs the nginx.snippet permission.
func (h *Host) NginxSnippetPut(ctx context.Context, name, content string) (changed bool, include string, err error) {
var res protocol.HostNginxSnippetPutResult
err = h.call(ctx, protocol.MethodHostNginxSnippetPut, protocol.HostNginxSnippetPutParams{Name: name, Content: content}, &res)
return res.Changed, res.Include, err
}
// NginxSnippetDelete removes one snippet of the plugin the same way. A
// snippet that is still included cannot go and is kept. It needs the
// nginx.snippet permission.
func (h *Host) NginxSnippetDelete(ctx context.Context, name string) (removed bool, err error) {
var res protocol.HostNginxSnippetDeleteResult
err = h.call(ctx, protocol.MethodHostNginxSnippetDelete, protocol.HostNginxSnippetDeleteParams{Name: name}, &res)
return res.Removed, err
}
// NginxSnippetList lists the snippets of the plugin. It needs the
// nginx.snippet permission.
func (h *Host) NginxSnippetList(ctx context.Context) ([]protocol.HostNginxSnippet, error) {
var res protocol.HostNginxSnippetListResult
if err := h.call(ctx, protocol.MethodHostNginxSnippetList, nil, &res); err != nil {
return nil, err
}
return res.Snippets, nil
}
// NginxConfigList lists the nginx configuration files, relative to the
// configuration directory. It needs the nginx.config.read permission.
func (h *Host) NginxConfigList(ctx context.Context) ([]string, error) {
var res protocol.HostNginxConfigListResult
if err := h.call(ctx, protocol.MethodHostNginxConfigList, nil, &res); err != nil {
return nil, err
}
return res.Files, nil
}
// NginxConfigGet reads one file NginxConfigList returns. It needs the
// nginx.config.read permission.
func (h *Host) NginxConfigGet(ctx context.Context, path string) (string, error) {
var res protocol.HostNginxConfigGetResult
if err := h.call(ctx, protocol.MethodHostNginxConfigGet, protocol.HostNginxConfigGetParams{Path: path}, &res); err != nil {
return "", err
}
return res.Content, nil
}
// SitesList lists the sites. It needs the sites.read permission.
func (h *Host) SitesList(ctx context.Context) ([]protocol.HostSite, error) {
var res protocol.HostSitesListResult
if err := h.call(ctx, protocol.MethodHostSitesList, nil, &res); err != nil {
return nil, err
}
return res.Sites, nil
}
// CertsList lists the certificates without their private keys. It needs the
// certs.read permission.
func (h *Host) CertsList(ctx context.Context) ([]protocol.HostCert, error) {
var res protocol.HostCertsListResult
if err := h.call(ctx, protocol.MethodHostCertsList, nil, &res); err != nil {
return nil, err
}
return res.Certs, nil
}