diff --git a/api/handler.go b/api/handler.go index 0ea69015b8..8500e2fa52 100644 --- a/api/handler.go +++ b/api/handler.go @@ -35,10 +35,14 @@ const ( containersApi = "containers" subcontainersApi = "subcontainers" machineApi = "machine" + + version1_0 = "v1.0" + version1_1 = "v1.1" ) var supportedApiVersions map[string]struct{} = map[string]struct{}{ - "v1.0": struct{}{}, + version1_0: struct{}{}, + version1_1: struct{}{}, } func RegisterHandlers(m manager.Manager) error { @@ -107,28 +111,50 @@ func handleRequest(m manager.Manager, w http.ResponseWriter, r *http.Request) er log.Printf("Api - Container(%s)", containerName) - var query info.ContainerInfoRequest - - // If a user does not specify number of stats/samples he wants, - // it's 64 by default - query.NumStats = 64 - query.NumSamples = 64 - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&query) - if err != nil && err != io.EOF { - return fmt.Errorf("unable to decode the json value: ", err) + // Get the query request. + query, err := getContainerInfoRequest(r.Body) + if err != nil { + return err } + // Get the container. - cont, err := m.GetContainerInfo(containerName, &query) + cont, err := m.GetContainerInfo(containerName, query) if err != nil { - fmt.Fprintf(w, "Failed to get container \"%s\" with error: %s", containerName, err) - return err + return fmt.Errorf("failed to get container %q with error: %s", containerName, err) } // Only output the container as JSON. out, err := json.Marshal(cont) if err != nil { - fmt.Fprintf(w, "Failed to marshall container %q with error: %s", containerName, err) + return fmt.Errorf("failed to marshall container %q with error: %s", containerName, err) + } + w.Write(out) + case requestType == subcontainersApi: + if version == version1_0 { + return fmt.Errorf("request type of %q not supported in API version %q", requestType, version) + } + + // The container name is the path after the requestType. + containerName := path.Join("/", strings.Join(requestArgs, "/")) + + log.Printf("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. + out, err := json.Marshal(containers) + if err != nil { + return fmt.Errorf("failed to marshall container %q with error: %s", containerName, err) } w.Write(out) default: @@ -138,3 +164,19 @@ func handleRequest(m manager.Manager, w http.ResponseWriter, r *http.Request) er log.Printf("Request took %s", time.Since(start)) return nil } + +func getContainerInfoRequest(body io.ReadCloser) (*info.ContainerInfoRequest, error) { + var query info.ContainerInfoRequest + + // Default stats and samples is 64. + query.NumStats = 64 + query.NumSamples = 64 + + decoder := json.NewDecoder(body) + err := decoder.Decode(&query) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("unable to decode the json value: %s", err) + } + + return &query, nil +} diff --git a/manager/manager.go b/manager/manager.go index a472df5b9b..0fff4b8c54 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -17,6 +17,8 @@ package manager import ( "fmt" "log" + "path" + "strings" "sync" "time" @@ -32,6 +34,9 @@ type Manager interface { // Get information about a container. GetContainerInfo(containerName string, query *info.ContainerInfoRequest) (*info.ContainerInfo, error) + // Get information about all subcontainers of the specified container (includes self). + SubcontainersInfo(containerName string, query *info.ContainerInfoRequest) ([]*info.ContainerInfo, error) + // Get information about the machine. GetMachineInfo() (*info.MachineInfo, error) @@ -106,21 +111,24 @@ func (m *manager) Start() error { } // Get a container by name. -func (m *manager) GetContainerInfo(containerName string, query *info.ContainerInfoRequest) (*info.ContainerInfo, error) { - log.Printf("Get(%s); %+v", containerName, query) +func (self *manager) GetContainerInfo(containerName string, query *info.ContainerInfoRequest) (*info.ContainerInfo, error) { var cont *containerData var ok bool func() { - m.containersLock.RLock() - defer m.containersLock.RUnlock() + self.containersLock.RLock() + defer self.containersLock.RUnlock() // Ensure we have the container. - cont, ok = m.containers[containerName] + cont, ok = self.containers[containerName] }() if !ok { - return nil, fmt.Errorf("unknown container \"%s\"", containerName) + return nil, fmt.Errorf("unknown container %q", containerName) } + return self.containerDataToContainerInfo(cont, query) +} + +func (self *manager) containerDataToContainerInfo(cont *containerData, query *info.ContainerInfoRequest) (*info.ContainerInfo, error) { // Get the info from the container. cinfo, err := cont.GetInfo() if err != nil { @@ -130,7 +138,7 @@ func (m *manager) GetContainerInfo(containerName string, query *info.ContainerIn var percentiles *info.ContainerStatsPercentiles var samples []*info.ContainerStatsSample var stats []*info.ContainerStats - percentiles, err = m.storageDriver.Percentiles( + percentiles, err = self.storageDriver.Percentiles( cinfo.Name, query.CpuUsagePercentiles, query.MemoryUsagePercentiles, @@ -138,12 +146,12 @@ func (m *manager) GetContainerInfo(containerName string, query *info.ContainerIn if err != nil { return nil, err } - samples, err = m.storageDriver.Samples(cinfo.Name, query.NumSamples) + samples, err = self.storageDriver.Samples(cinfo.Name, query.NumSamples) if err != nil { return nil, err } - stats, err = m.storageDriver.RecentStats(cinfo.Name, query.NumStats) + stats, err = self.storageDriver.RecentStats(cinfo.Name, query.NumStats) if err != nil { return nil, err } @@ -165,12 +173,46 @@ func (m *manager) GetContainerInfo(containerName string, query *info.ContainerIn if ret.Spec.Memory != nil { // Memory.Limit is 0 means there's no limit if ret.Spec.Memory.Limit == 0 { - ret.Spec.Memory.Limit = uint64(m.machineInfo.MemoryCapacity) + ret.Spec.Memory.Limit = uint64(self.machineInfo.MemoryCapacity) } } return ret, nil } +func (self *manager) SubcontainersInfo(containerName string, query *info.ContainerInfoRequest) ([]*info.ContainerInfo, error) { + var containers []*containerData + func() { + self.containersLock.RLock() + defer self.containersLock.RUnlock() + containers = make([]*containerData, 0, len(self.containers)) + + // Get all the subcontainers of the specified container + matchedName := path.Join(containerName, "/") + for i := range self.containers { + name := self.containers[i].info.Name + if name == containerName || strings.HasPrefix(name, matchedName) { + containers = append(containers, self.containers[i]) + } + } + }() + if len(containers) == 0 { + return nil, fmt.Errorf("unknown container %q", containerName) + } + + // Get the info for each container. + output := make([]*info.ContainerInfo, 0, len(containers)) + for i := range containers { + cinfo, err := self.containerDataToContainerInfo(containers[i], query) + if err != nil { + // Skip containers with errors, we try to degrade gracefully. + continue + } + output = append(output, cinfo) + } + + return output, nil +} + func (m *manager) GetMachineInfo() (*info.MachineInfo, error) { // Copy and return the MachineInfo. ret := m.machineInfo @@ -200,7 +242,7 @@ func (m *manager) createContainer(containerName string) (*containerData, error) m.containers[alias] = cont } }() - log.Printf("Added container: %s (aliases: %s)", containerName, cont.info.Aliases) + log.Printf("Added container: %q (aliases: %s)", containerName, cont.info.Aliases) // Start the container's housekeeping. cont.Start() diff --git a/manager/manager_test.go b/manager/manager_test.go index cf48f406c8..857b62e008 100644 --- a/manager/manager_test.go +++ b/manager/manager_test.go @@ -71,19 +71,9 @@ func createManagerAndAddContainers( return nil } -func TestGetContainerInfo(t *testing.T) { - containers := []string{ - "/c1", - "/c2", - } - - query := &info.ContainerInfoRequest{ - NumStats: 256, - NumSamples: 128, - CpuUsagePercentiles: []int{10, 50, 90}, - MemoryUsagePercentiles: []int{10, 80, 90}, - } - +// Expect a manager with the specified containers and query. Returns the manager, map of ContainerInfo objects, +// and map of MockContainerHandler objects.} +func expectManagerWithContainers(containers []string, query *info.ContainerInfoRequest, t *testing.T) (*manager, map[string]*info.ContainerInfo, map[string]*container.MockContainerHandler) { infosMap := make(map[string]*info.ContainerInfo, len(containers)) handlerMap := make(map[string]*container.MockContainerHandler, len(containers)) @@ -142,6 +132,24 @@ func TestGetContainerInfo(t *testing.T) { t, ) + return m, infosMap, handlerMap +} + +func TestGetContainerInfo(t *testing.T) { + containers := []string{ + "/c1", + "/c2", + } + + query := &info.ContainerInfoRequest{ + NumStats: 256, + NumSamples: 128, + CpuUsagePercentiles: []int{10, 50, 90}, + MemoryUsagePercentiles: []int{10, 80, 90}, + } + + m, infosMap, handlerMap := expectManagerWithContainers(containers, query, t) + returnedInfos := make(map[string]*info.ContainerInfo, len(containers)) for _, container := range containers { @@ -162,3 +170,39 @@ func TestGetContainerInfo(t *testing.T) { } } + +func TestSubcontainersInfo(t *testing.T) { + containers := []string{ + "/c1", + "/c2", + } + + query := &info.ContainerInfoRequest{ + NumStats: 64, + NumSamples: 64, + CpuUsagePercentiles: []int{10, 50, 90}, + MemoryUsagePercentiles: []int{10, 80, 90}, + } + + m, _, _ := expectManagerWithContainers(containers, query, t) + + result, err := m.SubcontainersInfo("/", query) + if err != nil { + t.Fatalf("expected to succeed: %s", err) + } + if len(result) != len(containers) { + t.Errorf("expected to received containers: %v, but received: %v", containers, result) + } + for _, res := range result { + found := false + for _, name := range containers { + if res.Name == name { + found = true + break + } + } + if !found { + t.Errorf("unexpected container %q in result, expected one of %v", res.Name, containers) + } + } +}