~juju-qa/ubuntu/xenial/juju/2.0-rc2

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/cmd/juju/application/config.go

  • Committer: Nicholas Skaggs
  • Date: 2016-09-30 14:39:30 UTC
  • mfrom: (1.8.1)
  • Revision ID: nicholas.skaggs@canonical.com-20160930143930-vwwhrefh6ftckccy
import upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2016 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
package application
 
4
 
 
5
import (
 
6
        "bytes"
 
7
        "fmt"
 
8
        "io/ioutil"
 
9
        "os"
 
10
        "strings"
 
11
        "unicode/utf8"
 
12
 
 
13
        "github.com/juju/cmd"
 
14
        "github.com/juju/errors"
 
15
        "github.com/juju/gnuflag"
 
16
 
 
17
        "github.com/juju/juju/api/application"
 
18
        "github.com/juju/juju/apiserver/params"
 
19
        "github.com/juju/juju/cmd/juju/block"
 
20
        "github.com/juju/juju/cmd/modelcmd"
 
21
        "github.com/juju/juju/cmd/output"
 
22
        "github.com/juju/utils/keyvalues"
 
23
)
 
24
 
 
25
const maxValueSize = 5242880 // Max size for a config file.
 
26
 
 
27
const (
 
28
        configSummary = `Gets, sets, or resets configuration for a deployed application.`
 
29
        configDetails = `By default, all configuration (keys, values, metadata) for the application are
 
30
displayed if a key is not specified.
 
31
 
 
32
Output includes the name of the charm used to deploy the application and a
 
33
listing of the application-specific configuration settings.
 
34
See ` + "`juju status`" + ` for application names.
 
35
 
 
36
Examples:
 
37
    juju config apache2
 
38
    juju config --format=json apache2
 
39
    juju config mysql dataset-size
 
40
    juju config mysql --reset dataset-size backup_dir
 
41
    juju config apache2 --file path/to/config.yaml
 
42
    juju config mysql dataset-size=80% backup_dir=/vol1/mysql/backups
 
43
    juju config apache2 --model mymodel --file /home/ubuntu/mysql.yaml
 
44
 
 
45
See also:
 
46
    deploy
 
47
    status
 
48
`
 
49
)
 
50
 
 
51
// NewConfigCommand returns a command used to get, reset, and set application
 
52
// attributes.
 
53
func NewConfigCommand() cmd.Command {
 
54
        return modelcmd.Wrap(&configCommand{})
 
55
}
 
56
 
 
57
type attributes map[string]string
 
58
 
 
59
// configCommand get, sets, and resets configuration values of an application.
 
60
type configCommand struct {
 
61
        api configCommandAPI
 
62
        modelcmd.ModelCommandBase
 
63
        out cmd.Output
 
64
 
 
65
        action          func(configCommandAPI, *cmd.Context) error // get, set, or reset action set in  Init
 
66
        applicationName string
 
67
        configFile      cmd.FileVar
 
68
        keys            []string
 
69
        reset           bool
 
70
        useFile         bool
 
71
        values          attributes
 
72
}
 
73
 
 
74
// configCommandAPI is an interface to allow passing in a fake implementation under test.
 
75
type configCommandAPI interface {
 
76
        Close() error
 
77
        Update(args params.ApplicationUpdate) error
 
78
        Get(application string) (*params.ApplicationGetResults, error)
 
79
        Set(application string, options map[string]string) error
 
80
        Unset(application string, options []string) error
 
81
}
 
82
 
 
83
// Info is part of the cmd.Command interface.
 
84
func (c *configCommand) Info() *cmd.Info {
 
85
        return &cmd.Info{
 
86
                Name:    "config",
 
87
                Args:    "<application name> [[--reset] <attribute-key>][=<value>] ...]",
 
88
                Purpose: configSummary,
 
89
                Doc:     configDetails,
 
90
        }
 
91
}
 
92
 
 
93
// SetFlags is part of the cmd.Command interface.
 
94
func (c *configCommand) SetFlags(f *gnuflag.FlagSet) {
 
95
        c.ModelCommandBase.SetFlags(f)
 
96
        c.out.AddFlags(f, "yaml", output.DefaultFormatters)
 
97
        f.Var(&c.configFile, "file", "path to yaml-formatted application config")
 
98
        f.BoolVar(&c.reset, "reset", false, "Reset the provided keys to be empty")
 
99
}
 
100
 
 
101
// getAPI either uses the fake API set at test time or that is nil, gets a real
 
