Skip to content
This repository has been archived by the owner on Mar 11, 2021. It is now read-only.

Commit

Permalink
Add API to show resource usage for the current space (#2306)
Browse files Browse the repository at this point in the history
This PR adds a deployments-related API under /deployments/environments/spaces/:spaceID. This endpoint returns resource usage for each of the user's environments, divided into usage by the specified space and usage by all other spaces combined. Currently this only returns CPU and memory usage, and not objects such as pods or secrets.

There is an older /deployments/spaces/:spaceID/environments endpoint which has been left for backwards compatibility until the front-end is switched over completely.

Example usage:
Request: GET https://openshift.io/api/deployments/environments/spaces/$SPACE_ID
Response:

{
  "data": [
    {
      "attributes": {
        "name": "run",
        "other_usage": {
          "cpucores": {
            "quota": 2,
            "used": 0.488
          },
          "memory": {
            "quota": 1073741824,
            "used": 262144000
          },
          "persistent_volume_claims": {
            "quota": 1,
            "used": 0
          },
          "replication_controllers": {
            "quota": 20,
            "used": 6
          },
          "secrets": {
            "quota": 20,
            "used": 9
          },
          "services": {
            "quota": 5,
            "used": 3
          }
        },
        "space_usage": {
          "cpucores": 1.488,
          "memory": 799014912
        }
      },
      "id": "run",
      "type": "environment"
    }
  ]
}

This work was started by @chrislessard, which I have put the finishing touches on and opened the PR on his behalf.

Fixes: openshiftio/openshift.io#3129
  • Loading branch information
ebaron authored Oct 1, 2018
1 parent af5e62d commit c824993
Show file tree
Hide file tree
Showing 7 changed files with 5,234 additions and 14 deletions.
29 changes: 29 additions & 0 deletions controller/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ func (c *DeploymentsController) ShowSpace(ctx *app.ShowSpaceDeploymentsContext)
}

