mirrored from https://chromium.googlesource.com/infra/luci/luci-go
/
settings.go
206 lines (186 loc) · 6.54 KB
/
settings.go
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
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gaeconfig
import (
"context"
"fmt"
"html"
"html/template"
"net/url"
"strings"
"go.chromium.org/luci/gae/service/info"
"go.chromium.org/luci/server/portal"
"go.chromium.org/luci/server/settings"
)
// configServiceAdmins is the default value for settings.AdministratorsGroup
// config setting below.
const configServiceAdmins = "administrators"
// Settings are stored in the datastore via appengine/gaesettings package.
type Settings struct {
// ConfigServiceHost is host name (and port) of the luci-config service to
// fetch configs from.
//
// For legacy reasons, the JSON value is "config_service_url".
ConfigServiceHost string `json:"config_service_url"`
// Administrators is the auth group of users that can call the validation
// endpoint.
AdministratorsGroup string `json:"administrators_group"`
}
// SetIfChanged sets "s" to be the new Settings if it differs from the current
// settings value.
func (s *Settings) SetIfChanged(c context.Context, who, why string) error {
return settings.SetIfChanged(c, settingsKey, s, who, why)
}
// FetchCachedSettings fetches Settings from the settings store.
//
// Uses in-process global cache to avoid hitting datastore often. The cache
// expiration time is 1 min (see gaesettings.expirationTime), meaning
// the instance will refetch settings once a minute (blocking only one unlucky
// request to do so).
//
// Returns errors only if there's no cached value (i.e. it is the first call
// to this function in this process ever) and datastore operation fails.
func FetchCachedSettings(c context.Context) (Settings, error) {
s := Settings{}
switch err := settings.Get(c, settingsKey, &s); err {
case nil:
// Backwards-compatibility with full URL: translate to host.
s.ConfigServiceHost = translateConfigURLToHost(s.ConfigServiceHost)
return s, nil
case settings.ErrNoSettings:
return DefaultSettings(c), nil
default:
return Settings{}, err
}
}
func mustFetchCachedSettings(c context.Context) *Settings {
settings, err := FetchCachedSettings(c)
if err != nil {
panic(err)
}
return &settings
}
// DefaultSettings returns Settings to use if setting store is empty.
func DefaultSettings(c context.Context) Settings {
return Settings{AdministratorsGroup: configServiceAdmins}
}
////////////////////////////////////////////////////////////////////////////////
// UI for settings.
// settingsKey is used internally to identify gaeconfig settings in settings
// store.
const settingsKey = "gaeconfig"
type settingsPage struct {
portal.BasePage
}
func (settingsPage) Title(c context.Context) (string, error) {
return "Configuration service settings", nil
}
func (settingsPage) Overview(c context.Context) (template.HTML, error) {
metadataURL := fmt.Sprintf("https://%s/api/config/v1/metadata", info.DefaultVersionHostname(c))
serviceAcc, err := info.ServiceAccount(c)
if err != nil {
return "", err
}
return template.HTML(fmt.Sprintf(`
<p>This service may fetch configuration files stored centrally in an instance of
<a href="https://chromium.googlesource.com/infra/luci/luci-py/+/master/appengine/config_service">luci-config</a>
service. This page can be used to configure the location of the config service
as well as parameters of the local cache that holds the fetched configuration
files.</p>
<p>Before your service can start fetching configs, it should be registered in
the config service's registry of known services (services.cfg config file), by
adding something similar to this:</p>
<pre>
services {
id: "%s"
owners: <your email>
metadata_url: "%s"
access: "%s"
}
</pre>
<p>Refer to the documentation in the services.cfg file for more info.</p>`,
html.EscapeString(info.TrimmedAppID(c)),
html.EscapeString(metadataURL),
html.EscapeString(serviceAcc)),
), nil
}
func (settingsPage) Fields(c context.Context) ([]portal.Field, error) {
return []portal.Field{
{
ID: "ConfigServiceHost",
Title: `Config service host`,
Type: portal.FieldText,
Validator: func(v string) error {
if strings.ContainsRune(v, '/') {
return fmt.Errorf("host must be a host name, not a URL")
}
return nil
},
Help: `<p>Host name (e.g., "luci-config.appspot.com") of a config service to fetch configuration files from.</p>`,
},
{
ID: "AdministratorsGroup",
Title: "Administrator group",
Type: portal.FieldText,
Validator: func(v string) error {
if v == "" {
return fmt.Errorf("administrator group cannot be an empty string")
}
return nil
},
Help: `<p>Members of this group can directly call the validation endpoint
of this service. Usually it is called only indirectly by the config service,
but it may be useful (e.g. for debugging) to call it directly.</p>`,
},
}, nil
}
func (settingsPage) ReadSettings(c context.Context) (map[string]string, error) {
s := DefaultSettings(c)
err := settings.GetUncached(c, settingsKey, &s)
if err != nil && err != settings.ErrNoSettings {
return nil, err
}
return map[string]string{
"ConfigServiceHost": s.ConfigServiceHost,
"AdministratorsGroup": s.AdministratorsGroup,
}, nil
}
func (settingsPage) WriteSettings(c context.Context, values map[string]string, who, why string) error {
modified := Settings{
ConfigServiceHost: translateConfigURLToHost(values["ConfigServiceHost"]),
AdministratorsGroup: values["AdministratorsGroup"],
}
return modified.SetIfChanged(c, who, why)
}
func translateConfigURLToHost(v string) string {
// If the host is a full URL, extract just the host component.
switch u, err := url.Parse(v); {
case err != nil:
return v
case u.Host != "":
// If we have a host (e.g., "example.com"), this will parse into the "Path"
// field with an empty host value. Therefore, if we have a "Host" value,
// we will use it directly (e.g., "http://example.com")
return u.Host
case u.Path != "":
// If this was just an empty (correct) host, it will have parsed into the
// Path field with an empty Host value.
return u.Path
default:
return v
}
}
func init() {
portal.RegisterPage(settingsKey, settingsPage{})
}