Skip to content

Commit

Permalink
Merge pull request #80 from kubecost/mmd/port-forward
Browse files Browse the repository at this point in the history
Use port forwarding instead of API server proxying
  • Loading branch information
michaelmdresser authored May 6, 2021
2 parents ec08776 + 5f5850a commit 17490c3
Show file tree
Hide file tree
Showing 12 changed files with 382 additions and 31 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ The following flags modify the behavior of the subcommands:
--service-name string The name of the kubecost cost analyzer service. Change if you're running a non-standard deployment, like the staging helm chart. (default "kubecost-cost-analyzer")
-n, --namespace string Limit results to only one namespace. Defaults to all namespaces.
-N, --kubecost-namespace string The namespace that kubecost is deployed in. Requests to the API will be directed to this namespace. (default "kubecost")
--use-proxy Instead of temporarily port-forwarding, proxy a request to Kubecost through the Kubernetes API server.
```
Expand All @@ -177,8 +178,24 @@ The following flags modify the behavior of the subcommands:
## Implementation Quirks
In order to provide a seamless experience for standard Kubernetes configurations, `kubectl-cost` talks to the Kubernetes API server based on your Kubeconfig and uses the API server to proxy a request to the Kubecost API. If you get an error like `failed to proxy get kubecost`, there is something going wrong with this behavior.
In order to provide a seamless experience for standard Kubernetes configurations, `kubectl-cost` temporarily forwards a port on your system to a Kubecost pod and uses that port to proxy a request. The port will only be bound to `localhost` and will only be open for the duration of the API request.
If you don't want a port to be temporarily forwarded, there is legacy behavior exposed with the flag `--use-proxy` that will instead use the Kubernetes API server to proxy a request to Kubecost. This behavior has its own pitfalls, especially with security policies that would prevent the API server from communicating with services. If you'd like to test this behavior, to make sure it will work with your cluster:
``` sh
kubectl proxy --port 8080