102
// API and sets that as the API.
 
103
func (c *configCommand) getAPI() (configCommandAPI, error) {
 
104
        if c.api != nil {
 
105
                return c.api, nil
 
106
        }
 
107
        root, err := c.NewAPIRoot()
 
108
        if err != nil {
 
109
                return nil, errors.Trace(err)
 
110
        }
 
111
        client := application.NewClient(root)
 
112
        return client, nil
 
113
}
 
114
 
 
115
// Init is part of the cmd.Command interface.
 
116
func (c *configCommand) Init(args []string) error {
 
117
        if len(args) == 0 || len(strings.Split(args[0], "=")) > 1 {
 
118
                return errors.New("no application name specified")
 
119
        }
 
120
        c.applicationName = args[0]
 
121
        if c.reset {
 
122
                c.action = c.resetConfig
 
123
                return c.parseReset(args[1:])
 
124
        }
 
125
        if c.configFile.Path != "" {
 
126
                return c.parseSet(args[1:], true)
 
127
        }
 
128
        if len(args[1:]) > 0 && strings.Contains(args[1], "=") {
 
129
                return c.parseSet(args[1:], false)
 
130
        }
 
131
        return c.parseGet(args[1:])
 
132
}
 
133
 
 
134
// parseReset parses command line args when the --reset flag is supplied.
 
135
func (c *configCommand) parseReset(args []string) error {
 
136
        if len(args) == 0 {
 
137
                return errors.New("no configuration options specified")
 
138
        }
 
139
        c.action = c.resetConfig
 
140
        c.keys = args
 
141
 
 
142
        return nil
 
143
}
 
144
 
 
145
// parseSet parses the command line args when --file is set or if the
 
146
// positional args are key=value pairs.
 
147
func (c *configCommand) parseSet(args []string, file bool) error {
 
148
        if file && len(args) > 0 {
 
149
                return errors.New("cannot specify --file and key=value arguments simultaneously")
 
150
        }
 
151
        c.action = c.setConfig
 
152
        if file {
 
153
                c.useFile = true
 
154
                return nil
 
155
        }
 
156
 
 
157
        settings, err := keyvalues.Parse(args, true)
 
158
        if err != nil {
 
159
                return err
 
160
        }
 
161
        c.values = settings
 
162
 
 
163
        return nil
 
164
}
 
165
 
 
166
// parseGet parses the command line args if we aren't setting or resetting.
 
167
func (c *configCommand) parseGet(args []string) error {
 
168
        if len(args) > 1 {
 
169
                return errors.New("can only retrieve a single value, or all values")
 
170
        }
 
171
        c.action = c.getConfig
 
172
        c.keys = args
 
173
        return nil
 
174
}
 
175
 
 
176
// Run implements the cmd.Command interface.
 
177
func (c *configCommand) Run(ctx *cmd.Context) error {
 
178
        client, err := c.getAPI()
 
179
        if err != nil {
 
180
                return errors.Trace(err)
 
181
        }
 
182
        defer client.Close()
 
183
 
 
184
        return c.action(client, ctx)
 
185
}
 
186
 
 
187
// resetConfig is the run action when we are resetting attributes.
 
188
func (c *configCommand) resetConfig(client configCommandAPI, ctx *cmd.Context) error {
 
189
        return block.ProcessBlockedError(client.Unset(c.applicationName, c.keys), block.BlockChange)
 
190
}
 
191
 
 
192
// validateValues reads the values provided as args and validates that they are
 
193
// valid UTF-8.
 
194
func (c *configCommand) validateValues(ctx *cmd.Context) (map[string]string, error) {
 
195
        settings := map[string]string{}
 
196
        for k, v := range c.values {
 
197
                //empty string is also valid as a setting value
 
198
                if v == "" {
 
199
                        settings[k] = v
 
200
                        continue
 
201
                }
 
202
 
 
203
                if v[0] != '@' {
 
204
                        if !utf8.ValidString(v) {
 
205
                                return nil, errors.Errorf("value for option %q contains non-UTF-8 sequences", k)
 
206
                        }
 
207
                        settings[k] = v
 
208
                        continue
 
209
                }
 
210
                nv, err := readValue(ctx, v[1:])
 
211
                if err != nil {
 
212
                        return nil, err
 
213
                }
 
214
                if !utf8.ValidString(nv) {
 
215
                        return nil, errors.Errorf("value for option %q contains non-UTF-8 sequences", k)
 
216
                }
 
217
                settings[k] = nv
 
218
        }
 
219
        return settings, nil
 
220
 
 
221
}
 
