Skip to content

Commit f54a3b2

Browse files
Merge pull request #143 from kubecost/mmd/use-new-spec-predict
Switch 'kubectl cost predict' to use /prediction/speccost
2 parents 24836aa + 808ebc9 commit f54a3b2

File tree

17 files changed

+748
-913
lines changed

17 files changed

+748
-913
lines changed

README.md

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ of any Kubernetes workload.
1515
now with `kubectl cost predict -f your-deployment.yaml`:
1616

1717
```
18-
+-------------------------------------+-----+-----+-----+------------+-----------+----------+-----------+-----------+------------+
19-
| WORKLOAD | CPU | MEM | GPU | CPU/MO | MEM/MO | GPU/MO | Δ CPU/MO | Δ MEM/MO | TOTAL/MO |
20-
+-------------------------------------+-----+-----+-----+------------+-----------+----------+-----------+-----------+------------+
21-
| default/Deployment/test1-deployment | 9 | 6Gi | 0 | 207.68 USD | 18.56 USD | 0.00 USD | 38.30 USD | 11.51 USD | 226.24 USD |
22-
+-------------------------------------+-----+-----+-----+------------+-----------+----------+-----------+-----------+------------+
18+
+-------------------------------------+------------+-----------+----------+------------+
19+
| WORKLOAD | Δ CPU/MO | Δ MEM/MO | Δ GPU/MO | Δ TOTAL/MO |
20+
+-------------------------------------+------------+-----------+----------+------------+
21+
| default/deployment/test1-deployment | 209.47 USD | 18.72 USD | 0.00 USD | 228.19 USD |
22+
+-------------------------------------+------------+-----------+----------+------------+
2323
```
2424

2525

