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

mendersoftware / mender-setup / 1020542436

29 Sep 2023 07:53AM UTC coverage: 36.2% (-63.8%) from 100.0%
1020542436

push

gitlab-ci

web-flow
Merge pull request #2 from oleorhagen/master

feat: Create the mender-setup CLI tool

362 of 1000 new or added lines in 2 files covered. (36.2%)

362 of 1000 relevant lines covered (36.2%)

2.97 hits per line

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

0.0
/cli/cli.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//        Licensed under the Apache License, Version 2.0 (the "License");
4
//        you may not use this file except in compliance with the License.
5
//        You may obtain a copy of the License at
6
//
7
//            http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//        Unless required by applicable law or agreed to in writing, software
10
//        distributed under the License is distributed on an "AS IS" BASIS,
11
//        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//        See the License for the specific language governing permissions and
13
//        limitations under the License.
14
package cli
15

16
import (
17
        "bytes"
18
        "fmt"
19
        "io"
20
        "io/ioutil"
21
        "os"
22
        "path"
23
        "runtime"
24
        "strings"
25

26
        log "github.com/sirupsen/logrus"
27

28
        "github.com/pkg/errors"
29
        "github.com/urfave/cli/v2"
30
        terminal "golang.org/x/term"
31

32
        setup_conf "github.com/mendersoftware/mender-setup/conf"
33

34
        "github.com/mendersoftware/mender/conf"
35
)
36

37
const (
38
        appDescription = `mender-setup is a cli tool for generating the mender.conf` +
39
                ` configuration files, either through specifying the parameters to the CLI,` +
40
                `or through running it interactively`
41
)
42

43
const (
44
        errMsgAmbiguousArgumentsGivenF = "Ambiguous arguments given - " +
45
                "unrecognized argument: %s"
46
        errMsgConflictingArgumentsF = "Conflicting arguments given, only one " +
47
                "of the following flags may be given: {%q, %q}"
48
)
49

50
type runOptionsType struct {
51
        config         string
52
        fallbackConfig string
53
        dataStore      string
54
        conf.HttpConfig
55
        setupOptions setupOptionsType // Options for setup subcommand
56
}
57

NEW
58
func ShowVersion() string {
×
NEW
59
        return fmt.Sprintf("%s\truntime: %s",
×
NEW
60
                setup_conf.VersionString(), runtime.Version())
×
NEW
61
}
×
62

