Skip to content

Commit

Permalink
fix: Make DOCKER_CONFIG available to buildctl
Browse files Browse the repository at this point in the history
When povisioning a finch VM, the CLI inserts a line into the
user's bash rc that points DOCKER_CONFIG to the user's finch dir.
Finch-daemon running through systemd doesn't load the bash rc
and so it doesn't get this config. As a result, build requests
that go through finch-daemon don't pass this variable to buildctl
so buildctl/buildkit is unable to build on base images in private
repositories.

This change temporarily fixes this by inserting DOCKER_CONFIG into
the finch-daemon process based on the owner of the socket. That
way, buildctl is able to load the finch config and retrieve credentials.

This should eventually be replaced by somehow passing the credentials
that come in to the build API directly to buildctl rather than loading
credentials based on the config.

Signed-off-by: Kern Walster <[email protected]>
  • Loading branch information
Kern-- committed Nov 12, 2024
1 parent 818fdd6 commit 8869ba2
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 1 deletion.
24 changes: 23 additions & 1 deletion cmd/finch-daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/coreos/go-systemd/v22/daemon"
"github.com/moby/moby/pkg/pidfile"
"github.com/runfinch/finch-daemon/api/router"
"github.com/runfinch/finch-daemon/internal/fs/passwd"
"github.com/runfinch/finch-daemon/pkg/flog"
"github.com/runfinch/finch-daemon/version"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -141,6 +142,12 @@ func run(options *DaemonOptions) error {
serverWg := &sync.WaitGroup{}
serverWg.Add(1)

if options.socketOwner >= 0 {
if err := defineDockerConfig(options.socketOwner); err != nil {
return fmt.Errorf("failed to get finch config: %w", err)
}
}

listener, err := getListener(options)
if err != nil {
return fmt.Errorf("failed to create a listener: %w", err)
Expand All @@ -162,7 +169,6 @@ func run(options *DaemonOptions) error {
}
}()
}

server := &http.Server{
Handler: r,
ReadHeaderTimeout: 5 * time.Minute,
Expand Down Expand Up @@ -235,3 +241,19 @@ func sdNotify(state string, logger *flog.Logrus) {
notified, err := daemon.SdNotify(false, state)
logger.Debugf("systemd-notify result: (signaled %t), (err: %v)", notified, err)
}

// defineDockerConfig defines the DOCKER_CONFIG environment variable for the process
// to be $HOME/.finch. When building an image via finch-daemon, buildctl uses this variable
// to load auth configs.
//
// This is a hack and should be fixed by passing the actual credentials that come in
// via the build API to buildctl instead.
func defineDockerConfig(uid int) error {
return passwd.Walk(func(e passwd.Entry) bool {
if e.UID == uid {
os.Setenv("DOCKER_CONFIG", fmt.Sprintf("%s/.finch", e.Home))
return false
}
return true
})
}
73 changes: 73 additions & 0 deletions internal/fs/passwd/passwd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package passwd

import (
"bufio"
"io"
"os"
"strconv"
"strings"
)

// open opens /etc/password for reading.
// abstracted so that it can be mocked for tests.
var open = func() (io.ReadCloser, error) {
return os.Open("/etc/passwd")
}

// PasswdEntry represents an entry in /etc/passwd.
type Entry struct {
Username string
UID int
GID int
Home string
Shell string
}

// Walk iterates over all entries in /etc/passwd and calls f for each one
// f returns whether the walk should continue - i.e. returning false from f will
// exit the walk without processing more entries.
func Walk(f func(Entry) bool) error {
file, err := open()
if err != nil {
return err
}
defer file.Close()
s := bufio.NewScanner(file)
for s.Scan() {
entry, err := parse(s.Text())
if err != nil {
return err
}
if !f(entry) {
return nil
}
}
return nil
}

func parse(s string) (Entry, error) {
parts := strings.Split(s, ":")
name := parts[0]
// parts[1] - ignore password info
uid, err := strconv.ParseInt(parts[2], 10, 32)
if err != nil {
return Entry{}, err
}
gid, err := strconv.ParseInt(parts[3], 10, 32)
if err != nil {
return Entry{}, nil
}
// parts[4] - ignore GECOS
home := parts[5]
shell := parts[6]
return Entry{
Username: name,
UID: int(uid),
GID: int(gid),
Home: home,
Shell: shell,
}, nil
}
63 changes: 63 additions & 0 deletions internal/fs/passwd/passwd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package passwd

import (
"bytes"
"io"
"strings"
"testing"

"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)

var (
content = []string{
"root:x:0:0:Super User:/root:/bin/bash",
"sshd:x:74:74:Privilege-separated SSH:/usr/share/empty.sshd:/usr/sbin/nologin",
"user:x:1000:1000::/home/user:/bin/bash",
}
entries = []Entry{
{Username: "root", UID: 0, GID: 0, Home: "/root", Shell: "/bin/bash"},
{Username: "sshd", UID: 74, GID: 74, Home: "/usr/share/empty.sshd", Shell: "/usr/sbin/nologin"},
{Username: "user", UID: 1000, GID: 1000, Home: "/home/user", Shell: "/bin/bash"},
}
)

func TestPasswd(t *testing.T) {
gomega.RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "UnitTests - passwd parsing")
}

var _ = ginkgo.Describe("passwd", func() {
ginkgo.DescribeTable("when parsing entries",
func(line string, entry Entry) {
parsed, err := parse(line)
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(parsed).To(gomega.Equal(entry))
},
ginkgo.Entry("should correctly parse root", content[0], entries[0]),
ginkgo.Entry("should correctly parse a daemon user", content[1], entries[1]),
ginkgo.Entry("should correctly parse a regular user", content[2], entries[2]),
)

ginkgo.DescribeTable("when walking passwd file", func(match bool, expectedCount int) {
i := 0
passwdReader := bytes.NewReader([]byte(strings.Join(content, "\n")))
open = func() (io.ReadCloser, error) {
return io.NopCloser(passwdReader), nil
}
Walk(func(e Entry) bool {
gomega.Expect(e).To(gomega.Equal(entries[i]))
i++
return !match
})
gomega.Expect(i).To(gomega.Equal(expectedCount))
},
ginkgo.Entry("should see all entries if no matches", false, len(entries)),
ginkgo.Entry("should see 1 entry if first matches", true, 1),
)
},
)

0 comments on commit 8869ba2

Please sign in to comment.