• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mindersec / minder / 13123856909

03 Feb 2025 10:12PM UTC coverage: 57.48% (-0.03%) from 57.505%
13123856909

Pull #5387

github

web-flow
Merge c75ff4c9d into f5f00edca
Pull Request #5387: Adds check to ensure config file exists. Fixes: #4513

18132 of 31545 relevant lines covered (57.48%)

37.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

17.53
/internal/util/cli/cli.go
1
// SPDX-FileCopyrightText: Copyright 2023 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
// Package cli contains utility for the cli
5
package cli
6

7
import (
8
        "context"
9
        "errors"
10
        "fmt"
11
        "os"
12
        "path/filepath"
13
        "regexp"
14
        "strings"
15
        "time"
16

17
        "github.com/erikgeiser/promptkit/confirmation"
18
        "github.com/spf13/cobra"
19
        "github.com/spf13/viper"
20
        "google.golang.org/grpc"
21
        "google.golang.org/grpc/codes"
22
        "google.golang.org/grpc/status"
23

24
        "github.com/mindersec/minder/internal/util"
25
        "github.com/mindersec/minder/pkg/config"
26
)
27

28
// ErrWrappedCLIError is an error that wraps another error and provides a message used from within the CLI
29
type ErrWrappedCLIError struct {
30
        Message string
31
        Err     error
32
}
33

34
func (e *ErrWrappedCLIError) Error() string {
×
35
        return e.Err.Error()
×
36
}
×
37

38
// PrintYesNoPrompt prints a yes/no prompt to the user and returns false if the user did not respond with yes or y
39
func PrintYesNoPrompt(cmd *cobra.Command, promptMsg, confirmMsg, fallbackMsg string, defaultYes bool) bool {
×
40
        // Print the warning banner with the prompt message
×
41
        cmd.Println(WarningBanner.Render(promptMsg))
×
42

×
43
        // Determine the default confirmation value
×
44
        defConf := confirmation.No
×
45
        if defaultYes {
×
46
                defConf = confirmation.Yes
×
47
        }
×
48

49
        // Prompt the user for confirmation
50
        input := confirmation.New(confirmMsg, defConf)
×
51
        ok, err := input.RunPrompt()
×
52
        if err != nil {
×
53
                cmd.Println(WarningBanner.Render(fmt.Sprintf("Error reading input: %v", err)))
×
54
                ok = false
×
55
        }
×
56

57
        // If the user did not confirm, print the fallback message
58
        if !ok {
×
59
                cmd.Println(Header.Render(fallbackMsg))
×
60
        }
×
61
        return ok
×
62
}
63

64
// GetAppContext is a helper for getting the cmd app context
65
func GetAppContext(ctx context.Context, v *viper.Viper) (context.Context, context.CancelFunc) {
×
66
        return GetAppContextWithTimeoutDuration(ctx, v, 20)
×
67
}
×
68

69
// GetAppContextWithTimeoutDuration is a helper for getting the cmd app context with a custom timeout
70
func GetAppContextWithTimeoutDuration(ctx context.Context, v *viper.Viper, tout int) (context.Context, context.CancelFunc) {
×
71
        v.SetDefault("cli.context_timeout", tout)
×
72
        timeout := v.GetInt("cli.context_timeout")
×
73

×
74
        ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
×
75
        return ctx, cancel
×
76
}
×
77

78
// GRPCClientWrapRunE is a wrapper for cobra commands that sets up the grpc client and context
79
func GRPCClientWrapRunE(
80
        runEFunc func(ctx context.Context, cmd *cobra.Command, args []string, c *grpc.ClientConn) error,
81
) func(cmd *cobra.Command, args []string) error {
62✔
82
        return func(cmd *cobra.Command, args []string) error {
62✔
83
                if err := viper.BindPFlags(cmd.Flags()); err != nil {
×
84
                        return fmt.Errorf("error binding flags: %s", err)
×
85
                }
×
86

87
                ctx, cancel := GetAppContext(cmd.Context(), viper.GetViper())
×
88
                defer cancel()
×
89

×
90
                c, err := GrpcForCommand(cmd, viper.GetViper())
×
91
                if err != nil {
×
92
                        return err
×
93
                }
×
94

95
                defer c.Close()
×
96

×
97
                return runEFunc(ctx, cmd, args, c)
×
98
        }
99
}
100

101
// MessageAndError prints a message and returns an error.
102
func MessageAndError(msg string, err error) error {
×
103
        return &ErrWrappedCLIError{Message: msg, Err: err}
×
104
}
×
105

