Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions cmd/cluster_analysis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package cmd

import (
"context"
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/spf13/cobra"

"github.com/qovery/qovery-client-go"

"github.com/qovery/qovery-cli/utils"
)

var (
clusterAnalysisClusterId string
clusterAnalysisId string
clusterAnalysisOutputFormat string
clusterAnalysisPrometheusUrl string
clusterAnalysisCmdArgs []string
clusterAnalysisTargetK8sVersion string
clusterAnalysisWatch bool
clusterAnalysisNoLogs bool
clusterAnalysisJson bool
)

var clusterAnalysisCmd = &cobra.Command{
Use: "analysis",
Short: "Run and inspect read-only cluster analyses (e.g. cost recommendations)",
Run: func(cmd *cobra.Command, args []string) {
utils.Capture(cmd)
if len(args) == 0 {
_ = cmd.Help()
os.Exit(0)
}
},
}

func init() {
clusterCmd.AddCommand(clusterAnalysisCmd)
}

// parseAnalysisOutput maps a CLI --output value to the engine output format.
func parseAnalysisOutput(s string) (qovery.ClusterAnalysisOutputFormat, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "", "json":
return qovery.CLUSTERANALYSISOUTPUTFORMAT_JSON, nil
Comment thread
pggb25 marked this conversation as resolved.
case "table":
return qovery.CLUSTERANALYSISOUTPUTFORMAT_TABLE, nil
case "csv":
return qovery.CLUSTERANALYSISOUTPUTFORMAT_CSV, nil
default:
return "", fmt.Errorf("invalid output format %q (allowed: table, json, csv)", s)
}
}

// isFinalAnalysisStatus reports whether the analysis reached a terminal state.
func isFinalAnalysisStatus(status qovery.ClusterAnalysisStatus) bool {
switch status {
case qovery.CLUSTERANALYSISSTATUS_SUCCEEDED,
qovery.CLUSTERANALYSISSTATUS_FAILED,
qovery.CLUSTERANALYSISSTATUS_TERMINATED:
return true
default:
return false
}
}

// httpError formats an API error using the response body when available.
func httpError(res *http.Response, err error) error {
if res == nil {
return err
}

if res.Body == nil {
if err != nil {
return fmt.Errorf("status code: %s: %w", res.Status, err)
}
return fmt.Errorf("status code: %s", res.Status)
}

defer func() { _ = res.Body.Close() }()
body, readErr := io.ReadAll(res.Body)
if readErr != nil {
if err != nil {
return fmt.Errorf("status code: %s ; cannot read response body: %w ; original error: %w", res.Status, readErr, err)
}
return fmt.Errorf("status code: %s ; cannot read response body: %w", res.Status, readErr)
}

if err != nil {
return fmt.Errorf("status code: %s ; body: %s ; original error: %w", res.Status, string(body), err)
}
return fmt.Errorf("status code: %s ; body: %s", res.Status, string(body))
}

// printAnalysisLogs fetches and prints the persisted report/log lines of an analysis.
func printAnalysisLogs(client *qovery.APIClient, clusterId string, analysisId string) error {
logs, res, err := client.ClustersAPI.ListClusterAnalysisLogs(context.Background(), clusterId, analysisId).Execute()
if err != nil {
return httpError(res, err)
}

utils.Println(analysisReportFromLogs(logs.GetResults()))

return nil
}

func analysisReportFromLogs(logs []qovery.ClusterAnalysisLogResponse) string {
lines := make([]string, 0, len(logs))
for _, line := range logs {
lines = append(lines, line.GetMessage())
}
return strings.Join(lines, "\n")
}
53 changes: 53 additions & 0 deletions cmd/cluster_analysis_cost_recommendation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package cmd

import (
"os"

"github.com/spf13/cobra"

"github.com/qovery/qovery-client-go"

"github.com/qovery/qovery-cli/utils"
)

