2016-12-25 14:31:52 -05:00
|
|
|
package configfile
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
2017-06-21 17:20:49 -04:00
|
|
|
"github.com/docker/cli/cli/config/credentials"
|
2017-10-15 15:39:56 -04:00
|
|
|
"github.com/docker/cli/cli/config/types"
|
2017-03-09 13:23:45 -05:00
|
|
|
"github.com/pkg/errors"
|
2020-06-19 14:14:14 -04:00
|
|
|
"github.com/sirupsen/logrus"
|
2016-12-25 14:31:52 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// ConfigFile ~/.docker/config.json file info
|
|
|
|
type ConfigFile struct {
|
2019-02-18 06:49:41 -05:00
|
|
|
AuthConfigs map[string]types.AuthConfig `json:"auths"`
|
|
|
|
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
|
|
|
|
PsFormat string `json:"psFormat,omitempty"`
|
|
|
|
ImagesFormat string `json:"imagesFormat,omitempty"`
|
|
|
|
NetworksFormat string `json:"networksFormat,omitempty"`
|
|
|
|
PluginsFormat string `json:"pluginsFormat,omitempty"`
|
|
|
|
VolumesFormat string `json:"volumesFormat,omitempty"`
|
|
|
|
StatsFormat string `json:"statsFormat,omitempty"`
|
|
|
|
DetachKeys string `json:"detachKeys,omitempty"`
|
|
|
|
CredentialsStore string `json:"credsStore,omitempty"`
|
|
|
|
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
|
|
|
|
Filename string `json:"-"` // Note: for internal use only
|
|
|
|
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
|
|
|
|
ServicesFormat string `json:"servicesFormat,omitempty"`
|
|
|
|
TasksFormat string `json:"tasksFormat,omitempty"`
|
|
|
|
SecretFormat string `json:"secretFormat,omitempty"`
|
|
|
|
ConfigFormat string `json:"configFormat,omitempty"`
|
|
|
|
NodesFormat string `json:"nodesFormat,omitempty"`
|
|
|
|
PruneFilters []string `json:"pruneFilters,omitempty"`
|
|
|
|
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
|
|
|
|
Experimental string `json:"experimental,omitempty"`
|
|
|
|
CurrentContext string `json:"currentContext,omitempty"`
|
|
|
|
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
|
|
|
|
Plugins map[string]map[string]string `json:"plugins,omitempty"`
|
2019-04-17 18:09:29 -04:00
|
|
|
Aliases map[string]string `json:"aliases,omitempty"`
|
2023-07-20 11:25:36 -04:00
|
|
|
Features map[string]string `json:"features,omitempty"`
|
2017-01-31 00:15:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// ProxyConfig contains proxy configuration settings
|
|
|
|
type ProxyConfig struct {
|
|
|
|
HTTPProxy string `json:"httpProxy,omitempty"`
|
|
|
|
HTTPSProxy string `json:"httpsProxy,omitempty"`
|
|
|
|
NoProxy string `json:"noProxy,omitempty"`
|
|
|
|
FTPProxy string `json:"ftpProxy,omitempty"`
|
2021-04-30 05:53:53 -04:00
|
|
|
AllProxy string `json:"allProxy,omitempty"`
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
2017-06-27 10:31:38 -04:00
|
|
|
// New initializes an empty configuration file for the given filename 'fn'
|
|
|
|
func New(fn string) *ConfigFile {
|
2017-06-12 14:36:49 -04:00
|
|
|
return &ConfigFile{
|
|
|
|
AuthConfigs: make(map[string]types.AuthConfig),
|
|
|
|
HTTPHeaders: make(map[string]string),
|
|
|
|
Filename: fn,
|
2019-02-18 06:49:41 -05:00
|
|
|
Plugins: make(map[string]map[string]string),
|
2019-04-17 18:09:29 -04:00
|
|
|
Aliases: make(map[string]string),
|
2017-06-12 14:36:49 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-25 14:31:52 -05:00
|
|
|
// LoadFromReader reads the configuration data given and sets up the auth config
|
|
|
|
// information with given directory and populates the receiver object
|
|
|
|
func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error {
|
2022-02-22 16:05:06 -05:00
|
|
|
if err := json.NewDecoder(configData).Decode(configFile); err != nil && !errors.Is(err, io.EOF) {
|
2016-12-25 14:31:52 -05:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
for addr, ac := range configFile.AuthConfigs {
|
2019-10-02 15:41:50 -04:00
|
|
|
if ac.Auth != "" {
|
|
|
|
ac.Username, ac.Password, err = decodeAuth(ac.Auth)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
ac.Auth = ""
|
|
|
|
ac.ServerAddress = addr
|
|
|
|
configFile.AuthConfigs[addr] = ac
|
|
|
|
}
|
2021-06-16 03:08:42 -04:00
|
|
|
return nil
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// ContainsAuth returns whether there is authentication configured
|
|
|
|
// in this file or not.
|
|
|
|
func (configFile *ConfigFile) ContainsAuth() bool {
|
|
|
|
return configFile.CredentialsStore != "" ||
|
|
|
|
len(configFile.CredentialHelpers) > 0 ||
|
|
|
|
len(configFile.AuthConfigs) > 0
|
|
|
|
}
|
|
|
|
|
2017-06-21 16:47:06 -04:00
|
|
|
// GetAuthConfigs returns the mapping of repo to auth configuration
|
|
|
|
func (configFile *ConfigFile) GetAuthConfigs() map[string]types.AuthConfig {
|
2023-07-12 18:57:39 -04:00
|
|
|
if configFile.AuthConfigs == nil {
|
|
|
|
configFile.AuthConfigs = make(map[string]types.AuthConfig)
|
|
|
|
}
|
2017-06-21 16:47:06 -04:00
|
|
|
return configFile.AuthConfigs
|
|
|
|
}
|
|
|
|
|
2016-12-25 14:31:52 -05:00
|
|
|
// SaveToWriter encodes and writes out all the authorization information to
|
|
|
|
// the given writer
|
|
|
|
func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error {
|
|
|
|
// Encode sensitive data into a new/temp struct
|
|
|
|
tmpAuthConfigs := make(map[string]types.AuthConfig, len(configFile.AuthConfigs))
|
|
|
|
for k, authConfig := range configFile.AuthConfigs {
|
|
|
|
authCopy := authConfig
|
|
|
|
// encode and save the authstring, while blanking out the original fields
|
|
|
|
authCopy.Auth = encodeAuth(&authCopy)
|
|
|
|
authCopy.Username = ""
|
|
|
|
authCopy.Password = ""
|
|
|
|
authCopy.ServerAddress = ""
|
|
|
|
tmpAuthConfigs[k] = authCopy
|
|
|
|
}
|
|
|
|
|
|
|
|
saveAuthConfigs := configFile.AuthConfigs
|
|
|
|
configFile.AuthConfigs = tmpAuthConfigs
|
|
|
|
defer func() { configFile.AuthConfigs = saveAuthConfigs }()
|
|
|
|
|
2020-09-29 08:49:47 -04:00
|
|
|
// User-Agent header is automatically set, and should not be stored in the configuration
|
|
|
|
for v := range configFile.HTTPHeaders {
|
|
|
|
if strings.EqualFold(v, "User-Agent") {
|
|
|
|
delete(configFile.HTTPHeaders, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-25 14:31:52 -05:00
|
|
|
data, err := json.MarshalIndent(configFile, "", "\t")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = writer.Write(data)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save encodes and writes out all the authorization information
|
2020-06-19 14:14:14 -04:00
|
|
|
func (configFile *ConfigFile) Save() (retErr error) {
|
2016-12-25 14:31:52 -05:00
|
|
|
if configFile.Filename == "" {
|
2017-03-09 13:23:45 -05:00
|
|
|
return errors.Errorf("Can't save config with empty filename")
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
2018-09-10 16:39:30 -04:00
|
|
|
dir := filepath.Dir(configFile.Filename)
|
2022-09-30 13:13:22 -04:00
|
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
2016-12-25 14:31:52 -05:00
|
|
|
return err
|
|
|
|
}
|
2022-02-25 08:36:33 -05:00
|
|
|
temp, err := os.CreateTemp(dir, filepath.Base(configFile.Filename))
|
2016-12-25 14:31:52 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-06-19 14:14:14 -04:00
|
|
|
defer func() {
|
|
|
|
temp.Close()
|
|
|
|
if retErr != nil {
|
|
|
|
if err := os.Remove(temp.Name()); err != nil {
|
|
|
|
logrus.WithError(err).WithField("file", temp.Name()).Debug("Error cleaning up temp file")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2018-09-10 16:39:30 -04:00
|
|
|
err = configFile.SaveToWriter(temp)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-06-19 14:14:14 -04:00
|
|
|
|
|
|
|
if err := temp.Close(); err != nil {
|
|
|
|
return errors.Wrap(err, "error closing temp file")
|
|
|
|
}
|
|
|
|
|
2020-07-15 11:59:47 -04:00
|
|
|
// Handle situation where the configfile is a symlink
|
|
|
|
cfgFile := configFile.Filename
|
|
|
|
if f, err := os.Readlink(cfgFile); err == nil {
|
|
|
|
cfgFile = f
|
|
|
|
}
|
|
|
|
|
2019-12-17 03:36:25 -05:00
|
|
|
// Try copying the current config file (if any) ownership and permissions
|
2020-07-15 11:59:47 -04:00
|
|
|
copyFilePermissions(cfgFile, temp.Name())
|
|
|
|
return os.Rename(temp.Name(), cfgFile)
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
2017-10-31 06:21:09 -04:00
|
|
|
// ParseProxyConfig computes proxy configuration by retrieving the config for the provided host and
|
2017-01-31 00:15:51 -05:00
|
|
|
// then checking this against any environment variables provided to the container
|
2017-10-15 15:39:56 -04:00
|
|
|
func (configFile *ConfigFile) ParseProxyConfig(host string, runOpts map[string]*string) map[string]*string {
|
2017-01-31 00:15:51 -05:00
|
|
|
var cfgKey string
|
|
|
|
|
|
|
|
if _, ok := configFile.Proxies[host]; !ok {
|
|
|
|
cfgKey = "default"
|
|
|
|
} else {
|
|
|
|
cfgKey = host
|
|
|
|
}
|
|
|
|
|
2017-06-21 00:11:59 -04:00
|
|
|
config := configFile.Proxies[cfgKey]
|
2017-01-31 00:15:51 -05:00
|
|
|
permitted := map[string]*string{
|
|
|
|
"HTTP_PROXY": &config.HTTPProxy,
|
|
|
|
"HTTPS_PROXY": &config.HTTPSProxy,
|
|
|
|
"NO_PROXY": &config.NoProxy,
|
|
|
|
"FTP_PROXY": &config.FTPProxy,
|
2021-04-30 05:53:53 -04:00
|
|
|
"ALL_PROXY": &config.AllProxy,
|
2017-01-31 00:15:51 -05:00
|
|
|
}
|
2017-10-15 15:39:56 -04:00
|
|
|
m := runOpts
|
|
|
|
if m == nil {
|
|
|
|
m = make(map[string]*string)
|
|
|
|
}
|
2017-01-31 00:15:51 -05:00
|
|
|
for k := range permitted {
|
|
|
|
if *permitted[k] == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if _, ok := m[k]; !ok {
|
|
|
|
m[k] = permitted[k]
|
|
|
|
}
|
|
|
|
if _, ok := m[strings.ToLower(k)]; !ok {
|
|
|
|
m[strings.ToLower(k)] = permitted[k]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
2016-12-25 14:31:52 -05:00
|
|
|
// encodeAuth creates a base64 encoded string to containing authorization information
|
|
|
|
func encodeAuth(authConfig *types.AuthConfig) string {
|
|
|
|
if authConfig.Username == "" && authConfig.Password == "" {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
authStr := authConfig.Username + ":" + authConfig.Password
|
|
|
|
msg := []byte(authStr)
|
|
|
|
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
|
|
|
|
base64.StdEncoding.Encode(encoded, msg)
|
|
|
|
return string(encoded)
|
|
|
|
}
|
|
|
|
|
|
|
|
// decodeAuth decodes a base64 encoded string and returns username and password
|
|
|
|
func decodeAuth(authStr string) (string, string, error) {
|
|
|
|
if authStr == "" {
|
|
|
|
return "", "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
decLen := base64.StdEncoding.DecodedLen(len(authStr))
|
|
|
|
decoded := make([]byte, decLen)
|
|
|
|
authByte := []byte(authStr)
|
|
|
|
n, err := base64.StdEncoding.Decode(decoded, authByte)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", err
|
|
|
|
}
|
|
|
|
if n > decLen {
|
2017-03-09 13:23:45 -05:00
|
|
|
return "", "", errors.Errorf("Something went wrong decoding auth config")
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
2022-12-27 11:45:19 -05:00
|
|
|
userName, password, ok := strings.Cut(string(decoded), ":")
|
|
|
|
if !ok || userName == "" {
|
2017-03-09 13:23:45 -05:00
|
|
|
return "", "", errors.Errorf("Invalid auth configuration file")
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
2022-12-27 11:45:19 -05:00
|
|
|
return userName, strings.Trim(password, "\x00"), nil
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
2017-06-21 17:20:49 -04:00
|
|
|
|
|
|
|
// GetCredentialsStore returns a new credentials store from the settings in the
|
|
|
|
// configuration file
|
2018-01-26 17:38:04 -05:00
|
|
|
func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store {
|
|
|
|
if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" {
|
|
|
|
return newNativeStore(configFile, helper)
|
2017-06-21 17:20:49 -04:00
|
|
|
}
|
|
|
|
return credentials.NewFileStore(configFile)
|
|
|
|
}
|
|
|
|
|
2018-01-26 17:38:04 -05:00
|
|
|
// var for unit testing.
|
|
|
|
var newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
|
|
|
|
return credentials.NewNativeStore(configFile, helperSuffix)
|
|
|
|
}
|
|
|
|
|
2017-06-21 17:20:49 -04:00
|
|
|
// GetAuthConfig for a repository from the credential store
|
2018-01-26 17:38:04 -05:00
|
|
|
func (configFile *ConfigFile) GetAuthConfig(registryHostname string) (types.AuthConfig, error) {
|
|
|
|
return configFile.GetCredentialsStore(registryHostname).Get(registryHostname)
|
2017-06-21 17:20:49 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// getConfiguredCredentialStore returns the credential helper configured for the
|
|
|
|
// given registry, the default credsStore, or the empty string if neither are
|
|
|
|
// configured.
|
2018-01-26 17:38:04 -05:00
|
|
|
func getConfiguredCredentialStore(c *ConfigFile, registryHostname string) string {
|
|
|
|
if c.CredentialHelpers != nil && registryHostname != "" {
|
|
|
|
if helper, exists := c.CredentialHelpers[registryHostname]; exists {
|
2017-06-21 17:20:49 -04:00
|
|
|
return helper
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return c.CredentialsStore
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetAllCredentials returns all of the credentials stored in all of the
|
|
|
|
// configured credential stores.
|
|
|
|
func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, error) {
|
|
|
|
auths := make(map[string]types.AuthConfig)
|
|
|
|
addAll := func(from map[string]types.AuthConfig) {
|
|
|
|
for reg, ac := range from {
|
|
|
|
auths[reg] = ac
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
defaultStore := configFile.GetCredentialsStore("")
|
|
|
|
newAuths, err := defaultStore.GetAll()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
addAll(newAuths)
|
2018-01-26 17:38:04 -05:00
|
|
|
|
|
|
|
// Auth configs from a registry-specific helper should override those from the default store.
|
|
|
|
for registryHostname := range configFile.CredentialHelpers {
|
|
|
|
newAuth, err := configFile.GetAuthConfig(registryHostname)
|
|
|
|
if err != nil {
|
test spring-cleaning
This makes a quick pass through our tests;
Discard output/err
----------------------------------------------
Many tests were testing for error-conditions, but didn't discard output.
This produced a lot of noise when running the tests, and made it hard
to discover if there were actual failures, or if the output was expected.
For example:
=== RUN TestConfigCreateErrors
Error: "create" requires exactly 2 arguments.
See 'create --help'.
Usage: create [OPTIONS] CONFIG file|- [flags]
Create a config from a file or STDIN
Error: "create" requires exactly 2 arguments.
See 'create --help'.
Usage: create [OPTIONS] CONFIG file|- [flags]
Create a config from a file or STDIN
Error: error creating config
--- PASS: TestConfigCreateErrors (0.00s)
And after discarding output:
=== RUN TestConfigCreateErrors
--- PASS: TestConfigCreateErrors (0.00s)
Use sub-tests where possible
----------------------------------------------
Some tests were already set-up to use test-tables, and even had a usable
name (or in some cases "error" to check for). Change them to actual sub-
tests. Same test as above, but now with sub-tests and output discarded:
=== RUN TestConfigCreateErrors
=== RUN TestConfigCreateErrors/requires_exactly_2_arguments
=== RUN TestConfigCreateErrors/requires_exactly_2_arguments#01
=== RUN TestConfigCreateErrors/error_creating_config
--- PASS: TestConfigCreateErrors (0.00s)
--- PASS: TestConfigCreateErrors/requires_exactly_2_arguments (0.00s)
--- PASS: TestConfigCreateErrors/requires_exactly_2_arguments#01 (0.00s)
--- PASS: TestConfigCreateErrors/error_creating_config (0.00s)
PASS
It's not perfect in all cases (in the above, there's duplicate "expected"
errors, but Go conveniently adds "#01" for the duplicate). There's probably
also various tests I missed that could still use the same changes applied;
we can improve these in follow-ups.
Set cmd.Args to prevent test-failures
----------------------------------------------
When running tests from my IDE, it compiles the tests before running,
then executes the compiled binary to run the tests. Cobra doesn't like
that, because in that situation `os.Args` is taken as argument for the
command that's executed. The command that's tested now sees the test-
flags as arguments (`-test.v -test.run ..`), which causes various tests
to fail ("Command XYZ does not accept arguments").
# compile the tests:
go test -c -o foo.test
# execute the test:
./foo.test -test.v -test.run TestFoo
=== RUN TestFoo
Error: "foo" accepts no arguments.
The Cobra maintainers ran into the same situation, and for their own
use have added a special case to ignore `os.Args` in these cases;
https://github.com/spf13/cobra/blob/v1.8.1/command.go#L1078-L1083
args := c.args
// Workaround FAIL with "go test -v" or "cobra.test -test.v", see #155
if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" {
args = os.Args[1:]
}
Unfortunately, that exception is too specific (only checks for `cobra.test`),
so doesn't automatically fix the issue for other test-binaries. They did
provide a `cmd.SetArgs()` utility for this purpose
https://github.com/spf13/cobra/blob/v1.8.1/command.go#L276-L280
// SetArgs sets arguments for the command. It is set to os.Args[1:] by default, if desired, can be overridden
// particularly useful when testing.
func (c *Command) SetArgs(a []string) {
c.args = a
}
And the fix is to explicitly set the command's args to an empty slice to
prevent Cobra from falling back to using `os.Args[1:]` as arguments.
cmd := newSomeThingCommand()
cmd.SetArgs([]string{})
Some tests already take this issue into account, and I updated some tests
for this, but there's likely many other ones that can use the same treatment.
Perhaps the Cobra maintainers would accept a contribution to make their
condition less specific and to look for binaries ending with a `.test`
suffix (which is what compiled binaries usually are named as).
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-03 19:29:04 -04:00
|
|
|
// TODO(thaJeztah): use context-logger, so that this output can be suppressed (in tests).
|
2023-01-30 12:48:01 -05:00
|
|
|
logrus.WithError(err).Warnf("Failed to get credentials for registry: %s", registryHostname)
|
|
|
|
continue
|
2018-01-26 17:38:04 -05:00
|
|
|
}
|
|
|
|
auths[registryHostname] = newAuth
|
|
|
|
}
|
2017-06-21 17:20:49 -04:00
|
|
|
return auths, nil
|
|
|
|
}
|
2018-03-26 10:18:32 -04:00
|
|
|
|
|
|
|
// GetFilename returns the file name that this config file is based on.
|
|
|
|
func (configFile *ConfigFile) GetFilename() string {
|
|
|
|
return configFile.Filename
|
|
|
|
}
|
2018-05-28 08:45:08 -04:00
|
|
|
|
2019-02-18 06:49:41 -05:00
|
|
|
// PluginConfig retrieves the requested option for the given plugin.
|
|
|
|
func (configFile *ConfigFile) PluginConfig(pluginname, option string) (string, bool) {
|
|
|
|
if configFile.Plugins == nil {
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
pluginConfig, ok := configFile.Plugins[pluginname]
|
|
|
|
if !ok {
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
value, ok := pluginConfig[option]
|
|
|
|
return value, ok
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetPluginConfig sets the option to the given value for the given
|
|
|
|
// plugin. Passing a value of "" will remove the option. If removing
|
|
|
|
// the final config item for a given plugin then also cleans up the
|
|
|
|
// overall plugin entry.
|
|
|
|
func (configFile *ConfigFile) SetPluginConfig(pluginname, option, value string) {
|
|
|
|
if configFile.Plugins == nil {
|
|
|
|
configFile.Plugins = make(map[string]map[string]string)
|
|
|
|
}
|
|
|
|
pluginConfig, ok := configFile.Plugins[pluginname]
|
|
|
|
if !ok {
|
|
|
|
pluginConfig = make(map[string]string)
|
|
|
|
configFile.Plugins[pluginname] = pluginConfig
|
|
|
|
}
|
|
|
|
if value != "" {
|
|
|
|
pluginConfig[option] = value
|
|
|
|
} else {
|
|
|
|
delete(pluginConfig, option)
|
|
|
|
}
|
|
|
|
if len(pluginConfig) == 0 {
|
|
|
|
delete(configFile.Plugins, pluginname)
|
|
|
|
}
|
|
|
|
}
|