@@ -145,11 +145,11 @@ echo "$DEF" | kubectl cost predict -f -
145145
```
146146
Example output:
147147
```
148-
+-------------------------------------+-----+-----+-----+------------+-----------+----------+-----------+-----------+------------+
149-
| WORKLOAD | CPU | MEM | GPU | CPU/MO | MEM/MO | GPU/MO | Δ CPU/MO | Δ MEM/MO | TOTAL/MO |
150-
+-------------------------------------+-----+-----+-----+------------+-----------+----------+-----------+-----------+------------+
151-
| default/Deployment/nginx-deployment | 9 | 6Gi | 0 | 207.68 USD | 18.56 USD | 0.00 USD | 38.30 USD | 11.51 USD | 226.24 USD |
152-
+-------------------------------------+-----+-----+-----+------------+-----------+----------+-----------+-----------+------------+
148+
+---------------------------------------+------------+-----------+----------+------------+
149+
| WORKLOAD | Δ CPU/MO | Δ MEM/MO | Δ GPU/MO | Δ TOTAL/MO |
150+
+---------------------------------------+------------+-----------+----------+------------+
151+
| michaelkc/deployment/nginx-deployment | 209.47 USD | 18.72 USD | 0.00 USD | 228.18 USD |
152+
+---------------------------------------+------------+-----------+----------+------------+
153153
```
154154

155155
Show how much each namespace cost over the past 5 days
@@ -253,17 +253,19 @@ subcommand has its own set of flags for adjusting query behavior and output.
253253
There are several flags that modify the behavior of queries to the backing
254254
Kubecost/OpenCost APIs:
255255
```
256-
-r, --release-name string The name of the Helm release, used to template service names if they are unset. For example, if Kubecost is installed with 'helm install kubecost2 kubecost/cost-analyzer', then this should be set to 'kubecost2'. (default "kubecost")
257-
--service-name string The name of the Kubecost cost analyzer service. By default, it is derived from the Helm release name and should not need to be overridden.
258-
--service-port int The port of the service at which the APIs are running. If using OpenCost, you may want to set this to 9003. (default 9090)
259-
-N, --kubecost-namespace string The namespace that Kubecost is deployed in. Requests to the API will be directed to this namespace. Defaults to the Helm release name.
256+
-r, --release-name string The name of the Helm release, used to template service names if they are unset. For example, if Kubecost is installed with 'helm install kubecost2 kubecost/cost-analyzer', then this should be set to 'kubecost2'. (default "kubecost")
257+
--service-name string The name of the Kubecost cost analyzer service. By default, it is derived from the Helm release name and should not need to be overridden.
258+
--service-port int The port of the service at which the APIs are running. If using OpenCost, you may want to set this to 9003. (default 9090)
259+
-N, --kubecost-namespace string The namespace that Kubecost is deployed in. Requests to the API will be directed to this namespace. Defaults to the Helm release name.
260260
261-
--use-proxy Instead of temporarily port-forwarding, proxy a request to Kubecost through the Kubernetes API server.
261+
--use-proxy Instead of temporarily port-forwarding, proxy a request to Kubecost through the Kubernetes API server.
262262
263-
--allocation-path string URL path at which Allocation queries can be served from the configured service. If using OpenCost, you may want to set this to '/allocation/compute' (default "/model/allocation")
264-
--predict-resource-cost-path string URL path at which Resource Cost Prediction queries can be served from the configured service. (default "/model/prediction/resourcecost")
265-
--predict-resource-cost-diff-path string URL path at which Resource Cost Prediction diff queries can be served from the configured service. (default "/model/prediction/resourcecostdiff")
266-
--no-diff Set true to not attempt a cost difference with a matching in-cluster workload, if one can be found.
263+
--allocation-path string URL path at which Allocation queries can be served from the configured service. If using OpenCost, you may want to set this to '/allocation/compute' (default "/model/allocation")
264+
265+
--predict-speccost-path string URL path at which Prediction queries can be served from the configured service. (default "/model/prediction/speccost")
266+
--no-usage Set true ignore historical usage data (if any exists) when performing cost prediction.
267+
--only-after Set true to only show the overall predicted cost of the workload.
268+
--only-diff Set true to only show the cost difference (cost "impact") instead of the overall cost plus diff. (default true)
267269
```
268270

269271

pkg/cmd/aggregatedcommandbuilder.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@ import (
99

1010
"github.com/spf13/cobra"
1111

12+
"github.com/kubecost/kubectl-cost/pkg/cmd/display"
13+
"github.com/kubecost/kubectl-cost/pkg/cmd/utilities"
1214
"github.com/kubecost/kubectl-cost/pkg/query"
1315
"github.com/opencost/opencost/pkg/log"
1416
)
1517

18+
type AggregatedAllocationOptions struct {
19+
CostOptions
20+
display.AllocationDisplayOptions
21+
}
22+
1623
func buildStandardAggregatedAllocationCommand(streams genericclioptions.IOStreams, commandName string, commandAliases []string, aggregation []string, enableNamespaceFilter bool) *cobra.Command {
17-
kubeO := NewKubeOptions(streams)
18-
costO := CostOptions{}
24+
kubeO := utilities.NewKubeOptions(streams)
25+
o := AggregatedAllocationOptions{}
1926

2027
cmd := &cobra.Command{
2128
Use: commandName,
@@ -29,33 +36,34 @@ func buildStandardAggregatedAllocationCommand(streams genericclioptions.IOStream
2936
return err
3037
}
3138

32-
if err := costO.Complete(kubeO.restConfig); err != nil {
39+
if err := o.CostOptions.Complete(kubeO.RestConfig); err != nil {
3340
return fmt.Errorf("completing options: %s", err)
3441
}
35-
if err := costO.Validate(); err != nil {
42+
if err := o.CostOptions.Validate(); err != nil {
3643
return err
3744
}
3845

39-
return runAggregatedAllocationCommand(kubeO, costO, aggregation)
46+
return runAggregatedAllocationCommand(kubeO, o, aggregation)
4047
},
4148
}
4249

4350
// TODO: Replace entirely when we have generic filter language (v2)
4451
if enableNamespaceFilter {
45-
cmd.Flags().StringVarP(&costO.filterNamespace, "namespace", "n", "", "Limit results to only one namespace. Defaults to all namespaces.")
52+
cmd.Flags().StringVarP(&o.CostOptions.filterNamespace, "namespace", "n", "", "Limit results to only one namespace. Defaults to all namespaces.")
4653
}
4754

48-
addCostOptionsFlags(cmd, &costO)
49-
addKubeOptionsFlags(cmd, kubeO)
55+
addCostOptionsFlags(cmd, &o.CostOptions)
56+
display.AddAllocationDisplayOptionsFlags(cmd, &o.AllocationDisplayOptions)
57+
utilities.AddKubeOptionsFlags(cmd, kubeO)
5058

5159
return cmd
5260
}
5361

54-
func runAggregatedAllocationCommand(ko *KubeOptions, co CostOptions, aggregation []string) error {
62+
func runAggregatedAllocationCommand(ko *utilities.KubeOptions, o AggregatedAllocationOptions, aggregation []string) error {
5563

5664
currencyCode, err := query.QueryCurrencyCode(query.CurrencyCodeParameters{
5765
Ctx: context.Background(),
58-
QueryBackendOptions: co.QueryBackendOptions,
66+
QueryBackendOptions: o.QueryBackendOptions,
5967
})
6068
if err != nil {
6169
log.Debugf("failed to get currency code, displaying as empty string: %s", err)
@@ -65,18 +73,18 @@ func runAggregatedAllocationCommand(ko *KubeOptions, co CostOptions, aggregation
6573
allocations, err := query.QueryAllocation(query.AllocationParameters{
6674
Ctx: context.Background(),
6775
QueryParams: map[string]string{
68-
"window": co.window,
76+
"window": o.window,
6977
"aggregate": strings.Join(aggregation, ","),
7078
"accumulate": "true",
71-
"filterNamespaces": co.filterNamespace,
79+
"filterNamespaces": o.filterNamespace,
7280
},
73-
QueryBackendOptions: co.QueryBackendOptions,
81+
QueryBackendOptions: o.QueryBackendOptions,
7482
})
7583
if err != nil {
7684
return fmt.Errorf("failed to query allocation API: %s", err)
7785
}
7886

79-
writeAllocationTable(ko.Out, aggregation, allocations[0], co.displayOptions, currencyCode, !co.isHistorical)
87+
display.WriteAllocationTable(ko.Out, aggregation, allocations[0], o.AllocationDisplayOptions, currencyCode, !o.isHistorical)
8088

8189
return nil
8290
}

pkg/cmd/common.go

Lines changed: 1 addition & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ package cmd
22

33
import (
44
"fmt"
5-
"strings"
65

76
"github.com/spf13/cobra"
8-
"github.com/spf13/viper"
97
"k8s.io/client-go/rest"
108

119
"github.com/kubecost/kubectl-cost/pkg/query"
@@ -19,97 +17,18 @@ type CostOptions struct {
1917
filterNamespace string
2018

2119
isHistorical bool
22-
showAll bool
2320

24-
displayOptions
2521
query.QueryBackendOptions
2622
}
2723

28-
const (
29-
envPrefix = "KUBECTL_COST"
30-
)
31-
32-
// With the addition of commands which query the assets API,
33-
// some of these don't apply to all commands. However, as they
34-
// are applied during the output, this shouldn't cause issues.
35-
type displayOptions struct {
36-
showCPUCost bool
37-
showMemoryCost bool
38-
showGPUCost bool
39-
showPVCost bool
40-
showNetworkCost bool
41-
showEfficiency bool
42-
showSharedCost bool
43-
showLoadBalancerCost bool
44-
showAssetType bool
45-
}
46-
4724
func addCostOptionsFlags(cmd *cobra.Command, options *CostOptions) {
4825
cmd.Flags().StringVar(&options.window, "window", "1d", "The window of data to query. See https://github.com/kubecost/docs/blob/master/allocation.md#querying for a detailed explanation of what can be passed here.")
4926
cmd.Flags().BoolVar(&options.isHistorical, "historical", false, "show the total cost during the window instead of the projected monthly rate based on the data in the window")
50-
cmd.Flags().BoolVar(&options.showCPUCost, "show-cpu", false, "show data for CPU cost")
51-
cmd.Flags().BoolVar(&options.showMemoryCost, "show-memory", false, "show data for memory cost")
52-
cmd.Flags().BoolVar(&options.showGPUCost, "show-gpu", false, "show data for GPU cost")
53-
cmd.Flags().BoolVar(&options.showPVCost, "show-pv", false, "show data for PV (physical volume) cost")
54-
cmd.Flags().BoolVar(&options.showNetworkCost, "show-network", false, "show data for network cost")
55-
cmd.Flags().BoolVar(&options.showSharedCost, "show-shared", false, "show shared cost data")
56-
cmd.Flags().BoolVar(&options.showLoadBalancerCost, "show-lb", false, "show load balancer cost data")
57-
cmd.Flags().BoolVar(&options.showEfficiency, "show-efficiency", true, "show efficiency of cost alongside CPU and memory cost")
58-
cmd.Flags().BoolVar(&options.showAssetType, "show-asset-type", false, "show type of assets displayed.")
59-
cmd.Flags().BoolVarP(&options.showAll, "show-all-resources", "A", false, "Equivalent to --show-cpu --show-memory --show-gpu --show-pv --show-network --show-efficiency for namespace, deployment, controller, lable and pod OR --show-type --show-cpu --show-memory for node.")
60-
61-
addQueryBackendOptionsFlags(cmd, &options.QueryBackendOptions)
62-
}
63-
64-
func addQueryBackendOptionsFlags(cmd *cobra.Command, options *query.QueryBackendOptions) {
65-
cmd.Flags().StringVarP(&options.HelmReleaseName, "release-name", "r", "kubecost", "The name of the Helm release, used to template service names if they are unset. For example, if Kubecost is installed with 'helm install kubecost2 kubecost/cost-analyzer', then this should be set to 'kubecost2'.")
66-
cmd.Flags().StringVarP(&options.KubecostNamespace, "kubecost-namespace", "N", "", "The namespace that Kubecost is deployed in. Requests to the API will be directed to this namespace. Defaults to the Helm release name.")
67-
68-
cmd.Flags().IntVar(&options.ServicePort, "service-port", 9090, "The port of the service at which the APIs are running. If using OpenCost, you may want to set this to 9003.")
69-
cmd.Flags().StringVar(&options.ServiceName, "service-name", "", "The name of the Kubecost cost analyzer service. By default, it is derived from the Helm release name and should not need to be overridden.")
70-
cmd.Flags().BoolVar(&options.UseProxy, "use-proxy", false, "Instead of temporarily port-forwarding, proxy a request to Kubecost through the Kubernetes API server.")
71-
cmd.Flags().StringVar(&options.AllocationPath, "allocation-path", "/model/allocation", "URL path at which Allocation queries can be served from the configured service. If using OpenCost, you may want to set this to '/allocation/compute'")
72-
cmd.Flags().StringVar(&options.PredictResourceCostPath, "predict-resource-cost-path", "/model/prediction/resourcecost", "URL path at which Resource Cost Prediction queries can be served from the configured service.")
73-
cmd.Flags().StringVar(&options.PredictResourceCostDiffPath, "predict-resource-cost-diff-path", "/model/prediction/resourcecostdiff", "URL path at which Resource Cost Prediction diff queries can be served from the configured service.")
7427

75-
//Check if environment variable KUBECTL_COST_USE_PROXY is set, it defaults to false
76-
v := viper.New()
77-
v.SetEnvPrefix(envPrefix)
78-
v.AutomaticEnv()
79-
bindAFlagToViperEnv(cmd, v, "use-proxy")
80-
}
81-
82-
// addKubeOptionsFlags sets up the cobra command with the flags from
83-
// KubeOptions' configFlags so that a kube client can be built to a
84-
// user's specification. Its one modification is to change the name
85-
// of the namespace flag to kubecost-namespace because we want to
86-
// "behave as expected", i.e. --namespace affects the request to the
87-
// kubecost API, not the request to the k8s API.
88-
func addKubeOptionsFlags(cmd *cobra.Command, ko *KubeOptions) {
89-
// By setting Namespace to nil, AddFlags won't create
90-
// the --namespace flag, which we want to use for scoping
91-
// kubecost requests (for some subcommands). We can then
92-
// create a differently-named flag for the same variable.
93-
ko.configFlags.Namespace = nil
94-
ko.configFlags.AddFlags(cmd.Flags())
95-
96-
// Reset Namespace to a valid string to avoid a nil pointer
97-
// deref.
98-
// emptyStr := ""
99-
// ko.configFlags.Namespace = &emptyStr
28+
query.AddQueryBackendOptionsFlags(cmd, &options.QueryBackendOptions)
10029
}
10130

10231
func (co *CostOptions) Complete(restConfig *rest.Config) error {
103-
if co.showAll {
104-
co.showCPUCost = true
105-
co.showMemoryCost = true
106-
co.showGPUCost = true
107-
co.showPVCost = true
108-
co.showNetworkCost = true
109-
co.showSharedCost = true
110-
co.showLoadBalancerCost = true
111-
co.showAssetType = true
112-
}
11332
if err := co.QueryBackendOptions.Complete(restConfig); err != nil {
11433
return fmt.Errorf("complete backend opts: %s", err)
11534
}
@@ -129,15 +48,3 @@ func (co *CostOptions) Validate() error {
12948

13049
return nil
13150
}
132-
133-
// Binds the flag with viper environment variable and ensures the order of precendence
134-
// command line > environment variable > default value
135-
func bindAFlagToViperEnv(cmd *cobra.Command, v *viper.Viper, flag string) {
136-
flagPtr := cmd.Flags().Lookup(flag)
137-
envVarSuffix := strings.ToUpper(strings.ReplaceAll(flagPtr.Name, "-", "_"))
138-
v.BindEnv(flagPtr.Name, fmt.Sprintf("%s_%s", envPrefix, envVarSuffix))
139-
if !flagPtr.Changed && v.IsSet(flagPtr.Name) {
140-
val := v.Get(flagPtr.Name)
141-
cmd.Flags().Set(flagPtr.Name, fmt.Sprintf("%v", val))
142-
}
143-
}

pkg/cmd/cost.go

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55

66
"k8s.io/cli-runtime/pkg/genericclioptions"
77
_ "k8s.io/client-go/plugin/pkg/client/auth"
8-
"k8s.io/client-go/rest"
98

109
"github.com/spf13/cobra"
1110
"github.com/spf13/viper"
@@ -79,31 +78,6 @@ var (
7978
errNoContext = fmt.Errorf("no context is currently set, use %q to select a new one", "kubectl config use-context <context>")
8079
)
8180

82-
// KubeOptions provides information required to communicate
83-
// with the Kubernetes API
84-
type KubeOptions struct {
85-
configFlags *genericclioptions.ConfigFlags
86-
87-
restConfig *rest.Config
88-
args []string
89-
90-
// Namespace should be the currently-configured defaultNamespace of the client.
91-
// This allows e.g. predict to fill in the defaultNamespace if one is not provided
92-
// in the workload spec.
93-
defaultNamespace string
94-
95-
genericclioptions.IOStreams
96-
}
97-
98-
// NewCommonCostOptions creates the default set of cost options
99-
func NewKubeOptions(streams genericclioptions.IOStreams) *KubeOptions {
100-
return &KubeOptions{
101-
configFlags: genericclioptions.NewConfigFlags(true),
102-
103-
IOStreams: streams,
104-
}
105-
}
106-
10781
// NewCmdCost provides a cobra command that acts as a parent command
10882
// for all subcommands. It provides only basic usage information. See
10983
// common.go and the subcommands for the actual functionality.
@@ -186,32 +160,7 @@ helm install \
186160
cmd.AddCommand(newCmdCostNode(streams))
187161
cmd.AddCommand(newCmdTUI(streams))
188162
cmd.AddCommand(newCmdVersion(streams, GitCommit, GitBranch, GitState, GitSummary, BuildDate))
189-
cmd.AddCommand(newCmdPredict(streams))
163+
cmd.AddCommand(NewCmdPredict(streams))
190164

191165
return cmd
192166
}
193-
194-
// Complete sets all information required for getting cost information
195-
func (o *KubeOptions) Complete(cmd *cobra.Command, args []string) error {
196-
o.args = args
197-
198-
var err error
199-
200-
o.restConfig, err = o.configFlags.ToRESTConfig()
201-
if err != nil {
202-
return fmt.Errorf("converting to REST config: %s", err)
203-
}
204-
205-
o.defaultNamespace, _, err = o.configFlags.ToRawKubeConfigLoader().Namespace()
206-
if err != nil {
207-
return fmt.Errorf("retrieving default namespace: %s", err)
208-
}
209-
210-
return nil
211-
}
212-
213-
// Validate ensures that all required arguments and flag values are provided
214-
func (o *KubeOptions) Validate() error {
215-
216-
return nil
217-
}

0 commit comments

Comments
 (0)