NEW
63
func SetupCLI(args []string) error {
×
NEW
64
        runOptions := &runOptionsType{}
×
NEW
65

×
NEW
66
        app := &cli.App{
×
NEW
67
                Description: appDescription,
×
NEW
68
                Name:        "mender-setup",
×
NEW
69
                Usage:       "Run to create a working mender-configuration file",
×
NEW
70
                ArgsUsage:   "[options]",
×
NEW
71
                Action:      runOptions.setupCLIHandler,
×
NEW
72
                Version:     ShowVersion(),
×
NEW
73
                Flags: []cli.Flag{
×
NEW
74
                        &cli.StringFlag{
×
NEW
75
                                Name:        "config",
×
NEW
76
                                Aliases:     []string{"c"},
×
NEW
77
                                Destination: &runOptions.setupOptions.configPath,
×
NEW
78
                                Value:       conf.DefaultConfFile,
×
NEW
79
                                Usage:       "`PATH` to configuration file.",
×
NEW
80
                        },
×
NEW
81
                        &cli.StringFlag{
×
NEW
82
                                Name:    "data",
×
NEW
83
                                Aliases: []string{"d"},
×
NEW
84
                                Usage:   "Mender state data `DIR`ECTORY path.",
×
NEW
85
                                Value:   conf.DefaultDataStore,
×
NEW
86
                        },
×
NEW
87
                        &cli.StringFlag{
×
NEW
88
                                Name:        "device-type",
×
NEW
89
                                Destination: &runOptions.setupOptions.deviceType,
×
NEW
90
                                Usage:       "Name of the device `type`.",
×
NEW
91
                        },
×
NEW
92
                        &cli.StringFlag{
×
NEW
93
                                Name:        "username",
×
NEW
94
                                Destination: &runOptions.setupOptions.username,
×
NEW
95
                                Usage:       "User `E-Mail` at hosted.mender.io.",
×
NEW
96
                        },
×
NEW
97
                        &cli.StringFlag{
×
NEW
98
                                Name:        "password",
×
NEW
99
                                Destination: &runOptions.setupOptions.password,
×
NEW
100
                                Usage:       "User `PASSWORD` at hosted.mender.io.",
×
NEW
101
                        },
×
NEW
102
                        &cli.StringFlag{
×
NEW
103
                                Name:        "server-url",
×
NEW
104
                                Aliases:     []string{"url"},
×
NEW
105
                                Destination: &runOptions.setupOptions.serverURL,
×
NEW
106
                                Usage:       "`URL` to Mender server.",
×
NEW
107
                                Value:       "https://docker.mender.io",
×
NEW
108
                        },
×
NEW
109
                        &cli.StringFlag{
×
NEW
110
                                Name:        "server-ip",
×
NEW
111
                                Destination: &runOptions.setupOptions.serverIP,
×
NEW
112
                                Usage:       "Server ip address.",
×
NEW
113
                        },
×
NEW
114
                        &cli.StringFlag{
×
NEW
115
                                Name:        "server-cert",
×
NEW
116
                                Aliases:     []string{"E"},
×
NEW
117
                                Destination: &runOptions.setupOptions.serverCert,
×
NEW
118
                                Usage:       "`PATH` to trusted server certificates",
×
NEW
119
                        },
×
NEW
120
                        &cli.StringFlag{
×
NEW
121
                                Name:        "tenant-token",
×
NEW
122
                                Destination: &runOptions.setupOptions.tenantToken,
×
NEW
123
                                Usage:       "Hosted Mender tenant `token`",
×
NEW
124
                        },
×
NEW
125
                        &cli.IntFlag{
×
NEW
126
                                Name:        "inventory-poll",
×
NEW
127
                                Destination: &runOptions.setupOptions.invPollInterval,
×
NEW
128
                                Usage:       "Inventory poll interval in `sec`onds.",
×
NEW
129
                                Value:       defaultInventoryPoll,
×
NEW
130
                        },
×
NEW
131
                        &cli.IntFlag{
×
NEW
132
                                Name:        "retry-poll",
×
NEW
133
                                Destination: &runOptions.setupOptions.retryPollInterval,
×
NEW
134
                                Usage:       "Retry poll interval in `sec`onds.",
×
NEW
135
                                Value:       defaultRetryPoll,
×
NEW
136
                        },
×
NEW
137
                        &cli.IntFlag{
×
NEW
138
                                Name:        "update-poll",
×
NEW
139
                                Destination: &runOptions.setupOptions.updatePollInterval,
×
NEW
140
                                Usage:       "Update poll interval in `sec`onds.",
×
NEW
141
                                Value:       defaultUpdatePoll,
×
NEW
142
                        },
×
NEW
143
                        &cli.BoolFlag{
×
NEW
144
                                Name:        "hosted-mender",
×
NEW
145
                                Destination: &runOptions.setupOptions.hostedMender,
×
NEW
146
                                Usage:       "Setup device towards Hosted Mender.",
×
NEW
147
                        },
×
NEW
148
                        &cli.BoolFlag{
×
NEW
149
                                Name:        "demo",
×
NEW
150
                                Destination: &runOptions.setupOptions.demo,
×
NEW
151
                                Usage: "Use demo configuration. DEPRECATED: use --demo-server and/or" +
×
NEW
152
                                        " --demo-polling instead",
×
NEW
153
                        },
×
NEW
154
                        &cli.BoolFlag{
×
NEW
155
                                Name:        "demo-server",
×
NEW
156
                                Destination: &runOptions.setupOptions.demoServer,
×
NEW
157
                                Usage:       "Use demo server configuration.",
×
NEW
158
                        },
×
NEW
159
                        &cli.BoolFlag{
×
NEW
160
                                Name:        "demo-polling",
×
NEW
161
                                Destination: &runOptions.setupOptions.demoIntervals,
×
NEW
162
                                Usage:       "Use demo polling intervals.",
×
NEW
163
                        },
×
NEW
164
                        &cli.BoolFlag{
×
NEW
165
                                Name:  "quiet",
×
NEW
166
                                Usage: "Suppress informative prompts.",
×
NEW
167
                        },
×
NEW
168
                },
×
NEW
169
        }
×
NEW
170

×
NEW
171
        cli.HelpPrinter = upgradeHelpPrinter(cli.HelpPrinter)
×
NEW
172
        cli.VersionPrinter = func(c *cli.Context) {
×
NEW
173
                fmt.Fprintf(c.App.Writer, "%s\n", ShowVersion())
×
NEW
174
        }
×
NEW
175
        return app.Run(args)
×
176
}
177

