github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/jujud/bootstrap.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/base64"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"net"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/juju/cmd"
    18  	"github.com/juju/errors"
    19  	"github.com/juju/loggo"
    20  	"github.com/juju/names"
    21  	"github.com/juju/utils"
    22  	"github.com/juju/utils/arch"
    23  	"github.com/juju/utils/series"
    24  	"github.com/juju/utils/ssh"
    25  	"github.com/juju/version"
    26  	goyaml "gopkg.in/yaml.v2"
    27  	"launchpad.net/gnuflag"
    28  
    29  	"github.com/juju/juju/agent"
    30  	"github.com/juju/juju/agent/agentbootstrap"
    31  	agenttools "github.com/juju/juju/agent/tools"
    32  	agentcmd "github.com/juju/juju/cmd/jujud/agent"
    33  	cmdutil "github.com/juju/juju/cmd/jujud/util"
    34  	"github.com/juju/juju/constraints"
    35  	"github.com/juju/juju/environs"
    36  	"github.com/juju/juju/environs/config"
    37  	"github.com/juju/juju/environs/imagemetadata"
    38  	"github.com/juju/juju/environs/simplestreams"
    39  	envtools "github.com/juju/juju/environs/tools"
    40  	"github.com/juju/juju/instance"
    41  	"github.com/juju/juju/mongo"
    42  	"github.com/juju/juju/network"
    43  	"github.com/juju/juju/state"
    44  	"github.com/juju/juju/state/binarystorage"
    45  	"github.com/juju/juju/state/cloudimagemetadata"
    46  	"github.com/juju/juju/state/multiwatcher"
    47  	"github.com/juju/juju/state/storage"
    48  	"github.com/juju/juju/storage/poolmanager"
    49  	"github.com/juju/juju/tools"
    50  	jujuversion "github.com/juju/juju/version"
    51  	"github.com/juju/juju/worker/peergrouper"
    52  )
    53  
    54  var (
    55  	initiateMongoServer  = peergrouper.InitiateMongoServer
    56  	agentInitializeState = agentbootstrap.InitializeState
    57  	sshGenerateKey       = ssh.GenerateKey
    58  	newStateStorage      = storage.NewStorage
    59  	minSocketTimeout     = 1 * time.Minute
    60  	logger               = loggo.GetLogger("juju.cmd.jujud")
    61  )
    62  
    63  // BootstrapCommand represents a jujud bootstrap command.
    64  type BootstrapCommand struct {
    65  	cmd.CommandBase
    66  	agentcmd.AgentConf
    67  	ControllerModelConfig map[string]interface{}
    68  	HostedModelConfig     map[string]interface{}
    69  	BootstrapConstraints  constraints.Value
    70  	ModelConstraints      constraints.Value
    71  	Hardware              instance.HardwareCharacteristics
    72  	InstanceId            string
    73  	AdminUsername         string
    74  	ImageMetadataDir      string
    75  }
    76  
    77  // NewBootstrapCommand returns a new BootstrapCommand that has been initialized.
    78  func NewBootstrapCommand() *BootstrapCommand {
    79  	return &BootstrapCommand{
    80  		AgentConf: agentcmd.NewAgentConf(""),
    81  	}
    82  }
    83  
    84  // Info returns a decription of the command.
    85  func (c *BootstrapCommand) Info() *cmd.Info {
    86  	return &cmd.Info{
    87  		Name:    "bootstrap-state",
    88  		Purpose: "initialize juju state",
    89  	}
    90  }
    91  
    92  // SetFlags adds the flags for this command to the passed gnuflag.FlagSet.
    93  func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
    94  	c.AgentConf.AddFlags(f)
    95  	yamlBase64Var(f, &c.ControllerModelConfig, "model-config", "", "controller model configuration (yaml, base64 encoded)")
    96  	yamlBase64Var(f, &c.HostedModelConfig, "hosted-model-config", "", "initial hosted model configuration (yaml, base64 encoded)")
    97  	f.Var(constraints.ConstraintsValue{Target: &c.BootstrapConstraints}, "bootstrap-constraints", "bootstrap machine constraints (space-separated strings)")
    98  	f.Var(constraints.ConstraintsValue{Target: &c.ModelConstraints}, "constraints", "initial constraints (space-separated strings)")
    99  	f.Var(&c.Hardware, "hardware", "hardware characteristics (space-separated strings)")
   100  	f.StringVar(&c.InstanceId, "instance-id", "", "unique instance-id for bootstrap machine")
   101  	f.StringVar(&c.AdminUsername, "admin-user", "admin", "set the name for the juju admin user")
   102  	f.StringVar(&c.ImageMetadataDir, "image-metadata", "", "custom image metadata source dir")
   103  }
   104  
   105  // Init initializes the command for running.
   106  func (c *BootstrapCommand) Init(args []string) error {
   107  	if len(c.ControllerModelConfig) == 0 {
   108  		return cmdutil.RequiredError("model-config")
   109  	}
   110  	if len(c.HostedModelConfig) == 0 {
   111  		return cmdutil.RequiredError("hosted-model-config")
   112  	}
   113  	if c.InstanceId == "" {
   114  		return cmdutil.RequiredError("instance-id")
   115  	}
   116  	if !names.IsValidUser(c.AdminUsername) {
   117  		return errors.Errorf("%q is not a valid username", c.AdminUsername)
   118  	}
   119  	return c.AgentConf.CheckArgs(args)
   120  }
   121  
   122  // Run initializes state for an environment.
   123  func (c *BootstrapCommand) Run(_ *cmd.Context) error {
   124  	envCfg, err := config.New(config.NoDefaults, c.ControllerModelConfig)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	err = c.ReadConfig("machine-0")
   129  	if err != nil {
   130  		return err
   131  	}
   132  	agentConfig := c.CurrentConfig()
   133  	network.SetPreferIPv6(agentConfig.PreferIPv6())
   134  
   135  	// agent.Jobs is an optional field in the agent config, and was
   136  	// introduced after 1.17.2. We default to allowing units on
   137  	// machine-0 if missing.
   138  	jobs := agentConfig.Jobs()
   139  	if len(jobs) == 0 {
   140  		jobs = []multiwatcher.MachineJob{
   141  			multiwatcher.JobManageModel,
   142  			multiwatcher.JobHostUnits,
   143  			multiwatcher.JobManageNetworking,
   144  		}
   145  	}
   146  
   147  	// Get the bootstrap machine's addresses from the provider.
   148  	env, err := environs.New(envCfg)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	newConfigAttrs := make(map[string]interface{})
   153  
   154  	// Check to see if a newer agent version has been requested
   155  	// by the bootstrap client.
   156  	desiredVersion, ok := envCfg.AgentVersion()
   157  	if ok && desiredVersion != jujuversion.Current {
   158  		// If we have been asked for a newer version, ensure the newer
   159  		// tools can actually be found, or else bootstrap won't complete.
   160  		stream := envtools.PreferredStream(&desiredVersion, envCfg.Development(), envCfg.AgentStream())
   161  		logger.Infof("newer tools requested, looking for %v in stream %v", desiredVersion, stream)
   162  		filter := tools.Filter{
   163  			Number: desiredVersion,
   164  			Arch:   arch.HostArch(),
   165  			Series: series.HostSeries(),
   166  		}
   167  		_, toolsErr := envtools.FindTools(env, -1, -1, stream, filter)
   168  		if toolsErr == nil {
   169  			logger.Infof("tools are available, upgrade will occur after bootstrap")
   170  		}
   171  		if errors.IsNotFound(toolsErr) {
   172  			// Newer tools not available, so revert to using the tools
   173  			// matching the current agent version.
   174  			logger.Warningf("newer tools for %q not available, sticking with version %q", desiredVersion, jujuversion.Current)
   175  			newConfigAttrs["agent-version"] = jujuversion.Current.String()
   176  		} else if toolsErr != nil {
   177  			logger.Errorf("cannot find newer tools: %v", toolsErr)
   178  			return toolsErr
   179  		}
   180  	}
   181  
   182  	instanceId := instance.Id(c.InstanceId)
   183  	instances, err := env.Instances([]instance.Id{instanceId})
   184  	if err != nil {
   185  		return err
   186  	}
   187  	addrs, err := instances[0].Addresses()
   188  	if err != nil {
   189  		return err
   190  	}
   191  
   192  	// When machine addresses are reported from state, they have
   193  	// duplicates removed.  We should do the same here so that
   194  	// there is not unnecessary churn in the mongo replicaset.
   195  	// TODO (cherylj) Add explicit unit tests for this - tracked
   196  	// by bug #1544158.
   197  	addrs = network.MergedAddresses([]network.Address{}, addrs)
   198  
   199  	// Generate a private SSH key for the controllers, and add
   200  	// the public key to the environment config. We'll add the
   201  	// private key to StateServingInfo below.
   202  	privateKey, publicKey, err := sshGenerateKey(config.JujuSystemKey)
   203  	if err != nil {
   204  		return errors.Annotate(err, "failed to generate system key")
   205  	}
   206  	authorizedKeys := config.ConcatAuthKeys(envCfg.AuthorizedKeys(), publicKey)
   207  	newConfigAttrs[config.AuthKeysConfig] = authorizedKeys
   208  
   209  	// Generate a shared secret for the Mongo replica set, and write it out.
   210  	sharedSecret, err := mongo.GenerateSharedSecret()
   211  	if err != nil {
   212  		return err
   213  	}
   214  	info, ok := agentConfig.StateServingInfo()
   215  	if !ok {
   216  		return fmt.Errorf("bootstrap machine config has no state serving info")
   217  	}
   218  	info.SharedSecret = sharedSecret
   219  	info.SystemIdentity = privateKey
   220  	err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error {
   221  		agentConfig.SetStateServingInfo(info)
   222  		return nil
   223  	})
   224  	if err != nil {
   225  		return fmt.Errorf("cannot write agent config: %v", err)
   226  	}
   227  
   228  	agentConfig = c.CurrentConfig()
   229  
   230  	// Create system-identity file
   231  	if err := agent.WriteSystemIdentityFile(agentConfig); err != nil {
   232  		return err
   233  	}
   234  
   235  	if err := c.startMongo(addrs, agentConfig); err != nil {
   236  		return err
   237  	}
   238  
   239  	logger.Infof("started mongo")
   240  	// Initialise state, and store any agent config (e.g. password) changes.
   241  	envCfg, err = env.Config().Apply(newConfigAttrs)
   242  	if err != nil {
   243  		return errors.Annotate(err, "failed to update model config")
   244  	}
   245  	var st *state.State
   246  	var m *state.Machine
   247  	err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error {
   248  		var stateErr error
   249  		dialOpts := mongo.DefaultDialOpts()
   250  
   251  		// Set a longer socket timeout than usual, as the machine
   252  		// will be starting up and disk I/O slower than usual. This
   253  		// has been known to cause timeouts in queries.
   254  		timeouts := envCfg.BootstrapSSHOpts()
   255  		dialOpts.SocketTimeout = timeouts.Timeout
   256  		if dialOpts.SocketTimeout < minSocketTimeout {
   257  			dialOpts.SocketTimeout = minSocketTimeout
   258  		}
   259  
   260  		// We shouldn't attempt to dial peers until we have some.
   261  		dialOpts.Direct = true
   262  
   263  		adminTag := names.NewLocalUserTag(c.AdminUsername)
   264  		st, m, stateErr = agentInitializeState(
   265  			adminTag,
   266  			agentConfig,
   267  			envCfg, c.HostedModelConfig,
   268  			agentbootstrap.BootstrapMachineConfig{
   269  				Addresses:            addrs,
   270  				BootstrapConstraints: c.BootstrapConstraints,
   271  				ModelConstraints:     c.ModelConstraints,
   272  				Jobs:                 jobs,
   273  				InstanceId:           instanceId,
   274  				Characteristics:      c.Hardware,
   275  				SharedSecret:         sharedSecret,
   276  			},
   277  			dialOpts,
   278  			environs.NewStatePolicy(),
   279  		)
   280  		return stateErr
   281  	})
   282  	if err != nil {
   283  		return err
   284  	}
   285  	defer st.Close()
   286  
   287  	// Populate the tools catalogue.
   288  	if err := c.populateTools(st, env); err != nil {
   289  		return err
   290  	}
   291  
   292  	// Populate the GUI archive catalogue.
   293  	if err := c.populateGUIArchive(st, env); err != nil {
   294  		// Do not stop the bootstrapping process for Juju GUI archive errors.
   295  		logger.Warningf("cannot set up Juju GUI: %s", err)
   296  	} else {
   297  		logger.Debugf("Juju GUI successfully set up")
   298  	}
   299  
   300  	// Add custom image metadata to environment storage.
   301  	if c.ImageMetadataDir != "" {
   302  		if err := c.saveCustomImageMetadata(st, env); err != nil {
   303  			return err
   304  		}
   305  
   306  		stor := newStateStorage(st.ModelUUID(), st.MongoSession())
   307  		if err := c.storeCustomImageMetadata(stor); err != nil {
   308  			return err
   309  		}
   310  	}
   311  
   312  	// Populate the storage pools.
   313  	if err = c.populateDefaultStoragePools(st); err != nil {
   314  		return err
   315  	}
   316  
   317  	// bootstrap machine always gets the vote
   318  	return m.SetHasVote(true)
   319  }
   320  
   321  func (c *BootstrapCommand) startMongo(addrs []network.Address, agentConfig agent.Config) error {
   322  	logger.Debugf("starting mongo")
   323  
   324  	info, ok := agentConfig.MongoInfo()
   325  	if !ok {
   326  		return fmt.Errorf("no state info available")
   327  	}
   328  	// When bootstrapping, we need to allow enough time for mongo
   329  	// to start as there's no retry loop in place.
   330  	// 5 minutes should suffice.
   331  	bootstrapDialOpts := mongo.DialOpts{Timeout: 5 * time.Minute}
   332  	dialInfo, err := mongo.DialInfo(info.Info, bootstrapDialOpts)
   333  	if err != nil {
   334  		return err
   335  	}
   336  	servingInfo, ok := agentConfig.StateServingInfo()
   337  	if !ok {
   338  		return fmt.Errorf("agent config has no state serving info")
   339  	}
   340  	// Use localhost to dial the mongo server, because it's running in
   341  	// auth mode and will refuse to perform any operations unless
   342  	// we dial that address.
   343  	dialInfo.Addrs = []string{
   344  		net.JoinHostPort("127.0.0.1", fmt.Sprint(servingInfo.StatePort)),
   345  	}
   346  
   347  	logger.Debugf("calling ensureMongoServer")
   348  	ensureServerParams, err := cmdutil.NewEnsureServerParams(agentConfig)
   349  	if err != nil {
   350  		return err
   351  	}
   352  	err = cmdutil.EnsureMongoServer(ensureServerParams)
   353  	if err != nil {
   354  		return err
   355  	}
   356  
   357  	peerAddr := mongo.SelectPeerAddress(addrs)
   358  	if peerAddr == "" {
   359  		return fmt.Errorf("no appropriate peer address found in %q", addrs)
   360  	}
   361  	peerHostPort := net.JoinHostPort(peerAddr, fmt.Sprint(servingInfo.StatePort))
   362  
   363  	return initiateMongoServer(peergrouper.InitiateMongoParams{
   364  		DialInfo:       dialInfo,
   365  		MemberHostPort: peerHostPort,
   366  	})
   367  }
   368  
   369  // populateDefaultStoragePools creates the default storage pools.
   370  func (c *BootstrapCommand) populateDefaultStoragePools(st *state.State) error {
   371  	settings := state.NewStateSettings(st)
   372  	return poolmanager.AddDefaultStoragePools(settings)
   373  }
   374  
   375  // populateTools stores uploaded tools in provider storage
   376  // and updates the tools metadata.
   377  func (c *BootstrapCommand) populateTools(st *state.State, env environs.Environ) error {
   378  	agentConfig := c.CurrentConfig()
   379  	dataDir := agentConfig.DataDir()
   380  
   381  	current := version.Binary{
   382  		Number: jujuversion.Current,
   383  		Arch:   arch.HostArch(),
   384  		Series: series.HostSeries(),
   385  	}
   386  	tools, err := agenttools.ReadTools(dataDir, current)
   387  	if err != nil {
   388  		return errors.Trace(err)
   389  	}
   390  
   391  	data, err := ioutil.ReadFile(filepath.Join(
   392  		agenttools.SharedToolsDir(dataDir, current),
   393  		"tools.tar.gz",
   394  	))
   395  	if err != nil {
   396  		return errors.Trace(err)
   397  	}
   398  
   399  	toolstorage, err := st.ToolsStorage()
   400  	if err != nil {
   401  		return errors.Trace(err)
   402  	}
   403  	defer toolstorage.Close()
   404  
   405  	var toolsVersions []version.Binary
   406  	if strings.HasPrefix(tools.URL, "file://") {
   407  		// Tools were uploaded: clone for each series of the same OS.
   408  		os, err := series.GetOSFromSeries(tools.Version.Series)
   409  		if err != nil {
   410  			return errors.Trace(err)
   411  		}
   412  		osSeries := series.OSSupportedSeries(os)
   413  		for _, series := range osSeries {
   414  			toolsVersion := tools.Version
   415  			toolsVersion.Series = series
   416  			toolsVersions = append(toolsVersions, toolsVersion)
   417  		}
   418  	} else {
   419  		// Tools were downloaded from an external source: don't clone.
   420  		toolsVersions = []version.Binary{tools.Version}
   421  	}
   422  
   423  	for _, toolsVersion := range toolsVersions {
   424  		metadata := binarystorage.Metadata{
   425  			Version: toolsVersion.String(),
   426  			Size:    tools.Size,
   427  			SHA256:  tools.SHA256,
   428  		}
   429  		logger.Debugf("Adding tools: %v", toolsVersion)
   430  		if err := toolstorage.Add(bytes.NewReader(data), metadata); err != nil {
   431  			return errors.Trace(err)
   432  		}
   433  	}
   434  	return nil
   435  }
   436  
   437  // populateGUIArchive stores the uploaded Juju GUI archive in provider storage,
   438  // updates the GUI metadata and set the current Juju GUI version.
   439  func (c *BootstrapCommand) populateGUIArchive(st *state.State, env environs.Environ) error {
   440  	agentConfig := c.CurrentConfig()
   441  	dataDir := agentConfig.DataDir()
   442  	guistorage, err := st.GUIStorage()
   443  	if err != nil {
   444  		return errors.Trace(err)
   445  	}
   446  	defer guistorage.Close()
   447  	gui, err := agenttools.ReadGUIArchive(dataDir)
   448  	if err != nil {
   449  		return errors.Annotate(err, "cannot fetch GUI info")
   450  	}
   451  	f, err := os.Open(filepath.Join(agenttools.SharedGUIDir(dataDir), "gui.tar.bz2"))
   452  	if err != nil {
   453  		return errors.Annotate(err, "cannot read GUI archive")
   454  	}
   455  	defer f.Close()
   456  	if err := guistorage.Add(f, binarystorage.Metadata{
   457  		Version: gui.Version.String(),
   458  		Size:    gui.Size,
   459  		SHA256:  gui.SHA256,
   460  	}); err != nil {
   461  		return errors.Annotate(err, "cannot store GUI archive")
   462  	}
   463  	if err = st.GUISetVersion(gui.Version); err != nil {
   464  		return errors.Annotate(err, "cannot set current GUI version")
   465  	}
   466  	return nil
   467  }
   468  
   469  // storeCustomImageMetadata reads the custom image metadata from disk,
   470  // and stores the files in environment storage with the same relative
   471  // paths.
   472  func (c *BootstrapCommand) storeCustomImageMetadata(stor storage.Storage) error {
   473  	logger.Debugf("storing custom image metadata from %q", c.ImageMetadataDir)
   474  	return filepath.Walk(c.ImageMetadataDir, func(abspath string, info os.FileInfo, err error) error {
   475  		if err != nil {
   476  			return err
   477  		}
   478  		if !info.Mode().IsRegular() {
   479  			return nil
   480  		}
   481  		relpath, err := filepath.Rel(c.ImageMetadataDir, abspath)
   482  		if err != nil {
   483  			return err
   484  		}
   485  		f, err := os.Open(abspath)
   486  		if err != nil {
   487  			return err
   488  		}
   489  		defer f.Close()
   490  		relpath = filepath.ToSlash(relpath)
   491  		logger.Debugf("storing %q in model storage (%d bytes)", relpath, info.Size())
   492  		return stor.Put(relpath, f, info.Size())
   493  	})
   494  }
   495  
   496  // Override for testing.
   497  var seriesFromVersion = series.VersionSeries
   498  
   499  // saveCustomImageMetadata reads the custom image metadata from disk,
   500  // and saves it in controller.
   501  func (c *BootstrapCommand) saveCustomImageMetadata(st *state.State, env environs.Environ) error {
   502  	logger.Debugf("saving custom image metadata from %q", c.ImageMetadataDir)
   503  	baseURL := fmt.Sprintf("file://%s", filepath.ToSlash(c.ImageMetadataDir))
   504  	publicKey, _ := simplestreams.UserPublicSigningKey()
   505  	datasource := simplestreams.NewURLSignedDataSource("custom", baseURL, publicKey, utils.NoVerifySSLHostnames, simplestreams.CUSTOM_CLOUD_DATA, false)
   506  	return storeImageMetadataFromFiles(st, env, datasource)
   507  }
   508  
   509  // storeImageMetadataFromFiles puts image metadata found in sources into state.
   510  // Declared as var to facilitate tests.
   511  var storeImageMetadataFromFiles = func(st *state.State, env environs.Environ, source simplestreams.DataSource) error {
   512  	// Read the image metadata, as we'll want to upload it to the environment.
   513  	imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{})
   514  	if inst, ok := env.(simplestreams.HasRegion); ok {
   515  		// If we can determine current region,
   516  		// we want only metadata specific to this region.
   517  		cloud, err := inst.Region()
   518  		if err != nil {
   519  			return err
   520  		}
   521  		imageConstraint.CloudSpec = cloud
   522  	}
   523  
   524  	existingMetadata, info, err := imagemetadata.Fetch([]simplestreams.DataSource{source}, imageConstraint)
   525  	if err != nil && !errors.IsNotFound(err) {
   526  		return errors.Annotate(err, "cannot read image metadata")
   527  	}
   528  	return storeImageMetadataInState(st, info.Source, source.Priority(), existingMetadata)
   529  }
   530  
   531  // storeImageMetadataInState writes image metadata into state store.
   532  func storeImageMetadataInState(st *state.State, source string, priority int, existingMetadata []*imagemetadata.ImageMetadata) error {
   533  	if len(existingMetadata) == 0 {
   534  		return nil
   535  	}
   536  	metadataState := make([]cloudimagemetadata.Metadata, len(existingMetadata))
   537  	for i, one := range existingMetadata {
   538  		m := cloudimagemetadata.Metadata{
   539  			cloudimagemetadata.MetadataAttributes{
   540  				Stream:          one.Stream,
   541  				Region:          one.RegionName,
   542  				Arch:            one.Arch,
   543  				VirtType:        one.VirtType,
   544  				RootStorageType: one.Storage,
   545  				Source:          source,
   546  			},
   547  			priority,
   548  			one.Id,
   549  		}
   550  		s, err := seriesFromVersion(one.Version)
   551  		if err != nil {
   552  			return errors.Annotatef(err, "cannot determine series for version %v", one.Version)
   553  		}
   554  		m.Series = s
   555  		metadataState[i] = m
   556  	}
   557  	if err := st.CloudImageMetadataStorage.SaveMetadata(metadataState); err != nil {
   558  		return errors.Annotatef(err, "cannot cache image metadata")
   559  	}
   560  	return nil
   561  }
   562  
   563  // yamlBase64Value implements gnuflag.Value on a map[string]interface{}.
   564  type yamlBase64Value map[string]interface{}
   565  
   566  // Set decodes the base64 value into yaml then expands that into a map.
   567  func (v *yamlBase64Value) Set(value string) error {
   568  	decoded, err := base64.StdEncoding.DecodeString(value)
   569  	if err != nil {
   570  		return err
   571  	}
   572  	return goyaml.Unmarshal(decoded, v)
   573  }
   574  
   575  func (v *yamlBase64Value) String() string {
   576  	return fmt.Sprintf("%v", *v)
   577  }
   578  
   579  // yamlBase64Var sets up a gnuflag flag analogous to the FlagSet.*Var methods.
   580  func yamlBase64Var(fs *gnuflag.FlagSet, target *map[string]interface{}, name string, value string, usage string) {
   581  	fs.Var((*yamlBase64Value)(target), name, usage)
   582  }