Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions cmd/agent/docker_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/loft-sh/devpod/cmd/agent/container"
"github.com/loft-sh/devpod/cmd/flags"
"github.com/loft-sh/devpod/pkg/dockercredentials"
devpodhttp "github.com/loft-sh/devpod/pkg/http"
"github.com/loft-sh/devpod/pkg/ts"
"github.com/loft-sh/log"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -111,6 +116,17 @@ func (cmd *DockerCredentialsCmd) handleGet(log log.Logger) error {
return fmt.Errorf("no credentials server URL")
}

credentials := getDockerCredentialsFromWorkspaceServer(&dockercredentials.Credentials{ServerURL: strings.TrimSpace(string(url))})
if credentials != nil {
raw, err := json.Marshal(credentials)
if err != nil {
log.Errorf("Error encoding credentials: %v", err)
return nil
}
fmt.Print(string(raw))
return nil
}

rawJSON, err := json.Marshal(&dockercredentials.Request{ServerURL: strings.TrimSpace(string(url))})
if err != nil {
return err
Expand Down Expand Up @@ -146,3 +162,65 @@ func (cmd *DockerCredentialsCmd) handleGet(log log.Logger) error {
fmt.Print(string(raw))
return nil
}

func getDockerCredentialsFromWorkspaceServer(credentials *dockercredentials.Credentials) *dockercredentials.Credentials {
if _, err := os.Stat(filepath.Join(container.RootDir, ts.RunnerProxySocket)); err != nil {
// workspace server is not running
return nil
}

httpClient := &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
Comment thread
janekbaraniewski marked this conversation as resolved.
return net.Dial("unix", filepath.Join(container.RootDir, ts.RunnerProxySocket))
},
},
Timeout: 15 * time.Second,
}

credentials, credentialsErr := requestDockerCredentials(httpClient, credentials, "http://runner-proxy/docker-credentials")
if credentialsErr != nil {
// append error to /var/devpod/docker-credentials.log
file, err := os.OpenFile("/var/devpod/docker-credentials-error.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil
}
defer file.Close()

_, _ = file.WriteString(fmt.Sprintf("get credentials from workspace server: %v\n", credentialsErr))
return nil
}

return credentials
}

func requestDockerCredentials(httpClient *http.Client, credentials *dockercredentials.Credentials, url string) (*dockercredentials.Credentials, error) {
rawJSON, err := json.Marshal(credentials)
if err != nil {
return nil, fmt.Errorf("error marshalling credentials: %w", err)
}

response, err := httpClient.Post(url, "application/json", bytes.NewReader(rawJSON))
if err != nil {
return nil, fmt.Errorf("error retrieving credentials from credentials server: %w", err)
}
defer response.Body.Close()

raw, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("error reading credentials: %w", err)
}

// has the request succeeded?
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error reading credentials (%d): %s", response.StatusCode, string(raw))
}

credentials = &dockercredentials.Credentials{}
err = json.Unmarshal(raw, credentials)
if err != nil {
return nil, fmt.Errorf("error decoding credentials: %w", err)
}

return credentials, nil
}
25 changes: 25 additions & 0 deletions pkg/daemon/platform/local_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/gorilla/handlers"
"github.com/julienschmidt/httprouter"
managementv1 "github.com/loft-sh/api/v4/pkg/apis/management/v1"
"github.com/loft-sh/devpod/pkg/dockercredentials"
"github.com/loft-sh/devpod/pkg/gitcredentials"
"github.com/loft-sh/devpod/pkg/platform"
platformclient "github.com/loft-sh/devpod/pkg/platform/client"
Expand All @@ -21,6 +22,7 @@ import (
"github.com/loft-sh/log"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
Expand Down Expand Up @@ -82,6 +84,7 @@ var (
routeGetUserProfile = "/user-profile"
routeUpdateUserProfile = "/update-user-profile"
routeGitCredentials = "/git-credentials"
routeDockerCredentials = "/docker-credentials"
)

func newLocalServer(lc *tailscale.LocalClient, pc platformclient.Client, devPodContext string, log log.Logger) (*localServer, error) {
Expand Down Expand Up @@ -116,6 +119,7 @@ func newLocalServer(lc *tailscale.LocalClient, pc platformclient.Client, devPodC
router.GET(routeGetUserProfile, l.userProfile)
router.POST(routeUpdateUserProfile, l.updateUserProfile)
router.GET(routeGitCredentials, l.getGitCredentials)
router.GET(routeDockerCredentials, l.getDockerCredentials)

handler := handlers.LoggingHandler(log.Writer(logrus.DebugLevel, true), router)
handler = handlers.RecoveryHandler(handlers.RecoveryLogger(panicLogger{log: l.log}), handlers.PrintRecoveryStack(true))(handler)
Expand Down Expand Up @@ -612,6 +616,27 @@ func (l *localServer) getGitCredentials(w http.ResponseWriter, r *http.Request,
tryJSON(w, credentials)
}

func (l *localServer) getDockerCredentials(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
host := r.URL.Query().Get("server")
if host == "" {
http.Error(w, "missing required query parameter \"server\"", http.StatusBadRequest)
return
}

all, err := dockercredentials.ListCredentials()
if err != nil {
klog.Errorf("failed to list docker credentials: %v", err)
http.Error(w, fmt.Errorf("list docker credentials: %w", err).Error(), http.StatusInternalServerError)
return
}
for registry, cred := range all.Registries {
if registry == host {
tryJSON(w, cred)
return
}
}
}

func tryJSON(w http.ResponseWriter, obj interface{}) {
out, err := json.Marshal(obj)
if err != nil {
Expand Down
46 changes: 44 additions & 2 deletions pkg/ts/workspace_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,18 @@ func (s *WorkspaceServer) startListeners(ctx context.Context, projectName, works
}
}()

// Setup HTTP handler for docker credentials.
go func() {
mux := http.NewServeMux()
transport := &http.Transport{DialContext: s.tsServer.Dial}
mux.HandleFunc("/docker-credentials", func(w http.ResponseWriter, r *http.Request) {
s.dockerCredentialsHandler(w, r, lc, transport, projectName, workspaceName)
})
if err := http.Serve(runnerProxyListener, mux); err != nil && err != http.ErrServerClosed {
s.log.Errorf("HTTP runner proxy server error: %v", err)
}
}()

// Setup HTTP handler for port forwarding.
go func() {
mux := http.NewServeMux()
Expand Down Expand Up @@ -283,8 +295,7 @@ func (s *WorkspaceServer) removeConnection() {
s.connectionCounter--
}

// httpPortForwardHandler is the HTTP reverse proxy handler for workspace.
// It reconstructs the target URL using custom headers and forwards the request.
// gitCredentialsHandler is the handler for git credentials requests for workspace.
func (s *WorkspaceServer) gitCredentialsHandler(w http.ResponseWriter, r *http.Request, lc *tailscale.LocalClient, transport *http.Transport, projectName, workspaceName string) {
s.log.Infof("Received git credentials request from %s", r.RemoteAddr)

Expand Down Expand Up @@ -315,6 +326,37 @@ func (s *WorkspaceServer) gitCredentialsHandler(w http.ResponseWriter, r *http.R
proxy.ServeHTTP(w, r)
}

// dockerCredentialsHandler is the handler for docker credentials requests for workspace.
func (s *WorkspaceServer) dockerCredentialsHandler(w http.ResponseWriter, r *http.Request, lc *tailscale.LocalClient, transport *http.Transport, projectName, workspaceName string) {
s.log.Infof("Received docker credentials request from %s", r.RemoteAddr)

// create a new http client with a custom transport
discoveredRunner, err := s.discoverRunner(r.Context(), lc)
if err != nil {
http.Error(w, "failed to discover runner", http.StatusInternalServerError)
return
}

// build the runner URL
runnerURL := fmt.Sprintf("http://%s.ts.loft/devpod/%s/%s/workspace-docker-credentials", discoveredRunner, projectName, workspaceName)
parsedURL, err := url.Parse(runnerURL)
if err != nil {
http.Error(w, "failed to parse runner URL", http.StatusInternalServerError)
return
}

// Build the reverse proxy with a custom Director.
proxy := httputil.NewSingleHostReverseProxy(parsedURL)
proxy.Director = func(req *http.Request) {
dest := *parsedURL
req.URL = &dest
req.Host = dest.Host
req.Header.Set("Authorization", "Bearer "+s.config.AccessKey)
}
proxy.Transport = transport
proxy.ServeHTTP(w, r)
}

// httpPortForwardHandler is the HTTP reverse proxy handler for workspace.
// It reconstructs the target URL using custom headers and forwards the request.
func (s *WorkspaceServer) httpPortForwardHandler(w http.ResponseWriter, r *http.Request) {
Expand Down
Loading