ptyin commented on code in PR #7: URL: https://github.com/apache/incubator-seata-ctl/pull/7#discussion_r1808234342
########## action/common/parser.go: ########## @@ -0,0 +1,34 @@ +package common + +import ( + "fmt" + "github.com/seata/seata-ctl/model" + "gopkg.in/yaml.v3" + "io/ioutil" + "os" +) + +// ReadYMLFile 读取并解析 YAML 文件 Review Comment: plz comment in English ########## go 2.sum: ########## Review Comment: What is this file used for? It should be removed if it is for backup. ########## cmd/root.go: ########## @@ -44,10 +43,10 @@ func init() { rootCmd.PersistentFlags().IntVar(&credential.ServerPort, "port", 7091, "Seata Server Admin Port") rootCmd.PersistentFlags().StringVar(&credential.Username, "username", "seata", "Username") rootCmd.PersistentFlags().StringVar(&credential.Password, "password", "seata", "Password") - viper.BindPFlag("ip", rootCmd.PersistentFlags().Lookup("ip")) - viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port")) - viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username")) - viper.BindPFlag("password", rootCmd.PersistentFlags().Lookup("password")) + //viper.BindPFlag("ip", rootCmd.PersistentFlags().Lookup("ip")) + //viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port")) + //viper.BindPFlag("username", rootCmd.PersistentFlags().Lookup("username")) + //viper.BindPFlag("password", rootCmd.PersistentFlags().Lookup("password")) Review Comment: Comment those lines will affect original usage. So don't comment. ########## action/log/utils/impl/elasticsearch.go: ########## @@ -0,0 +1,145 @@ +package impl + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "github.com/olivere/elastic/v7" + "github.com/seata/seata-ctl/action/log/utils" + "log" + "net/http" +) + +const ( + ElasticsearchAuth = "elastic" +) + +type Elasticsearch struct{} + +// QueryLogs is a function that queries specific documents +func (e *Elasticsearch) QueryLogs(filter map[string]interface{}, currency *utils.Currency, number int) error { + client, err := createElasticClient(currency) + if err != nil { + return fmt.Errorf("failed to create elasticsearch client: %w", err) + } + + indexName := currency.Source + + // Build the query based on the filter provided + query, err := BuildQueryFromFilter(filter) + if err != nil { + return err + } + + // Execute the search query + searchResult, err := client.Search(). + Index(indexName). + Size(number). + Query(query). + Do(context.Background()) + if err != nil { + return fmt.Errorf("error fetching documents: %w", err) + } + + fmt.Printf("Found %d hits.\n", searchResult.TotalHits()) + processSearchHits(searchResult) + return nil +} + +// createElasticClient configures and creates a new Elasticsearch client +func createElasticClient(currency *utils.Currency) (*elastic.Client, error) { + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + + client, err := elastic.NewClient( + elastic.SetURL(currency.Address), + elastic.SetHttpClient(httpClient), + elastic.SetSniff(false), + elastic.SetBasicAuth(ElasticsearchAuth, currency.Auth), + ) + if err != nil { + return nil, err + } + return client, nil +} + +// processSearchHits handles and formats the search results +func processSearchHits(searchResult *elastic.SearchResult) []string { + var result []string + + for _, hit := range searchResult.Hits.Hits { + fmt.Printf("=== Document ID: %s ===\n", hit.Id) + + var doc map[string]interface{} + if err := json.Unmarshal(hit.Source, &doc); err != nil { + log.Printf("Error parsing document: %s", err) + continue + } + + // Pretty print the document content + fmt.Println("Document Source:") + for key, value := range doc { + fmt.Printf(" %s: %v\n", key, value) + } + fmt.Println("----------------------------") Review Comment: Logging should be more formal. ########## cmd/root.go: ########## @@ -73,15 +72,16 @@ func Execute() { os.Exit(0) } } - address := seata.GetAuth().GetAddress() - err := seata.GetAuth().Login() - if err != nil { - fmt.Println("login failed!") - os.Exit(1) - } + //address := seata.GetAuth().GetAddress() + //err := seata.GetAuth().Login() + //if err != nil { + // fmt.Println("login failed!") + // os.Exit(1) + //} + var err error for { - printPrompt(address) + //printPrompt(address) Review Comment: Same as above ########## action/log/utils/interface.go: ########## Review Comment: And there are too many packages naming as`utils`, change a more suitable name would be better. ########## action/prometheus/metrics.go: ########## @@ -0,0 +1,147 @@ +package prometheus + +import ( + "encoding/json" + "fmt" + "github.com/guptarohit/asciigraph" + "github.com/seata/seata-ctl/model" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strconv" +) + +var MetricsCmd = &cobra.Command{ + Use: "metrics", + Short: "Show Prometheus metrics", + Run: func(cmd *cobra.Command, args []string) { + if err := showMetrics(); err != nil { + fmt.Println(err) + } + }, +} + +var Target string + +func init() { + MetricsCmd.PersistentFlags().StringVar(&Target, "target", "seata_transaction_summary", "Namespace name") +} + +// showMetrics executes the metrics collection and chart generation +func showMetrics() error { + prometheusURL, err := getPrometheusAddress() + if err != nil { + return err + } + + // Query Prometheus for metrics + result, err := queryPrometheusMetric(prometheusURL, Target) + if err != nil { + log.Fatalf("Error querying Prometheus: %v", err) + } + + // Generate terminal chart from the queried results + if err = generateTerminalLineChart(result, Target); err != nil { + return err + } + return nil +} + +// getPrometheusAddress fetches Prometheus server address from configuration +func getPrometheusAddress() (string, error) { + file, err := os.ReadFile("config.yml") + if err != nil { + log.Fatalf("Failed to read config.yml: %v", err) + } + + // Parse the configuration + var config model.Config + if err = yaml.Unmarshal(file, &config); err != nil { + log.Fatalf("Failed to unmarshal YAML: %v", err) + } + + // Extract Prometheus address based on context + contextName := config.Context.Prometheus + var contextPath string + for _, server := range config.Prometheus.Servers { + if server.Name == contextName { + contextPath = server.Address + } + } + if contextPath == "" { + log.Fatalf("Failed to find Prometheus context in config.yml") + return "", err + } + return contextPath, nil +} + +// PrometheusResponse defines the structure of a Prometheus query response +type PrometheusResponse struct { + Status string `json:"status"` + Data struct { + ResultType string `json:"resultType"` + Result []struct { + Metric map[string]string `json:"metric"` + Value []interface{} `json:"value"` + } `json:"result"` + } `json:"data"` +} + +// queryPrometheusMetric sends a query to the Prometheus API and returns the response +func queryPrometheusMetric(prometheusURL, query string) (*PrometheusResponse, error) { + queryURL := fmt.Sprintf("%s/api/v1/query?query=%s", prometheusURL, url.QueryEscape(query)) + resp, err := http.Get(queryURL) + if err != nil { + return nil, fmt.Errorf("error querying Prometheus: %w", err) + } + defer resp.Body.Close() + + // Read the response body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + // Parse JSON response into the PrometheusResponse structure + var result PrometheusResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("error unmarshalling JSON: %w", err) + } + return &result, nil +} + +// generateTerminalLineChart generates and prints an ASCII line chart based on the Prometheus response +func generateTerminalLineChart(response *PrometheusResponse, metricName string) error { + var yValues []float64 + + // Iterate over the results and extract the values for the specified metric + for _, result := range response.Data.Result { + if name, ok := result.Metric["__name__"]; ok && name == metricName { + // Parse the metric value + if valueStr, ok := result.Value[1].(string); ok { + value, err := strconv.ParseFloat(valueStr, 64) + if err != nil { + fmt.Println("Value: Invalid number format") + } else { + yValues = append(yValues, value) + } + } else { + fmt.Println("Value: Invalid value format") Review Comment: Why print instead of raising error ########## seata.json: ########## Review Comment: I guess this file should be auto-generated from the CRD, do not commit this file. ########## action/log/utils/interface.go: ########## Review Comment: It would be better to put implementation under the outer folder instead of 'impl' folder in conformance with golang guidelines. ########## qodana.yaml: ########## Review Comment: plz remove ########## action/log/utils/impl/local.go: ########## Review Comment: Maybe remove this file temporarily? We can supply another PR to implement the local one. ########## action/root.go: ########## @@ -1,9 +1,9 @@ /* * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with + * contributor license agreements. See the NOTICE config distributed with * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with + * The ASF licenses this config to You under the Apache License, Version 2.0 + * (the "License"); you may not use this config except in compliance with Review Comment: Don't change the license header ########## action/k8s/install.go: ########## @@ -0,0 +1,128 @@ +package k8s + +import ( + "context" + "fmt" + "github.com/seata/seata-ctl/action/k8s/utils" + "github.com/spf13/cobra" + _ "gopkg.in/yaml.v3" + _ "io/ioutil" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + _ "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + _ "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + _ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + _ "k8s.io/apimachinery/pkg/runtime/schema" + _ "k8s.io/client-go/applyconfigurations/meta/v1" + "log" +) + +const ( + CreateCrdPath = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions" + FilePath = "seata.yaml" + + CRDname = "seataservers.operator.seata.apache.org" + Deployname = "seata-k8s-controller-manager" +) + +var InstallCmd = &cobra.Command{ + Use: "install", + Short: "Install Kubernetes CRD controller", + Run: func(cmd *cobra.Command, args []string) { + err := DeployCRD() + if err != nil { + log.Fatal(err) + } + err = DeployController() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + InstallCmd.PersistentFlags().StringVar(&Namespace, "namespace", "default", "Namespace name") +} + +// DeployCRD deploys the custom resource definition. +func DeployCRD() error { + res, err := utils.CreateRequest(CreateCrdPath, FilePath) + fmt.Println(res) + if err != nil { + return err + } + return nil +} + +// DeployController deploys the controller for the custom resource. +func DeployController() error { + // Get Kubernetes client + clientset, err := utils.GetClient() + if err != nil { + return fmt.Errorf("error getting clientset: %v", err) + } + + // Define the Deployment name and namespace + deploymentName := Deployname + namespace := Namespace + + // Check if the Deployment already exists + _, err = clientset.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{}) + if err == nil { + // If the Deployment exists, output a message and return + fmt.Printf("Deployment '%s' already exists in the '%s' namespace\n", deploymentName, Namespace) + return nil + } else if !errors.IsNotFound(err) { + // If there is an error other than "not found", return it + return fmt.Errorf("error checking for existing deployment: %v", err) + } + + // Create Deployment object if it does not exist + deployment := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentName, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: func(i int32) *int32 { return &i }(1), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": deploymentName, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": deploymentName, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: deploymentName, + Image: "bearslyricattack/seata-controller:latest", Review Comment: Use official image here. ########## action/log/log.go: ########## @@ -0,0 +1,180 @@ +package log + +import ( + "fmt" + "github.com/seata/seata-ctl/action/log/utils" + "github.com/seata/seata-ctl/action/log/utils/impl" + "github.com/seata/seata-ctl/model" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + "log" + "os" +) + +const ( + ElasticSearchType = "ElasticSearch" + DefaultNumber = 10 +) +const ( + LokiType = "Loki" +) + +var LogCmd = &cobra.Command{ + Use: "log", + Short: "get seata log", + Run: func(cmd *cobra.Command, args []string) { + err := getLog() + if err != nil { + fmt.Println(err) + } + }, +} + +// ElasticSearch + +var LogLevel string +var Module string +var XID string +var BranchID string +var ResourceID string +var Message string +var Number int + +// Loki + +var Label string +var Start string +var End string Review Comment: Put them in parentheses like following ```go var ( LogLevel string Module string ) ``` ########## action/prometheus/metrics.go: ########## @@ -0,0 +1,147 @@ +package prometheus + +import ( + "encoding/json" + "fmt" + "github.com/guptarohit/asciigraph" + "github.com/seata/seata-ctl/model" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strconv" +) + +var MetricsCmd = &cobra.Command{ + Use: "metrics", + Short: "Show Prometheus metrics", + Run: func(cmd *cobra.Command, args []string) { + if err := showMetrics(); err != nil { + fmt.Println(err) + } + }, +} + +var Target string + +func init() { + MetricsCmd.PersistentFlags().StringVar(&Target, "target", "seata_transaction_summary", "Namespace name") +} + +// showMetrics executes the metrics collection and chart generation +func showMetrics() error { + prometheusURL, err := getPrometheusAddress() + if err != nil { + return err + } + + // Query Prometheus for metrics + result, err := queryPrometheusMetric(prometheusURL, Target) + if err != nil { + log.Fatalf("Error querying Prometheus: %v", err) + } + + // Generate terminal chart from the queried results + if err = generateTerminalLineChart(result, Target); err != nil { + return err + } + return nil +} + +// getPrometheusAddress fetches Prometheus server address from configuration +func getPrometheusAddress() (string, error) { + file, err := os.ReadFile("config.yml") + if err != nil { + log.Fatalf("Failed to read config.yml: %v", err) + } + + // Parse the configuration + var config model.Config + if err = yaml.Unmarshal(file, &config); err != nil { + log.Fatalf("Failed to unmarshal YAML: %v", err) + } + + // Extract Prometheus address based on context + contextName := config.Context.Prometheus + var contextPath string + for _, server := range config.Prometheus.Servers { + if server.Name == contextName { + contextPath = server.Address + } + } + if contextPath == "" { + log.Fatalf("Failed to find Prometheus context in config.yml") + return "", err + } + return contextPath, nil +} + +// PrometheusResponse defines the structure of a Prometheus query response +type PrometheusResponse struct { + Status string `json:"status"` + Data struct { + ResultType string `json:"resultType"` + Result []struct { + Metric map[string]string `json:"metric"` + Value []interface{} `json:"value"` + } `json:"result"` + } `json:"data"` +} + +// queryPrometheusMetric sends a query to the Prometheus API and returns the response +func queryPrometheusMetric(prometheusURL, query string) (*PrometheusResponse, error) { + queryURL := fmt.Sprintf("%s/api/v1/query?query=%s", prometheusURL, url.QueryEscape(query)) + resp, err := http.Get(queryURL) + if err != nil { + return nil, fmt.Errorf("error querying Prometheus: %w", err) + } + defer resp.Body.Close() + + // Read the response body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + // Parse JSON response into the PrometheusResponse structure + var result PrometheusResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("error unmarshalling JSON: %w", err) + } + return &result, nil +} + +// generateTerminalLineChart generates and prints an ASCII line chart based on the Prometheus response +func generateTerminalLineChart(response *PrometheusResponse, metricName string) error { Review Comment: Can we print line charts now? Plz provide some screenshots. ########## action/k8s/status.go: ########## @@ -0,0 +1,72 @@ +package k8s + +import ( + "context" + "fmt" + "github.com/seata/seata-ctl/action/k8s/utils" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var StatusCmd = &cobra.Command{ + Use: "status", + Short: "show seata status in k8s", + Run: func(cmd *cobra.Command, args []string) { + err := status() + if err != nil { + fmt.Println(err) + } + }, +} + +const Label = "cr_name" + +func init() { + StatusCmd.PersistentFlags().StringVar(&Name, "name", "list", "Seataserver name") + StatusCmd.PersistentFlags().StringVar(&Namespace, "namespace", "default", "Namespace name") +} + +func status() error { + statuses, err := getPodsStatusByLabel(Namespace, Name) + if err != nil { + return err + } + // Print formatted Pod status information + for _, status := range statuses { + fmt.Println(status) + } + return nil +} + +func getPodsStatusByLabel(namespace, labelSelector string) ([]string, error) { + client, err := utils.GetClient() + if err != nil { + return nil, fmt.Errorf("failed to create client: %v", err) + } + + // Retrieve all Pods with the specified label + // Use LabelSelector to filter Pods with the specified cr_name label + labelSelector = Label + "=" + labelSelector + pods, err := client.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{ + LabelSelector: labelSelector, + }) + if err != nil { + return nil, fmt.Errorf("failed to list pods: %v", err) + } + + // Iterate over all Pods and get their status + var statuses []string + //for _, pod := range pods.Items { + // statuses = append(statuses, fmt.Sprintf("Pod %s is in %s phase", pod.Name, pod.Status.Phase)) + //} + + // Build formatted status string for output + statuses = append(statuses, fmt.Sprintf("%-25s %-10s", "POD NAME", "STATUS")) // Header + statuses = append(statuses, fmt.Sprintf("%s", "-------------------------------------------")) Review Comment: Logging issues. Plz check again. ########## action/log/log.go: ########## @@ -0,0 +1,180 @@ +package log + +import ( + "fmt" + "github.com/seata/seata-ctl/action/log/utils" + "github.com/seata/seata-ctl/action/log/utils/impl" + "github.com/seata/seata-ctl/model" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + "log" + "os" +) + +const ( + ElasticSearchType = "ElasticSearch" + DefaultNumber = 10 +) +const ( + LokiType = "Loki" +) + +var LogCmd = &cobra.Command{ + Use: "log", + Short: "get seata log", + Run: func(cmd *cobra.Command, args []string) { + err := getLog() + if err != nil { + fmt.Println(err) + } + }, +} + +// ElasticSearch + +var LogLevel string +var Module string +var XID string +var BranchID string +var ResourceID string +var Message string +var Number int + +// Loki + +var Label string +var Start string +var End string + +func init() { + LogCmd.PersistentFlags().StringVar(&LogLevel, "Level", "", "seata log level") + LogCmd.PersistentFlags().StringVar(&Module, "Module", "", "seata module") + LogCmd.PersistentFlags().StringVar(&XID, "XID", "", "seata expression") + LogCmd.PersistentFlags().StringVar(&BranchID, "BranchID", "", "seata branchId") + LogCmd.PersistentFlags().StringVar(&ResourceID, "ResourceID", "", "seata resourceID") + LogCmd.PersistentFlags().StringVar(&Message, "Message", "", "seata message") + LogCmd.PersistentFlags().IntVar(&Number, "Number", DefaultNumber, "seata number") + LogCmd.PersistentFlags().StringVar(&Label, "label", "", "seata label") + LogCmd.PersistentFlags().StringVar(&Start, "start", "", "seata start") + LogCmd.PersistentFlags().StringVar(&End, "end", "", "seata end") +} + +func getLog() error { + context, currency, err := getContext() + if err != nil { + return err + } + logType := context.Types + + var client utils.LogQuery + var filter = make(map[string]interface{}) + + switch logType { + case ElasticSearchType: + { + client = &impl.Elasticsearch{} + filter = buildElasticSearchFilter() + } + case LokiType: + { + client = &impl.Loki{} + filter = buildLokiFilter() + } + //case "Local": + // { + // return &impl.Local{}, nil, nil + // } Review Comment: Plz remove comments ########## action/k8s/install.go: ########## @@ -0,0 +1,128 @@ +package k8s + +import ( + "context" + "fmt" + "github.com/seata/seata-ctl/action/k8s/utils" + "github.com/spf13/cobra" + _ "gopkg.in/yaml.v3" + _ "io/ioutil" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + _ "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + _ "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + _ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + _ "k8s.io/apimachinery/pkg/runtime/schema" + _ "k8s.io/client-go/applyconfigurations/meta/v1" + "log" +) + +const ( + CreateCrdPath = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions" + FilePath = "seata.yaml" + + CRDname = "seataservers.operator.seata.apache.org" + Deployname = "seata-k8s-controller-manager" +) + +var InstallCmd = &cobra.Command{ + Use: "install", + Short: "Install Kubernetes CRD controller", + Run: func(cmd *cobra.Command, args []string) { + err := DeployCRD() + if err != nil { + log.Fatal(err) + } + err = DeployController() + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + InstallCmd.PersistentFlags().StringVar(&Namespace, "namespace", "default", "Namespace name") +} + +// DeployCRD deploys the custom resource definition. +func DeployCRD() error { + res, err := utils.CreateRequest(CreateCrdPath, FilePath) + fmt.Println(res) + if err != nil { + return err + } + return nil +} + +// DeployController deploys the controller for the custom resource. +func DeployController() error { + // Get Kubernetes client + clientset, err := utils.GetClient() + if err != nil { + return fmt.Errorf("error getting clientset: %v", err) + } + + // Define the Deployment name and namespace + deploymentName := Deployname + namespace := Namespace + + // Check if the Deployment already exists + _, err = clientset.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{}) + if err == nil { + // If the Deployment exists, output a message and return + fmt.Printf("Deployment '%s' already exists in the '%s' namespace\n", deploymentName, Namespace) + return nil + } else if !errors.IsNotFound(err) { + // If there is an error other than "not found", return it + return fmt.Errorf("error checking for existing deployment: %v", err) + } + + // Create Deployment object if it does not exist + deployment := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentName, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: func(i int32) *int32 { return &i }(1), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": deploymentName, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": deploymentName, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: deploymentName, + Image: "bearslyricattack/seata-controller:latest", Review Comment: And I think we should provide users a choice to modify the controller image. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: dev-unsubscr...@seata.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@seata.apache.org For additional commands, e-mail: dev-h...@seata.apache.org