Skip to content

Commit 0d84e27

Browse files
authored
Expose initial and effective config for debugging purposes (#325)
* Expose initial and effective config for debugging purposes (Still incomplete, just the initial ideas) * Add tests * Remove outdated test comment * Add 'token' to the redaction list * Require an env var to be defined before serving config
1 parent f8d8894 commit 0d84e27

File tree

3 files changed

+374
-5
lines changed

3 files changed

+374
-5
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright Splunk, Inc.
2+
// Copyright The OpenTelemetry Authors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package configprovider
17+
18+
import (
19+
"net"
20+
"net/http"
21+
"os"
22+
"strings"
23+
24+
"github.com/spf13/cast"
25+
"go.uber.org/zap"
26+
"gopkg.in/yaml.v2"
27+
)
28+
29+
const (
30+
configServerEnabledEnvVar = "SPLUNK_DEBUG_CONFIG_SERVER"
31+
configServerPortEnvVar = "SPLUNK_DEBUG_CONFIG_SERVER_PORT"
32+
33+
defaultConfigServerEndpoint = "localhost:55555"
34+
)
35+
36+
type configServer struct {
37+
logger *zap.Logger
38+
initial map[string]interface{}
39+
effective map[string]interface{}
40+
server *http.Server
41+
doneCh chan struct{}
42+
}
43+
44+
func newConfigServer(logger *zap.Logger, initial, effective map[string]interface{}) *configServer {
45+
return &configServer{
46+
logger: logger,
47+
initial: initial,
48+
effective: effective,
49+
}
50+
}
51+
52+
func (cs *configServer) start() error {
53+
if enabled := os.Getenv(configServerEnabledEnvVar); enabled != "true" {
54+
// The config server needs to be explicitly enabled for the time being.
55+
return nil
56+
}
57+
58+
endpoint := defaultConfigServerEndpoint
59+
if portOverride, ok := os.LookupEnv(configServerPortEnvVar); ok {
60+
if portOverride == "" {
61+
// If explicitly set to empty do not start the server.
62+
return nil
63+
}
64+
65+
endpoint = "localhost:" + portOverride
66+
}
67+
68+
listener, err := net.Listen("tcp", endpoint)
69+
if err != nil {
70+
return err
71+
}
72+
73+
mux := http.NewServeMux()
74+
75+
initialHandleFunc, err := cs.muxHandleFunc(cs.initial)
76+
if err != nil {
77+
return err
78+
}
79+
mux.HandleFunc("/debug/configz/initial", initialHandleFunc)
80+
81+
effectiveHandleFunc, err := cs.muxHandleFunc(simpleRedact(cs.effective))
82+
if err != nil {
83+
return err
84+
}
85+
mux.HandleFunc("/debug/configz/effective", effectiveHandleFunc)
86+
87+
cs.server = &http.Server{
88+
Handler: mux,
89+
}
90+
cs.doneCh = make(chan struct{})
91+
go func() {
92+
defer close(cs.doneCh)
93+
94+
httpErr := cs.server.Serve(listener)
95+
if httpErr != http.ErrServerClosed {
96+
cs.logger.Error("config server error", zap.Error(err))
97+
}
98+
}()
99+
100+
return nil
101+
}
102+
103+
func (cs *configServer) shutdown() error {
104+
var err error
105+
if cs.server != nil {
106+
err = cs.server.Close()
107+
// If launched wait for Serve goroutine exit.
108+
<-cs.doneCh
109+
}
110+
111+
return err
112+
}
113+
114+
func (cs *configServer) muxHandleFunc(config map[string]interface{}) (func(http.ResponseWriter, *http.Request), error) {
115+
configYAML, err := yaml.Marshal(config)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
return func(writer http.ResponseWriter, request *http.Request) {
121+
if request.Method != "GET" {
122+
writer.WriteHeader(http.StatusMethodNotAllowed)
123+
return
124+
}
125+
_, _ = writer.Write(configYAML)
126+
}, nil
127+
}
128+
129+
func simpleRedact(config map[string]interface{}) map[string]interface{} {
130+
redactedConfig := make(map[string]interface{})
131+
for k, v := range config {
132+
switch value := v.(type) {
133+
case string:
134+
if shouldRedactKey(k) {
135+
v = "<redacted>"
136+
}
137+
case map[string]interface{}:
138+
v = simpleRedact(value)
139+
case map[interface{}]interface{}:
140+
v = simpleRedact(cast.ToStringMap(value))
141+
}
142+
143+
redactedConfig[k] = v
144+
}
145+
146+
return redactedConfig
147+
}
148+
149+
// shouldRedactKey applies a simple check to see if the contents of the given key
150+
// should be redacted or not.
151+
func shouldRedactKey(k string) bool {
152+
fragments := []string{
153+
"access",
154+
"api_key",
155+
"apikey",
156+
"auth",
157+
"credential",
158+
"creds",
159+
"login",
160+
"password",
161+
"pwd",
162+
"token",
163+
"user",
164+
}
165+
166+
for _, fragment := range fragments {
167+
if strings.Contains(k, fragment) {
168+
return true
169+
}
170+
}
171+
172+
return false
173+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright Splunk, Inc.
2+
// Copyright The OpenTelemetry Authors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package configprovider
17+
18+
import (
19+
"io/ioutil"
20+
"net/http"
21+
"os"
22+
"strconv"
23+
"testing"
24+
25+
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
27+
"go.opentelemetry.io/collector/testutil"
28+
"go.uber.org/zap"
29+
"gopkg.in/yaml.v2"
30+
)
31+
32+
func TestConfigServer_RequireEnvVar(t *testing.T) {
33+
initial := map[string]interface{}{
34+
"minimal": "config",
35+
}
36+
37+
cs := newConfigServer(zap.NewNop(), initial, initial)
38+
require.NotNil(t, cs)
39+
40+
require.NoError(t, cs.start())
41+
t.Cleanup(func() {
42+
require.NoError(t, cs.shutdown())
43+
})
44+
45+
client := &http.Client{}
46+
path := "/debug/configz/initial"
47+
_, err := client.Get("http://" + defaultConfigServerEndpoint + path)
48+
assert.Error(t, err)
49+
}
50+
51+
func TestConfigServer_EnvVar(t *testing.T) {
52+
alternativePort := strconv.FormatUint(uint64(testutil.GetAvailablePort(t)), 10)
53+
require.NoError(t, os.Setenv(configServerEnabledEnvVar, "true"))
54+
t.Cleanup(func() {
55+
assert.NoError(t, os.Unsetenv(configServerEnabledEnvVar))
56+
})
57+
58+
tests := []struct {
59+
name string
60+
portEnvVar string
61+
endpoint string
62+
setPortEnvVar bool
63+
serverDown bool
64+
}{
65+
{
66+
name: "default",
67+
},
68+
{
69+
name: "disable_server",
70+
setPortEnvVar: true, // Explicitly setting it to empty to disable the server.
71+
serverDown: true,
72+
},
73+
{
74+
name: "change_port",
75+
portEnvVar: alternativePort,
76+
endpoint: "http://localhost:" + alternativePort,
77+
},
78+
}
79+
80+
for _, tt := range tests {
81+
t.Run(tt.name, func(t *testing.T) {
82+
initial := map[string]interface{}{
83+
"key": "value",
84+
}
85+
86+
if tt.portEnvVar != "" || tt.setPortEnvVar {
87+
require.NoError(t, os.Setenv(configServerPortEnvVar, tt.portEnvVar))
88+
defer func() {
89+
assert.NoError(t, os.Unsetenv(configServerPortEnvVar))
90+
}()
91+
}
92+
93+
cs := newConfigServer(zap.NewNop(), initial, initial)
94+
require.NoError(t, cs.start())
95+
defer func() {
96+
assert.NoError(t, cs.shutdown())
97+
}()
98+
99+
endpoint := tt.endpoint
100+
if endpoint == "" {
101+
endpoint = "http://" + defaultConfigServerEndpoint
102+
}
103+
104+
path := "/debug/configz/initial"
105+
if tt.serverDown {
106+
client := &http.Client{}
107+
_, err := client.Get(endpoint + path)
108+
assert.Error(t, err)
109+
return
110+
}
111+
112+
client := &http.Client{}
113+
resp, err := client.Get(endpoint + path)
114+
require.NoError(t, err)
115+
assert.Equal(t, http.StatusOK, resp.StatusCode, "unsuccessful zpage %q GET", path)
116+
})
117+
}
118+
}
119+
120+
func TestConfigServer_Serve(t *testing.T) {
121+
require.NoError(t, os.Setenv(configServerEnabledEnvVar, "true"))
122+
t.Cleanup(func() {
123+
assert.NoError(t, os.Unsetenv(configServerEnabledEnvVar))
124+
})
125+
126+
initial := map[string]interface{}{
127+
"field": "not_redacted",
128+
"api_key": "not_redacted_on_initial",
129+
"int": 42,
130+
"map": map[interface{}]interface{}{
131+
"k0": true,
132+
"k1": -1,
133+
"password": "$ENV_VAR",
134+
},
135+
}
136+
effective := map[string]interface{}{
137+
"field": "not_redacted",
138+
"api_key": "<redacted>",
139+
"int": 42,
140+
"map": map[interface{}]interface{}{
141+
"k0": true,
142+
"k1": -1,
143+
"password": "<redacted>",
144+
},
145+
}
146+
147+
cs := newConfigServer(zap.NewNop(), initial, initial)
148+
require.NotNil(t, cs)
149+
150+
require.NoError(t, cs.start())
151+
t.Cleanup(func() {
152+
require.NoError(t, cs.shutdown())
153+
})
154+
155+
// Test for the pages to be actually valid YAML files.
156+
assertValidYAMLPages(t, initial, "/debug/configz/initial")
157+
assertValidYAMLPages(t, effective, "/debug/configz/effective")
158+
}
159+
160+
func assertValidYAMLPages(t *testing.T, expected map[string]interface{}, path string) {
161+
url := "http://" + defaultConfigServerEndpoint + path
162+
163+
client := &http.Client{}
164+
165+
// Anything other the GET should return 405.
166+
resp, err := client.Head(url)
167+
assert.NoError(t, err)
168+
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
169+
assert.NoError(t, resp.Body.Close())
170+
171+
resp, err = client.Get(url)
172+
if !assert.NoError(t, err, "error retrieving zpage at %q", path) {
173+
return
174+
}
175+
assert.Equal(t, http.StatusOK, resp.StatusCode, "unsuccessful zpage %q GET", path)
176+
t.Cleanup(func() {
177+
assert.NoError(t, resp.Body.Close())
178+
})
179+
180+
respBytes, err := ioutil.ReadAll(resp.Body)
181+
require.NoError(t, err)
182+
183+
var unmarshalled map[string]interface{}
184+
require.NoError(t, yaml.Unmarshal(respBytes, &unmarshalled))
185+
186+
assert.Equal(t, expected, unmarshalled)
187+
}

0 commit comments

Comments
 (0)