6 Commits

Author SHA1 Message Date
victor.kolomin 8665ea22e1 remove
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-11-30 16:34:14 +01:00
victor.kolomin 93b35eb1d9 add woodpecker CI
ci/woodpecker/tag/woodpecker Pipeline failed
2025-11-30 16:31:45 +01:00
victor.kolomin d0a8c5b3a8 change logic for prepull and file modification
continuous-integration/drone/tag Build is passing
2025-09-19 15:39:50 +02:00
victor.kolomin d3530314c2 add prepull before commit
continuous-integration/drone/tag Build is passing
2025-09-19 15:30:48 +02:00
victor.kolomin 64a96fa4cd migrate from aws to docker.io
continuous-integration/drone/tag Build is passing
2025-09-19 14:19:38 +02:00
victor.kolomin e7e52ca3b5 add WEB API check if Docker API check failed
continuous-integration/drone/tag Build is failing
2025-09-19 14:10:41 +02:00
6 changed files with 137 additions and 52 deletions
+21
View File
@@ -0,0 +1,21 @@
steps:
- name: build and push docker image
image: woodpeckerci/plugin-docker-buildx
settings:
repo: git.kolspace.cc/victor.kolomin/controlla
registry: git.kolspace.cc
platforms: linux/amd64
username:
from_secret: gitea_registry_user
password:
from_secret: gitea_registry_pass
tags:
- ${CI_COMMIT_TAG}
- latest
cache_from:
- git.kolspace.cc/victor.kolomin/controlla:latest
cache_to: type=inline
when:
event:
- tag
+2 -2
View File
@@ -1,4 +1,4 @@
FROM public.ecr.aws/docker/library/golang:1.25.1-alpine3.22 AS builder
FROM docker.io/library/golang:1.25.1-alpine3.22 AS builder
WORKDIR /app
COPY . .
@@ -7,7 +7,7 @@ COPY . .
RUN go build -o controlla .
# Final minimal image
FROM public.ecr.aws/docker/library/alpine:3.22.1
FROM docker.io/library/alpine:3.22.1
WORKDIR /app
# Copy the binary and config file from the builder stage
+65 -4
View File
@@ -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"`
+34 -22
View File
@@ -14,12 +14,38 @@ import (
"gopkg.in/yaml.v3"
)
// UpdateAndPushService updates the image tag of a specified service in a docker-compose.yaml file,
// commits the change to a Git repository, and pushes the commit to the remote repository.
func UpdateAndPushService(repoPath, filePath, serviceName, newTag, SSHKeyPath, authorName, authorEmail string) error {
// UpdateAndPushService updates a service's image tag in its docker-compose file and pushes the change to the Git repository.
func UpdateAndPushService(repoPath, filePath, serviceName, newTag, sshKeyPath, authorName, authorEmail string) error {
logrus.Infof("Starting update for service '%s' in file '%s' to tag '%s'", serviceName, filePath, newTag)
// Reading and updating the docker-compose.yaml file
// --- CORRECT LOGIC: Open repo and pull first to ensure we are up-to-date ---
repo, err := git.PlainOpen(repoPath)
if err != nil {
return fmt.Errorf("could not open repository: %w", err)
}
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
auth, err := ssh.NewPublicKeysFromFile("git", sshKeyPath, "")
if err != nil {
return fmt.Errorf("cannot generate public keys from file: %w", err)
}
logrus.Info("Pulling latest changes before modifying files...")
pullOpts := &git.PullOptions{
RemoteName: "origin",
Auth: auth,
}
err = worktree.Pull(pullOpts)
if err != nil && err != git.NoErrAlreadyUpToDate {
return fmt.Errorf("could not pull changes: %w", err)
}
// --- END CORRECT LOGIC ---
// 1. Reading and updating the docker-compose.yaml file
yamlFile, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("could not read file: %w", err)
@@ -30,7 +56,7 @@ func UpdateAndPushService(repoPath, filePath, serviceName, newTag, SSHKeyPath, a
return fmt.Errorf("could not unmarshal YAML: %w", err)
}
// Finding and updating the service image tag
// 2. Finding and updating the service image tag
services, ok := data["services"].(map[string]interface{})
if !ok {
return fmt.Errorf("invalid services structure in YAML")
@@ -55,16 +81,6 @@ func UpdateAndPushService(repoPath, filePath, serviceName, newTag, SSHKeyPath, a
logrus.Infof("docker-compose.yaml file updated successfully.")
// 4. Committing and pushing the changes to the Git repository
repo, err := git.PlainOpen(repoPath)
if err != nil {
return fmt.Errorf("failed to open Git repository: %w", err)
}
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
relativePath, err := filepath.Rel(repoPath, filePath)
if err != nil {
return fmt.Errorf("could not get relative path: %w", err)
@@ -83,22 +99,18 @@ func UpdateAndPushService(repoPath, filePath, serviceName, newTag, SSHKeyPath, a
},
})
if err != nil {
return fmt.Errorf("could not commit changes: %w", err)
return fmt.Errorf("could not create commit: %w", err)
}
logrus.Infof("New commit created: %s", commit)
publicKeys, err := ssh.NewPublicKeysFromFile("git", SSHKeyPath, "")
if err != nil {
return fmt.Errorf("failed to read SSH key: %w", err)
}
auth := publicKeys
// Push the changes to the remote repository
logrus.Info("Pushing changes to remote repository...")
err = repo.Push(&git.PushOptions{
Auth: auth,
})
if err != nil {
return fmt.Errorf("could not push changes: %w", err)
}
logrus.Infof("Changes pushed to remote repository successfully.")
return nil
}
+15 -23
View File
@@ -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)
Submodule repos/myrepo deleted from 27608766af