Ctrl K
ComunidadGuías…

builder/mirror/manager.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// RAINBOND, Application Management Platform
// Copyright (C) 2014-2026 Goodrain Co., Ltd.

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. For any non-GPL usage of Rainbond,
// one or multiple Commercial Licenses authorized by Goodrain Co., Ltd.
// must be obtained first.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package mirror

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"github.com/sirupsen/logrus"
	corev1 "k8s.io/api/core/v1"
	k8serror "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
)

// configMapName persists the last known good mirror list so a builder restart
// does not depend on the remote source being reachable.
const configMapName = "rbd-dynamic-mirrors"

const (
	fetchTimeout = 15 * time.Second
	probeTimeout = 5 * time.Second
)

// Manager holds the dynamically refreshed docker.io mirror list. All methods
// are safe for concurrent use; Mirrors is additionally nil-receiver safe so
// call sites do not need to care whether the feature was initialised.
type Manager struct {
	cfg       Config
	kube      kubernetes.Interface
	namespace string

	mu      sync.RWMutex
	mirrors []string
}

var (
	defaultManager *Manager
	defaultMu      sync.RWMutex
)

// New creates a Manager. kube may be nil in unit tests that skip persistence.
func New(cfg Config, kube kubernetes.Interface, namespace string) *Manager {
	return &Manager{cfg: cfg, kube: kube, namespace: namespace}
}

// Init creates the default Manager from the environment and starts its refresh
// loop. It is a no-op when the feature is disabled.
func Init(ctx context.Context, getenv func(string) string, kube kubernetes.Interface, namespace string) {
	cfg := LoadConfig(getenv)
	if !cfg.Enabled {
		logrus.Info("dynamic registry mirrors disabled")
		return
	}
	m := New(cfg, kube, namespace)
	defaultMu.Lock()
	defaultManager = m
	defaultMu.Unlock()
	go m.Start(ctx)
}

// Default returns the manager created by Init, or nil when the feature is
// disabled or not initialised. The returned value is safe to use either way.
func Default() *Manager {
	defaultMu.RLock()
	defer defaultMu.RUnlock()
	return defaultManager
}

// Mirrors returns a copy of the current mirror list, ordered by probe latency.
func (m *Manager) Mirrors() []string {
	if m == nil {
		return nil
	}
	m.mu.RLock()
	defer m.mu.RUnlock()
	if len(m.mirrors) == 0 {
		return nil
	}
	result := make([]string, len(m.mirrors))
	copy(result, m.mirrors)
	return result
}

// Start restores the persisted list, refreshes immediately and then keeps
// refreshing on the configured interval until ctx is cancelled.
func (m *Manager) Start(ctx context.Context) {
	if !m.cfg.Enabled {
		return
	}
	m.restore(ctx)
	if err := m.Refresh(ctx); err != nil {
		logrus.Warnf("initial dynamic mirror refresh failure: %v", err)
	}
	ticker := time.NewTicker(m.cfg.RefreshInterval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if err := m.Refresh(ctx); err != nil {
				logrus.Warnf("dynamic mirror refresh failure: %v", err)
			}
		}
	}
}

// Refresh fetches candidates, probes them and stores the fastest MaxCount
// alive mirrors. A fetch failure returns an error and keeps the previous list;
// an empty probe result is not an error and clears the list, so dead mirrors
// never reach the build path.
func (m *Manager) Refresh(ctx context.Context) error {
	if !m.cfg.Enabled {
		return nil
	}
	candidates, err := FetchCandidates(ctx, m.cfg.SourceURLs, fetchTimeout)
	if err != nil {
		return fmt.Errorf("fetch mirror candidates: %w", err)
	}
	alive := Probe(ctx, candidates, probeTimeout)
	if len(alive) > m.cfg.MaxCount {
		alive = alive[:m.cfg.MaxCount]
	}
	m.setMirrors(ctx, alive)
	logrus.Infof("dynamic registry mirrors refreshed: %d candidates, using %v", len(candidates), alive)
	return nil
}

func (m *Manager) setMirrors(ctx context.Context, mirrors []string) {
	m.mu.Lock()
	m.mirrors = mirrors
	m.mu.Unlock()
	if err := m.persist(ctx, mirrors); err != nil {
		logrus.Warnf("persist dynamic mirrors failure: %v", err)
	}
}

// restore loads the last persisted list so mirrors are usable right after a
// builder restart, before the first remote fetch completes.
func (m *Manager) restore(ctx context.Context) {
	if m.kube == nil {
		return
	}
	cm, err := m.kube.CoreV1().ConfigMaps(m.namespace).Get(ctx, configMapName, metav1.GetOptions{})
	if err != nil {
		if !k8serror.IsNotFound(err) {
			logrus.Warnf("restore dynamic mirrors failure: %v", err)
		}
		return
	}
	var mirrors []string
	if err := json.Unmarshal([]byte(cm.Data["mirrors"]), &mirrors); err != nil {
		logrus.Warnf("restore dynamic mirrors: invalid persisted payload: %v", err)
		return
	}
	if len(mirrors) == 0 {
		return
	}
	m.mu.Lock()
	m.mirrors = mirrors
	m.mu.Unlock()
	logrus.Infof("dynamic registry mirrors restored from configmap: %v", mirrors)
}

func (m *Manager) persist(ctx context.Context, mirrors []string) error {
	if m.kube == nil {
		return nil
	}
	payload, err := json.Marshal(mirrors)
	if err != nil {
		return fmt.Errorf("marshal mirrors: %w", err)
	}
	data := map[string]string{
		"mirrors":    string(payload),
		"updated_at": time.Now().UTC().Format(time.RFC3339),
	}
	cm, err := m.kube.CoreV1().ConfigMaps(m.namespace).Get(ctx, configMapName, metav1.GetOptions{})
	if k8serror.IsNotFound(err) {
		cm = &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{Name: configMapName, Namespace: m.namespace},
			Data:       data,
		}
		_, err = m.kube.CoreV1().ConfigMaps(m.namespace).Create(ctx, cm, metav1.CreateOptions{})
		return err
	}
	if err != nil {
		return err
	}
	cm.Data = data
	_, err = m.kube.CoreV1().ConfigMaps(m.namespace).Update(ctx, cm, metav1.UpdateOptions{})
	return err
}