github.com/pachyderm/pachyderm@v1.13.4/src/client/pkg/config/config.go (about)

     1  package config
     2  
     3  import (
     4  	"encoding/json"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"sync"
     9  
    10  	"github.com/gogo/protobuf/proto"
    11  	"github.com/pachyderm/pachyderm/src/client/pkg/errors"
    12  	"github.com/pachyderm/pachyderm/src/client/pkg/grpcutil"
    13  	uuid "github.com/satori/go.uuid"
    14  	log "github.com/sirupsen/logrus"
    15  )
    16  
    17  const (
    18  	configEnvVar  = "PACH_CONFIG"
    19  	contextEnvVar = "PACH_CONTEXT"
    20  )
    21  
    22  var defaultConfigDir = filepath.Join(os.Getenv("HOME"), ".pachyderm")
    23  var defaultConfigPath = filepath.Join(defaultConfigDir, "config.json")
    24  var pachctlConfigPath = filepath.Join("/pachctl", "config.json")
    25  
    26  var configMu sync.Mutex
    27  var value *Config
    28  
    29  func configPath() string {
    30  	if env, ok := os.LookupEnv(configEnvVar); ok {
    31  		return env
    32  	}
    33  	if _, err := os.Stat(pachctlConfigPath); err == nil {
    34  		return pachctlConfigPath
    35  	}
    36  	return defaultConfigPath
    37  }
    38  
    39  // ActiveContext gets the active context in the config
    40  func (c *Config) ActiveContext(errorOnNoActive bool) (string, *Context, error) {
    41  	if c.V2 == nil {
    42  		return "", nil, errors.Errorf("cannot get active context from non-v2 config")
    43  	}
    44  	if envContext, ok := os.LookupEnv(contextEnvVar); ok {
    45  		context := c.V2.Contexts[envContext]
    46  		if context == nil {
    47  			return "", nil, errors.Errorf("pachctl config error: `%s` refers to a context (%q) that does not exist", contextEnvVar, envContext)
    48  		}
    49  		return envContext, context, nil
    50  	}
    51  	context := c.V2.Contexts[c.V2.ActiveContext]
    52  	if context == nil {
    53  		if c.V2.ActiveContext == "" {
    54  			if errorOnNoActive {
    55  				return "", nil, errors.Errorf("pachctl config error: no active " +
    56  					"context configured.\n\nYou can fix your config by setting " +
    57  					"the active context like so: pachctl config set " +
    58  					"active-context <context>")
    59  			}
    60  		} else {
    61  			return "", nil, errors.Errorf("pachctl config error: pachctl's active "+
    62  				"context is %q, but no context named %q has been configured.\n\nYou can fix "+
    63  				"your config by setting the active context like so: pachctl config set "+
    64  				"active-context <context>",
    65  				c.V2.ActiveContext, c.V2.ActiveContext)
    66  		}
    67  
    68  	}
    69  	return c.V2.ActiveContext, context, nil
    70  }
    71  
    72  // Read loads the Pachyderm config on this machine.
    73  // If an existing configuration cannot be found, it sets up the defaults. Read
    74  // returns a nil Config if and only if it returns a non-nil error.
    75  func Read(ignoreCache, readOnly bool) (*Config, error) {
    76  	configMu.Lock()
    77  	defer configMu.Unlock()
    78  
    79  	if value == nil || ignoreCache {
    80  		// Read json file
    81  		p := configPath()
    82  		if raw, err := ioutil.ReadFile(p); err == nil {
    83  			err = json.Unmarshal(raw, &value)
    84  			if err != nil {
    85  				return nil, errors.Wrapf(err, "could not parse config json at %q", p)
    86  			}
    87  		} else if errors.Is(err, os.ErrNotExist) {
    88  			// File doesn't exist, so create a new config
    89  			log.Debugf("No config detected at %q. Generating new config...", p)
    90  			value = &Config{}
    91  		} else {
    92  			return nil, errors.Wrapf(err, "could not read config at %q", p)
    93  		}
    94  
    95  		updated := false
    96  
    97  		if value.UserID == "" {
    98  			updated = true
    99  			log.Debugln("No UserID present in config - generating new one.")
   100  			uuid := uuid.NewV4()
   101  			value.UserID = uuid.String()
   102  		}
   103  
   104  		if value.V2 == nil {
   105  			updated = true
   106  			log.Debugln("No config V2 present in config - generating a new one.")
   107  			if err := value.InitV2(); err != nil {
   108  				return nil, err
   109  			}
   110  		}
   111  
   112  		for contextName, context := range value.V2.Contexts {
   113  			pachdAddress, err := grpcutil.ParsePachdAddress(context.PachdAddress)
   114  			if err != nil {
   115  				if !errors.Is(err, grpcutil.ErrNoPachdAddress) {
   116  					return nil, errors.Wrapf(err, "could not parse pachd address for context '%s'", contextName)
   117  				}
   118  			} else {
   119  				if qualifiedPachdAddress := pachdAddress.Qualified(); qualifiedPachdAddress != context.PachdAddress {
   120  					log.Debugf("Non-qualified pachd address set for context '%s' - fixing", contextName)
   121  					context.PachdAddress = qualifiedPachdAddress
   122  					updated = true
   123  				}
   124  			}
   125  		}
   126  
   127  		if updated && !readOnly {
   128  			log.Debugf("Rewriting config at %q.", p)
   129  
   130  			if err := value.Write(); err != nil {
   131  				return nil, errors.Wrapf(err, "could not rewrite config at %q", p)
   132  			}
   133  		}
   134  	}
   135  
   136  	cloned := proto.Clone(value).(*Config)
   137  	// in the case of an empty map, `proto.Clone` incorrectly clones
   138  	// `Contexts` as nil. This fixes the issue.
   139  	if cloned.V2.Contexts == nil {
   140  		cloned.V2.Contexts = map[string]*Context{}
   141  	}
   142  
   143  	return cloned, nil
   144  }
   145  
   146  // InitV2 initializes the V2 object of the config
   147  func (c *Config) InitV2() error {
   148  	c.V2 = &ConfigV2{
   149  		ActiveContext: "default",
   150  		Contexts:      map[string]*Context{},
   151  		Metrics:       true,
   152  	}
   153  
   154  	if c.V1 != nil {
   155  		c.V2.Contexts["default"] = &Context{
   156  			Source:            ContextSource_CONFIG_V1,
   157  			PachdAddress:      c.V1.PachdAddress,
   158  			ServerCAs:         c.V1.ServerCAs,
   159  			SessionToken:      c.V1.SessionToken,
   160  			ActiveTransaction: c.V1.ActiveTransaction,
   161  		}
   162  
   163  		c.V1 = nil
   164  	} else {
   165  		c.V2.Contexts["default"] = &Context{
   166  			Source: ContextSource_NONE,
   167  		}
   168  	}
   169  	return nil
   170  }
   171  
   172  // Write writes the configuration in 'c' to this machine's Pachyderm config
   173  // file.
   174  func (c *Config) Write() error {
   175  	if c.V1 != nil {
   176  		panic("config V1 included (this is a bug)")
   177  	}
   178  
   179  	rawConfig, err := json.MarshalIndent(c, "", "  ")
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	p := configPath()
   185  
   186  	// Because we're writing the config back to disk, we'll also need to make sure
   187  	// that the directory we're writing the config into exists. The approach we
   188  	// use for doing this depends on whether PACH_CONFIG is set.
   189  	if _, ok := os.LookupEnv(configEnvVar); ok {
   190  		// using overridden config path: check that the parent dir exists, but don't
   191  		// create any new directories
   192  		d := filepath.Dir(p)
   193  		if _, err := os.Stat(d); err != nil {
   194  			return errors.Wrapf(err, "cannot use config at %s: could not stat parent directory", p)
   195  		}
   196  	} else {
   197  		// using the default config path, create the config directory
   198  		err = os.MkdirAll(defaultConfigDir, 0755)
   199  		if err != nil {
   200  			return err
   201  		}
   202  	}
   203  
   204  	// Write to a temporary file first, then rename the temporary file to `p`.
   205  	// This ensures the write is atomic on POSIX.
   206  	tmpfile, err := ioutil.TempFile("", "pachyderm-config-*.json")
   207  	if err != nil {
   208  		return err
   209  	}
   210  	defer os.Remove(tmpfile.Name())
   211  
   212  	if _, err = tmpfile.Write(rawConfig); err != nil {
   213  		return err
   214  	}
   215  	if err = tmpfile.Close(); err != nil {
   216  		return err
   217  	}
   218  	if err = os.Rename(tmpfile.Name(), p); err != nil {
   219  		// A rename could fail if the temporary directory is mounted on a
   220  		// different device than the config path. If the rename failed, try to
   221  		// just copy the bytes instead.
   222  		if err = ioutil.WriteFile(p, rawConfig, 0644); err != nil {
   223  			return errors.Wrapf(err, "failed to write config file")
   224  		}
   225  	}
   226  
   227  	// essentially short-cuts reading the new config back from disk
   228  	value = proto.Clone(c).(*Config)
   229  	return nil
   230  }
   231  
   232  // WritePachTokenToConfig sets the auth token for the current pachctl config.
   233  // Used during tests to ensure we don't lose access to a cluster if a test fails.
   234  func WritePachTokenToConfig(token string) error {
   235  	cfg, err := Read(false, false)
   236  	if err != nil {
   237  		return errors.Wrapf(err, "error reading Pachyderm config (for cluster address)")
   238  	}
   239  	_, context, err := cfg.ActiveContext(true)
   240  	if err != nil {
   241  		return errors.Wrapf(err, "error getting the active context")
   242  	}
   243  	context.SessionToken = token
   244  	if err := cfg.Write(); err != nil {
   245  		return errors.Wrapf(err, "error writing pachyderm config")
   246  	}
   247  	return nil
   248  }