178
func (runOptions *runOptionsType) commonCLIHandler(
NEW
179
        ctx *cli.Context) (*conf.MenderConfig, error) {
×
NEW
180

×
NEW
181
        fmt.Println("runOptions datastore: ", runOptions.dataStore)
×
NEW
182

×
NEW
183
        // Handle config flags
×
NEW
184
        config, err := conf.LoadConfig(
×
NEW
185
                runOptions.config, runOptions.fallbackConfig)
×
NEW
186
        if err != nil {
×
NEW
187
                return nil, err
×
NEW
188
        }
×
189

190
        // Make sure that paths that are not configurable via the config file is conconsistent with
191
        // --data flag
NEW
192
        config.ArtifactScriptsPath = path.Join(runOptions.dataStore, "scripts")
×
NEW
193
        config.ModulesWorkPath = path.Join(runOptions.dataStore, "modules", "v3")
×
NEW
194

×
NEW
195
        // Checks if the DeviceTypeFile is defined in config file.
×
NEW
196
        if config.MenderConfigFromFile.DeviceTypeFile != "" {
×
NEW
197
                // Sets the config.DeviceTypeFile to the value in config file.
×
NEW
198
                config.DeviceTypeFile = config.MenderConfigFromFile.DeviceTypeFile
×
NEW
199

×
NEW
200
        } else {
×
NEW
201
                config.MenderConfigFromFile.DeviceTypeFile = path.Join(
×
NEW
202
                        runOptions.dataStore, "device_type")
×
NEW
203
                config.DeviceTypeFile = path.Join(
×
NEW
204
                        runOptions.dataStore, "device_type")
×
NEW
205
        }
×
206

NEW
207
        if runOptions.HttpConfig.NoVerify {
×
NEW
208
                config.SkipVerify = true
×
NEW
209
        }
×
210

NEW
211
        return config, nil
×
212
}
213

NEW
214
func (runOptions *runOptionsType) handleCLIOptions(ctx *cli.Context) error {
×
NEW
215
        config, err := runOptions.commonCLIHandler(ctx)
×
NEW
216
        if err != nil {
×
NEW
217
                return err
×
NEW
218
        }
×
219

220
        // Execute commands
221
        // switch ctx.Command.Name {
222

223
        // Check that user has permission to directories so that
224
        // the user doesn't have to perform the setup before raising
225
        // an error.
NEW
226
        fmt.Println("runOptions config: ", runOptions.config)
×
NEW
227
        fmt.Println("runOptions config: ", path.Dir(runOptions.config))
×
NEW
228
        if err = checkWritePermissions(path.Dir(runOptions.config)); err != nil {
×
NEW
229
                return err
×
NEW
230
        }
×
NEW
231
        fmt.Println("runOptions config datastore: ", runOptions.dataStore)
×
NEW
232
        fmt.Println("runOptions config datastore: ", path.Dir(runOptions.dataStore))
×
NEW
233
        if err = checkWritePermissions(runOptions.dataStore); err != nil {
×
NEW
234
                return err
×
NEW
235
        }
×
236
        // Run cli setup prompts.
NEW
237
        if err := doSetup(ctx, &config.MenderConfigFromFile,
×
NEW
238
                &runOptions.setupOptions); err != nil {
×
NEW
239
                return err
×
NEW
240
        }
×
NEW
241
        if !ctx.Bool("quiet") {
×
NEW
242
                fmt.Println(promptDone)
×
NEW
243
        }
×
244

NEW
245
        return err
×
246
}
247

NEW
248
func (runOptions *runOptionsType) setupCLIHandler(ctx *cli.Context) error {
×
NEW
249
        if ctx.Args().Len() > 0 {
×
NEW
250
                return errors.Errorf(
×
NEW
251
                        errMsgAmbiguousArgumentsGivenF,
×
NEW
252
                        ctx.Args().First())
×
NEW
253
        }
×
NEW
254
        if !ctx.IsSet("log-level") {
×
NEW
255
                log.SetLevel(log.WarnLevel)
×
NEW
256
        }
×
NEW
257
        if err := runOptions.setupOptions.handleImplicitFlags(ctx); err != nil {
×
NEW
258
                return err
×
NEW
259
        }
×
260

261
        // Handle overlapping global flags
NEW
262
        if ctx.IsSet("config") && !ctx.IsSet("config") {
×
NEW
263
                runOptions.setupOptions.configPath = runOptions.config
×
NEW
264
        } else {
×
NEW
265
                runOptions.config = runOptions.setupOptions.configPath
×
NEW
266
        }
×
NEW
267
        runOptions.dataStore = ctx.String("data")
×
NEW
268
        if runOptions.HttpConfig.ServerCert != "" &&
×
NEW
269
                runOptions.setupOptions.serverCert == "" {
×
NEW
270
                runOptions.setupOptions.serverCert = runOptions.HttpConfig.ServerCert
×
NEW
271
        } else {
×
NEW
272
                runOptions.HttpConfig.ServerCert = runOptions.setupOptions.serverCert
×
NEW
273
        }
×
NEW
274
        return runOptions.handleCLIOptions(ctx)
×
275
}
276

