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

Add a config endpoint on a new api server #21025

Merged
merged 33 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
42b8aa8
refactor to prepare new server
pgimalac Nov 20, 2023
02344fd
feat: implement config api server
pgimalac Nov 21, 2023
1e315df
refactor: move each server to its own file
pgimalac Nov 21, 2023
66fe2eb
merge with main
pgimalac Nov 21, 2023
bcf2bcf
fix revive duplicate import lint
pgimalac Nov 22, 2023
17b1c34
refactor: rename variables to ipc and cmd
pgimalac Nov 24, 2023
cbfadde
rename api server files to cmd and ipc
pgimalac Nov 24, 2023
a1125dd
limit ipc config endpoint to api_key for now
pgimalac Nov 24, 2023
7f69bc9
feat: define proper config payload type configPayload
pgimalac Nov 24, 2023
59cd5c5
merge with main
pgimalac Nov 27, 2023
e457dc9
renaming configs to agent_ipc_host/port
pgimalac Nov 28, 2023
44113ca
directly return error as string, or marshalled value
pgimalac Nov 28, 2023
69606a1
disable new endpoint by default
pgimalac Nov 28, 2023
4b62f52
rename extraHosts additionalHostIdentities
pgimalac Nov 29, 2023
99993c7
add some docs and logs, clean up stop server
pgimalac Nov 29, 2023
d1c14cd
refactor: improve naming
pgimalac Nov 29, 2023
c21c66d
move config endpoint to separate function
pgimalac Nov 29, 2023
79ba640
add logs to config endpoint on failure and success
pgimalac Nov 29, 2023
23f8c5b
only add the host without port to the certificate
pgimalac Nov 29, 2023
3a04b99
refactor: move config endpoint to separate directory, add tests
pgimalac Dec 1, 2023
6d1e442
test logic around enabling ipc server
pgimalac Dec 1, 2023
9ecb46c
add expvar metrics in config endpoint
pgimalac Dec 1, 2023
fa3bfa2
rename endpoint server and path
pgimalac Dec 1, 2023
ed704ae
merge with main
pgimalac Dec 1, 2023
6071290
refactor: rewrite test cases as structs
pgimalac Dec 2, 2023
27b9332
early return getIPCServerAddressPort
pgimalac Dec 2, 2023
b7ea9f8
Update pkg/api/security/security.go
pgimalac Dec 4, 2023
a6abb74
clean up variable name and types
pgimalac Dec 4, 2023
70fd44c
simplify test naming
pgimalac Dec 4, 2023
36685a1
refactor: remove global state, clean test naming
pgimalac Dec 4, 2023
72d395d
chore: log in case of answer write error
pgimalac Dec 4, 2023
d6763cf
pass config as parameter to test helper
pgimalac Dec 4, 2023
fde8155
feat: test expvars
pgimalac Dec 4, 2023
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
73 changes: 73 additions & 0 deletions cmd/agent/api/internal/config/endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

// Package config defines the config endpoint of the IPC API Server.
package config

import (
"encoding/json"
"fmt"
"net/http"

gorilla "github.com/gorilla/mux"

"github.com/DataDog/datadog-agent/pkg/config"
"github.com/DataDog/datadog-agent/pkg/util/log"
)

type authorizedSet map[string]struct{}

var authorizedConfigPaths = authorizedSet{
"api_key": {},
}

// GetConfigEndpointMux builds and returns the mux for the config endpoint
func GetConfigEndpointMux(cfg config.Reader) *gorilla.Router {
return getConfigEndpointMux(cfg, authorizedConfigPaths)
}

func getConfigEndpointMux(cfg config.Reader, allowedConfigPaths map[string]struct{}) *gorilla.Router {
configEndpointHandler := func(w http.ResponseWriter, r *http.Request) {
body, statusCode, err := getConfigValueAsJSON(cfg, r, allowedConfigPaths)
if err != nil {
http.Error(w, err.Error(), statusCode)
return
}

w.WriteHeader(statusCode)
_, _ = w.Write(body)
}

configEndpointMux := gorilla.NewRouter()
configEndpointMux.HandleFunc("/", configEndpointHandler).Methods("GET")
configEndpointMux.HandleFunc("/{path}", configEndpointHandler).Methods("GET")

return configEndpointMux
}

// returns the marshalled JSON value of the config path requested
// or an error and http status code in case of failure
func getConfigValueAsJSON(cfg config.Reader, r *http.Request, allowedConfigPaths map[string]struct{}) ([]byte, int, error) {
vars := gorilla.Vars(r)
path := vars["path"]

if _, ok := allowedConfigPaths[path]; !ok {
log.Warnf("config endpoint received a request from '%s' for config '%s' which is not allowed", r.RemoteAddr, path)
return nil, http.StatusForbidden, fmt.Errorf("querying config value '%s' is not allowed", path)
}

log.Debug("config endpoint received a request from '%s' for config '%s'", r.RemoteAddr, path)
value := cfg.Get(path)
if value == nil {
return nil, http.StatusNotFound, fmt.Errorf("no runtime setting found for %s", path)
}

body, err := json.Marshal(value)
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("could not marshal config value of '%s': %v", path, err)
}

return body, http.StatusOK, nil
}
99 changes: 99 additions & 0 deletions cmd/agent/api/internal/config/endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

package config

import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"

"github.com/DataDog/datadog-agent/pkg/config"
)

