This is an automated email from the ASF dual-hosted git repository.

ccondit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/yunikorn-core.git


The following commit(s) were added to refs/heads/master by this push:
     new 8d139b10 [YUNIKORN-2965] Move statedump and stack REST to debug 
endpoint (#992)
8d139b10 is described below

commit 8d139b10e8b05be63906b50a2f2de1db31e8a7ec
Author: Wilfred Spiegelenburg <[email protected]>
AuthorDate: Tue Nov 12 14:06:26 2024 -0600

    [YUNIKORN-2965] Move statedump and stack REST to debug endpoint (#992)
    
    The REST calls to get a statedump and the stacks of the system are
    exposed on the wrong endpoint. They belong in /debug and not in /ws/v1.
    Both are for troubleshooting only and the content is not stable.
    
    Placing a 301 redirect on the old endpoints for clients to follow.
    The proxy build into the web UI does not proxy the debug endpoints.
    
    Closes: #992
    
    Signed-off-by: Craig Condit <[email protected]>
---
 pkg/webservice/handlers.go        |  18 +++++++
 pkg/webservice/handlers_test.go   |  27 ++++++++++
 pkg/webservice/routes.go          | 100 ++++++++++++++++++++---------------
 pkg/webservice/webservice_test.go | 107 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 209 insertions(+), 43 deletions(-)

diff --git a/pkg/webservice/handlers.go b/pkg/webservice/handlers.go
index 1180b32e..d06d6234 100644
--- a/pkg/webservice/handlers.go
+++ b/pkg/webservice/handlers.go
@@ -66,6 +66,9 @@ const (
        AppStateActive    = "active"
        AppStateRejected  = "rejected"
        AppStateCompleted = "completed"
+
+       WSBase    = "/ws/v1"
+       DebugBase = "/debug"
 )
 
 var allowedActiveStatusMsg string
@@ -108,6 +111,21 @@ func init() {
        maxRESTResponseSize.Store(configs.DefaultRESTResponseSize)
 }
 
+// redirectDebug redirect calls that used to be part of "/ws/v1" to "/debug"
+// Moving the URl permanently for requests that should not have been part of 
the standard list
+func redirectDebug(w http.ResponseWriter, r *http.Request) {
+       code := http.StatusMovedPermanently
+       // replace the path: we know we have the right URL as the router would 
not have called this
+       r.URL.Path = strings.Replace(r.URL.Path, WSBase, DebugBase, 1)
+       redirect := r.URL.String()
+       w.Header().Set("Content-Type", "text/html; charset=utf-8")
+       w.Header().Set("Location", redirect)
+       w.WriteHeader(code)
+       // do as recommended in the RFC 7321, 6.4.2
+       body := "<a href=\"" + redirect + "\">" + http.StatusText(code) + 
"</a>.\n"
+       _, _ = fmt.Fprintln(w, body)
+}
+
 func getStackInfo(w http.ResponseWriter, r *http.Request) {
        writeHeaders(w)
        var stack = func() []byte {
diff --git a/pkg/webservice/handlers_test.go b/pkg/webservice/handlers_test.go
index e3465b89..4ffb231c 100644
--- a/pkg/webservice/handlers_test.go
+++ b/pkg/webservice/handlers_test.go
@@ -2960,6 +2960,33 @@ func TestGetPartitionRuleHandler(t *testing.T) {
        assert.Equal(t, partitionRules[3].Name, types.Recovery)
 }
 
+func TestRedirectDebugHandler(t *testing.T) {
+       NewWebApp(&scheduler.ClusterContext{}, nil)
+       base := "http://yunikorn.host.com:9080";
+       code := http.StatusMovedPermanently
+       tests := []struct {
+               name     string
+               reqURL   string
+               redirect string
+       }{
+               {"statedump", "/ws/v1/fullstatedump", "/debug/fullstatedump"},
+               {"stacks", "/ws/v1/stack", "/debug/stack"},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       req, err := createRequest(t, base+tt.reqURL, 
map[string]string{})
+                       assert.NilError(t, err, httpRequestError)
+                       resp := &MockResponseWriter{}
+                       redirectDebug(resp, req)
+                       assert.Equal(t, resp.statusCode, code, "expected moved 
permanently status")
+                       loc := resp.Header().Get("Location")
+                       assert.Assert(t, loc != "", "expected redirect header 
to be set")
+                       assert.Assert(t, strings.HasSuffix(loc, tt.redirect), 
"expected redirect to debug")
+                       assert.Assert(t, 
strings.Contains(string(resp.outputBytes), http.StatusText(code)))
+               })
+       }
+}
+
 func TestSetMaxRESTResponseSize(t *testing.T) {
        current := configs.GetConfigMap()
        defer configs.SetConfigMap(current)
diff --git a/pkg/webservice/routes.go b/pkg/webservice/routes.go
index 3a03ff5b..50c5e20a 100644
--- a/pkg/webservice/routes.go
+++ b/pkg/webservice/routes.go
@@ -40,40 +40,26 @@ var webRoutes = routes{
                "/ws/v1/clusters",
                getClusterInfo,
        },
-
-       // endpoint to retrieve goroutines info
        route{
-               "Scheduler",
-               "GET",
-               "/ws/v1/stack",
-               getStackInfo,
-       },
-
-       // endpoint to retrieve server metrics
-       route{
-               "Scheduler",
+               "Cluster",
                "GET",
                "/ws/v1/metrics",
                getMetrics,
        },
-
-       // endpoint to retrieve the current conf
        route{
-               "Scheduler",
+               "Cluster",
                "GET",
                "/ws/v1/config",
                getClusterConfig,
        },
-
-       // endpoint to validate conf
        route{
-               "Scheduler",
+               "Cluster",
                "POST",
                "/ws/v1/validate-conf",
                validateConf,
        },
 
-       // endpoint to retrieve historical data
+       // endpoints to retrieve general scheduler info
        route{
                "Scheduler",
                "GET",
@@ -87,7 +73,7 @@ var webRoutes = routes{
                getContainerHistory,
        },
        route{
-               "Partitions",
+               "Scheduler",
                "GET",
                "/ws/v1/partitions",
                getPartitions,
@@ -176,12 +162,6 @@ var webRoutes = routes{
                "/ws/v1/partition/:partition/usage/group/:group",
                getGroupResourceUsage,
        },
-       route{
-               "Scheduler",
-               "GET",
-               "/ws/v1/fullstatedump",
-               getFullStateDump,
-       },
        route{
                "Scheduler",
                "GET",
@@ -194,10 +174,37 @@ var webRoutes = routes{
                "/ws/v1/events/stream",
                getStream,
        },
-       // endpoint to retrieve CPU, Memory profiling data,
-       // this works with pprof tool. By default, pprof endpoints
-       // are only registered to http.DefaultServeMux. Here, we
-       // need to explicitly register all handlers.
+       route{
+               "Scheduler",
+               "GET",
+               "/ws/v1/scheduler/healthcheck",
+               checkHealthStatus,
+       },
+       route{
+               "Scheduler",
+               "GET",
+               "/ws/v1/scheduler/node-utilizations",
+               getNodeUtilisations,
+       },
+
+       // endpoints to retrieve debug info
+       //
+       // These endpoints are not to be proxied by the web server. The content 
is not for general consumption.
+       // The content is not considered stable and can change from release to 
release.
+       // All pprof endpoints provide profiling data in the format expected by 
the pprof visualization tool.
+       // We need to explicitly register all handlers as we do not use the 
DefaultServeMux
+       route{
+               Name:        "System",
+               Method:      "GET",
+               Pattern:     "/debug/stack",
+               HandlerFunc: getStackInfo,
+       },
+       route{
+               Name:        "System",
+               Method:      "GET",
+               Pattern:     "/debug/fullstatedump",
+               HandlerFunc: getFullStateDump,
+       },
        route{
                Name:        "System",
                Method:      "GET",
@@ -264,24 +271,31 @@ var webRoutes = routes{
                Pattern:     "/debug/pprof/trace",
                HandlerFunc: pprof.Trace,
        },
-       // endpoint to check health status
+
+       // Deprecated REST calls
+       //
+       // Replaced with /ws/v1/scheduler/node-utilizations as part of YuniKorn 
1.5
+       // Remove as part of YuniKorn 1.8
        route{
-               "Scheduler",
-               "GET",
-               "/ws/v1/scheduler/healthcheck",
-               checkHealthStatus,
+               Name:        "Scheduler",
+               Method:      "GET",
+               Pattern:     "/ws/v1/scheduler/node-utilization",
+               HandlerFunc: getNodeUtilisation,
        },
-       // Deprecated - To be removed in next major release. Replaced with 
/ws/v1/scheduler/node-utilizations
+       // Permanently moved to the debug endpoint as part of YuniKorn 1.7
+       // Remove redirect in YuniKorn 1.10
        route{
-               "Scheduler",
-               "GET",
-               "/ws/v1/scheduler/node-utilization",
-               getNodeUtilisation,
+               Name:        "Scheduler",
+               Method:      "GET",
+               Pattern:     "/ws/v1/stack",
+               HandlerFunc: redirectDebug,
        },
+       // Permanently moved to the debug endpoint as part of YuniKorn 1.7
+       // Remove redirect in YuniKorn 1.10
        route{
-               "Scheduler",
-               "GET",
-               "/ws/v1/scheduler/node-utilizations",
-               getNodeUtilisations,
+               Name:        "Scheduler",
+               Method:      "GET",
+               Pattern:     "/ws/v1/fullstatedump",
+               HandlerFunc: redirectDebug,
        },
 }
diff --git a/pkg/webservice/webservice_test.go 
b/pkg/webservice/webservice_test.go
new file mode 100644
index 00000000..1b723f4a
--- /dev/null
+++ b/pkg/webservice/webservice_test.go
@@ -0,0 +1,107 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you 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 webservice
+
+import (
+       "fmt"
+       "io"
+       "net/http"
+       "testing"
+
+       "gotest.tools/v3/assert"
+
+       "github.com/apache/yunikorn-core/pkg/metrics/history"
+       "github.com/apache/yunikorn-core/pkg/scheduler"
+)
+
+func Test_RedirectDebugHandler(t *testing.T) {
+       defer ResetIMHistory()
+       s := NewWebApp(&scheduler.ClusterContext{}, 
history.NewInternalMetricsHistory(5))
+       s.StartWebApp()
+       defer func(s *WebService) {
+               err := s.StopWebApp()
+               if err != nil {
+                       t.Fatal("failed to stop webapp")
+               }
+       }(s)
+       base := "http://localhost:9080";
+       tests := []struct {
+               name     string
+               reqURL   string
+               redirect string
+       }{
+               {"statedump", "/ws/v1/fullstatedump", "/debug/fullstatedump"},
+               {"stacks", "/ws/v1/stack", "/debug/stack"},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       client := &http.Client{
+                               CheckRedirect: func(req *http.Request, via 
[]*http.Request) error {
+                                       if req.URL.Path != tt.redirect {
+                                               return fmt.Errorf("expected 
redirect to '%s' got '%s'", tt.redirect, req.URL.Path)
+                                       }
+                                       return nil
+                               },
+                       }
+                       resp, err := client.Get(base + tt.reqURL)
+                       assert.NilError(t, err, "unexpected error returned")
+                       _ = resp.Body.Close() // not interested in the error
+                       assert.Equal(t, resp.StatusCode, http.StatusOK, 
"expected OK after redirect")
+               })
+       }
+}
+
+func Test_RouterHandling(t *testing.T) {
+       s := NewWebApp(&scheduler.ClusterContext{}, nil)
+       s.StartWebApp()
+       defer func(s *WebService) {
+               err := s.StopWebApp()
+               if err != nil {
+                       t.Fatal("failed to stop webapp")
+               }
+       }(s)
+       base := "http://localhost:9080";
+       client := &http.Client{}
+       // unsupported POST
+       resp, err := client.Post(base+"/ws/v1/clusters", "application/json; 
charset=UTF-8", nil)
+       assert.NilError(t, err, "unexpected error returned")
+       assert.Equal(t, resp.StatusCode, http.StatusMethodNotAllowed, "expected 
method not allowed")
+       var body []byte
+       body, err = io.ReadAll(resp.Body)
+       _ = resp.Body.Close() // not interested in the error
+       assert.NilError(t, err, "unexpected error reading body")
+       assert.Assert(t, body != nil, "expected body with status text")
+       resp, err = client.Head(base + "/ws/v1/clusters")
+       assert.NilError(t, err, "unexpected error returned")
+       body, err = io.ReadAll(resp.Body)
+       _ = resp.Body.Close()
+       assert.NilError(t, err, "unexpected error reading body")
+       assert.Assert(t, body != nil, "expected body with status text")
+       assert.Equal(t, resp.StatusCode, http.StatusMethodNotAllowed, "expected 
method not allowed")
+       // get with trailing slash
+       resp, err = client.Get(base + "/ws/v1/clusters/")
+       assert.NilError(t, err, "unexpected error returned")
+       _ = resp.Body.Close()
+       assert.Equal(t, resp.StatusCode, http.StatusOK, "expected OK")
+       // get with case difference
+       resp, err = client.Get(base + "/ws/v1/CLUSTERS")
+       assert.NilError(t, err, "unexpected error returned")
+       _ = resp.Body.Close()
+       assert.Equal(t, resp.StatusCode, http.StatusOK, "expected OK")
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to