This commit is contained in:
2025-11-01 20:53:38 +01:00
parent aa8851aca0
commit 6e4452ddb9
12 changed files with 457 additions and 106 deletions

View File

@@ -37,6 +37,7 @@ func NewServer(data *storage.Repository, scheduler *cronruntime.Scheduler, port
router.MethodNotAllowed(func(writer http.ResponseWriter, request *http.Request) {
methodNotAllowed(writer, request)
})
chi.RegisterMethod("EXECUTE")
router.Use(middleware.Logger)
router.Use(recoverMiddleware)
router.Use(middleware.GetHead)
@@ -46,11 +47,12 @@ func NewServer(data *storage.Repository, scheduler *cronruntime.Scheduler, port
routerAPI.Route("/v1", func(r chi.Router) {
// Get information about the server
r.Get("/version", s.Information)
r.MethodFunc("EXECUTE", "/run", s.RunProjectHandler)
r.Route("/projects", func(r chi.Router) {
r.Get("/all", s.ProjectsHandler)
r.Get("/{name}", func(w http.ResponseWriter, r *http.Request) {})
r.Post("/{name}", s.ProjectPostHandler)
r.Delete("/{name}", func(w http.ResponseWriter, r *http.Request) {})
r.Get("/all", s.ProjectsGetHandler)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {})
r.Post("/", s.ProjectPostHandler)
r.Delete("/", s.ProjectDeleteHandler)
})
})
})
@@ -73,13 +75,6 @@ func (s *HTTPServer) Information(w http.ResponseWriter, r *http.Request) {
}
func (s *HTTPServer) ProjectPostHandler(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
if len(name) == 0 {
badRequest("project name cannot be empty", w, r)
return
}
var pr project.Project
d := json.NewDecoder(r.Body)
if err := d.Decode(&pr); err != nil {
@@ -104,7 +99,26 @@ func (s *HTTPServer) ProjectPostHandler(w http.ResponseWriter, r *http.Request)
w.WriteHeader(201)
}
func (s *HTTPServer) ProjectsHandler(w http.ResponseWriter, r *http.Request) {
func (s *HTTPServer) ProjectDeleteHandler(w http.ResponseWriter, r *http.Request) {
var pr project.Project
d := json.NewDecoder(r.Body)
if err := d.Decode(&pr); err != nil {
slog.Error("failed to parse project description", "err", err)
internalServerError(err, w, r)
return
}
s.scheduler.Remove(pr)
if err := s.data.Remove(pr); err != nil {
slog.Error("failed to remove project", "err", err, "uuid", pr.UUID)
internalServerError(err, w, r)
return
}
w.WriteHeader(204)
}
func (s *HTTPServer) ProjectsGetHandler(w http.ResponseWriter, r *http.Request) {
prs, err := s.data.List()
if err != nil {
slog.Error("failed to fetch all the projects from the database", "err", err)
@@ -114,3 +128,21 @@ func (s *HTTPServer) ProjectsHandler(w http.ResponseWriter, r *http.Request) {
ok(prs, w, r)
}
func (s *HTTPServer) RunProjectHandler(w http.ResponseWriter, r *http.Request) {
var pr project.Project
d := json.NewDecoder(r.Body)
if err := d.Decode(&pr); err != nil {
slog.Error("failed to parse project description", "err", err)
internalServerError(err, w, r)
return
}
if err := s.scheduler.RunOnce(pr); err != nil {
slog.Error("failed to run the project", "err", err)
internalServerError(err, w, r)
return
}
ok("ok", w, r)
}

View File

@@ -34,26 +34,10 @@ func New(prs []project.Project) (*Scheduler, error) {
func (s *Scheduler) Add(pr project.Project) error {
s.ids[pr.Name] = make(map[string]cron.EntryID)
for _, repo := range pr.Repositories {
var srcAuth git.Authentication = git.NoAuthentication{}
var dstAuth git.Authentication = git.NoAuthentication{}
if v, ok := repo.Authentications["source"]; ok {
if len(v.Token) > 0 {
srcAuth = git.NewTokenAuthentication(v.Token)
} else if v.Basic != nil {
srcAuth = git.NewBasicAuthentication(v.Basic.Username, v.Basic.Password)
}
}
if v, ok := repo.Authentications["mirror"]; ok {
if len(v.Token) > 0 {
dstAuth = git.NewTokenAuthentication(v.Token)
} else if v.Basic != nil {
dstAuth = git.NewBasicAuthentication(v.Basic.Username, v.Basic.Password)
}
}
r := git.NewRepository(repo.Source, repo.Destination, srcAuth, dstAuth)
gr := s.prepare(repo)
id, err := s.cr.AddFunc(repo.Schedule, func() {
slog.Info(fmt.Sprintf("[%s] starting sync...", repo.Name))
if err := git.Sync(r); err != nil {
if err := git.Sync(gr); err != nil {
slog.Error(fmt.Sprintf("[%s] failed to sync repository: %s", repo.Name, err))
return
}
@@ -80,6 +64,39 @@ func (s *Scheduler) Remove(pr project.Project) {
delete(s.ids, pr.Name)
}
func (s *Scheduler) RunOnce(pr project.Project) error {
for _, repo := range pr.Repositories {
gr := s.prepare(repo)
slog.Info(fmt.Sprintf("[%s] starting sync...", repo.Name))
if err := git.Sync(gr); err != nil {
slog.Error(fmt.Sprintf("[%s] failed to sync repository: %s", repo.Name, err))
continue
}
slog.Info(fmt.Sprintf("[%s] synced", repo.Name))
}
return nil
}
func (s *Scheduler) prepare(repo project.Repository) git.Repository {
var srcAuth git.Authentication = git.NoAuthentication{}
var dstAuth git.Authentication = git.NoAuthentication{}
if v, ok := repo.Authentications["source"]; ok {
if len(v.Token) > 0 {
srcAuth = git.NewTokenAuthentication(v.Token)
} else if v.Basic != nil {
srcAuth = git.NewBasicAuthentication(v.Basic.Username, v.Basic.Password)
}
}
if v, ok := repo.Authentications["mirror"]; ok {
if len(v.Token) > 0 {
dstAuth = git.NewTokenAuthentication(v.Token)
} else if v.Basic != nil {
dstAuth = git.NewBasicAuthentication(v.Basic.Username, v.Basic.Password)
}
}
return git.NewRepository(repo.Source, repo.Destination, srcAuth, dstAuth)
}
// Run the cron scheduler, or no-op if already running.
func (s *Scheduler) Run() {
s.cr.Run()

View File

@@ -279,6 +279,43 @@ func (r *Repository) updateRepository(tx *sql.Tx, repo project.Repository) error
return nil
}
func (r *Repository) Remove(pr project.Project) error {
tx, err := r.db.Begin()
if err != nil {
return fmt.Errorf("failed to create transaction: %s", err)
}
defer func() {
if err != nil {
tx.Rollback()
return
}
tx.Commit()
}()
uuid, err := r.ProjectUUID(pr.Name)
repos, err := r.listRepositories(uuid)
if err != nil {
return err
}
for _, repo := range repos {
if _, err := tx.Exec("DELETE FROM Authentication WHERE repository = ?", repo.UUID); err != nil {
return fmt.Errorf("failed to delete the authentication entries from the database: %s", err)
}
}
if _, err := tx.Exec("DELETE FROM Repositories WHERE project = ?", uuid); err != nil {
return fmt.Errorf("failed to delete the repositories from the database: %s", err)
}
if _, err := tx.Exec("DELETE FROM Projects WHERE uuid = ?", uuid); err != nil {
return fmt.Errorf("failed to delete the project from the database: %s", err)
}
return nil
}
func (r *Repository) List() ([]project.Project, error) {
var prs []project.Project
@@ -288,72 +325,88 @@ func (r *Repository) List() ([]project.Project, error) {
}
defer rows.Close()
stmt, err := r.db.Prepare("SELECT uuid, name, schedule, source, destination FROM Repositories WHERE project = ?")
if err != nil {
return nil, fmt.Errorf("invalid syntax: %w", err)
}
authStmt, err := r.db.Prepare("SELECT ref, username, password, token FROM Authentication WHERE repository = ?")
if err != nil {
return nil, fmt.Errorf("invalid syntax: %w", err)
}
for rows.Next() {
var pr project.Project
var prUUID string
if err := rows.Scan(&prUUID, &pr.Name); err != nil {
if err := rows.Scan(&pr.UUID, &pr.Name); err != nil {
return nil, fmt.Errorf("failed to scan project name: %w", err)
}
repoRows, err := stmt.Query(prUUID)
repos, err := r.listRepositories(pr.UUID)
if err != nil {
return nil, fmt.Errorf("failed to query repositories for the project %s: %w", prUUID, err)
return nil, err
}
for repoRows.Next() {
var uuid string
var repo project.Repository
if err := repoRows.Scan(&uuid, &repo.Name, &repo.Schedule, &repo.Source, &repo.Destination); err != nil {
repoRows.Close()
return nil, fmt.Errorf("failed to scan repository entry: %w", err)
}
pr.Repositories = repos
authRows, err := authStmt.Query(uuid)
if err != nil {
repoRows.Close()
return nil, fmt.Errorf("failed to query repositories for the project %s: %w", prUUID, err)
}
auth := make(map[string]project.AuthenticationSettings)
for authRows.Next() {
var ref string
var username, password, token *string
if err := authRows.Scan(&ref, &username, &password, &token); err != nil {
authRows.Close()
repoRows.Close()
return nil, fmt.Errorf("failed to scan authentication entry: %s", err)
}
if token != nil {
auth[ref] = project.AuthenticationSettings{
Token: *token,
}
} else if username != nil {
auth[ref] = project.AuthenticationSettings{
Basic: &project.BasicAuthenticationSettings{
Username: *username,
Password: *password,
},
}
}
}
authRows.Close()
repo.Authentications = auth
pr.Repositories = append(pr.Repositories, repo)
}
repoRows.Close()
prs = append(prs, pr)
}
return prs, nil
}
func (r *Repository) listRepositories(projectUUID string) ([]project.Repository, error) {
stmt, err := r.db.Prepare("SELECT uuid, name, schedule, source, destination FROM Repositories WHERE project = ?")
if err != nil {
return nil, fmt.Errorf("invalid syntax: %w", err)
}
rows, err := stmt.Query(projectUUID)
if err != nil {
return nil, fmt.Errorf("failed to query repositories for the project %s: %w", projectUUID, err)
}
defer rows.Close()
var repositories []project.Repository
for rows.Next() {
var repo project.Repository
if err := rows.Scan(&repo.UUID, &repo.Name, &repo.Schedule, &repo.Source, &repo.Destination); err != nil {
return nil, fmt.Errorf("failed to scan repository entry: %w", err)
}
auth, err := r.listAuthentications(repo.UUID)
if err != nil {
return nil, err
}
repo.Authentications = auth
repositories = append(repositories, repo)
}
return repositories, nil
}
func (r *Repository) listAuthentications(repositoryUUID string) (map[string]project.AuthenticationSettings, error) {
stmt, err := r.db.Prepare("SELECT ref, username, password, token FROM Authentication WHERE repository = ?")
if err != nil {
return nil, fmt.Errorf("invalid syntax: %w", err)
}
rows, err := stmt.Query(repositoryUUID)
if err != nil {
return nil, fmt.Errorf("failed to query repositories for the project %s: %w", repositoryUUID, err)
}
defer rows.Close()
res := make(map[string]project.AuthenticationSettings)
for rows.Next() {
var ref string
var username, password, token *string
if err := rows.Scan(&ref, &username, &password, &token); err != nil {
return nil, fmt.Errorf("failed to scan authentication entry: %s", err)
}
if token != nil {
res[ref] = project.AuthenticationSettings{
Token: *token,
}
} else if username != nil {
res[ref] = project.AuthenticationSettings{
Basic: &project.BasicAuthenticationSettings{
Username: *username,
Password: *password,
},
}
}
}
return res, nil
}