github.com/nats-io/nsc/v2@v2.8.7-0.20240307184528-efd7023c6896/cmd/generateprofile.go (about)

     1  /*
     2   * Copyright 2018-2023 The NATS Authors
     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   */
    15  
    16  package cmd
    17  
    18  import (
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"net/url"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	"github.com/nats-io/jwt/v2"
    28  	"github.com/nats-io/nsc/v2/cmd/store"
    29  	"github.com/spf13/cobra"
    30  )
    31  
    32  func createProfileCmd() *cobra.Command {
    33  	var params ProfileCmdParams
    34  	var cmd = &cobra.Command{
    35  		Use:   "profile",
    36  		Short: "Generate a profile from nsc 'URL' that can be used by tooling",
    37  		Example: `profile nsc://operator
    38  nsc profile nsc://operator/account
    39  nsc profile nsc://operator/account/user
    40  nsc profile nsc://operator/account/user?names&seeds&keys
    41  nsc profile nsc://operator/account/user?operatorSeed&accountSeed&userSeed
    42  nsc profile nsc://operator/account/user?operatorKey&accountKey&userKey
    43  nsc profile nsc://operator?key&seed
    44  nsc profile nsc://operator/account?key&seed
    45  nsc profile nsc://operator/account/user?key&seed&name
    46  nsc profile nsc://operator/account/user?store=/a/.nsc/nats&keystore=/foo/.nkeys
    47  
    48  Output of the program looks like:
    49  {
    50    "user_creds": "<filepath>",
    51    "operator" : {
    52       "service": "hostport"
    53     }
    54  }
    55  The user_creds is printed if an user is specified
    56  Other options (as query string arguments):
    57  keystore=<dir> that specifies the location of the keystore
    58  
    59  store=<dir> that specifies a directory that contains the named operator
    60  
    61  [user|account|operator]Key - includes the public key for user, account, 
    62  operator, If no prefix (user/account/operator is provided, it targets 
    63  the last object in the configuration path)
    64  
    65  keys - includes the public keys for all entities (same as 
    66  userKey&accountKey&operatorKey)
    67  
    68  [user|account|operator]Seed=<optional public key> - include the seed for 
    69  user, account, operator, if an argument is provided, the seed for the 
    70  specified public key is provided - this allows targeting a signing key.
    71  If no prefix (user/account/operator is provided, it targets the last 
    72  object in the configuration path)
    73  
    74  seeds - includes the private keys for all entities (same as
    75  userSeed&accountSeed&operatorSeed)
    76  
    77  [user|account|operator]Name - includes the friendly name for the for 
    78  user, account, operator, If no prefix (user/account/operator is provided, 
    79  it targets the last object in the configuration path)
    80  
    81  names - includes the friendly names for all the entities (same as 
    82  userName&accountName&operatorName)
    83  		`,
    84  
    85  		Args:         cobra.MinimumNArgs(1),
    86  		SilenceUsage: true,
    87  		RunE: func(cmd *cobra.Command, args []string) error {
    88  			us := args[0]
    89  			u, err := ParseNscURL(us)
    90  			if err != nil {
    91  				return fmt.Errorf("error parsing %q: %w", us, err)
    92  			}
    93  			config := GetConfig()
    94  			oldSR := config.StoreRoot
    95  			oldOp := config.Operator
    96  			oldAc := config.Account
    97  			defer func() {
    98  				_ = config.setStoreRoot(oldSR)
    99  				if oldOp != "" {
   100  					_ = config.SetOperator(oldOp)
   101  				}
   102  				if oldAc != "" {
   103  					_ = config.SetAccount(oldAc)
   104  				}
   105  			}()
   106  
   107  			params.nscu = u
   108  			q, err := u.query()
   109  			if err != nil {
   110  				return fmt.Errorf("error parsing query %q: %w", us, err)
   111  			}
   112  			if len(q) > 0 {
   113  				v, ok := q["store"]
   114  				if ok {
   115  					sr, err := Expand(v)
   116  					if err != nil {
   117  						return err
   118  					}
   119  					if err := config.setStoreRoot(sr); err != nil {
   120  						return err
   121  					}
   122  					if err := config.SetOperator(u.operator); err != nil {
   123  						return err
   124  					}
   125  				}
   126  
   127  				storeDir, ok := q[keystoreDir]
   128  				if ok {
   129  					ks, err := Expand(storeDir)
   130  					if err != nil {
   131  						return err
   132  					}
   133  					store.KeyStorePath = ks
   134  				}
   135  			}
   136  			if u.operator != "" {
   137  				if err := config.SetOperator(u.operator); err != nil {
   138  					return err
   139  				}
   140  			}
   141  			if u.account != "" {
   142  				if err := config.SetAccount(u.account); err != nil {
   143  					return err
   144  				}
   145  			}
   146  			return RunAction(cmd, args, &params)
   147  		},
   148  	}
   149  	cmd.Flags().StringVarP(&params.outputFile, "output-file", "o", "--", "output file, '--' is stdout")
   150  
   151  	return cmd
   152  }
   153  
   154  func init() {
   155  	generateCmd.AddCommand(createProfileCmd())
   156  }
   157  
   158  type Arg string
   159  
   160  const (
   161  	prefix           = "nsc://"
   162  	operatorKey  Arg = "operatorkey"
   163  	accountKey   Arg = "accountkey"
   164  	userKey      Arg = "userkey"
   165  	key          Arg = "key"
   166  	allKeys      Arg = "keys"
   167  	operatorSeed Arg = "operatorseed"
   168  	accountSeed  Arg = "accountseed"
   169  	userSeed     Arg = "userseed"
   170  	seed         Arg = "seed"
   171  	allSeeds     Arg = "seeds"
   172  	operatorName Arg = "operatorname"
   173  	accountName  Arg = "accountname"
   174  	userName     Arg = "username"
   175  	name         Arg = "name"
   176  	allNames     Arg = "names"
   177  	keystoreDir  Arg = "keystore"
   178  	storeDir     Arg = "store"
   179  )
   180  
   181  type ProfileCmdParams struct {
   182  	nscu       *NscURL
   183  	results    *Profile
   184  	conf       *ToolConfig
   185  	oc         *jwt.OperatorClaims
   186  	ac         *jwt.AccountClaims
   187  	uc         *jwt.UserClaims
   188  	outputFile string
   189  }
   190  
   191  type Details struct {
   192  	Service []string `json:"service,omitempty"`
   193  	Name    string   `json:"name,omitempty"`
   194  	Seed    string   `json:"seed,omitempty"`
   195  	Key     string   `json:"id,omitempty"`
   196  }
   197  
   198  type Profile struct {
   199  	UserCreds string   `json:"user_creds,omitempty"`
   200  	Operator  *Details `json:"operator,omitempty"`
   201  	Account   *Details `json:"account,omitempty"`
   202  	User      *Details `json:"user,omitempty"`
   203  }
   204  
   205  type NscURL struct {
   206  	operator string
   207  	account  string
   208  	user     string
   209  	qs       string
   210  }
   211  
   212  type stringSet struct {
   213  	set map[string]string
   214  }
   215  
   216  func newStringSet() *stringSet {
   217  	var s stringSet
   218  	s.set = make(map[string]string)
   219  	return &s
   220  }
   221  
   222  func (u *stringSet) add(s string) {
   223  	u.set[strings.ToLower(s)] = s
   224  }
   225  
   226  func (u *stringSet) contains(s string) bool {
   227  	return u.set[strings.ToLower(s)] != ""
   228  }
   229  
   230  func (u *NscURL) getOperator() (string, error) {
   231  	return url.QueryUnescape(u.operator)
   232  }
   233  
   234  func (u *NscURL) getAccount() (string, error) {
   235  	return url.QueryUnescape(u.account)
   236  }
   237  
   238  func (u *NscURL) getUser() (string, error) {
   239  	return url.QueryUnescape(u.user)
   240  }
   241  
   242  func (u *NscURL) query() (map[Arg]string, error) {
   243  	q := strings.ToLower(u.qs)
   244  	m := make(map[Arg]string)
   245  	for _, e := range strings.Split(q, "&") {
   246  		kv := strings.Split(e, "=")
   247  		k := strings.ToLower(kv[0])
   248  		v := ""
   249  		if len(kv) == 2 {
   250  			s, err := url.QueryUnescape(kv[1])
   251  			if err != nil {
   252  				return nil, err
   253  			}
   254  			v = s
   255  		}
   256  		m[Arg(k)] = v
   257  	}
   258  	return m, nil
   259  }
   260  
   261  func ParseNscURL(u string) (*NscURL, error) {
   262  	var v NscURL
   263  	s := u
   264  	if !strings.HasPrefix(strings.ToLower(u), prefix) {
   265  		return nil, errors.New("invalid nsc url: expecting 'nsc://'")
   266  	}
   267  	s = s[len(prefix):]
   268  
   269  	qs := strings.Index(s, "?")
   270  	if qs > 0 {
   271  		v.qs = s[qs+1:]
   272  		s = s[:qs]
   273  	}
   274  	if s == "" {
   275  		return nil, errors.New("invalid nsc url: expecting an operator name")
   276  	}
   277  	a := strings.Split(s, "/")
   278  	if len(a) >= 1 {
   279  		v.operator = a[0]
   280  	}
   281  	if len(a) >= 2 {
   282  		v.account = a[1]
   283  	}
   284  	if len(a) >= 3 {
   285  		v.user = a[2]
   286  	}
   287  	return &v, nil
   288  }
   289  
   290  func (p *ProfileCmdParams) SetDefaults(_ ActionCtx) error {
   291  	return nil
   292  }
   293  
   294  func (p *ProfileCmdParams) PreInteractive(_ ActionCtx) error {
   295  	return nil
   296  }
   297  
   298  func (p *ProfileCmdParams) Load(_ ActionCtx) error {
   299  	return nil
   300  }
   301  
   302  func (p *ProfileCmdParams) PostInteractive(_ ActionCtx) error {
   303  	return nil
   304  }
   305  
   306  func (p *ProfileCmdParams) loadNames(c jwt.Claims) *stringSet {
   307  	names := newStringSet()
   308  	cd := c.Claims()
   309  	names.add(cd.Name)
   310  	names.add(cd.Subject)
   311  
   312  	payload := c.Payload()
   313  	_, ok := payload.(jwt.Operator)
   314  	if ok && p.conf.Operator != "" {
   315  		names.add(p.conf.Operator)
   316  	}
   317  	return names
   318  }
   319  
   320  // checkLoadOperator is used to check if the operator in the nsc: URL is a
   321  // known operator, and thus needs to work no matter what the current context
   322  // is; it will mutate the ctx Store appropriately and store away a conf.
   323  func (p *ProfileCmdParams) checkLoadOperator(ctx ActionCtx) error {
   324  	p.conf = GetConfig()
   325  	var err error
   326  	// There's a sync mutex inside the store, we should try to only load each once.
   327  	allOperators := make(map[string]*store.Store)
   328  	aliases := make(map[string]string)
   329  	// We work with lower-cased aliases
   330  	lcURLOperator := strings.ToLower(p.nscu.operator)
   331  
   332  	// We need to be sure that we load all operators known to the local nsc store.
   333  	// Using ctx.StoreCtx().Store would only load the _current_ operator.
   334  	// We do still need to let the O.. nkey form be used though.
   335  	for _, opName := range p.conf.ListOperators() {
   336  		s, err := store.LoadStore(filepath.Join(p.conf.StoreRoot, opName))
   337  		if err != nil {
   338  			continue
   339  		}
   340  		oc, err := s.ReadOperatorClaim()
   341  		if err != nil {
   342  			continue
   343  		}
   344  		allOperators[strings.ToLower(opName)] = s
   345  		aliases[opName] = opName
   346  		aliases[strings.ToLower(opName)] = opName
   347  		for alias := range p.loadNames(oc).set {
   348  			allOperators[alias] = s
   349  			aliases[alias] = opName
   350  		}
   351  	}
   352  	if _, ok := allOperators[lcURLOperator]; !ok {
   353  		return fmt.Errorf("invalid operator %q: not known to current system", p.nscu.operator)
   354  	}
   355  	p.conf.Operator = aliases[lcURLOperator]
   356  	ctx.StoreCtx().Store = allOperators[lcURLOperator]
   357  
   358  	oc, err := ctx.StoreCtx().Store.ReadOperatorClaim()
   359  	if err != nil {
   360  		return fmt.Errorf("could not use alternative operator %q: %w", p.nscu.operator, err)
   361  	}
   362  
   363  	p.nscu.operator = oc.Name
   364  	p.oc = oc
   365  
   366  	return nil
   367  }
   368  
   369  func (p *ProfileCmdParams) checkLoadAccount(ctx ActionCtx) error {
   370  	if p.nscu.account == "" {
   371  		return nil
   372  	}
   373  	names, err := p.conf.ListAccounts()
   374  	if err != nil {
   375  		return err
   376  	}
   377  
   378  	m := make(map[string]string)
   379  	for _, n := range names {
   380  		m[strings.ToLower(n)] = n
   381  	}
   382  	an := m[strings.ToLower(p.nscu.account)]
   383  	if an != "" {
   384  		ac, err := ctx.StoreCtx().Store.ReadAccountClaim(an)
   385  		if err != nil {
   386  			return err
   387  		}
   388  		p.nscu.account = ac.Name
   389  		p.ac = ac
   390  		return nil
   391  	}
   392  
   393  	for _, n := range names {
   394  		ac, err := ctx.StoreCtx().Store.ReadAccountClaim(n)
   395  		if err != nil {
   396  			continue
   397  		}
   398  		aliases := p.loadNames(ac)
   399  		if aliases.contains(p.nscu.account) {
   400  			p.nscu.account = ac.Name
   401  			p.ac = ac
   402  			return nil
   403  		}
   404  	}
   405  	return fmt.Errorf("invalid account %q: account was not found", p.nscu.account)
   406  }
   407  
   408  func (p *ProfileCmdParams) checkLoadUser(ctx ActionCtx) error {
   409  	if p.nscu.user == "" {
   410  		return nil
   411  	}
   412  	names, err := ctx.StoreCtx().Store.ListEntries(store.Accounts, p.nscu.account, store.Users)
   413  	if err != nil {
   414  		return err
   415  	}
   416  
   417  	m := make(map[string]string)
   418  	for _, n := range names {
   419  		m[strings.ToLower(n)] = n
   420  	}
   421  	un := m[strings.ToLower(p.nscu.user)]
   422  	if un != "" {
   423  		uc, err := ctx.StoreCtx().Store.ReadUserClaim(p.nscu.account, un)
   424  		if err != nil {
   425  			return err
   426  		}
   427  		p.nscu.user = uc.Name
   428  		p.uc = uc
   429  		return nil
   430  	}
   431  
   432  	for _, n := range names {
   433  		uc, err := ctx.StoreCtx().Store.ReadUserClaim(p.nscu.account, n)
   434  		if err != nil {
   435  			continue
   436  		}
   437  		aliases := p.loadNames(uc)
   438  		if aliases.contains(p.nscu.user) {
   439  			p.nscu.user = uc.Name
   440  			p.uc = uc
   441  			return nil
   442  		}
   443  	}
   444  	return fmt.Errorf("invalid user %q: user was not found", p.nscu.user)
   445  }
   446  
   447  func (p *ProfileCmdParams) Validate(ctx ActionCtx) error {
   448  	if err := p.checkLoadOperator(ctx); err != nil {
   449  		return err
   450  	}
   451  	if err := p.checkLoadAccount(ctx); err != nil {
   452  		return err
   453  	}
   454  	return p.checkLoadUser(ctx)
   455  }
   456  
   457  func (p *ProfileCmdParams) addOperatorKeys() {
   458  	p.results.Operator.Key = p.oc.Subject
   459  }
   460  
   461  func (p *ProfileCmdParams) addAccountKeys() {
   462  	if p.results.Account == nil {
   463  		p.results.Account = &Details{}
   464  	}
   465  	p.results.Account.Key = p.ac.Subject
   466  }
   467  
   468  func (p *ProfileCmdParams) addUserKeys() {
   469  	if p.results.User == nil {
   470  		p.results.User = &Details{}
   471  	}
   472  	p.results.User.Key = p.uc.Subject
   473  }
   474  
   475  func (p *ProfileCmdParams) addKeys() error {
   476  	q, err := p.nscu.query()
   477  	if err != nil {
   478  		return err
   479  	}
   480  	if len(q) == 0 {
   481  		return nil
   482  	}
   483  	_, keys := q[allKeys]
   484  	_, ok := q[operatorKey]
   485  	if ok || keys {
   486  		p.addOperatorKeys()
   487  	}
   488  	_, ok = q[accountKey]
   489  	if ok || keys {
   490  		p.addAccountKeys()
   491  	}
   492  	_, ok = q[userKey]
   493  	if ok || keys {
   494  		p.addUserKeys()
   495  	}
   496  	_, ok = q[key]
   497  	if ok {
   498  		if p.nscu.user != "" {
   499  			p.addUserKeys()
   500  		} else if p.nscu.account != "" {
   501  			p.addAccountKeys()
   502  		} else {
   503  			p.addOperatorKeys()
   504  		}
   505  	}
   506  	return nil
   507  }
   508  
   509  func (p *ProfileCmdParams) getKeys(claim jwt.Claims) []string {
   510  	var keys []string
   511  	if claim != nil {
   512  		keys = append(keys, claim.Claims().Subject)
   513  		var payload = claim.Payload()
   514  		oc, ok := payload.(*jwt.Operator)
   515  		if ok {
   516  			keys = append(keys, oc.SigningKeys...)
   517  		}
   518  		ac, ok := payload.(*jwt.Account)
   519  		if ok {
   520  			keys = append(keys, ac.SigningKeys.Keys()...)
   521  		}
   522  	}
   523  	return keys
   524  }
   525  
   526  func (p *ProfileCmdParams) resolveSeed(ctx ActionCtx, s string, keys []string) (string, error) {
   527  	ks := ctx.StoreCtx().KeyStore
   528  	if s != "" {
   529  		found := false
   530  		s = strings.ToUpper(s)
   531  		for _, k := range keys {
   532  			if s == k {
   533  				found = true
   534  				break
   535  			}
   536  		}
   537  		if !found {
   538  			return "", fmt.Errorf("%q was not found", s)
   539  		}
   540  		if ks.HasPrivateKey(s) {
   541  			seed, err := ks.GetSeed(s)
   542  			if seed != "" && err == nil {
   543  				return seed, err
   544  			}
   545  		} else {
   546  			return "", fmt.Errorf("no seed was found for %q", keys[0])
   547  		}
   548  	}
   549  	for _, v := range keys {
   550  		if ks.HasPrivateKey(v) {
   551  			seed, err := ks.GetSeed(v)
   552  			if seed != "" && err == nil {
   553  				return seed, err
   554  			}
   555  		}
   556  	}
   557  	return "", fmt.Errorf("no seed was found for %q", keys[0])
   558  }
   559  
   560  func (p *ProfileCmdParams) addOperatorSeed(ctx ActionCtx, v string) error {
   561  	seed, err := p.resolveSeed(ctx, v, p.getKeys(p.oc))
   562  	if err != nil {
   563  		return err
   564  	}
   565  	p.results.Operator.Seed = seed
   566  	return nil
   567  }
   568  
   569  func (p *ProfileCmdParams) addAccountSeed(ctx ActionCtx, v string) error {
   570  	seed, err := p.resolveSeed(ctx, v, p.getKeys(p.ac))
   571  	if err != nil {
   572  		return err
   573  	}
   574  	if p.results.Account == nil {
   575  		p.results.Account = &Details{}
   576  	}
   577  	p.results.Account.Seed = seed
   578  	return nil
   579  }
   580  
   581  func (p *ProfileCmdParams) addUserSeed(ctx ActionCtx, v string) error {
   582  	seed, err := p.resolveSeed(ctx, v, p.getKeys(p.uc))
   583  	if err != nil {
   584  		return err
   585  	}
   586  	if p.results.User == nil {
   587  		p.results.User = &Details{}
   588  	}
   589  	p.results.User.Seed = seed
   590  	return nil
   591  }
   592  
   593  func (p *ProfileCmdParams) addSeeds(ctx ActionCtx) error {
   594  	q, err := p.nscu.query()
   595  	if err != nil {
   596  		return err
   597  	}
   598  	if len(q) == 0 {
   599  		return nil
   600  	}
   601  	_, seeds := q[allSeeds]
   602  	v, ok := q[operatorSeed]
   603  	if ok || seeds {
   604  		err := p.addOperatorSeed(ctx, v)
   605  		if err != nil {
   606  			return err
   607  		}
   608  	}
   609  	v, ok = q[accountSeed]
   610  	if ok || seeds {
   611  		err := p.addAccountSeed(ctx, v)
   612  		if err != nil {
   613  			return err
   614  		}
   615  	}
   616  	v, ok = q[userSeed]
   617  	if ok || seeds {
   618  		err := p.addUserSeed(ctx, v)
   619  		if err != nil {
   620  			return err
   621  		}
   622  	}
   623  	_, ok = q[seed]
   624  	if ok {
   625  		if p.nscu.user != "" {
   626  			err := p.addUserSeed(ctx, "")
   627  			if err != nil {
   628  				return err
   629  			}
   630  		}
   631  		if p.nscu.account != "" {
   632  			err := p.addAccountSeed(ctx, "")
   633  			if err != nil {
   634  				return err
   635  			}
   636  		}
   637  		if p.nscu.operator != "" {
   638  			err := p.addOperatorSeed(ctx, "")
   639  			if err != nil {
   640  				return err
   641  			}
   642  		}
   643  	}
   644  	return nil
   645  }
   646  
   647  func (p *ProfileCmdParams) addOperatorName() {
   648  	p.results.Operator.Name = p.conf.Operator
   649  }
   650  
   651  func (p *ProfileCmdParams) addAccountName() {
   652  	if p.results.Account == nil {
   653  		p.results.Account = &Details{}
   654  	}
   655  	p.results.Account.Name = p.ac.Name
   656  }
   657  
   658  func (p *ProfileCmdParams) addUserName() {
   659  	if p.results.User == nil {
   660  		p.results.User = &Details{}
   661  	}
   662  	p.results.User.Name = p.uc.Name
   663  }
   664  
   665  func (p *ProfileCmdParams) addNames() error {
   666  	q, err := p.nscu.query()
   667  	if err != nil {
   668  		return err
   669  	}
   670  	if len(q) == 0 {
   671  		return nil
   672  	}
   673  
   674  	_, names := q[allNames]
   675  	_, ok := q[operatorName]
   676  	if ok || names {
   677  		p.addOperatorName()
   678  	}
   679  	_, ok = q[accountName]
   680  	if ok || names {
   681  		p.addAccountName()
   682  	}
   683  	_, ok = q[userName]
   684  	if ok || names {
   685  		p.addUserName()
   686  	}
   687  	_, ok = q[name]
   688  	if ok {
   689  		if p.nscu.user != "" {
   690  			p.addUserName()
   691  		}
   692  		if p.nscu.account != "" {
   693  			p.addAccountName()
   694  		}
   695  		if p.nscu.operator != "" {
   696  			p.addOperatorName()
   697  		}
   698  	}
   699  	return nil
   700  }
   701  
   702  func (p *ProfileCmdParams) Run(ctx ActionCtx) (store.Status, error) {
   703  	p.results = &Profile{}
   704  	p.results.Operator = &Details{}
   705  	p.results.Operator.Service = p.oc.OperatorServiceURLs
   706  	if p.nscu.user != "" {
   707  		creds := ctx.StoreCtx().KeyStore.CalcUserCredsPath(p.nscu.account, p.nscu.user)
   708  		if _, err := os.Stat(creds); os.IsNotExist(err) {
   709  			// nothing
   710  		} else {
   711  			p.results.UserCreds = creds
   712  		}
   713  	}
   714  	if err := p.addNames(); err != nil {
   715  		return nil, err
   716  	}
   717  	if err := p.addKeys(); err != nil {
   718  		return nil, err
   719  	}
   720  	if err := p.addSeeds(ctx); err != nil {
   721  		return nil, err
   722  	}
   723  	v, err := json.MarshalIndent(p.results, "", "  ")
   724  	v = append(v, '\n')
   725  	if err != nil {
   726  		return nil, err
   727  	}
   728  	if err := Write(p.outputFile, v); err != nil {
   729  		return nil, err
   730  	}
   731  	var s store.Status
   732  	if !IsStdOut(p.outputFile) {
   733  		s = store.OKStatus("wrote tool configuration to %#q", AbbrevHomePaths(p.outputFile))
   734  	}
   735  	return s, nil
   736  }