github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/overlord/hookstate/ctlcmd/set.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package ctlcmd
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"strings"
    26  
    27  	"github.com/snapcore/snapd/i18n"
    28  	"github.com/snapcore/snapd/jsonutil"
    29  	"github.com/snapcore/snapd/overlord/configstate"
    30  	"github.com/snapcore/snapd/overlord/configstate/config"
    31  	"github.com/snapcore/snapd/overlord/hookstate"
    32  )
    33  
    34  type setCommand struct {
    35  	baseCommand
    36  
    37  	Positional struct {
    38  		PlugOrSlotSpec string   `positional-arg-name:":<plug|slot>"`
    39  		ConfValues     []string `positional-arg-name:"key=value"`
    40  	} `positional-args:"yes"`
    41  
    42  	String bool `short:"s" description:"parse the value as a string"`
    43  	Typed  bool `short:"t" description:"parse the value strictly as JSON document"`
    44  }
    45  
    46  var shortSetHelp = i18n.G("Changes configuration options")
    47  var longSetHelp = i18n.G(`
    48  The set command changes the provided configuration options as requested.
    49  
    50      $ snapctl set username=frank password=$PASSWORD
    51  
    52  All configuration changes are persisted at once, and only after the hook
    53  returns successfully.
    54  
    55  Nested values may be modified via a dotted path:
    56  
    57      $ snapctl set author.name=frank
    58  
    59  Configuration option may be unset with exclamation mark:
    60      $ snapctl set author!
    61  
    62  Plug and slot attributes may be set in the respective prepare and connect hooks by
    63  naming the respective plug or slot:
    64  
    65      $ snapctl set :myplug path=/dev/ttyS0
    66  `)
    67  
    68  func init() {
    69  	addCommand("set", shortSetHelp, longSetHelp, func() command { return &setCommand{} })
    70  }
    71  
    72  func (s *setCommand) Execute(args []string) error {
    73  	if s.Positional.PlugOrSlotSpec == "" && len(s.Positional.ConfValues) == 0 {
    74  		return fmt.Errorf(i18n.G("set which option?"))
    75  	}
    76  
    77  	context := s.context()
    78  	if context == nil {
    79  		return fmt.Errorf("cannot set without a context")
    80  	}
    81  
    82  	if s.Typed && s.String {
    83  		return fmt.Errorf("cannot use -t and -s together")
    84  	}
    85  
    86  	// treat PlugOrSlotSpec argument as key=value if it contains '=' or doesn't contain ':' - this is to support
    87  	// values such as "device-service.url=192.168.0.1:5555" and error out on invalid key=value if only "key" is given.
    88  	if strings.Contains(s.Positional.PlugOrSlotSpec, "=") || !strings.Contains(s.Positional.PlugOrSlotSpec, ":") {
    89  		s.Positional.ConfValues = append([]string{s.Positional.PlugOrSlotSpec}, s.Positional.ConfValues[0:]...)
    90  		s.Positional.PlugOrSlotSpec = ""
    91  		return s.setConfigSetting(context)
    92  	}
    93  
    94  	parts := strings.SplitN(s.Positional.PlugOrSlotSpec, ":", 2)
    95  	snap, name := parts[0], parts[1]
    96  	if name == "" {
    97  		return fmt.Errorf("plug or slot name not provided")
    98  	}
    99  	if snap != "" {
   100  		return fmt.Errorf(`"snapctl set %s" not supported, use "snapctl set :%s" instead`, s.Positional.PlugOrSlotSpec, parts[1])
   101  	}
   102  	return s.setInterfaceSetting(context, name)
   103  }
   104  
   105  func (s *setCommand) setConfigSetting(context *hookstate.Context) error {
   106  	context.Lock()
   107  	tr := configstate.ContextTransaction(context)
   108  	context.Unlock()
   109  
   110  	for _, patchValue := range s.Positional.ConfValues {
   111  		parts := strings.SplitN(patchValue, "=", 2)
   112  		if len(parts) == 1 && strings.HasSuffix(patchValue, "!") {
   113  			key := strings.TrimSuffix(patchValue, "!")
   114  			tr.Set(s.context().InstanceName(), key, nil)
   115  			continue
   116  		}
   117  		if len(parts) != 2 {
   118  			return fmt.Errorf(i18n.G("invalid parameter: %q (want key=value)"), patchValue)
   119  		}
   120  		key := parts[0]
   121  
   122  		var value interface{}
   123  		if s.String {
   124  			value = parts[1]
   125  		} else {
   126  			if err := jsonutil.DecodeWithNumber(strings.NewReader(parts[1]), &value); err != nil {
   127  				if s.Typed {
   128  					return fmt.Errorf("failed to parse JSON: %w", err)
   129  				}
   130  
   131  				// Not valid JSON-- just save the string as-is.
   132  				value = parts[1]
   133  			}
   134  		}
   135  
   136  		tr.Set(s.context().InstanceName(), key, value)
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  func setInterfaceAttribute(context *hookstate.Context, staticAttrs map[string]interface{}, dynamicAttrs map[string]interface{}, key string, value interface{}) error {
   143  	data, err := json.Marshal(value)
   144  	if err != nil {
   145  		return fmt.Errorf("cannot marshal snap %q option %q: %s", context.InstanceName(), key, err)
   146  	}
   147  	raw := json.RawMessage(data)
   148  
   149  	subkeys, err := config.ParseKey(key)
   150  	if err != nil {
   151  		return err
   152  	}
   153  
   154  	// We're called from setInterfaceSetting, subkeys is derived from key
   155  	// part of key=value argument and is guaranteed to be non-empty at this
   156  	// point.
   157  	if len(subkeys) == 0 {
   158  		return fmt.Errorf("internal error: unexpected empty subkeys for key %q", key)
   159  	}
   160  	var existing interface{}
   161  	err = getAttribute(context.InstanceName(), subkeys[:1], 0, staticAttrs, &existing)
   162  	if err == nil {
   163  		return fmt.Errorf(i18n.G("attribute %q cannot be overwritten"), key)
   164  	}
   165  	// we expect NoAttributeError here, any other error is unexpected (a real error)
   166  	if !isNoAttribute(err) {
   167  		return err
   168  	}
   169  
   170  	_, err = config.PatchConfig(context.InstanceName(), subkeys, 0, dynamicAttrs, &raw)
   171  	return err
   172  }
   173  
   174  func (s *setCommand) setInterfaceSetting(context *hookstate.Context, plugOrSlot string) error {
   175  	// Make sure set :<plug|slot> is only supported during the execution of prepare-[plug|slot] hooks
   176  	hookType, _ := interfaceHookType(context.HookName())
   177  	if hookType != preparePlugHook && hookType != prepareSlotHook {
   178  		return fmt.Errorf(i18n.G("interface attributes can only be set during the execution of prepare hooks"))
   179  	}
   180  
   181  	attrsTask, err := attributesTask(context)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	// check if the requested plug or slot is correct for this hook.
   187  	if err := validatePlugOrSlot(attrsTask, hookType == preparePlugHook, plugOrSlot); err != nil {
   188  		return err
   189  	}
   190  
   191  	var which string
   192  	if hookType == preparePlugHook {
   193  		which = "plug"
   194  	} else {
   195  		which = "slot"
   196  	}
   197  
   198  	context.Lock()
   199  	defer context.Unlock()
   200  
   201  	var staticAttrs, dynamicAttrs map[string]interface{}
   202  	if err = attrsTask.Get(which+"-static", &staticAttrs); err != nil {
   203  		return fmt.Errorf(i18n.G("internal error: cannot get %s from appropriate task, %s"), which, err)
   204  	}
   205  
   206  	dynKey := which + "-dynamic"
   207  	if err = attrsTask.Get(dynKey, &dynamicAttrs); err != nil {
   208  		return fmt.Errorf(i18n.G("internal error: cannot get %s from appropriate task, %s"), which, err)
   209  	}
   210  
   211  	for _, attrValue := range s.Positional.ConfValues {
   212  		parts := strings.SplitN(attrValue, "=", 2)
   213  		if len(parts) != 2 {
   214  			return fmt.Errorf(i18n.G("invalid parameter: %q (want key=value)"), attrValue)
   215  		}
   216  
   217  		var value interface{}
   218  		if err := jsonutil.DecodeWithNumber(strings.NewReader(parts[1]), &value); err != nil {
   219  			// Not valid JSON, save the string as-is
   220  			value = parts[1]
   221  		}
   222  		err = setInterfaceAttribute(context, staticAttrs, dynamicAttrs, parts[0], value)
   223  		if err != nil {
   224  			return fmt.Errorf(i18n.G("cannot set attribute: %v"), err)
   225  		}
   226  	}
   227  
   228  	attrsTask.Set(dynKey, dynamicAttrs)
   229  	return nil
   230  }