github.com/coreos/mantle@v0.13.0/sdk/enter.go (about)

     1  // Copyright 2015 CoreOS, Inc.
     2  //
     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  package sdk
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os"
    22  	"path/filepath"
    23  	"runtime"
    24  	"strings"
    25  	"syscall"
    26  	"text/template"
    27  
    28  	"github.com/coreos/mantle/system"
    29  	"github.com/coreos/mantle/system/exec"
    30  	"github.com/coreos/mantle/system/user"
    31  )
    32  
    33  var (
    34  	enterChrootCmd exec.Entrypoint
    35  
    36  	botoTemplate = template.Must(template.New("boto").Parse(`
    37  {{if eq .Type "authorized_user"}}
    38  [Credentials]
    39  gs_oauth2_refresh_token = {{.RefreshToken}}
    40  [OAuth2]
    41  client_id = {{.ClientID}}
    42  client_secret = {{.ClientSecret}}
    43  {{else}}{{if eq .Type "service_account"}}
    44  [Credentials]
    45  gs_service_key_file = {{.JsonPath}}
    46  {{end}}{{end}}
    47  [GSUtil]
    48  state_dir = {{.StateDir}}
    49  `))
    50  )
    51  
    52  const (
    53  	defaultResolv = "nameserver 8.8.8.8\nnameserver 8.8.4.4\n"
    54  )
    55  
    56  func init() {
    57  	enterChrootCmd = exec.NewEntrypoint("enterChroot", enterChrootHelper)
    58  }
    59  
    60  // Information on the chroot. Except for Cmd and CmdDir paths
    61  // are relative to the host system.
    62  type enter struct {
    63  	RepoRoot     string     `json:",omitempty"`
    64  	Chroot       string     `json:",omitempty"`
    65  	Cmd          []string   `json:",omitempty"`
    66  	CmdDir       string     `json:",omitempty"`
    67  	BindGpgAgent bool       `json:",omitempty"`
    68  	UseHostDNS   bool       `json:",omitempty"`
    69  	User         *user.User `json:",omitempty"`
    70  	UserRunDir   string     `json:",omitempty"`
    71  }
    72  
    73  type googleCreds struct {
    74  	// Path to JSON file (for template above)
    75  	JsonPath string
    76  
    77  	// Path gsutil will store cached credentials and other state.
    78  	// Must contain a pre-created 'tracker-files' directory because
    79  	// gsutil sometimes creates it with an inappropriate umask.
    80  	StateDir string
    81  
    82  	// Common fields
    83  	Type     string
    84  	ClientID string `json:"client_id"`
    85  
    86  	// User Credential fields
    87  	ClientSecret string `json:"client_secret"`
    88  	RefreshToken string `json:"refresh_token"`
    89  
    90  	// Service Account fields
    91  	ClientEmail  string `json:"client_email"`
    92  	PrivateKeyID string `json:"private_key_id"`
    93  	PrivateKey   string `json:"private_key"`
    94  }
    95  
    96  // MountAPI mounts standard Linux API filesystems.
    97  // When possible the filesystems are mounted read-only.
    98  func (e *enter) MountAPI() error {
    99  	var apis = []struct {
   100  		Path string
   101  		Type string
   102  		Opts string
   103  	}{
   104  		{"/proc", "proc", "ro,nosuid,nodev,noexec"},
   105  		{"/sys", "sysfs", "ro,nosuid,nodev,noexec"},
   106  		{"/run", "tmpfs", "nosuid,nodev,mode=755"},
   107  	}
   108  
   109  	// Make sure the new root directory itself is a mount point.
   110  	// `unshare` assumes that `mount --make-rprivate /` works.
   111  	if err := system.RecursiveBind(e.Chroot, e.Chroot); err != nil {
   112  		return err
   113  	}
   114  
   115  	for _, fs := range apis {
   116  		target := filepath.Join(e.Chroot, fs.Path)
   117  		if err := system.Mount("", target, fs.Type, fs.Opts); err != nil {
   118  			return err
   119  		}
   120  	}
   121  
   122  	// Since loop devices are dynamic we need the host's managed /dev
   123  	if err := system.ReadOnlyBind("/dev", filepath.Join(e.Chroot, "dev")); err != nil {
   124  		return err
   125  	}
   126  	// /dev/pts must be read-write because emerge chowns tty devices.
   127  	if err := system.Bind("/dev/pts", filepath.Join(e.Chroot, "dev/pts")); err != nil {
   128  		return err
   129  	}
   130  
   131  	// Unfortunately using the host's /dev complicates /dev/shm which may
   132  	// be a directory or a symlink into /run depending on the distro. :(
   133  	// XXX: catalyst does not work on systems with a /dev/shm symlink!
   134  	if system.IsSymlink("/dev/shm") {
   135  		shmPath, err := filepath.EvalSymlinks("/dev/shm")
   136  		if err != nil {
   137  			return err
   138  		}
   139  		// Only accept known values to avoid surprises.
   140  		if shmPath != "/run/shm" {
   141  			return fmt.Errorf("Unexpected shm path: %s", shmPath)
   142  		}
   143  		newPath := filepath.Join(e.Chroot, shmPath)
   144  		if err := os.Mkdir(newPath, 01777); err != nil {
   145  			return err
   146  		}
   147  		if err := os.Chmod(newPath, 01777); err != nil {
   148  			return err
   149  		}
   150  	} else {
   151  		shmPath := filepath.Join(e.Chroot, "dev/shm")
   152  		if err := system.Mount("", shmPath, "tmpfs", "nosuid,nodev"); err != nil {
   153  			return err
   154  		}
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  // MountAgent bind mounts a SSH or GnuPG agent socket into the chroot
   161  func (e *enter) MountSSHAgent() error {
   162  	origPath := os.Getenv("SSH_AUTH_SOCK")
   163  	if origPath == "" {
   164  		return nil
   165  	}
   166  
   167  	origDir, origFile := filepath.Split(origPath)
   168  	if _, err := os.Stat(origDir); err != nil {
   169  		// Just skip if the agent has gone missing.
   170  		return nil
   171  	}
   172  
   173  	newDir, err := ioutil.TempDir(e.UserRunDir, "agent-")
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	if err := system.Bind(origDir, newDir); err != nil {
   179  		return err
   180  	}
   181  
   182  	newPath := filepath.Join(newDir, origFile)
   183  	chrootPath := strings.TrimPrefix(newPath, e.Chroot)
   184  	return os.Setenv("SSH_AUTH_SOCK", chrootPath)
   185  }
   186  
   187  // MountGnupg bind mounts $GNUPGHOME or ~/.gnupg and the agent socket
   188  // if available. The agent is ignored if the home dir isn't available.
   189  func (e *enter) MountGnupgHome() error {
   190  	origHome := os.Getenv("GNUPGHOME")
   191  	if origHome == "" {
   192  		origHome = filepath.Join(e.User.HomeDir, ".gnupg")
   193  	}
   194  
   195  	if _, err := os.Stat(origHome); err != nil {
   196  		// Skip but do not pass along $GNUPGHOME
   197  		return os.Unsetenv("GNUPGHOME")
   198  	}
   199  
   200  	// gpg gets confused when GNUPGHOME isn't ~/.gnupg, so mount it there.
   201  	// Additionally, set the GNUPGHOME variable so commands run with sudo
   202  	// can also use it.
   203  	newHomeInChroot := filepath.Join("/home", e.User.Username, ".gnupg")
   204  	newHome := filepath.Join(e.Chroot, newHomeInChroot)
   205  	if err := os.Mkdir(newHome, 0700); err != nil && !os.IsExist(err) {
   206  		return err
   207  	}
   208  
   209  	if err := system.Bind(origHome, newHome); err != nil {
   210  		return err
   211  	}
   212  
   213  	return os.Setenv("GNUPGHOME", newHomeInChroot)
   214  }
   215  
   216  func (e *enter) MountGnupgAgent() error {
   217  	// Newer GPG releases make it harder to find out what dir has the sockets
   218  	// so use /run/user/$uid/gnupg which is the default
   219  	origAgentDir := filepath.Join("/run", "user", e.User.Uid, "gnupg")
   220  	if _, err := os.Stat(origAgentDir); err != nil {
   221  		// Skip
   222  		return nil
   223  	}
   224  
   225  	// gpg acts weird if this is elsewhere, so use /run/user/$uid/gnupg
   226  	newAgentDir := filepath.Join(e.Chroot, origAgentDir)
   227  	if err := os.Mkdir(newAgentDir, 0700); err != nil && !os.IsExist(err) {
   228  		return err
   229  	}
   230  
   231  	return system.Bind(origAgentDir, newAgentDir)
   232  }
   233  
   234  // CopyGoogleCreds copies a Google credentials JSON file if one exists.
   235  // Unfortunately gsutil only partially supports these JSON files and does not
   236  // respect GOOGLE_APPLICATION_CREDENTIALS at all so a boto file is created.
   237  // TODO(marineam): integrate with mantle/auth package to migrate towards
   238  // consistent handling of credentials across all of mantle and the SDK.
   239  func (e *enter) CopyGoogleCreds() error {
   240  	const (
   241  		botoName    = "boto"
   242  		jsonName    = "application_default_credentials.json"
   243  		trackerName = "tracker-files"
   244  		botoEnvName = "BOTO_PATH"
   245  		jsonEnvName = "GOOGLE_APPLICATION_CREDENTIALS"
   246  	)
   247  
   248  	jsonSrc := os.Getenv(jsonEnvName)
   249  	if jsonSrc == "" {
   250  		jsonSrc = filepath.Join(e.User.HomeDir, ".config", "gcloud", jsonName)
   251  	}
   252  
   253  	if _, err := os.Stat(jsonSrc); err != nil {
   254  		// Skip but do not pass along the invalid env var
   255  		os.Unsetenv(botoEnvName)
   256  		return os.Unsetenv(jsonEnvName)
   257  	}
   258  
   259  	stateDir, err := ioutil.TempDir(e.UserRunDir, "google-")
   260  	if err != nil {
   261  		return err
   262  	}
   263  	if err := os.Chown(stateDir, e.User.UidNo, e.User.GidNo); err != nil {
   264  		return err
   265  	}
   266  
   267  	var (
   268  		botoPath       = filepath.Join(stateDir, botoName)
   269  		jsonPath       = filepath.Join(stateDir, jsonName)
   270  		trackerDir     = filepath.Join(stateDir, trackerName)
   271  		chrootBotoPath = strings.TrimPrefix(botoPath, e.Chroot)
   272  		chrootJsonPath = strings.TrimPrefix(jsonPath, e.Chroot)
   273  		chrootStateDir = strings.TrimPrefix(stateDir, e.Chroot)
   274  	)
   275  
   276  	if err := os.Mkdir(trackerDir, 0700); err != nil {
   277  		return err
   278  	}
   279  	if err := os.Chown(trackerDir, e.User.UidNo, e.User.GidNo); err != nil {
   280  		return err
   281  	}
   282  
   283  	credsRaw, err := ioutil.ReadFile(jsonSrc)
   284  	if err != nil {
   285  		return err
   286  	}
   287  	var creds googleCreds
   288  	if err := json.Unmarshal(credsRaw, &creds); err != nil {
   289  		return fmt.Errorf("Unmarshal GoogleCreds failed: %s", err)
   290  	}
   291  	creds.JsonPath = chrootJsonPath
   292  	creds.StateDir = chrootStateDir
   293  
   294  	boto, err := os.OpenFile(botoPath, os.O_CREATE|os.O_WRONLY, 0600)
   295  	if err != nil {
   296  		return err
   297  	}
   298  	defer boto.Close()
   299  
   300  	if err := botoTemplate.Execute(boto, &creds); err != nil {
   301  		return err
   302  	}
   303  
   304  	if err := boto.Chown(e.User.UidNo, e.User.GidNo); err != nil {
   305  		return err
   306  	}
   307  
   308  	// Include the default boto path as well for user customization.
   309  	botoEnv := fmt.Sprintf("%s:/home/%s/.boto", chrootBotoPath, e.User.Username)
   310  	if err := os.Setenv(botoEnvName, botoEnv); err != nil {
   311  		return err
   312  	}
   313  
   314  	if err := system.CopyRegularFile(jsonSrc, jsonPath); err != nil {
   315  		return err
   316  	}
   317  
   318  	if err := os.Chown(jsonPath, e.User.UidNo, e.User.GidNo); err != nil {
   319  		return err
   320  	}
   321  
   322  	return os.Setenv(jsonEnvName, jsonPath)
   323  }
   324  
   325  func (e enter) SetupDNS() error {
   326  	resolv := "/etc/resolv.conf"
   327  	chrootResolv := filepath.Join(e.Chroot, resolv)
   328  	if !e.UseHostDNS {
   329  		return ioutil.WriteFile(chrootResolv, []byte(defaultResolv), 0644)
   330  	}
   331  
   332  	if _, err := os.Stat(resolv); err == nil {
   333  		// Only copy if resolv.conf exists, if missing resolver uses localhost
   334  		return system.InstallRegularFile(resolv, chrootResolv)
   335  	}
   336  	return nil
   337  }
   338  
   339  // bind mount the repo source tree into the chroot and run a command
   340  // Called via the multicall interface. Should only have 1 arg which is an
   341  // enter struct encoded in json.
   342  func enterChrootHelper(args []string) (err error) {
   343  	if len(args) != 1 {
   344  		return fmt.Errorf("got %d args, need exactly 1", len(args))
   345  	}
   346  
   347  	var e enter
   348  	if err := json.Unmarshal([]byte(args[0]), &e); err != nil {
   349  		return err
   350  	}
   351  
   352  	username := os.Getenv("SUDO_USER")
   353  	if username == "" {
   354  		return fmt.Errorf("SUDO_USER environment variable is not set.")
   355  	}
   356  	if e.User, err = user.Lookup(username); err != nil {
   357  		return err
   358  	}
   359  	e.UserRunDir = filepath.Join(e.Chroot, "run", "user", e.User.Uid)
   360  
   361  	newRepoRoot := filepath.Join(e.Chroot, chrootRepoRoot)
   362  	if err := os.MkdirAll(newRepoRoot, 0755); err != nil {
   363  		return err
   364  	}
   365  
   366  	if err := e.SetupDNS(); err != nil {
   367  		return err
   368  	}
   369  
   370  	// namespaces are per-thread attributes
   371  	runtime.LockOSThread()
   372  	defer runtime.UnlockOSThread()
   373  
   374  	if err := syscall.Unshare(syscall.CLONE_NEWNS); err != nil {
   375  		return fmt.Errorf("Unsharing mount namespace failed: %v", err)
   376  	}
   377  
   378  	if err := system.RecursiveSlave("/"); err != nil {
   379  		return err
   380  	}
   381  
   382  	if err := system.RecursiveBind(e.RepoRoot, newRepoRoot); err != nil {
   383  		return err
   384  	}
   385  
   386  	if err := e.MountAPI(); err != nil {
   387  		return err
   388  	}
   389  
   390  	if err = os.MkdirAll(e.UserRunDir, 0755); err != nil {
   391  		return err
   392  	}
   393  
   394  	if err = os.Chown(e.UserRunDir, e.User.UidNo, e.User.GidNo); err != nil {
   395  		return err
   396  	}
   397  
   398  	if err := e.MountSSHAgent(); err != nil {
   399  		return err
   400  	}
   401  
   402  	if err := e.MountGnupgHome(); err != nil {
   403  		return err
   404  	}
   405  
   406  	if e.BindGpgAgent {
   407  		if err := e.MountGnupgAgent(); err != nil {
   408  			return err
   409  		}
   410  	}
   411  
   412  	if err := e.CopyGoogleCreds(); err != nil {
   413  		return err
   414  	}
   415  
   416  	if err := syscall.Chroot(e.Chroot); err != nil {
   417  		return fmt.Errorf("Chrooting to %q failed: %v", e.Chroot, err)
   418  	}
   419  
   420  	if e.CmdDir != "" {
   421  		if err := os.Chdir(e.CmdDir); err != nil {
   422  			return err
   423  		}
   424  	}
   425  
   426  	sudo := "/usr/bin/sudo"
   427  	sudoArgs := append([]string{sudo, "-u", username}, e.Cmd...)
   428  	return syscall.Exec(sudo, sudoArgs, os.Environ())
   429  }
   430  
   431  // Set an environment variable if it isn't already defined.
   432  func setDefault(environ []string, key, value string) []string {
   433  	prefix := key + "="
   434  	for _, env := range environ {
   435  		if strings.HasPrefix(env, prefix) {
   436  			return environ
   437  		}
   438  	}
   439  	return append(environ, prefix+value)
   440  }
   441  
   442  // copies a user's config file from user's home directory to the equivalent
   443  // location in the chroot
   444  func copyUserConfigFile(source, chroot string) error {
   445  	userInfo, err := user.Current()
   446  	if err != nil {
   447  		return err
   448  	}
   449  
   450  	sourcepath := filepath.Join(userInfo.HomeDir, source)
   451  	if _, err := os.Stat(sourcepath); err != nil {
   452  		return nil
   453  	}
   454  
   455  	chrootHome := filepath.Join(chroot, "home", userInfo.Username)
   456  	sourceDir := filepath.Dir(source)
   457  	if sourceDir != "." {
   458  		if err := os.MkdirAll(
   459  			filepath.Join(chrootHome, sourceDir), 0700); err != nil {
   460  			return err
   461  		}
   462  	}
   463  
   464  	tartgetpath := filepath.Join(chrootHome, source)
   465  	if err := system.CopyRegularFile(sourcepath, tartgetpath); err != nil {
   466  		return err
   467  	}
   468  
   469  	return nil
   470  }
   471  
   472  func copyUserConfig(chroot string) error {
   473  	if err := copyUserConfigFile(".ssh/config", chroot); err != nil {
   474  		return err
   475  	}
   476  
   477  	if err := copyUserConfigFile(".ssh/known_hosts", chroot); err != nil {
   478  		return err
   479  	}
   480  
   481  	if err := copyUserConfigFile(".gitconfig", chroot); err != nil {
   482  		return err
   483  	}
   484  
   485  	return nil
   486  }
   487  
   488  // Set a default email address so repo doesn't explode on 'u@h.(none)'
   489  func setDefaultEmail(environ []string) []string {
   490  	username := "nobody"
   491  	if u, err := user.Current(); err == nil {
   492  		username = u.Username
   493  	}
   494  	domain := system.FullHostname()
   495  	email := fmt.Sprintf("%s@%s", username, domain)
   496  	return setDefault(environ, "EMAIL", email)
   497  }
   498  
   499  // Enter the chroot and run a command in the given dir. The args specified in cmd
   500  //get passed directly to sudo so things like -i for a login shell are allowed.
   501  func enterChroot(e enter) error {
   502  	if e.RepoRoot == "" {
   503  		e.RepoRoot = RepoRoot()
   504  	}
   505  	if e.Chroot == "" {
   506  		e.Chroot = "chroot"
   507  	}
   508  	e.Chroot = filepath.Join(e.RepoRoot, e.Chroot)
   509  
   510  	enterJson, err := json.Marshal(e)
   511  	if err != nil {
   512  		return err
   513  	}
   514  	sudo := enterChrootCmd.Sudo(string(enterJson))
   515  	sudo.Env = setDefaultEmail(os.Environ())
   516  	sudo.Stdin = os.Stdin
   517  	sudo.Stdout = os.Stdout
   518  	sudo.Stderr = os.Stderr
   519  
   520  	if err := copyUserConfig(e.Chroot); err != nil {
   521  		return err
   522  	}
   523  
   524  	// will call enterChrootHelper via the multicall interface
   525  	return sudo.Run()
   526  }
   527  
   528  // Enter the chroot with a login shell, optionally invoking a command.
   529  // The command may be prefixed by environment variable assignments.
   530  func Enter(name string, bindGpgAgent, useHostDNS bool, args ...string) error {
   531  	// pass -i to sudo to invoke a login shell
   532  	cmd := []string{"-i", "--"}
   533  	if len(args) > 0 {
   534  		cmd = append(cmd, "env", "--")
   535  		cmd = append(cmd, args...)
   536  	}
   537  	// the CmdDir doesn't matter here, sudo -i will chdir to $HOME
   538  	e := enter{
   539  		Chroot:       name,
   540  		Cmd:          cmd,
   541  		BindGpgAgent: bindGpgAgent,
   542  		UseHostDNS:   useHostDNS,
   543  	}
   544  	return enterChroot(e)
   545  }