277
func upgradeHelpPrinter(defaultPrinter func(w io.Writer, templ string, data interface{})) func(
NEW
278
        w io.Writer, templ string, data interface{}) {
×
NEW
279
        // Applies the ordinary help printer with column post processing
×
NEW
280
        return func(stdout io.Writer, templ string, data interface{}) {
×
NEW
281
                // Need at least 10 characters for last column in order to
×
NEW
282
                // pretty print; otherwise the output is unreadable.
×
NEW
283
                const minColumnWidth = 10
×
NEW
284
                isLowerCase := func(c rune) bool {
×
NEW
285
                        // returns true if c in [a-z] else false
×
NEW
286
                        asciiVal := int(c)
×
NEW
287
                        if asciiVal >= 0x61 && asciiVal <= 0x7A {
×
NEW
288
                                return true
×
NEW
289
                        }
×
NEW
290
                        return false
×
291
                }
292
                // defaultPrinter parses the text-template and outputs to buffer
NEW
293
                var buf bytes.Buffer
×
NEW
294
                defaultPrinter(&buf, templ, data)
×
NEW
295
                terminalWidth, _, err := terminal.GetSize(int(os.Stdout.Fd()))
×
NEW
296
                if err != nil || terminalWidth <= 0 {
×
NEW
297
                        // Just write help as is.
×
NEW
298
                        stdout.Write(buf.Bytes())
×
NEW
299
                        return
×
NEW
300
                }
×
NEW
301
                for line, err := buf.ReadString('\n'); err == nil; line, err = buf.ReadString('\n') {
×
NEW
302
                        if len(line) <= terminalWidth+1 {
×
NEW
303
                                stdout.Write([]byte(line))
×
NEW
304
                                continue
×
305
                        }
NEW
306
                        newLine := line
×
NEW
307
                        indent := strings.LastIndex(
×
NEW
308
                                line[:terminalWidth], "  ")
×
NEW
309
                        // find indentation of last column
×
NEW
310
                        if indent == -1 {
×
NEW
311
                                indent = 0
×
NEW
312
                        }
×
NEW
313
                        indent += strings.IndexFunc(
×
NEW
314
                                strings.ToLower(line[indent:]), isLowerCase) - 1
×
NEW
315
                        if indent >= terminalWidth-minColumnWidth ||
×
NEW
316
                                indent == -1 {
×
NEW
317
                                indent = 0
×
NEW
318
                        }
×
319
                        // Format the last column to be aligned
NEW
320
                        for len(newLine) > terminalWidth {
×
NEW
321
                                // find word to insert newline
×
NEW
322
                                idx := strings.LastIndex(newLine[:terminalWidth], " ")
×
NEW
323
                                if idx == indent || idx == -1 {
×
NEW
324
                                        idx = terminalWidth
×
NEW
325
                                }
×
NEW
326
                                stdout.Write([]byte(newLine[:idx] + "\n"))
×
NEW
327
                                newLine = newLine[idx:]
×
NEW
328
                                newLine = strings.Repeat(" ", indent) + newLine
×
329
                        }
NEW
330
                        stdout.Write([]byte(newLine))
×
331
                }
NEW
332
                if err != nil {
×
NEW
333
                        log.Fatalf("CLI HELP: error writing help string: %v\n", err)
×
NEW
334
                }
×
335
        }
336
}
337

NEW
338
func checkWritePermissions(dir string) error {
×
NEW
339
        fmt.Println("Checking the permissions for: ", dir)
×
NEW
340
        _, err := os.Stat(dir)
×
NEW
341
        if os.IsNotExist(err) {
×
NEW
342
                err := os.MkdirAll(dir, 0755)
×
NEW
343
                if err != nil {
×
NEW
344
                        return errors.Wrapf(err, "Error creating "+
×
NEW
345
                                "directory %q", dir)
×
NEW
346
                }
×
NEW
347
        } else if os.IsPermission(err) {
×
NEW
348
                return errors.Wrapf(os.ErrPermission,
×
NEW
349
                        "Error trying to stat directory %q", dir)
×
NEW
350
        } else if err != nil {
×
NEW
351
                return errors.Errorf("Error trying to stat directory %q", dir)
×
NEW
352
        }
×
NEW
353
        f, err := ioutil.TempFile(dir, "temporaryFile")
×
NEW
354
        if os.IsPermission(err) {
×
NEW
355
                return errors.Wrapf(err, "User does not have "+
×
NEW
356
                        "permission to write to data store "+
×
NEW
357
                        "directory %q", dir)
×
NEW
358
        } else if err != nil {
×
NEW
359
                return errors.Wrapf(err,
×
NEW
360
                        "Error checking write permissions to "+
×
NEW
361
                                "directory %q", dir)
×
NEW
362
        }
×
NEW
363
        os.Remove(f.Name())
×
NEW
364
        return nil
×
365
}
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