```
``` sh
curl -G 'http://localhost:8080/api/v1/namespaces/kubecost/services/kubecost-cost-analyzer:tcp-model/proxy/getConfigs'
```
> If you are running an old version of Kubecost, you may have to replace `tcp-model` with `model`
If that `curl` succeeds, `--use-proxy` should work for you.
Otherwise:
- There may be an underlying problem with your Kubecost install, try `kubectl port-forward`ing the `kubecost-cost-analyzer` service, port 9090, and querying [one of our APIs](https://github.com/kubecost/docs/blob/master/apis.md).
- Your problem could be a security configuration that is preventing the API server communicating with certain namespaces or proxying requests in general.
- If you're still having problems, hit us up on Slack (see below) or open an issue on this repo.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ require (
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/oauth2 v0.0.0-20210126194326-f9ce19ea3013 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
k8s.io/api v0.20.2 // indirect
k8s.io/apimachinery v0.20.2 // indirect
k8s.io/cli-runtime v0.20.2
k8s.io/client-go v0.20.2
k8s.io/klog v1.0.0
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
Expand Down
5 changes: 5 additions & 0 deletions pkg/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
// CostOptions holds common options for querying and displaying
// data from the kubecost API
type CostOptions struct {
// If set, will proxy a request through the K8s API server
// instead of port forwarding.
useProxy bool

window string

isHistorical bool
Expand Down Expand Up @@ -48,6 +52,7 @@ func addCostOptionsFlags(cmd *cobra.Command, options *CostOptions) {
cmd.Flags().BoolVar(&options.showEfficiency, "show-efficiency", true, "show efficiency of cost alongside CPU and memory cost")
cmd.Flags().BoolVarP(&options.showAll, "show-all-resources", "A", false, "Equivalent to --show-cpu --show-memory --show-gpu --show-pv --show-network --show-efficiency.")
cmd.Flags().StringVar(&options.serviceName, "service-name", "kubecost-cost-analyzer", "The name of the kubecost cost analyzer service. Change if you're running a non-standard deployment, like the staging helm chart.")
cmd.Flags().BoolVar(&options.useProxy, "use-proxy", false, "Instead of temporarily port-forwarding, proxy a request to Kubecost through the Kubernetes API server.")
}

// addKubeOptionsFlags sets up the cobra command with the flags from
Expand Down
32 changes: 26 additions & 6 deletions pkg/cmd/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/spf13/cobra"

"github.com/kubecost/cost-model/pkg/kubecost"
"github.com/kubecost/kubectl-cost/pkg/query"
)

Expand Down Expand Up @@ -59,9 +60,19 @@ func runCostController(ko *KubeOptions, no *CostOptionsController) error {
}

if !no.isHistorical {
aggs, err := query.QueryAggCostModel(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "controller", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
var aggs map[string]query.Aggregation
var err error

if no.useProxy {
aggs, err = query.QueryAggCostModel(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "controller", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
}
} else {
aggs, err = query.QueryAggCostModelFwd(ko.restConfig, *ko.configFlags.Namespace, no.serviceName, no.window, "controller", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
}
}

// don't show unallocated controller data
Expand All @@ -78,9 +89,18 @@ func runCostController(ko *KubeOptions, no *CostOptionsController) error {
currencyCode,
)
} else {
allocations, err := query.QueryAllocation(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "controller", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
var allocations []map[string]kubecost.Allocation
var err error
if no.useProxy {
allocations, err = query.QueryAllocation(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "controller", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
}
} else {
allocations, err = query.QueryAllocationFwd(ko.restConfig, *ko.configFlags.Namespace, no.serviceName, no.window, "controller", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
}
}

// Use allocations[0] because the query accumulates to a single result
Expand Down
31 changes: 25 additions & 6 deletions pkg/cmd/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,19 @@ func runCostDeployment(ko *KubeOptions, no *CostOptionsDeployment) error {
}

if !no.isHistorical {
aggs, err := query.QueryAggCostModel(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "deployment", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
var aggs map[string]query.Aggregation
var err error

if no.useProxy {
aggs, err = query.QueryAggCostModel(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "deployment", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
}
} else {
aggs, err = query.QueryAggCostModelFwd(ko.restConfig, *ko.configFlags.Namespace, no.serviceName, no.window, "deployment", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
}
}

// don't show unallocated deployment data
Expand All @@ -81,9 +91,18 @@ func runCostDeployment(ko *KubeOptions, no *CostOptionsDeployment) error {
currencyCode,
)
} else {
allocations, err := query.QueryAllocation(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "deployment", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
var allocations []map[string]kubecost.Allocation
var err error
if no.useProxy {
allocations, err = query.QueryAllocation(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "deployment", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
}
} else {
allocations, err = query.QueryAllocationFwd(ko.restConfig, *ko.configFlags.Namespace, no.serviceName, no.window, "deployment", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
}
}

// Use allocations[0] because the query accumulates to a single result
Expand Down
32 changes: 26 additions & 6 deletions pkg/cmd/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/spf13/cobra"

"github.com/kubecost/cost-model/pkg/kubecost"
"github.com/kubecost/kubectl-cost/pkg/query"
)

Expand Down Expand Up @@ -66,9 +67,19 @@ func runCostLabel(ko *KubeOptions, no *CostOptionsLabel) error {
}

if !no.isHistorical {
aggs, err := query.QueryAggCostModel(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "label", no.queryLabel, context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
var aggs map[string]query.Aggregation
var err error

if no.useProxy {
aggs, err = query.QueryAggCostModel(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "label", no.queryLabel, context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
}
} else {
aggs, err = query.QueryAggCostModelFwd(ko.restConfig, *ko.configFlags.Namespace, no.serviceName, no.window, "label", no.queryLabel, context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
}
}

// don't show unallocated controller data
Expand All @@ -83,9 +94,18 @@ func runCostLabel(ko *KubeOptions, no *CostOptionsLabel) error {
currencyCode,
)
} else {
allocations, err := query.QueryAllocation(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, fmt.Sprintf("label:%s", no.queryLabel), context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
var allocations []map[string]kubecost.Allocation
var err error
if no.useProxy {
allocations, err = query.QueryAllocation(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, fmt.Sprintf("label:%s", no.queryLabel), context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
}
} else {
allocations, err = query.QueryAllocationFwd(ko.restConfig, *ko.configFlags.Namespace, no.serviceName, no.window, fmt.Sprintf("label:%s", no.queryLabel), context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
}
}

// Use allocations[0] because the query accumulates to a single result
Expand Down
32 changes: 26 additions & 6 deletions pkg/cmd/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/spf13/cobra"

"github.com/kubecost/cost-model/pkg/kubecost"
"github.com/kubecost/kubectl-cost/pkg/query"
)

Expand Down Expand Up @@ -56,9 +57,19 @@ func runCostNamespace(ko *KubeOptions, no *CostOptionsNamespace) error {
}

if !no.isHistorical {
aggs, err := query.QueryAggCostModel(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "namespace", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
var aggs map[string]query.Aggregation
var err error

if no.useProxy {
aggs, err = query.QueryAggCostModel(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "namespace", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
}
} else {
aggs, err = query.QueryAggCostModelFwd(ko.restConfig, *ko.configFlags.Namespace, no.serviceName, no.window, "namespace", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
}
}

writeAggregationRateTable(
Expand All @@ -70,9 +81,18 @@ func runCostNamespace(ko *KubeOptions, no *CostOptionsNamespace) error {
currencyCode,
)
} else {
allocations, err := query.QueryAllocation(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "namespace", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
var allocations []map[string]kubecost.Allocation
var err error
if no.useProxy {
allocations, err = query.QueryAllocation(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "namespace", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
}
} else {
allocations, err = query.QueryAllocationFwd(ko.restConfig, *ko.configFlags.Namespace, no.serviceName, no.window, "namespace", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
}
}

// Use allocations[0] because the query accumulates to a single result
Expand Down
32 changes: 26 additions & 6 deletions pkg/cmd/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/spf13/cobra"

"github.com/kubecost/cost-model/pkg/kubecost"
"github.com/kubecost/kubectl-cost/pkg/query"
)

Expand Down Expand Up @@ -59,9 +60,19 @@ func runCostPod(ko *KubeOptions, no *CostOptionsPod) error {
}

if !no.isHistorical {
aggs, err := query.QueryAggCostModel(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "pod", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
var aggs map[string]query.Aggregation
var err error

if no.useProxy {
aggs, err = query.QueryAggCostModel(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "pod", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
}
} else {
aggs, err = query.QueryAggCostModelFwd(ko.restConfig, *ko.configFlags.Namespace, no.serviceName, no.window, "pod", "", context.Background())
if err != nil {
return fmt.Errorf("failed to query agg cost model: %s", err)
}
}

// don't show unallocated controller data
Expand All @@ -78,9 +89,18 @@ func runCostPod(ko *KubeOptions, no *CostOptionsPod) error {
currencyCode,
)
} else {
allocations, err := query.QueryAllocation(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "pod", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
var allocations []map[string]kubecost.Allocation
var err error
if no.useProxy {
allocations, err = query.QueryAllocation(ko.clientset, *ko.configFlags.Namespace, no.serviceName, no.window, "pod", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
}
} else {
allocations, err = query.QueryAllocationFwd(ko.restConfig, *ko.configFlags.Namespace, no.serviceName, no.window, "pod", context.Background())
if err != nil {
return fmt.Errorf("failed to query allocation API: %s", err)
}
}

// Use allocations[0] because the query accumulates to a single result
Expand Down
31 changes: 31 additions & 0 deletions pkg/query/aggapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"

// "github.com/kubecost/cost-model/pkg/costmodel"
"github.com/kubecost/cost-model/pkg/kubecost"
Expand All @@ -18,6 +19,8 @@ type aggCostModelResponse struct {
Data map[string]Aggregation `json:"data"`
}

// QueryAggCostModel queries /model/aggregatedCostModel by proxying a request to Kubecost
// through the Kubernetes API server.
func QueryAggCostModel(clientset *kubernetes.Clientset, kubecostNamespace, serviceName, window, aggregate, aggregationSubfield string, ctx context.Context) (map[string]Aggregation, error) {
params := map[string]string{
"window": window,
Expand Down Expand Up @@ -45,6 +48,34 @@ func QueryAggCostModel(clientset *kubernetes.Clientset, kubecostNamespace, servi
return ar.Data, nil
}

// QueryAggCostModelFwd queries /model/aggregatedCostModel by temporarily port-forwarding
// to a Kubecost pod and executing a request against the forwarded port.
func QueryAggCostModelFwd(restConfig *rest.Config, kubecostNamespace, serviceName, window, aggregate, aggregationSubfield string, ctx context.Context) (map[string]Aggregation, error) {
params := map[string]string{
"window": window,
"aggregation": aggregate,
"rate": "monthly",
"etl": "true",
}

if aggregationSubfield != "" {
params["aggregationSubfield"] = aggregationSubfield
}

data, err := portForwardedQueryService(restConfig, kubecostNamespace, serviceName, "model/aggregatedCostModel", params, ctx)
if err != nil {
return nil, fmt.Errorf("failed to port forward query: %s", err)
}

var ar aggCostModelResponse
err = json.Unmarshal(data, &ar)
if err != nil {
return ar.Data, fmt.Errorf("failed to unmarshal allocation response: %s", err)
}

return ar.Data, nil
}

// Hardcoded instead of imported because of dependency problems introduced when
// github.com/kubecost/cost-model/pkg/costmodel is imported. The breakage involves
// Azure's go-autorest, the azure-sdk-for-go, and k8s client-go. Basically, cost-model
Expand Down
Loading

0 comments on commit 17490c3

Please sign in to comment.