func testConfigValue(t *testing.T, server *httptest.Server, configName string, expectedStatus int) {
t.Helper()

resp, err := server.Client().Get(server.URL + "/" + configName)
require.NoError(t, err)
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

require.Equal(t, expectedStatus, resp.StatusCode, string(body))

if resp.StatusCode != http.StatusOK {
return
}

var configValue interface{}
err = json.Unmarshal(body, &configValue)
require.NoError(t, err)

require.EqualValues(t, config.Datadog.Get(configName), configValue)
}

func TestConfigEndpoint(t *testing.T) {
t.Run("real allowed default", func(t *testing.T) {
cfg, server := getConfigServer(t, authorizedConfigPaths)
for configName := range authorizedConfigPaths {
var expectedStatus int
if cfg.IsSet(configName) {
expectedStatus = http.StatusOK
} else {
expectedStatus = http.StatusNotFound
}
testConfigValue(t, server, configName, expectedStatus)
}
})

t.Run("authorized existing", func(t *testing.T) {
configName := "my.config.value"
cfg, server := getConfigServer(t, authorizedSet{configName: {}})
cfg.SetWithoutSource(configName, "some_value")
testConfigValue(t, server, configName, http.StatusOK)
})

t.Run("authorized missing", func(t *testing.T) {
configName := "my.config.value"
_, server := getConfigServer(t, authorizedSet{configName: {}})
testConfigValue(t, server, configName, http.StatusNotFound)
})

t.Run("authorized not marshallable", func(t *testing.T) {
configName := "my.config.value"
cfg, server := getConfigServer(t, authorizedSet{configName: {}})
cfg.SetWithoutSource(configName, make(chan int))
testConfigValue(t, server, "my.config.value", http.StatusInternalServerError)
})

t.Run("unauthorized existing", func(t *testing.T) {
cfg, server := getConfigServer(t, authorizedSet{})
configName := "my.config.value"
cfg.SetWithoutSource(configName, "some_value")
testConfigValue(t, server, configName, http.StatusForbidden)
})

t.Run("unauthorized missing", func(t *testing.T) {
_, server := getConfigServer(t, authorizedSet{})
testConfigValue(t, server, "my.config.value", http.StatusForbidden)
})
pgimalac marked this conversation as resolved.
Show resolved Hide resolved
}

func getConfigServer(t *testing.T, authorizedConfigPaths map[string]struct{}) (*config.MockConfig, *httptest.Server) {
t.Helper()

cfg := config.Mock(t)
configEndpointMux := getConfigEndpointMux(cfg, authorizedConfigPaths)
pgimalac marked this conversation as resolved.
Show resolved Hide resolved
server := httptest.NewServer(configEndpointMux)
t.Cleanup(server.Close)

return cfg, server
}
49 changes: 4 additions & 45 deletions cmd/agent/api/server_ipc.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,30 @@ package api

import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"time"

gorilla "github.com/gorilla/mux"

configendpoint "github.com/DataDog/datadog-agent/cmd/agent/api/internal/config"
"github.com/DataDog/datadog-agent/pkg/config"
"github.com/DataDog/datadog-agent/pkg/util/log"
)

const ipc_server_name string = "IPC API Server"

var ipcListener net.Listener
var allowedConfigPaths = map[string]struct{}{
"api_key": {},
}

func startIPCServer(ipcServerAddr string, tlsConfig *tls.Config) (err error) {
ipcListener, err = getListener(ipcServerAddr)
if err != nil {
return err
}

configEndpointMux := configendpoint.GetConfigEndpointMux(config.Datadog)
configEndpointMux.Use(validateToken)
ipcMux := http.NewServeMux()
ipcMux.Handle(
"/config/",
http.StripPrefix("/config", getConfigEndpointMux()))
http.StripPrefix("/config", configEndpointMux))
pgimalac marked this conversation as resolved.
Show resolved Hide resolved

ipcServer := &http.Server{
Addr: ipcServerAddr,
Expand All @@ -48,42 +43,6 @@ func startIPCServer(ipcServerAddr string, tlsConfig *tls.Config) (err error) {
return nil
}

func getConfigEndpointMux() *gorilla.Router {
configEndpointHandler := func(w http.ResponseWriter, r *http.Request) {
body, err := getConfigValueAsJSON(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_, _ = w.Write(body)
}

configEndpointMux := gorilla.NewRouter()
configEndpointMux.HandleFunc("/", configEndpointHandler).Methods("GET")
configEndpointMux.HandleFunc("/{path}", configEndpointHandler).Methods("GET")
configEndpointMux.Use(validateToken)

return configEndpointMux
}

func getConfigValueAsJSON(r *http.Request) ([]byte, error) {
vars := gorilla.Vars(r)
path := vars["path"]

if _, ok := allowedConfigPaths[path]; !ok {
log.Warn("config endpoint received a request from '%s' for config '%s' which is not allowed", r.RemoteAddr, path)
return nil, fmt.Errorf("querying config value '%s' is not allowed", path)
}

log.Debug("config endpoint received a request from '%s' for config '%s'", r.RemoteAddr, path)
value := config.Datadog.Get(path)
if value == nil {
return nil, fmt.Errorf("no runtime setting found for %s", path)
}

return json.Marshal(value)
}

func stopIPCServer() {
stopServer(ipcListener, ipc_server_name)
}