diff --git a/alr-updater.example.toml b/alr-updater.example.toml index 361ca31..91b2209 100644 --- a/alr-updater.example.toml +++ b/alr-updater.example.toml @@ -1,15 +1,28 @@ -[git] +# Базовый каталог для хранения всех репозиториев +reposBaseDir = "/var/cache/alr-updater" + +# Конфигурация репозиториев (можно настроить несколько) +[repositories.alr-repo] repoURL = "https://gitea.plemya-x.ru/Plemya-x/alr-repo.git" - repoDir = "/etc/alr-updater/repo" - [git.commit] + [repositories.alr-repo.commit] # Имя и адрес электронной почты, которые будут использоваться при фиксации git name = "CHANGE ME" email = "CHANGE ME" - [git.credentials] + [repositories.alr-repo.credentials] # Имя пользователя и пароль для git push. Используйте личный токен доступа в качестве пароля для Gitea. username = "CHANGE ME" password = "CHANGE ME" +# Можно добавить дополнительные репозитории +# [repositories.another-repo] +# repoURL = "https://github.com/example/another-repo.git" +# [repositories.another-repo.commit] +# name = "Bot User" +# email = "bot@example.com" +# [repositories.another-repo.credentials] +# username = "bot-user" +# password = "github-token" + [webhook] # Хэш пароля для webhook. Сгенерируйте его, используя `alr-updater -g`. pwd_hash = "CHANGE ME" \ No newline at end of file diff --git a/internal/builtins/updater.go b/internal/builtins/updater.go index 449b79a..fdc938c 100644 --- a/internal/builtins/updater.go +++ b/internal/builtins/updater.go @@ -41,7 +41,7 @@ func updaterModule(cfg *config.Config) *starlarkstruct.Module { return &starlarkstruct.Module{ Name: "updater", Members: starlark.StringDict{ - "repo_dir": starlark.String(cfg.Git.RepoDir), + "repos_base_dir": starlark.String(cfg.ReposBaseDir), "pull": updaterPull(cfg), "push_changes": updaterPushChanges(cfg), "get_package_file": getPackageFile(cfg), @@ -57,10 +57,22 @@ var repoMtx = &sync.Mutex{} func updaterPull(cfg *config.Config) *starlark.Builtin { return starlark.NewBuiltin("updater.pull", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var repoName string + err := starlark.UnpackArgs("updater.pull", args, kwargs, "repo", &repoName) + if err != nil { + return nil, err + } + + repoConfig, exists := cfg.Repositories[repoName] + if !exists { + return nil, fmt.Errorf("repository '%s' not found in configuration", repoName) + } + repoMtx.Lock() defer repoMtx.Unlock() - repo, err := git.PlainOpen(cfg.Git.RepoDir) + repoDir := filepath.Join(cfg.ReposBaseDir, repoName) + repo, err := git.PlainOpen(repoDir) if err != nil { return nil, err } @@ -75,22 +87,29 @@ func updaterPull(cfg *config.Config) *starlark.Builtin { return nil, err } + _ = repoConfig // Избегаем неиспользованной переменной return starlark.None, nil }) } func updaterPushChanges(cfg *config.Config) *starlark.Builtin { return starlark.NewBuiltin("updater.push_changes", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { - var msg string - err := starlark.UnpackArgs("updater.push_changes", args, kwargs, "msg", &msg) + var repoName, msg string + err := starlark.UnpackArgs("updater.push_changes", args, kwargs, "repo", &repoName, "msg", &msg) if err != nil { return nil, err } + repoConfig, exists := cfg.Repositories[repoName] + if !exists { + return nil, fmt.Errorf("repository '%s' not found in configuration", repoName) + } + repoMtx.Lock() defer repoMtx.Unlock() - repo, err := git.PlainOpen(cfg.Git.RepoDir) + repoDir := filepath.Join(cfg.ReposBaseDir, repoName) + repo, err := git.PlainOpen(repoDir) if err != nil { return nil, err } @@ -120,8 +139,8 @@ func updaterPushChanges(cfg *config.Config) *starlark.Builtin { } sig := &object.Signature{ - Name: cfg.Git.Commit.Name, - Email: cfg.Git.Commit.Email, + Name: repoConfig.Commit.Name, + Email: repoConfig.Commit.Email, When: time.Now(), } @@ -138,15 +157,15 @@ func updaterPushChanges(cfg *config.Config) *starlark.Builtin { err = repo.Push(&git.PushOptions{ Progress: os.Stderr, Auth: &http.BasicAuth{ - Username: cfg.Git.Credentials.Username, - Password: cfg.Git.Credentials.Password, + Username: repoConfig.Credentials.Username, + Password: repoConfig.Credentials.Password, }, }) if err != nil { return nil, err } - log.Debug("Successfully pushed to repo").Stringer("pos", thread.CallFrame(1).Pos).Send() + log.Debug("Successfully pushed to repo").Str("repo", repoName).Stringer("pos", thread.CallFrame(1).Pos).Send() return starlark.None, nil }) @@ -154,38 +173,50 @@ func updaterPushChanges(cfg *config.Config) *starlark.Builtin { func getPackageFile(cfg *config.Config) *starlark.Builtin { return starlark.NewBuiltin("updater.get_package_file", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { - var pkg, filename string - err := starlark.UnpackArgs("updater.get_package_file", args, kwargs, "pkg", &pkg, "filename", &filename) + var repoName, pkg, filename string + err := starlark.UnpackArgs("updater.get_package_file", args, kwargs, "repo", &repoName, "pkg", &pkg, "filename", &filename) if err != nil { return nil, err } + _, exists := cfg.Repositories[repoName] + if !exists { + return nil, fmt.Errorf("repository '%s' not found in configuration", repoName) + } + repoMtx.Lock() defer repoMtx.Unlock() - path := filepath.Join(cfg.Git.RepoDir, pkg, filename) + repoDir := filepath.Join(cfg.ReposBaseDir, repoName) + path := filepath.Join(repoDir, pkg, filename) data, err := os.ReadFile(path) if err != nil { return nil, err } - log.Debug("Got package file").Str("package", pkg).Str("filename", filename).Stringer("pos", thread.CallFrame(1).Pos).Send() + log.Debug("Got package file").Str("repo", repoName).Str("package", pkg).Str("filename", filename).Stringer("pos", thread.CallFrame(1).Pos).Send() return starlark.String(data), nil }) } func writePackageFile(cfg *config.Config) *starlark.Builtin { return starlark.NewBuiltin("updater.write_package_file", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { - var pkg, filename, content string - err := starlark.UnpackArgs("updater.write_package_file", args, kwargs, "pkg", &pkg, "filename", &filename, "content", &content) + var repoName, pkg, filename, content string + err := starlark.UnpackArgs("updater.write_package_file", args, kwargs, "repo", &repoName, "pkg", &pkg, "filename", &filename, "content", &content) if err != nil { return nil, err } + _, exists := cfg.Repositories[repoName] + if !exists { + return nil, fmt.Errorf("repository '%s' not found in configuration", repoName) + } + repoMtx.Lock() defer repoMtx.Unlock() - path := filepath.Join(cfg.Git.RepoDir, pkg, filename) + repoDir := filepath.Join(cfg.ReposBaseDir, repoName) + path := filepath.Join(repoDir, pkg, filename) // Читаем старый файл для сравнения версий var oldContent string @@ -200,13 +231,14 @@ func writePackageFile(cfg *config.Config) *starlark.Builtin { if err != nil { return nil, err } + defer fl.Close() _, err = io.Copy(fl, strings.NewReader(finalContent)) if err != nil { return nil, err } - log.Debug("Wrote package file").Str("package", pkg).Str("filename", filename).Stringer("pos", thread.CallFrame(1).Pos).Send() + log.Debug("Wrote package file").Str("repo", repoName).Str("package", pkg).Str("filename", filename).Stringer("pos", thread.CallFrame(1).Pos).Send() return starlark.None, nil }) } @@ -268,9 +300,10 @@ func updateChecksumsInContent(content string, checksums []string) string { func updateChecksums(cfg *config.Config) *starlark.Builtin { return starlark.NewBuiltin("updater.update_checksums", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { - var pkg, filename, content string + var repoName, pkg, filename, content string var checksums *starlark.List err := starlark.UnpackArgs("updater.update_checksums", args, kwargs, + "repo", &repoName, "pkg", &pkg, "filename", &filename, "content", &content, @@ -279,6 +312,11 @@ func updateChecksums(cfg *config.Config) *starlark.Builtin { return nil, err } + _, exists := cfg.Repositories[repoName] + if !exists { + return nil, fmt.Errorf("repository '%s' not found in configuration", repoName) + } + // Конвертируем starlark.List в []string checksumStrings := make([]string, 0, checksums.Len()) iter := checksums.Iterate() @@ -297,7 +335,8 @@ func updateChecksums(cfg *config.Config) *starlark.Builtin { repoMtx.Lock() defer repoMtx.Unlock() - path := filepath.Join(cfg.Git.RepoDir, pkg, filename) + repoDir := filepath.Join(cfg.ReposBaseDir, repoName) + path := filepath.Join(repoDir, pkg, filename) // Читаем старый файл для сравнения версий var oldContent string @@ -321,6 +360,7 @@ func updateChecksums(cfg *config.Config) *starlark.Builtin { } log.Debug("Updated package file with checksums"). + Str("repo", repoName). Str("package", pkg). Str("filename", filename). Int("checksums_count", len(checksumStrings)). diff --git a/internal/config/config.go b/internal/config/config.go index 430e250..0b52cab 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,15 +19,15 @@ package config type Config struct { - Git Git `toml:"git" envPrefix:"GIT_"` - Webhook Webhook `toml:"webhook" envPrefix:"WEBHOOK_"` + ReposBaseDir string `toml:"reposBaseDir" env:"REPOS_BASE_DIR"` + Repositories map[string]GitRepo `toml:"repositories"` + Webhook Webhook `toml:"webhook" envPrefix:"WEBHOOK_"` } -type Git struct { - RepoDir string `toml:"repoDir" env:"REPO_DIR"` - RepoURL string `toml:"repoURL" env:"REPO_URL"` - Commit Commit `toml:"commit" envPrefix:"COMMIT_"` - Credentials Credentials `toml:"credentials" envPrefix:"CREDENTIALS_"` +type GitRepo struct { + RepoURL string `toml:"repoURL"` + Commit Commit `toml:"commit"` + Credentials Credentials `toml:"credentials"` } type Credentials struct { diff --git a/main.go b/main.go index 4b23d12..aab201c 100644 --- a/main.go +++ b/main.go @@ -19,10 +19,12 @@ package main import ( + "bufio" "fmt" "net/http" "os" "path/filepath" + "regexp" "strings" "sync" @@ -44,9 +46,39 @@ func init() { log.Logger = logger.NewPretty(os.Stderr) } +// parseRepositoryFromPlugin парсит комментарий '# Repository: repo-name' из .star файла +func parseRepositoryFromPlugin(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + repoRegex := regexp.MustCompile(`^#\s*Repository:\s*(.+?)\s*$`) + + // Читаем первые несколько строк файла + lineCount := 0 + for scanner.Scan() && lineCount < 10 { + line := strings.TrimSpace(scanner.Text()) + lineCount++ + + if matches := repoRegex.FindStringSubmatch(line); matches != nil { + return strings.TrimSpace(matches[1]), nil + } + + // Если встретили строку без комментария, прекращаем поиск + if line != "" && !strings.HasPrefix(line, "#") { + break + } + } + + return "", scanner.Err() +} + func main() { configPath := pflag.StringP("config", "c", "/etc/alr-updater/config.toml", "Path to config file") - dbPath := pflag.StringP("database", "d", "/etc/alr-updater/db", "Path to database file") + dbPath := pflag.StringP("database", "d", "/var/lib/alr-updater/db", "Path to database file") pluginDir := pflag.StringP("plugin-dir", "p", "/etc/alr-updater/plugins", "Path to plugin directory") serverAddr := pflag.StringP("address", "a", ":8080", "Webhook server address") genHash := pflag.BoolP("gen-hash", "g", false, "Generate a password hash for webhooks") @@ -99,21 +131,51 @@ func main() { } } - if _, err := os.Stat(cfg.Git.RepoDir); os.IsNotExist(err) { - err = os.MkdirAll(cfg.Git.RepoDir, 0o755) + // Создаем базовый каталог для репозиториев + if cfg.ReposBaseDir == "" { + cfg.ReposBaseDir = "/var/cache/alr-updater" + } + + if _, err := os.Stat(cfg.ReposBaseDir); os.IsNotExist(err) { + err = os.MkdirAll(cfg.ReposBaseDir, 0o755) if err != nil { - log.Fatal("Error creating repository directory").Err(err).Send() - } - - _, err := git.PlainClone(cfg.Git.RepoDir, false, &git.CloneOptions{ - URL: cfg.Git.RepoURL, - Progress: os.Stderr, - }) - if err != nil { - log.Fatal("Error cloning repository").Err(err).Send() + log.Fatal("Error creating repositories base directory").Err(err).Send() } } else if err != nil { - log.Fatal("Cannot stat configured repo directory").Err(err).Send() + log.Fatal("Cannot stat configured repos base directory").Err(err).Send() + } + + // Клонируем все сконфигурированные репозитории + for repoName, repoConfig := range cfg.Repositories { + repoDir := filepath.Join(cfg.ReposBaseDir, repoName) + + if _, err := os.Stat(repoDir); os.IsNotExist(err) { + log.Info("Cloning repository").Str("name", repoName).Str("url", repoConfig.RepoURL).Send() + + err = os.MkdirAll(repoDir, 0o755) + if err != nil { + log.Fatal("Error creating repository directory").Str("repo", repoName).Err(err).Send() + } + + _, err := git.PlainClone(repoDir, false, &git.CloneOptions{ + URL: repoConfig.RepoURL, + Progress: os.Stderr, + }) + if err != nil { + log.Fatal("Error cloning repository").Str("repo", repoName).Err(err).Send() + } + + log.Info("Repository cloned successfully").Str("name", repoName).Send() + } else if err != nil { + log.Fatal("Cannot stat repository directory").Str("repo", repoName).Err(err).Send() + } else { + log.Info("Repository already exists").Str("name", repoName).Send() + } + } + + // Проверяем, что есть хотя бы один репозиторий + if len(cfg.Repositories) == 0 { + log.Fatal("No repositories configured. At least one repository is required.").Send() } starFiles, err := filepath.Glob(filepath.Join(*pluginDir, "*.star")) @@ -129,9 +191,26 @@ func main() { for _, starFile := range starFiles { pluginName := filepath.Base(strings.TrimSuffix(starFile, ".star")) + + // Парсим комментарий Repository из файла плагина + repoName, err := parseRepositoryFromPlugin(starFile) + if err != nil { + log.Fatal("Error parsing repository from plugin").Str("file", starFile).Err(err).Send() + } + if repoName == "" { + log.Fatal("Plugin must specify repository").Str("file", starFile).Msg("Add '# Repository: repo-name' comment at the top") + } + + // Проверяем, что указанный репозиторий существует в конфигурации + if _, exists := cfg.Repositories[repoName]; !exists { + log.Fatal("Repository not found in configuration").Str("plugin", pluginName).Str("repository", repoName).Send() + } + thread := &starlark.Thread{Name: pluginName} - predeclared := starlark.StringDict{} + predeclared := starlark.StringDict{ + "REPO": starlark.String(repoName), // Передаем имя репозитория как глобальную переменную + } builtins.Register(predeclared, &builtins.Options{ Name: pluginName, Config: cfg, @@ -145,7 +224,7 @@ func main() { log.Fatal("Error executing starlark file").Str("file", starFile).Err(err).Send() } - log.Info("Initialized plugin").Str("name", pluginName).Send() + log.Info("Initialized plugin").Str("name", pluginName).Str("repository", repoName).Send() } // Запускаем все зарегистрированные функции немедленно если установлен флаг --now