daemon: Gitea v1 API adapter (git.westphal.fr is Gitea, not GitLab); fix golang Dockerfile to 1.26; sudo docker in deploy

This commit is contained in:
Raphael Westphal
2026-08-18 15:07:02 +02:00
parent bcd976a8ac
commit 49840225e2
10 changed files with 79 additions and 79 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# docker build -f daemon/Dockerfile -t lvmh-daemon:latest .
# To bake the real web UI, copy web/dist over the placeholder first:
# cp -r web/dist daemon/webdist/
FROM golang:1.24-alpine AS build
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY daemon/go.mod daemon/go.sum ./
RUN go mod download
+6 -6
View File
@@ -188,20 +188,20 @@ func TestAPIDeleteContainerStopFailure(t *testing.T) {
func newGitLabAPIServer(t *testing.T, broken bool) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("/api/v4/user", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Private-Token") != "pat-good" {
mux.HandleFunc("/api/v1/user", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "token pat-good" {
w.WriteHeader(http.StatusUnauthorized)
return
}
_, _ = io.WriteString(w, `{"username":"alice"}`)
_, _ = io.WriteString(w, `{"login":"alice"}`)
})
mux.HandleFunc("/api/v4/projects", func(w http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/api/v1/user/repos", func(w http.ResponseWriter, r *http.Request) {
if broken {
w.WriteHeader(http.StatusInternalServerError)
return
}
_, _ = io.WriteString(w, `[{"path_with_namespace":"g/p","name":"P","namespace":{"path":"g"},
"last_activity_at":"2024-01-01T00:00:00Z","web_url":"https://gl/g/p","default_branch":"main"}]`)
_, _ = io.WriteString(w, `[{"full_name":"g/p","name":"P","owner":{"login":"g"},
"updated_at":"2024-01-01T00:00:00Z","html_url":"https://gl/g/p","default_branch":"main"}]`)
})
up := httptest.NewServer(mux)
t.Cleanup(up.Close)
+1 -1
View File
@@ -288,7 +288,7 @@ func (s *Spawner) cloneURL(repo string) (string, error) {
return "", err
}
if token, ok, _ := s.store.GetSetting(settingGitLabToken); ok && token != "" && u.User == nil {
u.User = url.UserPassword("oauth2", token)
u.User = url.User(token)
}
return u.String(), nil
}
+26 -25
View File
@@ -38,16 +38,17 @@ type GitLabRepo struct {
DefaultBranch string `json:"defaultBranch"`
}
// gitlabProject is the subset of the upstream projects API we map from.
type gitlabProject struct {
PathWithNamespace string `json:"path_with_namespace"`
Name string `json:"name"`
Namespace struct {
Path string `json:"path"`
} `json:"namespace"`
LastActivityAt string `json:"last_activity_at"`
WebURL string `json:"web_url"`
DefaultBranch string `json:"default_branch"`
// giteaRepo is the subset of the upstream Gitea /api/v1/user/repos item we
// map from.
type giteaRepo struct {
FullName string `json:"full_name"`
Name string `json:"name"`
Owner struct {
Login string `json:"login"`
} `json:"owner"`
UpdatedAt string `json:"updated_at"`
HTMLURL string `json:"html_url"`
DefaultBranch string `json:"default_branch"`
}
// GitLab stores/validates the PAT in the settings table and lists projects.
@@ -92,24 +93,24 @@ func (g *GitLab) Status() map[string]any {
const settingGitLabUsername string = "gitlab_username"
// Connect validates the PAT against /api/v4/user and stores it.
// Connect validates the token against /api/v1/user and stores it.
func (g *GitLab) Connect(ctx context.Context, token string) (string, error) {
var user struct {
Username string `json:"username"`
Login string `json:"login"`
}
if err := g.do(ctx, "/api/v4/user", token, &user); err != nil {
if err := g.do(ctx, "/api/v1/user", token, &user); err != nil {
return "", err
}
if user.Username == "" {
return "", &GitLabError{msg: "gitlab returned no username for token"}
if user.Login == "" {
return "", &GitLabError{msg: "gitea returned no login for token"}
}
if err := g.store.SetSetting(settingGitLabToken, token); err != nil {
return "", err
}
if err := g.store.SetSetting(settingGitLabUsername, user.Username); err != nil {
if err := g.store.SetSetting(settingGitLabUsername, user.Login); err != nil {
return "", err
}
return user.Username, nil
return user.Login, nil
}
// Disconnect drops the stored PAT.
@@ -138,22 +139,22 @@ func (g *GitLab) Repos(ctx context.Context) ([]GitLabRepo, error) {
if err != nil {
return nil, err
}
var projects []gitlabProject
path := fmt.Sprintf("/api/v4/projects?membership=true&order_by=last_activity_at&per_page=%d", projectsPerPage)
var projects []giteaRepo
path := fmt.Sprintf("/api/v1/user/repos?limit=%d", projectsPerPage)
if err := g.do(ctx, path, token, &projects); err != nil {
return nil, err
}
repos := make([]GitLabRepo, 0, len(projects))
for _, p := range projects {
if p.PathWithNamespace == "" {
if p.FullName == "" {
continue
}
repos = append(repos, GitLabRepo{
Path: p.PathWithNamespace,
Path: p.FullName,
Name: p.Name,
Namespace: p.Namespace.Path,
LastActivityAt: p.LastActivityAt,
WebURL: p.WebURL,
Namespace: p.Owner.Login,
LastActivityAt: p.UpdatedAt,
WebURL: p.HTMLURL,
DefaultBranch: p.DefaultBranch,
})
}
@@ -168,7 +169,7 @@ func (g *GitLab) do(ctx context.Context, path, token string, out any) error {
if err != nil {
return err
}
req.Header.Set("Private-Token", token)
req.Header.Set("Authorization", "token "+token)
resp, err := g.httpClient().Do(req)
if err != nil {
return &GitLabError{msg: fmt.Sprintf("gitlab request failed: %v", err)}
+3 -3
View File
@@ -43,7 +43,7 @@ func TestGitLabConnectDecodeAndEmptyUser(t *testing.T) {
want string
}{
{"decode-failure", "not json", "decode"},
{"empty-username", `{}`, "no username"},
{"empty-login", `{}`, "no login"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -102,7 +102,7 @@ func TestGitLabStatusStoreFailure(t *testing.T) {
func TestGitLabConnectStoreFailures(t *testing.T) {
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"username":"alice"}`))
_, _ = w.Write([]byte(`{"login":"alice"}`))
}))
t.Cleanup(up.Close)
dead := openTestStore(t)
@@ -115,7 +115,7 @@ func TestGitLabConnectStoreFailures(t *testing.T) {
func TestGitLabReposSkipsEmptyPaths(t *testing.T) {
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`[{"path_with_namespace":"","name":"X"}]`))
_, _ = w.Write([]byte(`[{"full_name":"","name":"X"}]`))
}))
t.Cleanup(up.Close)
store := openTestStore(t)
+11 -12
View File
@@ -1,6 +1,6 @@
package main
// gitlab_test.go — PAT connect + project mapping against httptest upstream.
// gitlab_test.go — token connect + repo mapping against httptest upstream (Gitea v1).
import (
"context"
@@ -14,30 +14,29 @@ import (
func newGitLabUpstream(t *testing.T) (*httptest.Server, *GitLab) {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("/api/v4/user", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Private-Token") != "pat-good" {
mux.HandleFunc("/api/v1/user", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "token pat-good" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"username":"alice"}`))
_, _ = w.Write([]byte(`{"login":"alice"}`))
})
mux.HandleFunc("/api/v4/projects", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Private-Token") != "pat-good" {
mux.HandleFunc("/api/v1/user/repos", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "token pat-good" {
w.WriteHeader(http.StatusUnauthorized)
return
}
q := r.URL.Query()
if q.Get("membership") != "true" || q.Get("order_by") != "last_activity_at" {
if r.URL.Query().Get("limit") == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[
{"path_with_namespace":"team/b","name":"B","namespace":{"path":"team"},
"last_activity_at":"2024-02-02T00:00:00Z","web_url":"https://gl/team/b","default_branch":"main"},
{"path_with_namespace":"team/a","name":"A","namespace":{"path":"team"},
"last_activity_at":"2024-03-03T00:00:00Z","web_url":"https://gl/team/a","default_branch":"trunk"}
{"full_name":"team/b","name":"B","owner":{"login":"team"},
"updated_at":"2024-02-02T00:00:00Z","html_url":"https://gl/team/b","default_branch":"main"},
{"full_name":"team/a","name":"A","owner":{"login":"team"},
"updated_at":"2024-03-03T00:00:00Z","html_url":"https://gl/team/a","default_branch":"trunk"}
]`))
})
ts := httptest.NewServer(mux)
+1 -1
View File
@@ -271,7 +271,7 @@ func TestSpawnerCloneURLInjectsPAT(t *testing.T) {
if err != nil {
t.Fatalf("cloneURL: %v", err)
}
if u != "https://oauth2:pat-1@gitlab.example/group/project.git" {
if u != "https://pat-1@gitlab.example/group/project.git" {
t.Fatalf("cloneURL with PAT = %q", u)
}
}