diff --git a/cmd/mesh/mesh.go b/cmd/mesh/mesh.go index ac79014bb..233f78fbf 100644 --- a/cmd/mesh/mesh.go +++ b/cmd/mesh/mesh.go @@ -83,10 +83,7 @@ func traefikMeshCommand(config *Configuration) error { logger.Debugf("ACL mode enabled: %t", config.ACL) - apiServer, err := api.NewAPI(logger, config.APIPort, config.APIHost, clients.KubernetesClient(), config.Namespace) - if err != nil { - return fmt.Errorf("unable to create the API server: %w", err) - } + apiServer := api.NewAPI(logger, config.APIPort, config.APIHost, config.Namespace) ctr := controller.NewMeshController(clients, controller.Config{ ACLEnabled: config.ACL, diff --git a/docs/content/api.md b/docs/content/api.md index 117283526..b1e1b61eb 100644 --- a/docs/content/api.md +++ b/docs/content/api.md @@ -5,25 +5,22 @@ This can be useful when Traefik Mesh is not working as intended. The API is accessed via the controller pod, and for security reasons is not exposed via service. The API can be accessed by making a `GET` request to `http://:9000` combined with one of the following paths: -## `/api/configuration/current` +## `/api/configuration` This endpoint provides raw json of the current configuration built by the controller. !!! Note This may change on each request, as it is a live data structure. -## `/api/status/nodes` +## `/api/topology` -This endpoint provides a json array containing some details about the readiness of the Traefik Mesh nodes visible by the controller. -This endpoint will still return a 200 if there are no visible nodes. +This endpoint provides raw json of the current topology built by the controller. -## `/api/status/node/{traefik-mesh-pod-name}/configuration` +!!! Note + This may change on each request, as it is a live data structure. -This endpoint provides raw json of the current configuration on the Traefik Mesh node with the pod name given in `{traefik-mesh-pod-name}`. -This endpoint provides a 404 response if the pod cannot be found, or other non-200 status codes on other errors. -If errors are encountered, the error will be returned in the body, and logged on the controller. -## `/api/status/readiness` +## `/api/ready` This endpoint returns a 200 response if the controller has successfully started. Otherwise, it will return a 500. diff --git a/integration/testdata/traefik-mesh/controller-acl-disabled.yaml b/integration/testdata/traefik-mesh/controller-acl-disabled.yaml index 872b06946..866884ab5 100644 --- a/integration/testdata/traefik-mesh/controller-acl-disabled.yaml +++ b/integration/testdata/traefik-mesh/controller-acl-disabled.yaml @@ -128,7 +128,7 @@ spec: containerPort: 9000 readinessProbe: httpGet: - path: /api/status/readiness + path: /api/ready port: api initialDelaySeconds: 3 periodSeconds: 1 diff --git a/integration/testdata/traefik-mesh/controller-acl-enabled.yaml b/integration/testdata/traefik-mesh/controller-acl-enabled.yaml index 2bc5e0e5a..0f0d18be1 100644 --- a/integration/testdata/traefik-mesh/controller-acl-enabled.yaml +++ b/integration/testdata/traefik-mesh/controller-acl-enabled.yaml @@ -130,7 +130,7 @@ spec: containerPort: 9000 readinessProbe: httpGet: - path: /api/status/readiness + path: /api/ready port: api initialDelaySeconds: 3 periodSeconds: 1 diff --git a/integration/testdata/traefik-mesh/proxy.yaml b/integration/testdata/traefik-mesh/proxy.yaml index 0ab2e272a..7c19231b0 100644 --- a/integration/testdata/traefik-mesh/proxy.yaml +++ b/integration/testdata/traefik-mesh/proxy.yaml @@ -67,7 +67,7 @@ spec: - "--entryPoints.udp-15002.address=:15002/udp" - "--entryPoints.udp-15003.address=:15003/udp" - "--entryPoints.udp-15004.address=:15004/udp" - - "--providers.http.endpoint=http://traefik-mesh-controller.traefik-mesh.svc.cluster.local:9000/api/configuration/current" + - "--providers.http.endpoint=http://traefik-mesh-controller.traefik-mesh.svc.cluster.local:9000/api/configuration" - "--providers.http.pollInterval=100ms" - "--providers.http.pollTimeout=100ms" - "--api.dashboard=false" diff --git a/pkg/api/api.go b/pkg/api/api.go index eb7776e95..af259cd86 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -1,26 +1,17 @@ package api import ( - "context" "encoding/json" "fmt" - "io/ioutil" "net/http" "time" "github.com/gorilla/mux" "github.com/sirupsen/logrus" - "github.com/traefik/mesh/v2/pkg/k8s" "github.com/traefik/mesh/v2/pkg/provider" "github.com/traefik/mesh/v2/pkg/safe" "github.com/traefik/mesh/v2/pkg/topology" "github.com/traefik/traefik/v2/pkg/config/dynamic" - kubeerror "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" - listers "k8s.io/client-go/listers/core/v1" ) // API is an implementation of an api. @@ -32,37 +23,11 @@ type API struct { topology *safe.Safe namespace string - podLister listers.PodLister logger logrus.FieldLogger } -type podInfo struct { - Name string - IP string - Ready bool -} - // NewAPI creates a new api. -func NewAPI(logger logrus.FieldLogger, port int32, host string, client kubernetes.Interface, namespace string) (*API, error) { - informerFactory := informers.NewSharedInformerFactoryWithOptions(client, k8s.ResyncPeriod, - informers.WithNamespace(namespace), - informers.WithTweakListOptions(func(options *metav1.ListOptions) { - options.LabelSelector = k8s.ProxySelector().String() - })) - - podLister := informerFactory.Core().V1().Pods().Lister() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - informerFactory.Start(ctx.Done()) - - for t, ok := range informerFactory.WaitForCacheSync(ctx.Done()) { - if !ok { - return nil, fmt.Errorf("timed out while waiting for informer cache to sync: %s", t) - } - } - +func NewAPI(logger logrus.FieldLogger, port int32, host, namespace string) *API { router := mux.NewRouter() api := &API{ @@ -75,18 +40,15 @@ func NewAPI(logger logrus.FieldLogger, port int32, host string, client kubernete configuration: safe.New(provider.NewDefaultDynamicConfig()), topology: safe.New(topology.NewTopology()), readiness: safe.New(false), - podLister: podLister, namespace: namespace, logger: logger, } - router.HandleFunc("/api/configuration/current", api.getCurrentConfiguration) - router.HandleFunc("/api/topology/current", api.getCurrentTopology) - router.HandleFunc("/api/status/nodes", api.getMeshNodes) - router.HandleFunc("/api/status/node/{node}/configuration", api.getMeshNodeConfiguration) - router.HandleFunc("/api/status/readiness", api.getReadiness) + router.HandleFunc("/api/configuration", api.getConfiguration) + router.HandleFunc("/api/topology", api.getTopology) + router.HandleFunc("/api/ready", api.getReadiness) - return api, nil + return api } // SetReadiness sets the readiness flag in the API. @@ -95,8 +57,8 @@ func (a *API) SetReadiness(isReady bool) { a.logger.Debugf("API readiness: %t", isReady) } -// SetConfig sets the current dynamic configuration. -func (a *API) SetConfig(cfg *dynamic.Configuration) { +// SetConfiguration sets the current dynamic configuration. +func (a *API) SetConfiguration(cfg *dynamic.Configuration) { a.configuration.Set(cfg) } @@ -105,18 +67,18 @@ func (a *API) SetTopology(topo *topology.Topology) { a.topology.Set(topo) } -// getCurrentConfiguration returns the current configuration. -func (a *API) getCurrentConfiguration(w http.ResponseWriter, _ *http.Request) { +// getConfiguration returns the current configuration. +func (a *API) getConfiguration(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(a.configuration.Get()); err != nil { - a.logger.Errorf("Unable to serialize dynamic configuration: %v", err) + a.logger.Errorf("Unable to serialize configuration: %v", err) http.Error(w, "", http.StatusInternalServerError) } } -// getCurrentTopology returns the current topology. -func (a *API) getCurrentTopology(w http.ResponseWriter, _ *http.Request) { +// getTopology returns the current topology. +func (a *API) getTopology(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(a.topology.Get()); err != nil { @@ -140,88 +102,3 @@ func (a *API) getReadiness(w http.ResponseWriter, _ *http.Request) { http.Error(w, "", http.StatusInternalServerError) } } - -// getMeshNodes returns a list of mesh nodes visible from the controller, and some basic readiness info. -func (a *API) getMeshNodes(w http.ResponseWriter, _ *http.Request) { - podList, err := a.podLister.List(labels.Everything()) - if err != nil { - a.logger.Errorf("Unable to retrieve pod list: %v", err) - http.Error(w, "", http.StatusInternalServerError) - - return - } - - podInfoList := make([]podInfo, len(podList)) - - for i, pod := range podList { - readiness := true - - for _, status := range pod.Status.ContainerStatuses { - if !status.Ready { - // If there is a non-ready container, pod is not ready. - readiness = false - break - } - } - - podInfoList[i] = podInfo{ - Name: pod.Name, - IP: pod.Status.PodIP, - Ready: readiness, - } - } - - w.Header().Set("Content-Type", "application/json") - - if err := json.NewEncoder(w).Encode(podInfoList); err != nil { - a.logger.Errorf("Unable to serialize mesh nodes: %v", err) - http.Error(w, "", http.StatusInternalServerError) - } -} - -// getMeshNodeConfiguration returns the configuration for a named pod. -func (a *API) getMeshNodeConfiguration(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - pod, err := a.podLister.Pods(a.namespace).Get(vars["node"]) - if err != nil { - if kubeerror.IsNotFound(err) { - http.Error(w, "", http.StatusNotFound) - - return - } - - http.Error(w, "", http.StatusInternalServerError) - - return - } - - resp, err := http.Get(fmt.Sprintf("http://%s:8080/api/rawdata", pod.Status.PodIP)) - if err != nil { - a.logger.Errorf("Unable to get configuration from pod %q: %v", pod.Name, err) - http.Error(w, "", http.StatusBadGateway) - - return - } - - defer func() { - if closeErr := resp.Body.Close(); closeErr != nil { - a.logger.Errorf("Unable to close response body: %v", closeErr) - } - }() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - a.logger.Errorf("Unable to get configuration response body from pod %q: %v", pod.Name, err) - http.Error(w, "", http.StatusBadGateway) - - return - } - - w.Header().Set("Content-Type", "application/json") - - if _, err := w.Write(body); err != nil { - a.logger.Errorf("Unable to write mesh nodes: %v", err) - http.Error(w, "", http.StatusInternalServerError) - } -} diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index d37e8f3b2..081aa7ad9 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -1,32 +1,20 @@ package api import ( - "net" "net/http" "net/http/httptest" - "os" "testing" - "github.com/gorilla/mux" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/traefik/mesh/v2/pkg/k8s" - "k8s.io/client-go/kubernetes/fake" ) var localhost = "127.0.0.1" func TestEnableReadiness(t *testing.T) { - logger := logrus.New() + api := NewAPI(logrus.New(), 9000, localhost, "foo") - logger.SetOutput(os.Stdout) - logger.SetLevel(logrus.DebugLevel) - - client := fake.NewSimpleClientset() - api, err := NewAPI(logger, 9000, localhost, client, "foo") - - require.NoError(t, err) assert.Equal(t, false, api.readiness.Get().(bool)) api.SetReadiness(true) @@ -57,24 +45,14 @@ func TestGetReadiness(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - logger := logrus.New() - - logger.SetOutput(os.Stdout) - logger.SetLevel(logrus.DebugLevel) - - client := fake.NewSimpleClientset() - api, err := NewAPI(logger, 9000, localhost, client, "foo") + api := NewAPI(logrus.New(), 9000, localhost, "foo") - require.NoError(t, err) api.readiness.Set(test.readiness) res := httptest.NewRecorder() - req, err := http.NewRequest(http.MethodGet, "/api/status/readiness", nil) - if err != nil { - require.NoError(t, err) - return - } + req, err := http.NewRequest(http.MethodGet, "/api/ready", nil) + require.NoError(t, err) api.getReadiness(res, req) @@ -83,168 +61,32 @@ func TestGetReadiness(t *testing.T) { } } -func TestGetCurrentConfiguration(t *testing.T) { - logger := logrus.New() - - logger.SetOutput(os.Stdout) - logger.SetLevel(logrus.DebugLevel) +func TestGetConfiguration(t *testing.T) { + api := NewAPI(logrus.New(), 9000, localhost, "foo") - client := fake.NewSimpleClientset() - api, err := NewAPI(logger, 9000, localhost, client, "foo") - - require.NoError(t, err) api.configuration.Set("foo") res := httptest.NewRecorder() - req, err := http.NewRequest(http.MethodGet, "/api/configuration/current", nil) - if err != nil { - require.NoError(t, err) - return - } + req, err := http.NewRequest(http.MethodGet, "/api/configuration", nil) + require.NoError(t, err) - api.getCurrentConfiguration(res, req) + api.getConfiguration(res, req) assert.Equal(t, "\"foo\"\n", res.Body.String()) } -func TestGetMeshNodes(t *testing.T) { - testCases := []struct { - desc string - mockFile string - expectedBody string - expectedStatusCode int - podError bool - }{ - { - desc: "empty mesh node list", - mockFile: "getmeshnodes_empty.yaml", - expectedBody: "[]\n", - expectedStatusCode: http.StatusOK, - }, - { - desc: "one item in mesh node list", - mockFile: "getmeshnodes_one_mesh_pod.yaml", - expectedBody: "[{\"Name\":\"mesh-pod-1\",\"IP\":\"10.4.3.2\",\"Ready\":true}]\n", - expectedStatusCode: http.StatusOK, - }, - { - desc: "one item in mesh node list with non ready pod", - mockFile: "getmeshnodes_one_nonready_mesh_pod.yaml", - expectedBody: "[{\"Name\":\"mesh-pod-1\",\"IP\":\"10.4.19.1\",\"Ready\":false}]\n", - expectedStatusCode: http.StatusOK, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - logger := logrus.New() - - logger.SetOutput(os.Stdout) - logger.SetLevel(logrus.DebugLevel) - - clientMock := k8s.NewClientMock(t, test.mockFile) - api, err := NewAPI(logger, 9000, localhost, clientMock.KubernetesClient(), "foo") - - require.NoError(t, err) - - res := httptest.NewRecorder() - - req, err := http.NewRequest(http.MethodGet, "/api/status/nodes", nil) - if err != nil { - require.NoError(t, err) - return - } - - api.getMeshNodes(res, req) - - assert.Equal(t, test.expectedBody, res.Body.String()) - assert.Equal(t, test.expectedStatusCode, res.Code) - }) - } -} - -func TestGetMeshNodeConfiguration(t *testing.T) { - testCases := []struct { - desc string - mockFile string - expectedBody string - expectedStatusCode int - podError bool - }{ - { - desc: "simple mesh node configuration", - mockFile: "getmeshnodeconfiguration_simple.yaml", - expectedBody: "{test_configuration_json}", - expectedStatusCode: http.StatusOK, - }, - { - desc: "pod not found", - mockFile: "getmeshnodeconfiguration_empty.yaml", - expectedBody: "\n", - expectedStatusCode: http.StatusNotFound, - }, - } - - apiServer := startTestAPIServer("8080", http.StatusOK, []byte("{test_configuration_json}")) - defer apiServer.Close() - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - logger := logrus.New() - - logger.SetOutput(os.Stdout) - logger.SetLevel(logrus.DebugLevel) - - clientMock := k8s.NewClientMock(t, test.mockFile) - api, err := NewAPI(logger, 9000, localhost, clientMock.KubernetesClient(), "foo") - - require.NoError(t, err) - - res := httptest.NewRecorder() - - req, err := http.NewRequest(http.MethodGet, "/api/status/node/mesh-pod-1/configuration", nil) - if err != nil { - require.NoError(t, err) - return - } - - // fake gorilla/mux vars - vars := map[string]string{ - "node": "mesh-pod-1", - } - - req = mux.SetURLVars(req, vars) - - api.getMeshNodeConfiguration(res, req) +func TestGetTopology(t *testing.T) { + api := NewAPI(logrus.New(), 9000, localhost, "foo") - assert.Equal(t, test.expectedBody, res.Body.String()) - assert.Equal(t, test.expectedStatusCode, res.Code) - }) - } -} - -func startTestAPIServer(port string, statusCode int, bodyData []byte) (ts *httptest.Server) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(statusCode) - w.Header().Set("Content-Type", "application/json") + api.topology.Set("foo") - _, _ = w.Write(bodyData) - }) + res := httptest.NewRecorder() - listener, err := net.Listen("tcp", "127.0.0.1:"+port) - if err != nil { - panic(err) - } + req, err := http.NewRequest(http.MethodGet, "/api/topology", nil) + require.NoError(t, err) - ts = &httptest.Server{ - Listener: listener, - Config: &http.Server{Handler: handler}, - } - ts.Start() + api.getTopology(res, req) - return ts + assert.Equal(t, "\"foo\"\n", res.Body.String()) } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 268404789..a6936873b 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -41,7 +41,7 @@ const ( // SharedStore is used to share the controller state. type SharedStore interface { - SetConfig(cfg *dynamic.Configuration) + SetConfiguration(cfg *dynamic.Configuration) SetTopology(topo *topology.Topology) SetReadiness(isReady bool) } @@ -359,7 +359,7 @@ func (c *Controller) processNextWorkItem() bool { conf := c.provider.BuildConfig(topo) c.store.SetTopology(topo) - c.store.SetConfig(conf) + c.store.SetConfiguration(conf) c.workQueue.Forget(key) diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index 1a5ca42fd..5e08f16c4 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -23,9 +23,9 @@ const ( type storeMock struct{} -func (a *storeMock) SetConfig(_ *dynamic.Configuration) {} -func (a *storeMock) SetTopology(_ *topology.Topology) {} -func (a *storeMock) SetReadiness(_ bool) {} +func (a *storeMock) SetConfiguration(_ *dynamic.Configuration) {} +func (a *storeMock) SetTopology(_ *topology.Topology) {} +func (a *storeMock) SetReadiness(_ bool) {} func TestController_NewMeshController(t *testing.T) { store := &storeMock{}