2014-06-06 23:40:48 +00:00
|
|
|
/*
|
2015-05-01 16:19:44 +00:00
|
|
|
Copyright 2014 The Kubernetes Authors All rights reserved.
|
2014-06-06 23:40:48 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
2014-06-16 05:34:16 +00:00
|
|
|
|
2014-06-06 23:40:48 +00:00
|
|
|
package apiserver
|
|
|
|
|
|
|
|
import (
|
2014-11-25 23:11:43 +00:00
|
|
|
"bytes"
|
2014-07-29 21:35:20 +00:00
|
|
|
"encoding/json"
|
2014-12-12 01:25:07 +00:00
|
|
|
"fmt"
|
2015-03-22 03:25:38 +00:00
|
|
|
"io"
|
2014-06-06 23:40:48 +00:00
|
|
|
"io/ioutil"
|
2015-05-28 04:38:21 +00:00
|
|
|
"net"
|
2014-06-06 23:40:48 +00:00
|
|
|
"net/http"
|
2014-11-11 07:11:45 +00:00
|
|
|
"path"
|
2015-02-10 22:23:02 +00:00
|
|
|
"strconv"
|
2014-06-06 23:40:48 +00:00
|
|
|
"strings"
|
2014-06-19 04:04:11 +00:00
|
|
|
"time"
|
2014-06-17 01:03:44 +00:00
|
|
|
|
2015-01-06 16:44:43 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
|
2014-11-11 07:11:45 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
2015-04-15 23:33:35 +00:00
|
|
|
apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
2015-01-29 22:46:54 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
|
2015-03-21 16:24:16 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
|
2015-06-23 07:14:43 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/metrics"
|
2014-07-15 22:38:56 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/healthz"
|
2014-09-06 02:22:03 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
2015-01-27 01:54:29 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
2015-01-28 13:26:54 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
|
2015-04-06 16:58:00 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/flushwriter"
|
2014-07-25 19:28:20 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
|
2014-11-06 01:22:18 +00:00
|
|
|
|
2014-11-11 07:11:45 +00:00
|
|
|
"github.com/emicklei/go-restful"
|
2014-06-25 03:51:57 +00:00
|
|
|
"github.com/golang/glog"
|
2015-02-10 01:04:37 +00:00
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
2014-06-06 23:40:48 +00:00
|
|
|
)
|
|
|
|
|
2015-02-10 01:04:37 +00:00
|
|
|
func init() {
|
2015-06-23 07:14:43 +00:00
|
|
|
metrics.Register()
|
2015-02-10 01:04:37 +00:00
|
|
|
}
|
|
|
|
|
2015-02-11 22:07:23 +00:00
|
|
|
// monitorFilter creates a filter that reports the metrics for a given resource and action.
|
|
|
|
func monitorFilter(action, resource string) restful.FilterFunction {
|
|
|
|
return func(req *restful.Request, res *restful.Response, chain *restful.FilterChain) {
|
|
|
|
reqStart := time.Now()
|
|
|
|
chain.ProcessFilter(req, res)
|
2015-03-06 22:36:03 +00:00
|
|
|
httpCode := res.StatusCode()
|
2015-06-23 07:14:43 +00:00
|
|
|
metrics.Monitor(&action, &resource, util.GetClient(req.Request), &httpCode, reqStart)
|
2015-02-11 22:07:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-08-25 21:36:15 +00:00
|
|
|
// mux is an object that can register http handlers.
|
2014-10-23 20:56:18 +00:00
|
|
|
type Mux interface {
|
2014-08-09 21:12:55 +00:00
|
|
|
Handle(pattern string, handler http.Handler)
|
|
|
|
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
|
|
|
|
}
|
|
|
|
|
2015-03-21 16:24:16 +00:00
|
|
|
// APIGroupVersion is a helper for exposing rest.Storage objects as http.Handlers via go-restful
|
2014-06-06 23:40:48 +00:00
|
|
|
// It handles URLs of the form:
|
2014-08-09 21:12:55 +00:00
|
|
|
// /${storage_key}[/${object_name}]
|
2015-03-21 16:24:16 +00:00
|
|
|
// Where 'storage_key' points to a rest.Storage object stored in storage.
|
2015-06-16 02:39:31 +00:00
|
|
|
// This object should contain all parameterization necessary for running a particular API version
|
2014-11-11 07:11:45 +00:00
|
|
|
type APIGroupVersion struct {
|
2015-03-21 16:24:16 +00:00
|
|
|
Storage map[string]rest.Storage
|
2014-06-06 23:40:48 +00:00
|
|
|
|
2015-03-04 20:57:05 +00:00
|
|
|
Root string
|
|
|
|
Version string
|
|
|
|
|
2015-03-22 21:43:00 +00:00
|
|
|
// ServerVersion controls the Kubernetes APIVersion used for common objects in the apiserver
|
|
|
|
// schema like api.Status, api.DeleteOptions, and api.ListOptions. Other implementors may
|
2015-07-23 04:52:05 +00:00
|
|
|
// define a version "v1beta1" but want to use the Kubernetes "v1" internal objects. If
|
2015-03-22 21:43:00 +00:00
|
|
|
// empty, defaults to Version.
|
|
|
|
ServerVersion string
|
|
|
|
|
2015-03-04 20:57:05 +00:00
|
|
|
Mapper meta.RESTMapper
|
|
|
|
|
2015-03-22 21:43:00 +00:00
|
|
|
Codec runtime.Codec
|
|
|
|
Typer runtime.ObjectTyper
|
|
|
|
Creater runtime.ObjectCreater
|
|
|
|
Convertor runtime.ObjectConvertor
|
|
|
|
Linker runtime.SelfLinker
|
2015-03-04 20:57:05 +00:00
|
|
|
|
|
|
|
Admit admission.Interface
|
|
|
|
Context api.RequestContextMapper
|
2015-06-16 02:39:31 +00:00
|
|
|
|
|
|
|
ProxyDialerFn ProxyDialerFunc
|
|
|
|
MinRequestTimeout time.Duration
|
2014-08-09 21:12:55 +00:00
|
|
|
}
|
2014-08-06 17:06:42 +00:00
|
|
|
|
2015-06-16 02:39:31 +00:00
|
|
|
type ProxyDialerFunc func(network, addr string) (net.Conn, error)
|
|
|
|
|
2015-05-12 02:41:13 +00:00
|
|
|
// TODO: Pipe these in through the apiserver cmd line
|
|
|
|
const (
|
|
|
|
// Minimum duration before timing out read/write requests
|
|
|
|
MinTimeoutSecs = 300
|
|
|
|
// Maximum duration before timing out read/write requests
|
|
|
|
MaxTimeoutSecs = 600
|
|
|
|
)
|
|
|
|
|
2015-01-28 21:13:10 +00:00
|
|
|
// InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container.
|
2014-11-11 07:11:45 +00:00
|
|
|
// It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
|
|
|
|
// in a slash. A restful WebService is created for the group and version.
|
2015-06-16 02:39:31 +00:00
|
|
|
func (g *APIGroupVersion) InstallREST(container *restful.Container) error {
|
2015-03-04 20:57:05 +00:00
|
|
|
info := &APIRequestInfoResolver{util.NewStringSet(strings.TrimPrefix(g.Root, "/")), g.Mapper}
|
|
|
|
|
|
|
|
prefix := path.Join(g.Root, g.Version)
|
2015-02-09 14:47:13 +00:00
|
|
|
installer := &APIInstaller{
|
2015-05-27 01:39:42 +00:00
|
|
|
group: g,
|
|
|
|
info: info,
|
|
|
|
prefix: prefix,
|
2015-06-16 02:39:31 +00:00
|
|
|
minRequestTimeout: g.MinRequestTimeout,
|
|
|
|
proxyDialerFn: g.ProxyDialerFn,
|
2015-02-09 14:47:13 +00:00
|
|
|
}
|
2015-06-16 02:39:31 +00:00
|
|
|
ws, registrationErrors := installer.Install()
|
2014-11-11 07:11:45 +00:00
|
|
|
container.Add(ws)
|
2015-01-28 13:26:54 +00:00
|
|
|
return errors.NewAggregate(registrationErrors)
|
2014-11-11 07:11:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: document all handlers
|
|
|
|
// InstallSupport registers the APIServer support functions
|
2015-07-14 19:30:43 +00:00
|
|
|
func InstallSupport(mux Mux, ws *restful.WebService, enableResettingMetrics bool, checks ...healthz.HealthzChecker) {
|
2015-02-10 01:04:37 +00:00
|
|
|
// TODO: convert healthz and metrics to restful and remove container arg
|
2015-07-14 19:30:43 +00:00
|
|
|
healthz.InstallHandler(mux, checks...)
|
2015-02-10 01:04:37 +00:00
|
|
|
mux.Handle("/metrics", prometheus.Handler())
|
2015-06-23 07:14:43 +00:00
|
|
|
if enableResettingMetrics {
|
|
|
|
mux.HandleFunc("/resetMetrics", metrics.Reset)
|
|
|
|
}
|
2015-01-07 23:43:38 +00:00
|
|
|
|
|
|
|
// Set up a service to return the git code version.
|
|
|
|
ws.Path("/version")
|
|
|
|
ws.Doc("git code version from which this is built")
|
|
|
|
ws.Route(
|
|
|
|
ws.GET("/").To(handleVersion).
|
|
|
|
Doc("get the code version").
|
|
|
|
Operation("getCodeVersion").
|
|
|
|
Produces(restful.MIME_JSON).
|
|
|
|
Consumes(restful.MIME_JSON))
|
2014-06-06 23:40:48 +00:00
|
|
|
}
|
|
|
|
|
2014-10-09 23:26:34 +00:00
|
|
|
// InstallLogsSupport registers the APIServer log support function into a mux.
|
2014-10-23 20:56:18 +00:00
|
|
|
func InstallLogsSupport(mux Mux) {
|
2014-11-11 07:11:45 +00:00
|
|
|
// TODO: use restful: ws.Route(ws.GET("/logs/{logpath:*}").To(fileHandler))
|
|
|
|
// See github.com/emicklei/go-restful/blob/master/examples/restful-serve-static.go
|
2014-10-09 23:26:34 +00:00
|
|
|
mux.Handle("/logs/", http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/"))))
|
|
|
|
}
|
|
|
|
|
2015-04-23 07:06:59 +00:00
|
|
|
func InstallServiceErrorHandler(container *restful.Container, requestResolver *APIRequestInfoResolver, apiVersions []string) {
|
|
|
|
container.ServiceErrorHandler(func(serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) {
|
|
|
|
serviceErrorHandler(requestResolver, apiVersions, serviceErr, request, response)
|
|
|
|
})
|
2015-04-15 23:33:35 +00:00
|
|
|
}
|
|
|
|
|
2015-04-23 07:06:59 +00:00
|
|
|
func serviceErrorHandler(requestResolver *APIRequestInfoResolver, apiVersions []string, serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) {
|
|
|
|
requestInfo, err := requestResolver.GetAPIRequestInfo(request.Request)
|
|
|
|
codec := latest.Codec
|
|
|
|
if err == nil && requestInfo.APIVersion != "" {
|
|
|
|
// check if the api version is valid.
|
|
|
|
for _, version := range apiVersions {
|
|
|
|
if requestInfo.APIVersion == version {
|
|
|
|
// valid api version.
|
|
|
|
codec = runtime.CodecFor(api.Scheme, requestInfo.APIVersion)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
errorJSON(apierrors.NewGenericServerResponse(serviceErr.Code, "", "", "", "", 0, false), codec, response.ResponseWriter)
|
2015-04-15 23:33:35 +00:00
|
|
|
}
|
|
|
|
|
2015-01-07 23:43:38 +00:00
|
|
|
// Adds a service to return the supported api versions.
|
|
|
|
func AddApiWebService(container *restful.Container, apiPrefix string, versions []string) {
|
|
|
|
// TODO: InstallREST should register each version automatically
|
|
|
|
|
|
|
|
versionHandler := APIVersionHandler(versions[:]...)
|
2015-02-09 14:47:13 +00:00
|
|
|
ws := new(restful.WebService)
|
|
|
|
ws.Path(apiPrefix)
|
|
|
|
ws.Doc("get available API versions")
|
|
|
|
ws.Route(ws.GET("/").To(versionHandler).
|
|
|
|
Doc("get available API versions").
|
|
|
|
Operation("getAPIVersions").
|
2015-01-07 23:43:38 +00:00
|
|
|
Produces(restful.MIME_JSON).
|
|
|
|
Consumes(restful.MIME_JSON))
|
2015-02-09 14:47:13 +00:00
|
|
|
container.Add(ws)
|
2015-01-07 23:43:38 +00:00
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// handleVersion writes the server's version information.
|
2014-11-11 07:11:45 +00:00
|
|
|
func handleVersion(req *restful.Request, resp *restful.Response) {
|
|
|
|
// TODO: use restful's Response methods
|
|
|
|
writeRawJSON(http.StatusOK, version.Get(), resp.ResponseWriter)
|
2014-07-29 21:36:41 +00:00
|
|
|
}
|
|
|
|
|
2014-10-29 00:20:40 +00:00
|
|
|
// APIVersionHandler returns a handler which will list the provided versions as available.
|
2014-11-11 07:11:45 +00:00
|
|
|
func APIVersionHandler(versions ...string) restful.RouteFunction {
|
|
|
|
return func(req *restful.Request, resp *restful.Response) {
|
|
|
|
// TODO: use restful's Response methods
|
|
|
|
writeRawJSON(http.StatusOK, api.APIVersions{Versions: versions}, resp.ResponseWriter)
|
|
|
|
}
|
2014-10-29 00:20:40 +00:00
|
|
|
}
|
|
|
|
|
2015-03-25 20:21:40 +00:00
|
|
|
// write renders a returned runtime.Object to the response as a stream or an encoded object. If the object
|
|
|
|
// returned by the response implements rest.ResourceStreamer that interface will be used to render the
|
|
|
|
// response. The Accept header and current API version will be passed in, and the output will be copied
|
|
|
|
// directly to the response body. If content type is returned it is used, otherwise the content type will
|
|
|
|
// be "application/octet-stream". All other objects are sent to standard JSON serialization.
|
2015-03-22 03:25:38 +00:00
|
|
|
func write(statusCode int, apiVersion string, codec runtime.Codec, object runtime.Object, w http.ResponseWriter, req *http.Request) {
|
|
|
|
if stream, ok := object.(rest.ResourceStreamer); ok {
|
2015-04-06 16:58:00 +00:00
|
|
|
out, flush, contentType, err := stream.InputStream(apiVersion, req.Header.Get("Accept"))
|
2015-03-22 03:25:38 +00:00
|
|
|
if err != nil {
|
|
|
|
errorJSONFatal(err, codec, w)
|
|
|
|
return
|
|
|
|
}
|
2015-04-06 16:58:00 +00:00
|
|
|
if out == nil {
|
|
|
|
// No output provided - return StatusNoContent
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return
|
|
|
|
}
|
2015-03-22 03:25:38 +00:00
|
|
|
defer out.Close()
|
|
|
|
if len(contentType) == 0 {
|
|
|
|
contentType = "application/octet-stream"
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", contentType)
|
|
|
|
w.WriteHeader(statusCode)
|
2015-04-06 16:58:00 +00:00
|
|
|
writer := w.(io.Writer)
|
|
|
|
if flush {
|
|
|
|
writer = flushwriter.Wrap(w)
|
|
|
|
}
|
|
|
|
io.Copy(writer, out)
|
2015-03-22 03:25:38 +00:00
|
|
|
return
|
|
|
|
}
|
2015-06-08 23:33:58 +00:00
|
|
|
writeJSON(statusCode, codec, object, w, isPrettyPrint(req))
|
|
|
|
}
|
|
|
|
|
|
|
|
func isPrettyPrint(req *http.Request) bool {
|
|
|
|
pp := req.URL.Query().Get("pretty")
|
|
|
|
if len(pp) > 0 {
|
|
|
|
pretty, _ := strconv.ParseBool(pp)
|
|
|
|
return pretty
|
|
|
|
}
|
|
|
|
userAgent := req.UserAgent()
|
|
|
|
// This covers basic all browers and cli http tools
|
|
|
|
if strings.HasPrefix(userAgent, "curl") || strings.HasPrefix(userAgent, "Wget") || strings.HasPrefix(userAgent, "Mozilla/5.0") {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
2015-03-22 03:25:38 +00:00
|
|
|
}
|
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// writeJSON renders an object as JSON to the response.
|
2015-06-08 23:33:58 +00:00
|
|
|
func writeJSON(statusCode int, codec runtime.Codec, object runtime.Object, w http.ResponseWriter, pretty bool) {
|
2014-08-06 03:10:48 +00:00
|
|
|
output, err := codec.Encode(object)
|
2014-07-29 22:14:00 +00:00
|
|
|
if err != nil {
|
2014-12-12 01:25:07 +00:00
|
|
|
errorJSONFatal(err, codec, w)
|
2014-07-29 22:14:00 +00:00
|
|
|
return
|
|
|
|
}
|
2015-06-08 23:33:58 +00:00
|
|
|
if pretty {
|
|
|
|
// PR #2243: Pretty-print JSON by default.
|
|
|
|
formatted := &bytes.Buffer{}
|
|
|
|
err = json.Indent(formatted, output, "", " ")
|
|
|
|
if err != nil {
|
|
|
|
errorJSONFatal(err, codec, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
output = formatted.Bytes()
|
2014-11-25 23:11:43 +00:00
|
|
|
}
|
2014-07-29 22:54:20 +00:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
w.WriteHeader(statusCode)
|
2015-06-08 23:33:58 +00:00
|
|
|
w.Write(output)
|
2014-07-29 22:14:00 +00:00
|
|
|
}
|
|
|
|
|
2015-02-10 01:04:37 +00:00
|
|
|
// errorJSON renders an error to the response. Returns the HTTP status code of the error.
|
|
|
|
func errorJSON(err error, codec runtime.Codec, w http.ResponseWriter) int {
|
2014-07-31 18:26:34 +00:00
|
|
|
status := errToAPIStatus(err)
|
2015-06-08 23:33:58 +00:00
|
|
|
writeJSON(status.Code, codec, status, w, true)
|
2015-02-10 01:04:37 +00:00
|
|
|
return status.Code
|
2014-07-31 18:26:34 +00:00
|
|
|
}
|
|
|
|
|
2015-02-10 01:04:37 +00:00
|
|
|
// errorJSONFatal renders an error to the response, and if codec fails will render plaintext.
|
|
|
|
// Returns the HTTP status code of the error.
|
|
|
|
func errorJSONFatal(err error, codec runtime.Codec, w http.ResponseWriter) int {
|
2015-01-27 01:54:29 +00:00
|
|
|
util.HandleError(fmt.Errorf("apiserver was unable to write a JSON response: %v", err))
|
2014-12-12 01:25:07 +00:00
|
|
|
status := errToAPIStatus(err)
|
|
|
|
output, err := codec.Encode(status)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(status.Code)
|
|
|
|
fmt.Fprintf(w, "%s: %s", status.Reason, status.Message)
|
2015-02-10 01:04:37 +00:00
|
|
|
return status.Code
|
2014-12-12 01:25:07 +00:00
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
w.WriteHeader(status.Code)
|
|
|
|
w.Write(output)
|
2015-02-10 01:04:37 +00:00
|
|
|
return status.Code
|
2014-12-12 01:25:07 +00:00
|
|
|
}
|
|
|
|
|
2014-07-29 22:14:00 +00:00
|
|
|
// writeRawJSON writes a non-API object in JSON.
|
|
|
|
func writeRawJSON(statusCode int, object interface{}, w http.ResponseWriter) {
|
2014-11-25 23:11:43 +00:00
|
|
|
output, err := json.MarshalIndent(object, "", " ")
|
2014-07-29 22:14:00 +00:00
|
|
|
if err != nil {
|
2014-07-31 18:26:34 +00:00
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
2014-07-29 22:14:00 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
w.WriteHeader(statusCode)
|
|
|
|
w.Write(output)
|
|
|
|
}
|
|
|
|
|
2014-07-29 22:13:02 +00:00
|
|
|
func parseTimeout(str string) time.Duration {
|
|
|
|
if str != "" {
|
|
|
|
timeout, err := time.ParseDuration(str)
|
|
|
|
if err == nil {
|
|
|
|
return timeout
|
|
|
|
}
|
2014-11-20 10:00:36 +00:00
|
|
|
glog.Errorf("Failed to parse %q: %v", str, err)
|
2014-07-29 22:13:02 +00:00
|
|
|
}
|
2015-03-13 19:24:33 +00:00
|
|
|
// TODO: change back to 30s once #5180 is fixed
|
|
|
|
return 2 * time.Minute
|
2014-07-29 22:13:02 +00:00
|
|
|
}
|
|
|
|
|
2014-07-29 22:10:29 +00:00
|
|
|
func readBody(req *http.Request) ([]byte, error) {
|
|
|
|
defer req.Body.Close()
|
|
|
|
return ioutil.ReadAll(req.Body)
|
|
|
|
}
|
2014-08-06 17:06:42 +00:00
|
|
|
|
2014-09-02 10:00:28 +00:00
|
|
|
// splitPath returns the segments for a URL path.
|
2014-08-06 17:06:42 +00:00
|
|
|
func splitPath(path string) []string {
|
|
|
|
path = strings.Trim(path, "/")
|
|
|
|
if path == "" {
|
|
|
|
return []string{}
|
|
|
|
}
|
|
|
|
return strings.Split(path, "/")
|
|
|
|
}
|