This commit is contained in:
@@ -5,6 +5,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CommonRegistryClient struct {
|
||||
@@ -15,13 +18,71 @@ type CommonRegistryClient struct {
|
||||
// NewCommonRegistryClient creates a new CommonRegistryClient for the specified registry host.
|
||||
func NewCommonRegistryClient(host string) *CommonRegistryClient {
|
||||
return &CommonRegistryClient{
|
||||
Client: &http.Client{},
|
||||
Host: host,
|
||||
Client: &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
},
|
||||
Host: host,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTags fetches the list of tags for a given image from the registry.
|
||||
// GetTags retrieves a list of tags for a given image from the registry.
|
||||
// It first tries the standard Docker Registry API, and if that fails,
|
||||
// it falls back to a Gitea-like Web API.
|
||||
func (c *CommonRegistryClient) GetTags(image string) ([]string, error) {
|
||||
tags, err := c.getTagsFromDockerAPI(image)
|
||||
if err != nil {
|
||||
logrus.Warnf("Standard Docker API failed for %s (error: %v). Trying Web API fallback...", c.Host, err)
|
||||
|
||||
// Try the Web API fallback
|
||||
fallbackTags, fallbackErr := c.getTagsFromWebAPI(image)
|
||||
if fallbackErr != nil {
|
||||
logrus.Errorf("Web API fallback also failed for %s (error: %v).", c.Host, fallbackErr)
|
||||
// Возвращаем оригинальную, более информативную ошибку от Docker API.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logrus.Infof("Successfully fetched tags for %s using Web API fallback.", c.Host)
|
||||
return fallbackTags, nil
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// getTagsFromWebAPI fetches tags using a Gitea-like Web API endpoint.
|
||||
func (c *CommonRegistryClient) getTagsFromWebAPI(image string) ([]string, error) {
|
||||
url := fmt.Sprintf("https://%s/%s/tags/list", c.Host, image)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("web api request for %s failed with status %d: %s", c.Host, resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Gitea uses "results", some others might use "tags". Let's try to be flexible.
|
||||
var result struct {
|
||||
Tags []string `json:"tags"`
|
||||
Results []string `json:"results"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Results) > 0 {
|
||||
return result.Results, nil
|
||||
}
|
||||
return result.Tags, nil
|
||||
}
|
||||
|
||||
// getTagsFromDockerAPI fetches tags using the standard Docker Registry v2 API.
|
||||
func (c *CommonRegistryClient) getTagsFromDockerAPI(image string) ([]string, error) {
|
||||
url := fmt.Sprintf("https://%s/v2/%s/tags/list", c.Host, image)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
@@ -34,7 +95,7 @@ func (c *CommonRegistryClient) GetTags(image string) ([]string, error) {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("registry %s request failed: %s", c.Host, string(body))
|
||||
return nil, fmt.Errorf("registry %s request failed with status %d: %s", c.Host, resp.StatusCode, string(body))
|
||||
}
|
||||
var result struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -36,11 +36,6 @@ type UpdateInfo struct {
|
||||
NewTag string
|
||||
}
|
||||
|
||||
var commonClients = map[string]*registry.CommonRegistryClient{
|
||||
"docker.gitea.com": registry.NewCommonRegistryClient("docker.gitea.com"),
|
||||
"quay.io": registry.NewCommonRegistryClient("quay.io"),
|
||||
}
|
||||
|
||||
// updateCache stores information about found updates.
|
||||
var updateCache = make(map[string]UpdateInfo)
|
||||
var cacheMutex sync.Mutex
|
||||
@@ -90,11 +85,10 @@ func handleCallbackQuery(bot *tgbotapi.BotAPI, callback *tgbotapi.CallbackQuery,
|
||||
|
||||
logrus.Infof("Received update request for image: %s, tag: %s", fullImageName, newTag)
|
||||
|
||||
// --- NEW: Remove from sentNotifications when acted upon ---
|
||||
// Remove from sentNotifications when acted upon ---
|
||||
sentNotificationsMutex.Lock()
|
||||
delete(sentNotifications, fullImageName+":"+newTag)
|
||||
sentNotificationsMutex.Unlock()
|
||||
// --- End NEW ---
|
||||
|
||||
// Looking for servies to update
|
||||
repoPath := appConfig.Git.LocalRepoPath
|
||||
@@ -155,11 +149,10 @@ func handleCallbackQuery(bot *tgbotapi.BotAPI, callback *tgbotapi.CallbackQuery,
|
||||
|
||||
logrus.Infof("Received ignore request for image: %s, version: %s", fullImageName, tagToIgnore)
|
||||
|
||||
// --- NEW: Remove from sentNotifications when acted upon ---
|
||||
// Remove from sentNotifications when acted upon ---
|
||||
sentNotificationsMutex.Lock()
|
||||
delete(sentNotifications, fullImageName+":"+tagToIgnore)
|
||||
sentNotificationsMutex.Unlock()
|
||||
// --- End NEW ---
|
||||
|
||||
err := sl.Add(fullImageName, tagToIgnore)
|
||||
if err != nil {
|
||||
@@ -168,7 +161,7 @@ func handleCallbackQuery(bot *tgbotapi.BotAPI, callback *tgbotapi.CallbackQuery,
|
||||
logrus.Infof("Update for %s with tag %s has been added to the skip list.", fullImageName, tagToIgnore)
|
||||
}
|
||||
}
|
||||
// --- MODIFICATION: Clear ALL related notifications/warnings for this image ---
|
||||
// Clear ALL related notifications/warnings for this image ---
|
||||
var fullImageName string
|
||||
if strings.HasPrefix(callbackData, "update_to:") {
|
||||
fullImageName, _, _ = decodeCallbackData(callbackData, "update_to")
|
||||
@@ -186,7 +179,6 @@ func handleCallbackQuery(bot *tgbotapi.BotAPI, callback *tgbotapi.CallbackQuery,
|
||||
sentNotificationsMutex.Unlock()
|
||||
logrus.Debugf("Cleared all pending notifications/warnings for %s after user action.", fullImageName)
|
||||
}
|
||||
// --- END MODIFICATION ---
|
||||
|
||||
// Send confirmation to user and delete original message
|
||||
deleteConfig := tgbotapi.NewDeleteMessage(callback.Message.Chat.ID, callback.Message.MessageID)
|
||||
@@ -285,8 +277,9 @@ func main() {
|
||||
// Initizle registry clients
|
||||
dockerHubClient := registry.NewDockerHubClient(appConfig.DockerUser, appConfig.DockerPass)
|
||||
ghcrClient := registry.NewGhcrClient()
|
||||
commonClients := make(map[string]*registry.CommonRegistryClient) // Created on-the-fly
|
||||
|
||||
runCheck(ctx, appConfig, bot, sl, dockerHubClient, ghcrClient)
|
||||
runCheck(ctx, appConfig, bot, sl, dockerHubClient, ghcrClient, commonClients)
|
||||
|
||||
// Main loop: periodic checks
|
||||
for {
|
||||
@@ -294,13 +287,13 @@ func main() {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
runCheck(ctx, appConfig, bot, sl, dockerHubClient, ghcrClient)
|
||||
runCheck(ctx, appConfig, bot, sl, dockerHubClient, ghcrClient, commonClients)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runCheck checks all container images for updates and sends notifications if updates are available.
|
||||
func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.BotAPI, sl *skiplist.SkipList, dockerHubClient *registry.DockerHubClient, ghcrClient *registry.GhcrClient) {
|
||||
func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.BotAPI, sl *skiplist.SkipList, dockerHubClient *registry.DockerHubClient, ghcrClient *registry.GhcrClient, commonClients map[string]*registry.CommonRegistryClient) {
|
||||
logrus.Infof("Starting image update check...")
|
||||
|
||||
repoPath := appConfig.Git.LocalRepoPath
|
||||
@@ -322,12 +315,12 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
|
||||
logrus.Infof("Repository cloned or pulled successfully.")
|
||||
|
||||
// Get list of relevant files in the repository
|
||||
relevantFiles, err := checker.FindRelevantFiles(repoPath, appConfig.Checker.WatchFolder) // <-- Передаем watchFolder
|
||||
relevantFiles, err := checker.FindRelevantFiles(repoPath, appConfig.Checker.WatchFolder)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error finding relevant files: %v", err)
|
||||
return
|
||||
}
|
||||
logrus.Infof("Found %d relevant files in watch folder '%s'.", len(relevantFiles), appConfig.Checker.WatchFolder) // <-- Обновляем лог
|
||||
logrus.Infof("Found %d relevant files in watch folder '%s'.", len(relevantFiles), appConfig.Checker.WatchFolder) // Debug log
|
||||
|
||||
// Parse relevant files to extract services and their images
|
||||
services, err := checker.ParseRelevantFiles(relevantFiles)
|
||||
@@ -354,7 +347,7 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
|
||||
currentImageTag := services[0].ImageInfo.Tag
|
||||
logrus.Infof("Parsing image: Registry=%s, Image=%s, Tag=%s", services[0].ImageInfo.Registry, services[0].ImageInfo.Image, currentImageTag)
|
||||
|
||||
// --- MODIFICATION: Clear notifications if image is skipped/ignored ---
|
||||
// Clear notifications if image is skipped/ignored ---
|
||||
if sl.IsSkipped(fullImageName, currentImageTag) || sl.IsImageIgnored(fullImageName) {
|
||||
if sl.IsSkipped(fullImageName, currentImageTag) {
|
||||
logrus.Infof("Skipping update for %s. It's in the skip list.", fullImageName)
|
||||
@@ -370,7 +363,6 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
|
||||
sentNotificationsMutex.Unlock()
|
||||
continue
|
||||
}
|
||||
// --- END MODIFICATION ---
|
||||
|
||||
var tags []string
|
||||
var err error
|
||||
@@ -382,16 +374,14 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
|
||||
case "ghcr.io":
|
||||
tags, err = ghcrClient.GetTags(services[0].ImageInfo.Image)
|
||||
default:
|
||||
// --- MODIFICATION: Use generic client for all other registries ---
|
||||
logrus.Debugf("Using generic registry client for %s", registryName)
|
||||
// Create client on-the-fly ---
|
||||
client, ok := commonClients[registryName]
|
||||
if !ok {
|
||||
// Create a new client on-the-fly if not already created
|
||||
logrus.Debugf("Creating new common client for registry: %s", registryName)
|
||||
client = registry.NewCommonRegistryClient(registryName)
|
||||
commonClients[registryName] = client
|
||||
}
|
||||
tags, err = client.GetTags(services[0].ImageInfo.Image)
|
||||
// --- END MODIFICATION ---
|
||||
}
|
||||
|
||||
logrus.Debugf("Avaliable tags for %s: %v", fullImageName, tags)
|
||||
@@ -425,6 +415,7 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
|
||||
services[0].ImageInfo.Tag,
|
||||
dockerHubClient,
|
||||
ghcrClient,
|
||||
commonClients, // Pass it here
|
||||
)
|
||||
latestDigest, err2 := getDigestForImage(
|
||||
services[0].ImageInfo.Registry,
|
||||
@@ -432,6 +423,7 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
|
||||
"latest",
|
||||
dockerHubClient,
|
||||
ghcrClient,
|
||||
commonClients, // And here
|
||||
)
|
||||
|
||||
logrus.Debugf("Digest fallback for %s: currentDigest=%s (err=%v), latestDigest=%s (err=%v)",
|
||||
@@ -579,7 +571,7 @@ func encodeCallbackData(prefix, fullImageName, tag string) string {
|
||||
}
|
||||
|
||||
// Get image digest for given registry, image and tag
|
||||
func getDigestForImage(registryName, image, tag string, dockerHubClient *registry.DockerHubClient, ghcrClient *registry.GhcrClient) (string, error) {
|
||||
func getDigestForImage(registryName, image, tag string, dockerHubClient *registry.DockerHubClient, ghcrClient *registry.GhcrClient, commonClients map[string]*registry.CommonRegistryClient) (string, error) {
|
||||
switch registryName {
|
||||
case "docker.io":
|
||||
return dockerHubClient.GetDigest(image, tag)
|
||||
|
||||
Reference in New Issue
Block a user