1 package cli
2
3 import (
4 "context"
5 "errors"
6 "flag"
7 "fmt"
8 "io"
9 "log"
10 "os"
11 "strings"
12 "text/tabwriter"
13
14 gptext "github.com/jedib0t/go-pretty/v6/text"
15 "github.com/peterbourgon/ff/v3/ffcli"
16
17 "edge-infra.dev/pkg/lib/cli/commands"
18 metrics "edge-infra.dev/pkg/lib/gcp/monitoring/metrics"
19 "edge-infra.dev/pkg/lib/gcp/monitoring/monutil"
20 )
21
22 const (
23 mmTitle = `
24 ___ ___ _ ___ ___ _ _
25 | \/ | | | | \/ | (_) | |
26 | . . | ___| |_ ___ _ __| . . | __ _ _ __| |
27 | |\/| |/ _ \ __/ _ \ '__| |\/| |/ _` + "`" + ` | |/ _` + "`" + ` |
28 | | | | __/ || __/ | | | | | (_| | | (_| |
29 \_| |_/\___|\__\___|_| \_| |_/\__,_|_|\__,_|`
30
31 mmBroomTitle = `
32 ('"-,_
33 "-,_"-,_
34 "` + "`" + `-,_"-,_
35 "-,_"-,_
36 ` + "`" + `-,_"-,_
37 "-,_"-,_
38 ___ ___ _ ___ ___ _ _ ` + "`" + `-,_"-,_
39 | \/ | | | | \/ | (_) | | "-,_"-,_ __,
40 | . . | ___| |_ ___ _ __| . . | __ _ _ __| | ` + "`" + `-,_"-,/_ /~=,_
41 | |\/| |/ _ \ __/ _ \ '__| |\/| |/ _` + "`" + ` | |/ _` + "`" + ` | "-/-,//~=,_~=,_
42 | | | | __/ || __/ | | | | | (_| | | (_| | \_//_~=,_~=,~
43 \_| |_/\___|\__\___|_| \_| |_/\__,_|_|\__,_| '=,_~=,_~=
44 ~=,_~`
45 )
46
47 var (
48 Stderr io.Writer = os.Stderr
49 Stdout io.Writer = os.Stdout
50 Stdin io.Writer = os.Stdin
51 Println = fmt.Println
52 Fatalf = log.Fatalf
53 Errorf = fmt.Errorf
54 Sprintf = fmt.Sprintf
55 Printf = fmt.Printf
56 filterForeman = false
57 multiProject = false
58 )
59
60 func isBoolFlag(f *flag.Flag) bool {
61 bf, ok := f.Value.(interface {
62 IsBoolFlag() bool
63 })
64 return ok && bf.IsBoolFlag()
65 }
66
67 func countFlags(fs *flag.FlagSet) (n int) {
68 fs.VisitAll(func(*flag.Flag) { n++ })
69 return n
70 }
71
72 func newFlagSet(name string) *flag.FlagSet {
73 onError := flag.ExitOnError
74 fs := flag.NewFlagSet(name, onError)
75 fs.SetOutput(Stderr)
76 return fs
77 }
78
79 var (
80 projectID string
81 metricPrefix string
82 metricName string
83 jsonFormat bool
84 monoFormat bool
85 silent bool
86 )
87
88 var globalFlags = func() *flag.FlagSet {
89 fset := flag.NewFlagSet("global", flag.ExitOnError)
90 fset.StringVar(&projectID, "project", "", "GCP project ID(s) for Metric Descriptor(s).")
91 fset.StringVar(&metricPrefix, "metric-prefix", "prometheus.googleapis.com", "GCP Metric Descriptor(s) URL prefix.")
92 fset.StringVar(&metricName, "metric-name", "", "Comma separated list of metric descriptor names. All metric descriptors will be acted upon if not provided.")
93 fset.BoolVar(&jsonFormat, "json", false, "Sets the output to JSON formatting instead of the default visual formatting.")
94 fset.BoolVar(&monoFormat, "monochrome", false, "Sets the visually formatted output to monochromatic (grayscale).")
95 fset.BoolVar(&silent, "silent", false, "Executes the specified action without confirmation.")
96 return fset
97 }()
98
99
100 func withGlobalFlags(fset *flag.FlagSet) *flag.FlagSet {
101 globalFlags.VisitAll(func(f *flag.Flag) {
102 fset.Var(f.Value, f.Name, f.Usage)
103 })
104 return fset
105 }
106
107
108 func Run(args []string) (err error) {
109 rootfs := newFlagSet("metermaid")
110 rootCmd := &ffcli.Command{
111 Name: "metermaid",
112 ShortUsage: "metermaid <subcommand> [command flags]",
113 ShortHelp: "GCP Metric Descriptor alignment utility",
114 LongHelp: strings.TrimSpace(`
115 For help on subcommands, add --help after: "metermaid get --help".
116 `),
117 Subcommands: []*ffcli.Command{
118 alignCmd,
119 compareCmd,
120 deleteCmd,
121 getCmd,
122 listCmd,
123 commands.Version(),
124 },
125 FlagSet: rootfs,
126 Exec: func(context.Context, []string) error { return flag.ErrHelp },
127 UsageFunc: usageFunc,
128 }
129
130 for _, c := range rootCmd.Subcommands {
131 c.UsageFunc = usageFunc
132 }
133
134 if !silent && !jsonFormat {
135 monutil.ClearTerminal()
136 fmt.Printf("%s\n\n", monutil.White(mmTitle))
137 }
138
139 if err = rootCmd.Parse(args); err != nil {
140 if errors.Is(err, flag.ErrHelp) {
141 return nil
142 }
143 return err
144 }
145
146 err = rootCmd.Run(context.Background())
147 if errors.Is(err, flag.ErrHelp) {
148 return nil
149 }
150 return err
151 }
152
153 func usageFunc(c *ffcli.Command) string {
154 var b strings.Builder
155 fmt.Fprintf(&b, "USAGE\n")
156 if c.ShortUsage != "" {
157 fmt.Fprintf(&b, " %s\n", c.ShortUsage)
158 } else {
159 fmt.Fprintf(&b, " %s\n", c.Name)
160 }
161 fmt.Fprintf(&b, "\n")
162
163 if c.LongHelp != "" {
164 fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
165 }
166
167 if len(c.Subcommands) > 0 {
168 fmt.Fprintf(&b, "SUBCOMMANDS\n")
169 tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
170 for _, subcommand := range c.Subcommands {
171 fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
172 }
173 tw.Flush()
174 fmt.Fprintf(&b, "\n")
175 }
176
177 if countFlags(c.FlagSet) > 0 {
178 fmt.Fprintf(&b, "FLAGS\n")
179 tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
180 c.FlagSet.VisitAll(func(f *flag.Flag) {
181 var s string
182 name, usage := flag.UnquoteUsage(f)
183 if isBoolFlag(f) {
184 s = fmt.Sprintf(" --%s, --%s=false", f.Name, f.Name)
185 } else {
186 s = fmt.Sprintf(" --%s", f.Name)
187 if len(name) > 0 {
188 s += " " + name
189 }
190 }
191
192
193 s += "\n \t"
194 s += strings.ReplaceAll(usage, "\n", "\n \t")
195
196 if f.DefValue != "" {
197 s += fmt.Sprintf(" (default %s)", f.DefValue)
198 }
199
200 fmt.Fprintln(&b, s+"\n")
201 })
202 tw.Flush()
203 fmt.Fprintf(&b, "\n")
204 }
205 return strings.TrimSpace(b.String())
206 }
207
208
209 func checkGlobalFlags() bool {
210 if len(projectID) == 0 {
211 fmt.Println("Error: no value specified for [project] - a valid project-id is required")
212 return false
213 }
214
215
216 if jsonFormat && monoFormat {
217 fmt.Println("Error: json format option not supported with monochrome format selection - please choose only one of the formatting options")
218 return false
219 }
220
221
222 if monutil.IsList(projectID) {
223 multiProject = true
224 }
225
226
227 if !monutil.IsForeman(projectID) && multiProject {
228 filterForeman = true
229 }
230
231
232 if monutil.IsList(metricName) {
233 mNames := strings.Split(strings.ReplaceAll(metricName, " ", ""), ",")
234 for i := 0; i < len(mNames); i++ {
235 if metrics.HasMetricPrefix(mNames[i]) {
236 fmt.Println("Error: metric prefix should not be included as part of the specified metric name - use the '--metric-prefix' to specify the metric URL")
237 return false
238 }
239 }
240
241
242 if len(mNames) < 1 {
243 fmt.Println("Error: only single metric name searches are currently supported")
244 return false
245 }
246 }
247
248
249 if monoFormat {
250 gptext.DisableColors()
251 }
252
253 return true
254 }
255
256
257 func retrieveDescriptors(ctx context.Context, p []string) (*metrics.Descriptors, error) {
258 var descs metrics.Descriptors
259
260 for i := 0; i < len(p); i++ {
261
262 client, err := metrics.New(ctx, p[i])
263 if err != nil {
264 return nil, Errorf("failed to create metric client: %w", err)
265 }
266
267
268 d, err := client.ListDescriptors(metricPrefix, metricName)
269 if err != nil {
270 return nil, Errorf("failed to retrieve metric descriptors: %w", err)
271 }
272
273
274 descs, err = metrics.AppendDescriptors(descs.Descriptor, d.Descriptor)
275 if err != nil {
276 return nil, Errorf("failed to append metric descriptors lists: %w", err)
277 }
278 }
279 return &descs, nil
280 }
281
View as plain text