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

mindersec / minder / 11475941036

23 Oct 2024 08:21AM UTC coverage: 54.714% (-0.01%) from 54.724%
11475941036

push

github

web-flow
Make `minder ruletype` `apply`/`create` smarter (#4798)

This changes the behavior of these commands...

If we only give one file or standard input, it will actually parse
and fail if it's not a minder resource (you probably REALLY wanted
to apply that file).

If it's a directory, it'll try to be smart and apply as much as it can.

It all depends on if the file was expanded or not.

Consequently, this fixes the issue we used to have with test files in
the `minder-rules-and-profiles` repo.

Signed-off-by: Juan Antonio Osorio <ozz@stacklok.com>
Co-authored-by: Michelangelo Mori <mmori@stacklok.com>

0 of 15 new or added lines in 2 files covered. (0.0%)

3 existing lines in 1 file now uncovered.

14921 of 27271 relevant lines covered (54.71%)

41.42 hits per line

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

16.31
/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/config"
25
        "github.com/mindersec/minder/internal/util"
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 {
57✔
82
        return func(cmd *cobra.Command, args []string) error {
57✔
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
// GetDefaultCLIConfigPath returns the default path for the CLI config file
208
// Returns an empty string if the path cannot be determined
209
func GetDefaultCLIConfigPath() string {
3✔
210
        //nolint:errcheck // ignore error as we are just checking if the file exists
3✔
211
        cfgDirPath, _ := util.GetConfigDirPath()
3✔
212

3✔
213
        var xdgConfigPath string
3✔
214
        if cfgDirPath != "" {
6✔
215
                xdgConfigPath = filepath.Join(cfgDirPath, "config.yaml")
3✔
216
        }
3✔
217

218
        return xdgConfigPath
3✔
219
}
220

221
// GetRelevantCLIConfigPath returns the relevant CLI config path.
222
// It will return the first path that exists from the following:
223
// 1. The path specified in the config flag
224
// 2. The local config.yaml file
225
// 3. The default CLI config path
226
func GetRelevantCLIConfigPath(v *viper.Viper) string {
2✔
227
        cfgFile := v.GetString("config")
2✔
228
        return config.GetRelevantCfgPath(append([]string{cfgFile},
2✔
229
                filepath.Join(".", "config.yaml"),
2✔
230
                GetDefaultCLIConfigPath(),
2✔
231
        ))
2✔
232
}
2✔
233

234
// IsYAMLFileAndNotATest checks if a file is a YAML file and not a test file
235
func IsYAMLFileAndNotATest(path string) bool {
×
NEW
236
        return (filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml")
×
237
}
×
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

© 2026 Coveralls, Inc