var clusterAnalysisCostRecommendationCmd = &cobra.Command{
Use: "cost-recommendation",
Short: "Start a cluster cost recommendation analysis, optionally wait for completion, then print its report",
Run: func(cmd *cobra.Command, args []string) {
utils.Capture(cmd)

request, err := newCostRecommendationRequest()
if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}

runClusterAnalysis(request)
},
}

func newCostRecommendationRequest() (*qovery.ClusterAnalysisRequest, error) {
outputFormat, err := parseAnalysisOutput(clusterAnalysisOutputFormat)
if err != nil {
return nil, err
}

request := qovery.NewClusterAnalysisRequest(qovery.CLUSTERANALYSISKIND_COST_RECOMMENDATION, outputFormat)
if clusterAnalysisPrometheusUrl != "" {
request.SetPrometheusUrl(clusterAnalysisPrometheusUrl)
}
if len(clusterAnalysisCmdArgs) > 0 {
request.SetCmdArgs(clusterAnalysisCmdArgs)
}

return request, nil
}

func init() {
clusterAnalysisCmd.AddCommand(clusterAnalysisCostRecommendationCmd)

addClusterAnalysisRunFlags(clusterAnalysisCostRecommendationCmd)
clusterAnalysisCostRecommendationCmd.Flags().StringVar(&clusterAnalysisPrometheusUrl, "prometheus-url", "", "Optional Prometheus URL")
clusterAnalysisCostRecommendationCmd.Flags().StringArrayVar(&clusterAnalysisCmdArgs, "cmd-arg", nil, "Optional allowlisted command argument. Repeat for each argument, e.g. --cmd-arg=--history_duration --cmd-arg=336")
}
49 changes: 49 additions & 0 deletions cmd/cluster_analysis_deprecated_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cmd

import (
"os"

"github.com/spf13/cobra"

"github.com/qovery/qovery-client-go"

"github.com/qovery/qovery-cli/utils"
)

var clusterAnalysisDeprecatedApiCmd = &cobra.Command{
Use: "deprecated-api",
Short: "Start a deprecated Kubernetes API analysis, optionally wait for completion, then print its report",
Run: func(cmd *cobra.Command, args []string) {
utils.Capture(cmd)

request, err := newDeprecatedApiRequest()
if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}

runClusterAnalysis(request)
},
}

func newDeprecatedApiRequest() (*qovery.ClusterAnalysisRequest, error) {
outputFormat, err := parseAnalysisOutput(clusterAnalysisOutputFormat)
if err != nil {
return nil, err
}

request := qovery.NewClusterAnalysisRequest(qovery.CLUSTERANALYSISKIND_DEPRECATED_API_CHECK, outputFormat)
if clusterAnalysisTargetK8sVersion != "" {
request.SetTargetKubernetesVersion(clusterAnalysisTargetK8sVersion)
}

return request, nil
}

func init() {
clusterAnalysisCmd.AddCommand(clusterAnalysisDeprecatedApiCmd)

addClusterAnalysisRunFlags(clusterAnalysisDeprecatedApiCmd)
clusterAnalysisDeprecatedApiCmd.Flags().StringVar(&clusterAnalysisTargetK8sVersion, "target-kubernetes-version", "", "Optional target Kubernetes version")
}
88 changes: 88 additions & 0 deletions cmd/cluster_analysis_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package cmd

import (
"context"
"encoding/json"
"os"
"time"

"github.com/spf13/cobra"

"github.com/qovery/qovery-client-go"

"github.com/qovery/qovery-cli/utils"
)

var clusterAnalysisListCmd = &cobra.Command{
Use: "list",
Short: "List previous analyses for a cluster",
Run: func(cmd *cobra.Command, args []string) {
utils.Capture(cmd)

client := utils.GetQoveryClientPanicInCaseOfError()

analyses, res, err := client.ClustersAPI.ListClusterAnalyses(context.Background(), clusterAnalysisClusterId).Execute()
if err != nil {
utils.PrintlnError(httpError(res, err))
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}

if clusterAnalysisJson {
utils.Println(getAnalysisJsonOutput(analyses.GetResults()))
return
}

var data [][]string
for _, a := range analyses.GetResults() {
data = append(data, []string{
a.GetId(),
string(a.GetKind()),
string(a.GetStatus()),
a.GetCreatedAt().Format(time.RFC3339),
a.GetTriggeredBy(),
a.GetErrorMessage(),
})
}

err = utils.PrintTable([]string{"Id", "Kind", "Status", "Created At", "Triggered By", "Error"}, data)
if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}
},
}