// ShowSpaceEnvironments runs the showSpaceEnvironments action.
// FIXME Remove this method once showSpaceEnvironments API is removed.
func (c *DeploymentsController) ShowSpaceEnvironments(ctx *app.ShowSpaceEnvironmentsDeploymentsContext) error {

kc, err := c.GetKubeClient(ctx)
Expand All @@ -411,6 +412,34 @@ func (c *DeploymentsController) ShowSpaceEnvironments(ctx *app.ShowSpaceEnvironm
return ctx.OK(res)
}

// ShowEnvironmentsBySpace runs the showEnvironmentsBySpace action.
func (c *DeploymentsController) ShowEnvironmentsBySpace(ctx *app.ShowEnvironmentsBySpaceDeploymentsContext) error {

kc, err := c.GetKubeClient(ctx)
defer cleanup(kc)
if err != nil {
return jsonapi.JSONErrorResponse(ctx, err)
}

kubeSpaceName, err := c.getSpaceNameFromSpaceID(ctx, ctx.SpaceID)
if err != nil || kubeSpaceName == nil {
return jsonapi.JSONErrorResponse(ctx, errors.NewNotFoundError("osio space", ctx.SpaceID.String()))
}

usage, err := kc.GetSpaceAndOtherEnvironmentUsage(*kubeSpaceName)

// Model the response
if err != nil {
return jsonapi.JSONErrorResponse(ctx, errs.Wrap(err, "error retrieving environments"))
}

res := &app.SpaceAndOtherEnvironmentUsageList{
Data: usage,
}

return ctx.OK(res)
}

// ShowAllEnvironments runs the showAllEnvironments action.
func (c *DeploymentsController) ShowAllEnvironments(ctx *app.ShowAllEnvironmentsDeploymentsContext) error {
kc, err := c.GetKubeClient(ctx)
Expand Down
109 changes: 106 additions & 3 deletions controller/deployments_blackbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ type deleteTestResults struct {
envName string
}

func (c *testKubeClient) DeleteDeployment(spaceName string, appName string, envName string) error {
c.deleteResults = &deleteTestResults{
func (kc *testKubeClient) DeleteDeployment(spaceName string, appName string, envName string) error {
kc.deleteResults = &deleteTestResults{
spaceName: spaceName,
appName: appName,
envName: envName,
}
return c.fixture.deleteDeploymentError
return kc.fixture.deleteDeploymentError
}

func (fixture *deploymentsTestFixture) GetAndCheckOSIOClient(ctx context.Context) (controller.OpenshiftIOClient, error) {
Expand Down Expand Up @@ -516,6 +516,7 @@ func TestShowDeploymentStatSeries(t *testing.T) {
func convertToTime(unixMillis int64) time.Time {
return time.Unix(0, unixMillis*int64(time.Millisecond))
}

func TestShowSpaceEnvironments(t *testing.T) {
// given
clientGetterMock := testcontroller.NewClientGetterMock(t)
Expand Down Expand Up @@ -585,6 +586,108 @@ func TestShowSpaceEnvironments(t *testing.T) {
})
}

func TestShowEnvironmentsBySpace(t *testing.T) {

fakeEnvName := "fakeEnvName"
fakeCPUCores := 6.0
fakeMemUsage := 5.0

fakeQuota := &app.SpaceEnvironmentUsageQuota{
Cpucores: &fakeCPUCores,
Memory: &fakeMemUsage,
}

genericQuota := 5.0
genericUsage := 1.0
fakeOtherUsage := &app.EnvStats{
Cpucores: &app.EnvStatQuota{
Quota: &genericQuota,
Used: &genericUsage,
},
Memory: &app.EnvStatQuota{
Quota: &genericQuota,
Used: &genericUsage,
},
}

fakeSpaceUse := []*app.SpaceAndOtherEnvironmentUsage{
{
Attributes: &app.SpaceAndOtherEnvironmentUsageAttributes{
Name: &fakeEnvName,
SpaceUsage: fakeQuota,
OtherUsage: fakeOtherUsage,
},
ID: fakeEnvName,
Type: "environment",
},
}

// given
clientGetterMock := testcontroller.NewClientGetterMock(t)
svc, ctrl, err := createDeploymentsController()
require.NoError(t, err)
ctrl.ClientGetter = clientGetterMock

t.Run("ok", func(t *testing.T) {
kubeClientMock := testk8s.NewKubeClientMock(t)

kubeClientMock.GetSpaceAndOtherEnvironmentUsageFunc = func(p string) ([]*app.SpaceAndOtherEnvironmentUsage, error) {
return fakeSpaceUse, nil
}

kubeClientMock.CloseFunc = func() {}

clientGetterMock.GetKubeClientFunc = func(p context.Context) (kubernetes.KubeClientInterface, error) {
return kubeClientMock, nil
}
osioClientMock := createOSIOClientMock(t, "testSpace")

clientGetterMock.GetAndCheckOSIOClientFunc = func(p context.Context) (controller.OpenshiftIOClient, error) {
return osioClientMock, nil
}

// when
_, result := test.ShowEnvironmentsBySpaceDeploymentsOK(t, context.Background(), svc, ctrl, space.SystemSpace)

require.Equal(t, fakeSpaceUse, result.Data, "deployment space environment information does match")

// then verify that the Close method was called
require.Equal(t, uint64(1), kubeClientMock.CloseCounter)
})

t.Run("failure", func(t *testing.T) {

t.Run("kube client init failure", func(t *testing.T) {
// given
clientGetterMock.GetKubeClientFunc = func(p context.Context) (r kubernetes.KubeClientInterface, r1 error) {
return nil, fmt.Errorf("failure")
}
// when/then
test.ShowEnvironmentsBySpaceDeploymentsInternalServerError(t, context.Background(), svc, ctrl, space.SystemSpace)
})

t.Run("get all space environments bad request", func(t *testing.T) {
// given
kubeClientMock := testk8s.NewKubeClientMock(t)
kubeClientMock.GetSpaceAndOtherEnvironmentUsageFunc = func(p string) ([]*app.SpaceAndOtherEnvironmentUsage, error) {
return nil, witerrors.NewBadParameterErrorFromString("TEST")
}
kubeClientMock.CloseFunc = func() {}
clientGetterMock.GetKubeClientFunc = func(p context.Context) (kubernetes.KubeClientInterface, error) {
return kubeClientMock, nil
}
osioClientMock := createOSIOClientMock(t, "testSpace")
clientGetterMock.GetAndCheckOSIOClientFunc = func(p context.Context) (controller.OpenshiftIOClient, error) {
return osioClientMock, nil
}
// when
test.ShowEnvironmentsBySpaceDeploymentsBadRequest(t, context.Background(), svc, ctrl, space.SystemSpace)
// then verify that the Close method was called
assert.Equal(t, uint64(1), kubeClientMock.CloseCounter)
})
})
}

func TestShowAllEnvironments(t *testing.T) {
// given
clientGetterMock := testcontroller.NewClientGetterMock(t)
Expand Down
51 changes: 49 additions & 2 deletions design/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,43 @@ var simpleDeploymentPodLimitRange = a.Type("SimpleDeploymentPodLimitRange", func
a.Attribute("limits", podsQuota)
})

var spaceAndOtherEnvironmentUsage = a.Type("SpaceAndOtherEnvironmentUsage", func() {
a.Description("Environment usage by specific space and all others")
a.Attribute("type", d.String, "The type of the related resource", func() {
a.Enum("environment")
})
a.Attribute("id", d.String, "ID of the environment (same as 'name')")
a.Attribute("attributes", spaceAndOtherEnvironmentAttributes)
a.Required("type", "id", "attributes")
})

var spaceAndOtherEnvironmentAttributes = a.Type("SpaceAndOtherEnvironmentUsageAttributes", func() {
a.Description("Attributes for environment usage info for a single space")
a.Attribute("name", d.String)
a.Attribute("space_usage", spaceEnvironmentUsageQuota)
a.Attribute("other_usage", envStats)
})

var spaceEnvironmentUsageQuota = a.Type("SpaceEnvironmentUsageQuota", func() {
a.Description("Quota info for space-aware environment usage")
a.Attribute("cpucores", d.Number)
a.Attribute("memory", d.Number)
})

var spaceAndOtherEnvironmentUsageMultiple = JSONList(
"spaceAndOtherEnvironmentUsage",
"Holds a response to environment usage for a space compared to other spaces",
spaceAndOtherEnvironmentUsage,
nil,
nil)

var simpleSpaceSingle = JSONSingle(
"SimpleSpace", "Holds a single response to a space request",
simpleSpace,
nil)

var simpleEnvironmentMultiple = JSONList(
"SimpleEnvironment", "Holds a response to a space/environment request",
"SimpleEnvironment", "Holds a response to a environment request",
simpleEnvironment,
nil,
nil)
Expand Down Expand Up @@ -293,11 +323,13 @@ var _ = a.Resource("deployments", func() {
a.Response(d.BadRequest, JSONAPIErrors)
})

// FIXME Keep original API around until frontend is completely moved over to
// showEnvironmentsBySpace, since this is a breaking change.
a.Action("showSpaceEnvironments", func() {
a.Routing(
a.GET("/spaces/:spaceID/environments"),
)
a.Description("list all environments for a space")
a.Description("DEPRECATED: please use /environments/spaces/:spaceID instead")
a.Params(func() {
a.Param("spaceID", d.UUID, "ID of the space")
})
Expand All @@ -308,6 +340,21 @@ var _ = a.Resource("deployments", func() {
a.Response(d.BadRequest, JSONAPIErrors)
})

a.Action("showEnvironmentsBySpace", func() {
a.Routing(
a.GET("/environments/spaces/:spaceID"),
)
a.Description("list all environments for a space and information for all others")
a.Params(func() {
a.Param("spaceID", d.UUID, "ID of the space")
})
a.Response(d.OK, spaceAndOtherEnvironmentUsageMultiple)
a.Response(d.Unauthorized, JSONAPIErrors)
a.Response(d.InternalServerError, JSONAPIErrors)
a.Response(d.NotFound, JSONAPIErrors)
a.Response(d.BadRequest, JSONAPIErrors)
})

a.Action("showAllEnvironments", func() {
a.Routing(
a.GET("/environments"),
Expand Down
73 changes: 73 additions & 0 deletions kubernetes/deployments_kubeclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type KubeClientInterface interface {
GetMetricsClient(envNS string) (Metrics, error)
WatchEventsInNamespace(nameSpace string) (*cache.FIFO, chan struct{})
GetDeploymentPodQuota(spaceName string, appName string, envName string) (*app.SimpleDeploymentPodLimitRange, error)
GetSpaceAndOtherEnvironmentUsage(spaceName string) ([]*app.SpaceAndOtherEnvironmentUsage, error)
Close()
KubeAccessControl
}
Expand Down Expand Up @@ -2364,3 +2365,75 @@ func (kc *kubeClient) WatchEventsInNamespace(nameSpace string) (*cache.FIFO, cha

return store, stopCh
}

func (kc *kubeClient) GetSpaceAndOtherEnvironmentUsage(spaceName string) ([]*app.SpaceAndOtherEnvironmentUsage, error) {

space, err := kc.GetSpace(spaceName)
if err != nil {
return nil, err
}

envs, err := kc.GetEnvironments()
if err != nil {
return nil, err
}

result := make([]*app.SpaceAndOtherEnvironmentUsage, 0, len(envs))
envMap := calculateEnvMap(space)
for _, env := range envs {
if env.Attributes.Name != nil && env.Attributes.Quota != nil {
envName := *env.Attributes.Name
otherUsage := *env.Attributes.Quota

spaceUsage, pres := envMap[envName]
if pres {
// Subtract usage by this space from total environment usage to determine
// usage by all other spaces combined
*otherUsage.Cpucores.Used -= *spaceUsage.Cpucores
*otherUsage.Memory.Used -= *spaceUsage.Memory
} else {
cpuUsage := float64(0)
memUsage := float64(0)
spaceUsage = &app.SpaceEnvironmentUsageQuota{
Cpucores: &cpuUsage,
Memory: &memUsage,
}
}

usage := &app.SpaceAndOtherEnvironmentUsage{
Attributes: &app.SpaceAndOtherEnvironmentUsageAttributes{
Name: &envName,
SpaceUsage: spaceUsage,
OtherUsage: &otherUsage,
},
ID: envName,
Type: "environment",
}

result = append(result, usage)
}
}

return result, nil
}

func calculateEnvMap(space *app.SimpleSpace) map[string]*app.SpaceEnvironmentUsageQuota {
envMap := make(map[string]*app.SpaceEnvironmentUsageQuota)
for _, appl := range space.Attributes.Applications {
for _, dep := range appl.Attributes.Deployments {
if value, ok := envMap[dep.Attributes.Name]; ok {
*value.Cpucores += *dep.Attributes.PodsQuota.Cpucores
*value.Memory += *dep.Attributes.PodsQuota.Memory
} else {
cpucores := *dep.Attributes.PodsQuota.Cpucores
memory := *dep.Attributes.PodsQuota.Memory
envMap[dep.Attributes.Name] = &app.SpaceEnvironmentUsageQuota{
Cpucores: &cpucores,
Memory: &memory,
}
}
}
}

return envMap
}
Loading

0 comments on commit c824993

Please sign in to comment.