From 77283196f68a413f58bbd7a5c69b87c551f6c72b Mon Sep 17 00:00:00 2001 From: Victor Marmol Date: Mon, 9 Feb 2015 12:51:02 -0800 Subject: [PATCH] Refactoring API version handling. This should make it easier to add future API versions and makes it a bit simpler to look through the API. --- api/handler.go | 201 +++++++++++++------------------------------ api/versions.go | 221 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 140 deletions(-) create mode 100644 api/versions.go diff --git a/api/handler.go b/api/handler.go index f480dc7f..68083f6b 100644 --- a/api/handler.go +++ b/api/handler.go @@ -21,6 +21,8 @@ import ( "io" "net/http" "path" + "regexp" + "sort" "strings" "time" @@ -30,26 +32,18 @@ import ( ) const ( - apiResource = "/api/" - containersApi = "containers" - subcontainersApi = "subcontainers" - machineApi = "machine" - dockerApi = "docker" - - version1_0 = "v1.0" - version1_1 = "v1.1" - version1_2 = "v1.2" + apiResource = "/api/" ) -var supportedApiVersions map[string]struct{} = map[string]struct{}{ - version1_0: {}, - version1_1: {}, - version1_2: {}, -} - func RegisterHandlers(m manager.Manager) error { + apiVersions := getApiVersions() + supportedApiVersions := make(map[string]ApiVersion, len(apiVersions)) + for _, v := range apiVersions { + supportedApiVersions[v.Version()] = v + } + http.HandleFunc(apiResource, func(w http.ResponseWriter, r *http.Request) { - err := handleRequest(m, w, r) + err := handleRequest(supportedApiVersions, m, w, r) if err != nil { http.Error(w, err.Error(), 500) } @@ -58,145 +52,68 @@ func RegisterHandlers(m manager.Manager) error { return nil } -func handleRequest(m manager.Manager, w http.ResponseWriter, r *http.Request) error { +// Captures the API version, requestType [optional], and remaining request [optional]. +var apiRegexp = regexp.MustCompile("/api/([^/]+)/?([^/]+)?(.*)") + +const ( + apiVersion = iota + 1 + apiRequestType + apiRequestArgs +) + +func handleRequest(supportedApiVersions map[string]ApiVersion, m manager.Manager, w http.ResponseWriter, r *http.Request) error { start := time.Now() - defer glog.V(2).Infof("Request took %s", time.Since(start)) + defer func() { + glog.V(2).Infof("Request took %s", time.Since(start)) + }() request := r.URL.Path - requestElements := strings.Split(r.URL.Path, "/") - // Verify that we have all the elements we expect: - // /api//[/] - // [0] [1] [2] [3] [4...] - if len(requestElements) < 4 { + const apiPrefix = "/api" + if !strings.HasPrefix(request, apiPrefix) { return fmt.Errorf("incomplete API request %q", request) } - // Get all the element parts. - emptyElement := requestElements[0] - apiElement := requestElements[1] - version := requestElements[2] - requestType := requestElements[3] - requestArgs := []string{} - if len(requestElements) > 4 { - requestArgs = requestElements[4:] + // If the request doesn't have an API version, list those. + if request == apiPrefix || request == apiResource { + versions := make([]string, 0, len(supportedApiVersions)) + for v := range supportedApiVersions { + versions = append(versions, v) + } + sort.Strings(versions) + fmt.Fprintf(w, "Supported API versions: %s", strings.Join(versions, ",")) + return nil } - // The container name is the path after the requestType. - containerName := path.Join("/", strings.Join(requestArgs, "/")) + // Verify that we have all the elements we expect: + // //[/] + requestElements := apiRegexp.FindStringSubmatch(request) + if len(requestElements) == 0 { + return fmt.Errorf("malformed request %q", request) + } + version := requestElements[apiVersion] + requestType := requestElements[apiRequestType] + requestArgs := strings.Split(requestElements[apiRequestArgs], "/") - // Check elements. - if len(emptyElement) != 0 { - return fmt.Errorf("unexpected API request format %q", request) - } - if apiElement != "api" { - return fmt.Errorf("invalid API request format %q", request) - } - if _, ok := supportedApiVersions[version]; !ok { + // Check supported versions. + versionHandler, ok := supportedApiVersions[version] + if !ok { return fmt.Errorf("unsupported API version %q", version) } - switch { - case requestType == machineApi: - glog.V(2).Infof("Api - Machine") - - // Get the MachineInfo - machineInfo, err := m.GetMachineInfo() - if err != nil { - return err - } - - err = writeResult(machineInfo, w) - if err != nil { - return err - } - case requestType == containersApi: - glog.V(2).Infof("Api - Container(%s)", containerName) - - // Get the query request. - query, err := getContainerInfoRequest(r.Body) - if err != nil { - return err - } - - // Get the container. - cont, err := m.GetContainerInfo(containerName, query) - if err != nil { - return fmt.Errorf("failed to get container %q with error: %s", containerName, err) - } - - // Only output the container as JSON. - err = writeResult(cont, w) - if err != nil { - return err - } - case requestType == subcontainersApi: - if version == version1_0 { - return fmt.Errorf("request type of %q not supported in API version %q", requestType, version) - } - - glog.V(2).Infof("Api - Subcontainers(%s)", containerName) - - // Get the query request. - query, err := getContainerInfoRequest(r.Body) - if err != nil { - return err - } - - // Get the subcontainers. - containers, err := m.SubcontainersInfo(containerName, query) - if err != nil { - return fmt.Errorf("failed to get subcontainers for container %q with error: %s", containerName, err) - } - - // Only output the containers as JSON. - err = writeResult(containers, w) - if err != nil { - return err - } - case requestType == dockerApi: - if version == version1_0 || version == version1_1 { - return fmt.Errorf("request type of %q not supported in API version %q", requestType, version) - } - - containerName = strings.TrimLeft(containerName, "/") - glog.V(2).Infof("Api - Docker(%s)", containerName) - - // Get the query request. - query, err := getContainerInfoRequest(r.Body) - if err != nil { - return err - } - - var containers map[string]info.ContainerInfo - if containerName == "" { - // Get all Docker containers. - containers, err = m.AllDockerContainers(query) - if err != nil { - return fmt.Errorf("failed to get all Docker containers with error: %v", err) - } - } else { - // Get one Docker container. - var cont info.ContainerInfo - cont, err = m.DockerContainer(containerName, query) - if err != nil { - return fmt.Errorf("failed to get Docker container %q with error: %v", containerName, err) - } - containers = map[string]info.ContainerInfo{ - cont.Name: cont, - } - } - - // Only output the containers as JSON. - err = writeResult(containers, w) - if err != nil { - return err - } - default: - return fmt.Errorf("unknown API request type %q", requestType) + // If no request type, list possible request types. + if requestType == "" { + requestTypes := versionHandler.SupportedRequestTypes() + sort.Strings(requestTypes) + fmt.Fprintf(w, "Supported request types: %q", strings.Join(requestTypes, ",")) + return nil } - return nil + // Trim the first empty element from the request. + if len(requestArgs) > 0 && requestArgs[0] == "" { + requestArgs = requestArgs[1:] + } + return versionHandler.HandleRequest(requestType, requestArgs, m, w, r) } func writeResult(res interface{}, w http.ResponseWriter) error { @@ -224,3 +141,7 @@ func getContainerInfoRequest(body io.ReadCloser) (*info.ContainerInfoRequest, er return &query, nil } + +func getContainerName(request []string) string { + return path.Join("/", strings.Join(request, "/")) +} diff --git a/api/versions.go b/api/versions.go new file mode 100644 index 00000000..75c57b68 --- /dev/null +++ b/api/versions.go @@ -0,0 +1,221 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// 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. + +package api + +import ( + "fmt" + "net/http" + + "github.com/golang/glog" + "github.com/google/cadvisor/info" + "github.com/google/cadvisor/manager" +) + +const ( + containersApi = "containers" + subcontainersApi = "subcontainers" + machineApi = "machine" + dockerApi = "docker" +) + +// Interface for a cAdvisor API version +type ApiVersion interface { + // Returns the version string. + Version() string + + // List of supported API endpoints. + SupportedRequestTypes() []string + + // Handles a request. The second argument is the parameters after /api// + HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error +} + +// Gets all supported API versions. +func getApiVersions() []ApiVersion { + v1_0 := &version1_0{} + v1_1 := newVersion1_1(v1_0) + v1_2 := newVersion1_2(v1_1) + return []ApiVersion{v1_0, v1_1, v1_2} +} + +// API v1.0 + +type version1_0 struct { +} + +func (self *version1_0) Version() string { + return "v1.0" +} + +func (self *version1_0) SupportedRequestTypes() []string { + return []string{containersApi, machineApi} +} + +func (self *version1_0) HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error { + switch { + case requestType == machineApi: + glog.V(2).Infof("Api - Machine") + + // Get the MachineInfo + machineInfo, err := m.GetMachineInfo() + if err != nil { + return err + } + + err = writeResult(machineInfo, w) + if err != nil { + return err + } + case requestType == containersApi: + containerName := getContainerName(request) + glog.V(2).Infof("Api - Container(%s)", containerName) + + // Get the query request. + query, err := getContainerInfoRequest(r.Body) + if err != nil { + return err + } + + // Get the container. + cont, err := m.GetContainerInfo(containerName, query) + if err != nil { + return fmt.Errorf("failed to get container %q with error: %s", containerName, err) + } + + // Only output the container as JSON. + err = writeResult(cont, w) + if err != nil { + return err + } + default: + return fmt.Errorf("unknown request type %q", requestType) + } + return nil +} + +// API v1.1 + +type version1_1 struct { + baseVersion *version1_0 +} + +// v1.1 builds on v1.0. +func newVersion1_1(v *version1_0) *version1_1 { + return &version1_1{ + baseVersion: v, + } +} + +func (self *version1_1) Version() string { + return "v1.1" +} + +func (self *version1_1) SupportedRequestTypes() []string { + return append(self.baseVersion.SupportedRequestTypes(), subcontainersApi) +} + +func (self *version1_1) HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error { + switch { + case requestType == subcontainersApi: + containerName := getContainerName(request) + glog.V(2).Infof("Api - Subcontainers(%s)", containerName) + + // Get the query request. + query, err := getContainerInfoRequest(r.Body) + if err != nil { + return err + } + + // Get the subcontainers. + containers, err := m.SubcontainersInfo(containerName, query) + if err != nil { + return fmt.Errorf("failed to get subcontainers for container %q with error: %s", containerName, err) + } + + // Only output the containers as JSON. + err = writeResult(containers, w) + if err != nil { + return err + } + return nil + default: + return self.baseVersion.HandleRequest(requestType, request, m, w, r) + } +} + +// API v1.2 + +type version1_2 struct { + baseVersion *version1_1 +} + +// v1.2 builds on v1.1. +func newVersion1_2(v *version1_1) *version1_2 { + return &version1_2{ + baseVersion: v, + } +} + +func (self *version1_2) Version() string { + return "v1.2" +} + +func (self *version1_2) SupportedRequestTypes() []string { + return append(self.baseVersion.SupportedRequestTypes(), dockerApi) +} + +func (self *version1_2) HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error { + switch { + case requestType == dockerApi: + glog.V(2).Infof("Api - Docker(%v)", request) + + // Get the query request. + query, err := getContainerInfoRequest(r.Body) + if err != nil { + return err + } + + var containers map[string]info.ContainerInfo + switch len(request) { + case 0: + // Get all Docker containers. + containers, err = m.AllDockerContainers(query) + if err != nil { + return fmt.Errorf("failed to get all Docker containers with error: %v", err) + } + case 1: + // Get one Docker container. + var cont info.ContainerInfo + cont, err = m.DockerContainer(request[0], query) + if err != nil { + return fmt.Errorf("failed to get Docker container %q with error: %v", request[0], err) + } + containers = map[string]info.ContainerInfo{ + cont.Name: cont, + } + default: + return fmt.Errorf("unknown request for Docker container %v", request) + } + + // Only output the containers as JSON. + err = writeResult(containers, w) + if err != nil { + return err + } + return nil + default: + return self.baseVersion.HandleRequest(requestType, request, m, w, r) + } +}