106
// ExitNicelyOnError print a message and exit with the right code
107
func ExitNicelyOnError(err error, userMsg string) {
2✔
108
        var message string
2✔
109
        var details string
2✔
110
        exitCode := 1 // Default to 1
2✔
111
        if err != nil {
2✔
112
                if userMsg != "" {
×
113
                        // This handles the case where we want to print an explicit message before processing the error
×
114
                        fmt.Fprintf(os.Stderr, "Message: %s\n", userMsg)
×
115
                }
×
116
                // Check if the error is wrapped
117
                var wrappedErr *ErrWrappedCLIError
×
118
                if errors.As(err, &wrappedErr) {
×
119
                        // Print the wrapped message
×
120
                        message = wrappedErr.Message
×
121
                        // Continue processing the wrapped error
×
122
                        err = wrappedErr.Err
×
123
                }
×
124
                // Check if the error is a grpc status
125
                if rpcStatus, ok := status.FromError(err); ok {
×
126
                        nice := util.FromRpcError(rpcStatus)
×
127
                        // If the error is unauthenticated, we want to print a helpful message and exit, no need to print details
×
128
                        if rpcStatus.Code() == codes.Unauthenticated {
×
129
                                message = "It seems you are logged out. Please run \"minder auth login\" first."
×
130
                        } else {
×
131
                                details = nice.Details
×
132
                        }
×
133
                        exitCode = int(nice.Code)
×
134
                } else {
×
135
                        details = err.Error()
×
136
                }
×
137
                // Print the message, if any
138
                if message != "" {
×
139
                        fmt.Fprintf(os.Stderr, "Message: %s\n", message)
×
140
                }
×
141
                // Print the details, if any
142
                if details != "" {
×
143
                        fmt.Fprintf(os.Stderr, "Details: %s\n", details)
×
144
                }
×
145
                // Exit with the right code
146
                os.Exit(exitCode)
×
147
        }
148
}
149

150
// GetRepositoryName returns the repository name in the format owner/name
151
func GetRepositoryName(owner, name string) string {
×
152
        if owner == "" {
×
153
                return name
×
154
        }
×
155
        return fmt.Sprintf("%s/%s", owner, name)
×
156
}
157

158
var validRepoSlugRe = regexp.MustCompile(`(?i)^[-a-z0-9_\.]+\/[-a-z0-9_\.]+$`)
159

160
// ValidateRepositoryName checks if a repository name is valid
161
func ValidateRepositoryName(repository string) error {
×
162
        if !validRepoSlugRe.MatchString(repository) {
×
163
                return fmt.Errorf("invalid repository name: %s", repository)
×
164
        }
×
165
        return nil
×
166
}
167

168
// GetNameAndOwnerFromRepository returns the owner and name from a repository name in the format owner/name
169
func GetNameAndOwnerFromRepository(repository string) (string, string) {
×
170
        first, second, found := strings.Cut(repository, "/")
×
171
        if !found {
×
172
                return "", first
×
173
        }
×
174

175
        return first, second
×
176
}
177

178
// ConcatenateAndWrap takes a string and a maximum line length (maxLen),
179
// then outputs the string as a multiline string where each line does not exceed maxLen characters.
180
func ConcatenateAndWrap(input string, maxLen int) string {
×
181
        if maxLen <= 0 {
×
182
                return input
×
183
        }
×
184

185
        var result string
×
186
        var lineLength int
×
187

×
188
        for _, runeValue := range input {
×
189
                // If the line length equals the len, append a newline and reset lineLength
×
190
                if lineLength == maxLen {
×
191
                        if result[len(result)-1] != ' ' {
×
192
                                // We trim at a word
×
193
                                result += "-\n"
×
194
                        } else {
×
195
                                // We trim at a space, no need to add "-"
×
196
                                result += "\n"
×
197
                        }
×
198
                        lineLength = 0
×
199
                }
200
                result += string(runeValue)
×
201
                lineLength++
×
202
        }
203

204
        return result
×
205
}
206

207
// GetConfigDirPath returns the path to the config directory
208
func GetConfigDirPath() (string, error) {
9✔
209
        // Get the XDG_CONFIG_HOME environment variable
9✔
210
        xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
9✔
211

9✔
212
        // If XDG_CONFIG_HOME is not set or empty, use $HOME/.config as the base directory
9✔
213
        if xdgConfigHome == "" {
10✔
214
                homeDir, err := os.UserHomeDir()
1✔
215
                if err != nil {
1✔
216
                        return "", fmt.Errorf("error getting home directory: %v", err)
×
217
                }
×
218
                xdgConfigHome = filepath.Join(homeDir, ".config")
1✔
219
        }
220

221
        filePath := filepath.Join(xdgConfigHome, "minder")
9✔
222
        return filePath, nil
9✔
223
}
224

225
// GetDefaultCLIConfigPath returns the default path for the CLI config file
226
// Returns an empty string if the path cannot be determined
227
func GetDefaultCLIConfigPath() string {
1✔
228
        //nolint:errcheck // ignore error as we are just checking if the file exists
1✔
229
        cfgDirPath, _ := GetConfigDirPath()
1✔
230

1✔
231
        var xdgConfigPath string
1✔
232
        if cfgDirPath != "" {
2✔
233
                xdgConfigPath = filepath.Join(cfgDirPath, "config.yaml")
1✔
234
        }
1✔
235

236
        return xdgConfigPath
1✔
237
}
238

239
// GetRelevantCLIConfigPath returns the relevant CLI config path.
240
// It will return the first path that exists from the following:
241
// 1. The path specified in the config flag
242
// 2. The local config.yaml file
243
// 3. The default CLI config path
244
func GetRelevantCLIConfigPath(v *viper.Viper) string {
×
245
        cfgFile := v.GetString("config")
×
246
        return config.GetRelevantCfgPath(append([]string{cfgFile},
×
247
                filepath.Join(".", "config.yaml"),
×
248
                GetDefaultCLIConfigPath(),
×
249
        ))
×
250
}
×
251

252
// IsYAMLFileAndNotATest checks if a file is a YAML file and not a test file
253
func IsYAMLFileAndNotATest(path string) bool {
×
254
        return (filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml")
×
255
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc