Skip to content

Commit f9ad033

Browse files
Add support for platform user docker credentials (#1884)
1 parent f197d76 commit f9ad033

File tree

3 files changed

+147
-2
lines changed

3 files changed

+147
-2
lines changed

cmd/agent/docker_credentials.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9+
"net"
910
"net/http"
1011
"os"
12+
"path/filepath"
1113
"strconv"
1214
"strings"
15+
"time"
1316

17+
"github.com/loft-sh/devpod/cmd/agent/container"
1418
"github.com/loft-sh/devpod/cmd/flags"
1519
"github.com/loft-sh/devpod/pkg/dockercredentials"
1620
devpodhttp "github.com/loft-sh/devpod/pkg/http"
21+
"github.com/loft-sh/devpod/pkg/ts"
1722
"github.com/loft-sh/log"
1823
"github.com/spf13/cobra"
1924
)
@@ -111,6 +116,17 @@ func (cmd *DockerCredentialsCmd) handleGet(log log.Logger) error {
111116
return fmt.Errorf("no credentials server URL")
112117
}
113118

119+
credentials := getDockerCredentialsFromWorkspaceServer(&dockercredentials.Credentials{ServerURL: strings.TrimSpace(string(url))})
120+
if credentials != nil {
121+
raw, err := json.Marshal(credentials)
122+
if err != nil {
123+
log.Errorf("Error encoding credentials: %v", err)
124+
return nil
125+
}
126+
fmt.Print(string(raw))
127+
return nil
128+
}
129+
114130
rawJSON, err := json.Marshal(&dockercredentials.Request{ServerURL: strings.TrimSpace(string(url))})
115131
if err != nil {
116132
return err
@@ -146,3 +162,65 @@ func (cmd *DockerCredentialsCmd) handleGet(log log.Logger) error {
146162
fmt.Print(string(raw))
147163
return nil
148164
}
165+
166+
func getDockerCredentialsFromWorkspaceServer(credentials *dockercredentials.Credentials) *dockercredentials.Credentials {
167+
if _, err := os.Stat(filepath.Join(container.RootDir, ts.RunnerProxySocket)); err != nil {
168+
// workspace server is not running
169+
return nil
170+
}
171+
172+
httpClient := &http.Client{
173+
Transport: &http.Transport{
174+
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
175+
return net.Dial("unix", filepath.Join(container.RootDir, ts.RunnerProxySocket))
176+
},
177+
},
178+
Timeout: 15 * time.Second,
179+
}
180+
181+
credentials, credentialsErr := requestDockerCredentials(httpClient, credentials, "http://runner-proxy/docker-credentials")
182+
if credentialsErr != nil {
183+
// append error to /var/devpod/docker-credentials.log
184+
file, err := os.OpenFile("/var/devpod/docker-credentials-error.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
185+
if err != nil {
186+
return nil
187+
}
188+
defer file.Close()
189+
190+
_, _ = file.WriteString(fmt.Sprintf("get credentials from workspace server: %v\n", credentialsErr))
191+
return nil
192+
}
193+
194+
return credentials
195+
}
196+
197+
func requestDockerCredentials(httpClient *http.Client, credentials *dockercredentials.Credentials, url string) (*dockercredentials.Credentials, error) {
198+
rawJSON, err := json.Marshal(credentials)
199+
if err != nil {
200+
return nil, fmt.Errorf("error marshalling credentials: %w", err)
201+
}
202+
203+
response, err := httpClient.Post(url, "application/json", bytes.NewReader(rawJSON))
204+
if err != nil {
205+
return nil, fmt.Errorf("error retrieving credentials from credentials server: %w", err)
206+
}
207+
defer response.Body.Close()
208+
209+
raw, err := io.ReadAll(response.Body)
210+
if err != nil {
211+
return nil, fmt.Errorf("error reading credentials: %w", err)
212+
}
213+
214+
// has the request succeeded?
215+
if response.StatusCode != http.StatusOK {
216+
return nil, fmt.Errorf("error reading credentials (%d): %s", response.StatusCode, string(raw))
217+
}
218+
219+
credentials = &dockercredentials.Credentials{}
220+
err = json.Unmarshal(raw, credentials)
221+
if err != nil {
222+
return nil, fmt.Errorf("error decoding credentials: %w", err)
223+
}
224+
225+
return credentials, nil
226+
}

pkg/daemon/platform/local_server.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/gorilla/handlers"
1414
"github.com/julienschmidt/httprouter"
1515
managementv1 "github.com/loft-sh/api/v4/pkg/apis/management/v1"
16+
"github.com/loft-sh/devpod/pkg/dockercredentials"
1617
"github.com/loft-sh/devpod/pkg/gitcredentials"
1718
"github.com/loft-sh/devpod/pkg/platform"
1819
platformclient "github.com/loft-sh/devpod/pkg/platform/client"
@@ -21,6 +22,7 @@ import (
2122
"github.com/loft-sh/log"
2223
"github.com/sirupsen/logrus"
2324
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/klog/v2"
2426
"tailscale.com/client/tailscale"
2527
"tailscale.com/ipn"
2628
"tailscale.com/ipn/ipnstate"
@@ -82,6 +84,7 @@ var (
8284
routeGetUserProfile = "/user-profile"
8385
routeUpdateUserProfile = "/update-user-profile"
8486
routeGitCredentials = "/git-credentials"
87+
routeDockerCredentials = "/docker-credentials"
8588
)
8689

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

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

619+
func (l *localServer) getDockerCredentials(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
620+
host := r.URL.Query().Get("server")
621+
if host == "" {
622+
http.Error(w, "missing required query parameter \"server\"", http.StatusBadRequest)
623+
return
624+
}
625+
626+
all, err := dockercredentials.ListCredentials()
627+
if err != nil {
628+
klog.Errorf("failed to list docker credentials: %v", err)
629+
http.Error(w, fmt.Errorf("list docker credentials: %w", err).Error(), http.StatusInternalServerError)
630+
return
631+
}
632+
for registry, cred := range all.Registries {
633+
if registry == host {
634+
tryJSON(w, cred)
635+
return
636+
}
637+
}
638+
}
639+
615640
func tryJSON(w http.ResponseWriter, obj interface{}) {
616641
out, err := json.Marshal(obj)
617642
if err != nil {

pkg/ts/workspace_server.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,18 @@ func (s *WorkspaceServer) startListeners(ctx context.Context, projectName, works
245245
}
246246
}()
247247

248+
// Setup HTTP handler for docker credentials.
249+
go func() {
250+
mux := http.NewServeMux()
251+
transport := &http.Transport{DialContext: s.tsServer.Dial}
252+
mux.HandleFunc("/docker-credentials", func(w http.ResponseWriter, r *http.Request) {
253+
s.dockerCredentialsHandler(w, r, lc, transport, projectName, workspaceName)
254+
})
255+
if err := http.Serve(runnerProxyListener, mux); err != nil && err != http.ErrServerClosed {
256+
s.log.Errorf("HTTP runner proxy server error: %v", err)
257+
}
258+
}()
259+
248260
// Setup HTTP handler for port forwarding.
249261
go func() {
250262
mux := http.NewServeMux()
@@ -283,8 +295,7 @@ func (s *WorkspaceServer) removeConnection() {
283295
s.connectionCounter--
284296
}
285297

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

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

329+
// dockerCredentialsHandler is the handler for docker credentials requests for workspace.
330+
func (s *WorkspaceServer) dockerCredentialsHandler(w http.ResponseWriter, r *http.Request, lc *tailscale.LocalClient, transport *http.Transport, projectName, workspaceName string) {
331+
s.log.Infof("Received docker credentials request from %s", r.RemoteAddr)
332+
333+
// create a new http client with a custom transport
334+
discoveredRunner, err := s.discoverRunner(r.Context(), lc)
335+
if err != nil {
336+
http.Error(w, "failed to discover runner", http.StatusInternalServerError)
337+
return
338+
}
339+
340+
// build the runner URL
341+
runnerURL := fmt.Sprintf("http://%s.ts.loft/devpod/%s/%s/workspace-docker-credentials", discoveredRunner, projectName, workspaceName)
342+
parsedURL, err := url.Parse(runnerURL)
343+
if err != nil {
344+
http.Error(w, "failed to parse runner URL", http.StatusInternalServerError)
345+
return
346+
}
347+
348+
// Build the reverse proxy with a custom Director.
349+
proxy := httputil.NewSingleHostReverseProxy(parsedURL)
350+
proxy.Director = func(req *http.Request) {
351+
dest := *parsedURL
352+
req.URL = &dest
353+
req.Host = dest.Host
354+
req.Header.Set("Authorization", "Bearer "+s.config.AccessKey)
355+
}
356+
proxy.Transport = transport
357+
proxy.ServeHTTP(w, r)
358+
}
359+
318360
// httpPortForwardHandler is the HTTP reverse proxy handler for workspace.
319361
// It reconstructs the target URL using custom headers and forwards the request.
320362
func (s *WorkspaceServer) httpPortForwardHandler(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)