func getAnalysisJsonOutput(analyses []qovery.ClusterAnalysisResponse) string {
var results []interface{}
for _, a := range analyses {
createdAt := a.GetCreatedAt()
updatedAt := a.GetUpdatedAt()
results = append(results, map[string]interface{}{
"id": a.GetId(),
"cluster_id": a.GetClusterId(),
"kind": a.GetKind(),
"status": a.GetStatus(),
"created_at": utils.ToIso8601(&createdAt),
"updated_at": utils.ToIso8601(&updatedAt),
"triggered_by": a.GetTriggeredBy(),
"error": a.GetErrorMessage(),
})
}

j, err := json.Marshal(results)
if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}
return string(j)
}

func init() {
clusterAnalysisCmd.AddCommand(clusterAnalysisListCmd)
clusterAnalysisListCmd.Flags().StringVarP(&clusterAnalysisClusterId, "cluster-id", "c", "", "Cluster ID")
clusterAnalysisListCmd.Flags().BoolVar(&clusterAnalysisJson, "json", false, "JSON output")
_ = clusterAnalysisListCmd.MarkFlagRequired("cluster-id")
}
67 changes: 67 additions & 0 deletions cmd/cluster_analysis_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cmd

import (
"context"
"encoding/json"
"os"

"github.com/spf13/cobra"

"github.com/qovery/qovery-client-go"

"github.com/qovery/qovery-cli/utils"
)

var clusterAnalysisLogsCmd = &cobra.Command{
Use: "logs",
Short: "Print the report/logs of a past cluster analysis",
Run: func(cmd *cobra.Command, args []string) {
utils.Capture(cmd)

client := utils.GetQoveryClientPanicInCaseOfError()

logs, res, err := client.ClustersAPI.ListClusterAnalysisLogs(context.Background(), clusterAnalysisClusterId, clusterAnalysisId).Execute()
if err != nil {
utils.PrintlnError(httpError(res, err))
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}

if clusterAnalysisJson {
utils.Println(getAnalysisLogsJsonOutput(logs.GetResults()))
return
}

utils.Println(analysisReportFromLogs(logs.GetResults()))
},
}

func getAnalysisLogsJsonOutput(logs []qovery.ClusterAnalysisLogResponse) string {
var results []interface{}
for _, line := range logs {
timestamp := line.GetTimestamp()
results = append(results, map[string]interface{}{
"timestamp": utils.ToIso8601(&timestamp),
"level": line.GetLevel(),
"message": line.GetMessage(),
"line_order": line.GetLineOrder(),
})
}

j, err := json.Marshal(results)
if err != nil {
utils.PrintlnError(err)
os.Exit(1)
panic("unreachable") // staticcheck false positive: https://staticcheck.io/docs/checks#SA5011
}
return string(j)
}

func init() {
clusterAnalysisCmd.AddCommand(clusterAnalysisLogsCmd)
clusterAnalysisLogsCmd.Flags().StringVarP(&clusterAnalysisClusterId, "cluster-id", "c", "", "Cluster ID")
clusterAnalysisLogsCmd.Flags().StringVarP(&clusterAnalysisId, "analysis-id", "a", "", "Analysis ID")
clusterAnalysisLogsCmd.Flags().BoolVar(&clusterAnalysisJson, "json", false, "JSON output")
_ = clusterAnalysisLogsCmd.MarkFlagRequired("cluster-id")
_ = clusterAnalysisLogsCmd.MarkFlagRequired("analysis-id")
}
Loading
Loading