diff --git a/Documentation/subcommands/run.md b/Documentation/subcommands/run.md index c3af68a9..faa84ff3 100644 --- a/Documentation/subcommands/run.md +++ b/Documentation/subcommands/run.md @@ -15,6 +15,38 @@ In order to be able to run the command, all dependencies of the current ACI must be fetched. The first time `run` is called, the dependencies will be downloaded and expanded. +### Authentication + +acbuild can use HTTP Basic authentication when fetching dependencies. To use +authentication, specify a directory using the `--auth-config-dir` flag. By +default acbuild will look in `auth.d`. + +acbuild looks for configuration files with a `.json` file name extension in the +specified directory and its subdirectories. Each file is expected to contain +two fields: `domains` and `credentials`. + +The `domains` field is an array of strings describing hosts for which the +following credentials should be used. Each entry must consist of a host in a +URL as specified by RFC 3986. + +The `credentials` field is a map with two keys - `user` and `password`. These +should be the values needed for successful authentication with the given +hosts. + +For example: + +`auth.d/coreos-basic.json` + +``` +{ + "domains": ["coreos.com", "tectonic.com"], + "credentials": { + "user": "foo", + "password": "bar" + } +} +``` + ## Overlayfs acbuild utilizes overlayfs when running a command in an ACI with dependencies. diff --git a/acbuild/acbuild.go b/acbuild/acbuild.go index 4913944a..0a596650 100644 --- a/acbuild/acbuild.go +++ b/acbuild/acbuild.go @@ -194,7 +194,7 @@ func runWrapper(cf func(cmd *cobra.Command, args []string) (exit int)) func(cmd a := newACBuild() - err = a.Begin(absoluteAciToModify, false) + err = a.Begin(absoluteAciToModify, false, "") if err != nil { stderr("%v", err) cmdExitCode = getErrorCode(err) diff --git a/acbuild/begin.go b/acbuild/begin.go index 2ecaa0ea..7aaff151 100644 --- a/acbuild/begin.go +++ b/acbuild/begin.go @@ -31,6 +31,7 @@ var ( func init() { cmdAcbuild.AddCommand(cmdBegin) cmdBegin.Flags().BoolVar(&insecure, "insecure", false, "Allows fetching dependencies over an unencrypted connection") + cmdBegin.Flags().StringVar(&authConfigDir, "auth-config-dir", "auth.d", "Directory with authentication config file(s)") } func runBegin(cmd *cobra.Command, args []string) (exit int) { @@ -49,9 +50,9 @@ func runBegin(cmd *cobra.Command, args []string) (exit int) { var err error if len(args) == 0 { - err = newACBuild().Begin("", insecure) + err = newACBuild().Begin("", insecure, authConfigDir) } else { - err = newACBuild().Begin(args[0], insecure) + err = newACBuild().Begin(args[0], insecure, authConfigDir) } if err != nil { diff --git a/acbuild/run.go b/acbuild/run.go index 337bdc6f..7617f468 100644 --- a/acbuild/run.go +++ b/acbuild/run.go @@ -26,10 +26,12 @@ import ( ) var ( - insecure = false - workingdir = "" - engineName = "" - cmdRun = &cobra.Command{ + insecure = false + workingdir = "" + engineName = "" + authConfigDir = "" + + cmdRun = &cobra.Command{ Use: "run -- CMD [ARGS]", Short: "Run a command in an ACI", Long: "Run a given command in an ACI, and save the resulting container as a new ACI", @@ -55,6 +57,7 @@ func init() { cmdRun.Flags().BoolVar(&insecure, "insecure", false, "Allows fetching dependencies over http") cmdRun.Flags().StringVar(&workingdir, "working-dir", "", "The working directory inside the container for this command") cmdRun.Flags().StringVar(&engineName, "engine", "systemd-nspawn", "The engine used to run the command. Supported engines: "+engineList) + cmdRun.Flags().StringVar(&authConfigDir, "auth-config-dir", "auth.d", "Directory with authentication config file(s)") } func runRun(cmd *cobra.Command, args []string) (exit int) { @@ -73,7 +76,7 @@ func runRun(cmd *cobra.Command, args []string) (exit int) { return 1 } - err := newACBuild().Run(args, workingdir, insecure, engine) + err := newACBuild().Run(args, workingdir, insecure, engine, authConfigDir) if err != nil { stderr("run: %v", err) diff --git a/lib/begin.go b/lib/begin.go index 0609c5ae..ccfb8d10 100644 --- a/lib/begin.go +++ b/lib/begin.go @@ -47,7 +47,7 @@ var ( // at a.CurrentACIPath. If start is the empty string, the build will begin with // an empty ACI, otherwise the ACI stored at start will be used at the starting // point. -func (a *ACBuild) Begin(start string, insecure bool) (err error) { +func (a *ACBuild) Begin(start string, insecure bool, authConfigDir string) (err error) { _, err = os.Stat(a.ContextPath) switch { case os.IsNotExist(err): @@ -103,7 +103,7 @@ func (a *ACBuild) Begin(start string, insecure bool) (err error) { start = strings.TrimPrefix(start, dockerPrefix) return a.beginFromRemoteDockerImage(start, insecure) } - return a.beginFromRemoteImage(start, insecure) + return a.beginFromRemoteImage(start, insecure, authConfigDir) } } return a.beginWithEmptyACI() @@ -252,7 +252,7 @@ func (a *ACBuild) writeEmptyManifest() error { return nil } -func (a *ACBuild) beginFromRemoteImage(start string, insecure bool) error { +func (a *ACBuild) beginFromRemoteImage(start string, insecure bool, authConfigDir string) error { app, err := discovery.NewAppFromString(start) if err != nil { return err @@ -274,9 +274,15 @@ func (a *ACBuild) beginFromRemoteImage(start string, insecure bool) error { } defer os.RemoveAll(tmpDepStoreExpandedPath) + authConfig, err := registry.ReadAuthConfig(authConfigDir) + if err != nil { + return err + } + reg := registry.Registry{ DepStoreTarPath: tmpDepStoreTarPath, DepStoreExpandedPath: tmpDepStoreExpandedPath, + AuthConfig: authConfig, Insecure: insecure, Debug: a.Debug, } @@ -333,6 +339,7 @@ func (a *ACBuild) beginFromRemoteDockerImage(start string, insecure bool) (err e AllowHTTP: insecure, } + // TODO: Docker authentication config := docker2aci.RemoteConfig{ CommonConfig: docker2aci.CommonConfig{ Squash: true, diff --git a/lib/run.go b/lib/run.go index 5a6c1f76..a19d8795 100644 --- a/lib/run.go +++ b/lib/run.go @@ -47,7 +47,7 @@ import ( // changed to its value before running the given command. // // - runEngine: The engine used to perform the execution of the command. -func (a *ACBuild) Run(cmd []string, workingDir string, insecure bool, runEngine engine.Engine) (err error) { +func (a *ACBuild) Run(cmd []string, workingDir string, insecure bool, runEngine engine.Engine, authConfigDir string) (err error) { if err = a.lock(); err != nil { return err } @@ -110,7 +110,7 @@ func (a *ACBuild) Run(cmd []string, workingDir string, insecure bool, runEngine } } - deps, err := a.renderACI(insecure, a.Debug) + deps, err := a.renderACI(insecure, authConfigDir, a.Debug) if err != nil { return err } @@ -179,10 +179,16 @@ func supportsOverlay() bool { return false } -func (a *ACBuild) renderACI(insecure, debug bool) ([]string, error) { +func (a *ACBuild) renderACI(insecure bool, authConfigDir string, debug bool) ([]string, error) { + authConfig, err := registry.ReadAuthConfig(authConfigDir) + if err != nil { + return nil, err + } + reg := registry.Registry{ DepStoreTarPath: a.DepStoreTarPath, DepStoreExpandedPath: a.DepStoreExpandedPath, + AuthConfig: authConfig, Insecure: insecure, Debug: debug, } diff --git a/registry/auth-config.go b/registry/auth-config.go new file mode 100644 index 00000000..7c8437a2 --- /dev/null +++ b/registry/auth-config.go @@ -0,0 +1,81 @@ +package registry + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" +) + +type HostHeaders map[string]http.Header + +type AuthConfig []AuthFile + +type AuthFile struct { + Domains []string `json:"domains"` + Credentials Credentials `json:"credentials"` +} + +type Credentials struct { + User string `json:"user"` + Password string `json:"password"` +} + +func ReadAuthConfig(directory string) (*AuthConfig, error) { + authConfig := AuthConfig{} + + err := filepath.Walk(directory, func(path string, file os.FileInfo, err error) error { + if err != nil { + return nil + } + + if !validAuthFile(file) { + return nil + } + + jsonContents, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("auth-config: %s", err) + } + + authFile := AuthFile{} + err = json.Unmarshal(jsonContents, &authFile) + if err != nil { + return fmt.Errorf("auth-config: %s", err) + } + + authConfig = append(authConfig, authFile) + + return nil + }) + + return &authConfig, err +} + +func validAuthFile(file os.FileInfo) bool { + if !file.Mode().IsRegular() { + return false + } + + if filepath.Ext(file.Name()) != ".json" { + return false + } + + return true +} + +func (ac *AuthConfig) HostHeaders() HostHeaders { + hostHeaders := HostHeaders{} + for _, authFile := range *ac { + fakeRequest := http.Request{Header: http.Header{}} + fakeRequest.SetBasicAuth(authFile.Credentials.User, authFile.Credentials.Password) + + for _, domain := range authFile.Domains { + hostHeaders[domain] = fakeRequest.Header + } + } + + return hostHeaders +} diff --git a/registry/fetch.go b/registry/fetch.go index d2be8cf0..8aae551f 100644 --- a/registry/fetch.go +++ b/registry/fetch.go @@ -336,7 +336,7 @@ func (r Registry) discoverEndpoint(imageName types.ACIdentifier, labels types.La insecure = discovery.InsecureHTTP } - acis, attempts, err := discovery.DiscoverACIEndpoints(*app, nil, insecure, 0) + acis, attempts, err := discovery.DiscoverACIEndpoints(*app, r.AuthConfig.HostHeaders(), insecure, 0) if err != nil { return nil, err } @@ -355,11 +355,15 @@ func (r Registry) discoverEndpoint(imageName types.ACIdentifier, labels types.La } func (r Registry) download(url, path, label string) error { - //TODO: auth req, err := http.NewRequest("GET", url, nil) if err != nil { return err } + + if header, ok := r.AuthConfig.HostHeaders()[req.URL.Host]; ok { + req.Header = header + } + transport := http.DefaultTransport transport.(*http.Transport).Proxy = http.ProxyFromEnvironment if r.Insecure { @@ -373,6 +377,11 @@ func (r Registry) download(url, path, label string) error { if len(via) >= 10 { return fmt.Errorf("too many redirects") } + + if header, ok := r.AuthConfig.HostHeaders()[req.URL.Host]; ok { + req.Header = header + } + //f.setHTTPHeaders(req, etag) return nil } diff --git a/registry/registry.go b/registry/registry.go index 8cbb2dd1..528b0864 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -49,6 +49,7 @@ var ( type Registry struct { DepStoreTarPath string DepStoreExpandedPath string + AuthConfig *AuthConfig Insecure bool Debug bool }