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 }