222
 
 
223
// setConfig is the run action when we are setting new attribute values as args
 
224
// or as a file passed in.
 
225
func (c *configCommand) setConfig(client configCommandAPI, ctx *cmd.Context) error {
 
226
        if c.useFile {
 
227
                return c.setConfigFromFile(client, ctx)
 
228
        }
 
229
 
 
230
        settings, err := c.validateValues(ctx)
 
231
        if err != nil {
 
232
                return errors.Trace(err)
 
233
        }
 
234
 
 
235
        result, err := client.Get(c.applicationName)
 
236
        if err != nil {
 
237
                return err
 
238
        }
 
239
 
 
240
        for k, v := range settings {
 
241
                configValue := result.Config[k]
 
242
 
 
243
                configValueMap, ok := configValue.(map[string]interface{})
 
244
                if ok {
 
245
                        // convert the value to string and compare
 
246
                        if fmt.Sprintf("%v", configValueMap["value"]) == v {
 
247
                                logger.Warningf("the configuration setting %q already has the value %q", k, v)
 
248
                        }
 
249
                }
 
250
        }
 
251
 
 
252
        return block.ProcessBlockedError(client.Set(c.applicationName, settings), block.BlockChange)
 
253
}
 
254
 
 
255
// setConfigFromFile sets the application configuration from settings passed
 
256
// in a YAML file.
 
257
func (c *configCommand) setConfigFromFile(client configCommandAPI, ctx *cmd.Context) error {
 
258
        var (
 
259
                b   []byte
 
260
                err error
 
261
        )
 
262
        if c.configFile.Path == "-" {
 
263
                buf := bytes.Buffer{}
 
264
                buf.ReadFrom(ctx.Stdin)
 
265
                b = buf.Bytes()
 
266
        } else {
 
267
                b, err = c.configFile.Read(ctx)
 
268
                if err != nil {
 
269
                        return err
 
270
                }
 
271
        }
 
272
        return block.ProcessBlockedError(
 
273
                client.Update(
 
274
                        params.ApplicationUpdate{
 
275
                                ApplicationName: c.applicationName,
 
276
                                SettingsYAML:    string(b)}), block.BlockChange)
 
277
 
 
278
}
 
279
 
 
280
// getConfig is the run action to return one or all configuration values.
 
281
func (c *configCommand) getConfig(client configCommandAPI, ctx *cmd.Context) error {
 
282
        results, err := client.Get(c.applicationName)
 
283
        if err != nil {
 
284
                return err
 
285
        }
 
286
        if len(c.keys) == 1 {
 
287
                key := c.keys[0]
 
288
                info, found := results.Config[key].(map[string]interface{})
 
289
                if !found {
 
290
                        return errors.Errorf("key %q not found in %q application settings.", key, c.applicationName)
 
291
                }
 
292
                out := &bytes.Buffer{}
 
293
                err := cmd.FormatYaml(out, info["value"])
 
294
                if err != nil {
 
295
                        return err
 
296
                }
 
297
                fmt.Fprint(ctx.Stdout, out.String())
 
298
                return nil
 
299
        }
 
300
 
 
301
        resultsMap := map[string]interface{}{
 
302
                "application": results.Application,
 
303
                "charm":       results.Charm,
 
304
                "settings":    results.Config,
 
305
        }
 
306
        return c.out.Write(ctx, resultsMap)
 
307
}
 
308
 
 
309
// readValue reads the value of an option out of the named file.
 
310
// An empty content is valid, like in parsing the options. The upper
 
311
// size is 5M.
 
312
func readValue(ctx *cmd.Context, filename string) (string, error) {
 
313
        absFilename := ctx.AbsPath(filename)
 
314
        fi, err := os.Stat(absFilename)
 
315
        if err != nil {
 
316
                return "", errors.Errorf("cannot read option from file %q: %v", filename, err)
 
317
        }
 
318
        if fi.Size() > maxValueSize {
 
319
                return "", errors.Errorf("size of option file is larger than 5M")
 
320
        }
 
321
        content, err := ioutil.ReadFile(ctx.AbsPath(filename))
 
322
        if err != nil {
 
323
                return "", errors.Errorf("cannot read option from file %q: %v", filename, err)
 
324
        }
 
325
        return string(content), nil
 
326
}