The scheme of a git negotiation came from the request line the client writes, so "ssh://any-host/x.git/info/refs" made the cache run git clone over ssh, with its service account keys, toward a host the client chose. A git client reaches an HTTP proxy only over HTTP(S), so nothing cached is lost. DepotDeURL now refuses other schemes, the clone ends its options with "--", and GIT_ALLOW_PROTOCOL=http:https bounds git itself. Checked: ssh, git, file and ext refused, a real clone through the mirror still passes, full qemu_cache Go suite green. --- FR --- [FIX] qemu cache : le miroir git ne clone qu'en http et https Le schéma d'une négociation git venait de la ligne de requête écrite par le client : « ssh://hôte-quelconque/x.git/info/refs » faisait lancer au cache un git clone en ssh, avec les clés de son compte de service, vers un hôte choisi par le client. Un client git ne joint un mandataire HTTP qu'en HTTP(S) : rien de ce qui se met en cache n'est perdu. DepotDeURL refuse les autres schémas, le clone clôt ses options par « -- », et GIT_ALLOW_PROTOCOL=http:https borne git lui-même. Vérifié : ssh, git, file et ext refusés, un vrai clone à travers le miroir passe toujours, suite Go de qemu_cache au vert. Assisted-by: Claude Opus 5.5
611 lines
20 KiB
Go
611 lines
20 KiB
Go
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"net/http/cgi"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDepotDeURL(t *testing.T) {
|
|
cas := []struct {
|
|
brut, depot, reste string
|
|
}{
|
|
{"https://h/o/d.git/info/refs?service=git-upload-pack",
|
|
"https://h/o/d.git", "/info/refs"},
|
|
{"https://h/o/d/info/refs?service=git-upload-pack",
|
|
"https://h/o/d", "/info/refs"},
|
|
{"https://h/o/d.git/git-upload-pack", "https://h/o/d.git",
|
|
"/git-upload-pack"},
|
|
}
|
|
for _, c := range cas {
|
|
u, _ := url.Parse(c.brut)
|
|
depot, reste, ok := DepotDeURL(u)
|
|
if !ok || depot != c.depot || reste != c.reste {
|
|
t.Errorf("%s → (%q, %q, %v)", c.brut, depot, reste, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDepotDeURLRefuse(t *testing.T) {
|
|
for _, brut := range []string{
|
|
"https://h/o/d.git/objects/ab/cd",
|
|
"https://h/info/refs",
|
|
"https://h/miroir/core.db",
|
|
} {
|
|
u, _ := url.Parse(brut)
|
|
if _, _, ok := DepotDeURL(u); ok {
|
|
t.Errorf("%s pris pour une négociation", brut)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Le schéma vient de la ligne de requête absolue, que le client écrit. Un
|
|
// schéma autre que HTTP(S) ferait lancer « git clone » sur un transport que
|
|
// jamais un client passant par un mandataire HTTP n'emprunte — ssh, avec les
|
|
// clés du compte du service, vers un hôte choisi par le client.
|
|
func TestDepotDeURLRefuseLesSchemasNonHTTP(t *testing.T) {
|
|
for _, brut := range []string{
|
|
"ssh://h/o/d.git/info/refs?service=git-upload-pack",
|
|
"git://h/o/d.git/info/refs?service=git-upload-pack",
|
|
"file:///srv/o/d.git/info/refs?service=git-upload-pack",
|
|
"ext::sh%20-c%20id/info/refs",
|
|
} {
|
|
u, err := url.Parse(brut)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if depot, _, ok := DepotDeURL(u); ok {
|
|
t.Errorf("%s accepté comme dépôt %q", brut, depot)
|
|
}
|
|
}
|
|
// url.Parse ramène le schéma en minuscules : la casse ne refuse rien.
|
|
u, _ := url.Parse("HTTP://h/o/d.git/info/refs?service=git-upload-pack")
|
|
if _, _, ok := DepotDeURL(u); !ok {
|
|
t.Error("HTTP en majuscules refusé")
|
|
}
|
|
}
|
|
|
|
// Le dernier rempart est git lui-même : même une URL qui passerait le tri de
|
|
// DepotDeURL n'emprunte aucun transport hors HTTP(S). Le dépôt local, que git
|
|
// clone sans réseau ni sonde, prouve le refus sans dépendre d'aucun hôte.
|
|
func TestGitNEmprunteQueHTTP(t *testing.T) {
|
|
nu := depotDEssai(t)
|
|
g := &GitMirror{Dir: t.TempDir()}
|
|
err := g.git(context.Background(), "", "ls-remote", "--", "file://"+nu)
|
|
if err == nil {
|
|
t.Fatal("ls-remote file:// accepté")
|
|
}
|
|
// Le message suit la langue de git ; le transport, lui, y est nommé tel
|
|
// quel dans toutes les langues.
|
|
if !strings.Contains(err.Error(), "'file'") {
|
|
t.Errorf("refus inattendu : %v", err)
|
|
}
|
|
}
|
|
|
|
// Le chemin du miroir porte l'HÔTE : deux forges peuvent servir « /odoo/odoo »,
|
|
// et les confondre donnerait à l'une le contenu de l'autre.
|
|
func TestCheminMiroirSepareLesForges(t *testing.T) {
|
|
g := &GitMirror{Dir: "/var/x"}
|
|
a, err := g.CheminMiroir("https://github.com/odoo/odoo.git")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := g.CheminMiroir("https://autre.example/odoo/odoo.git")
|
|
if a == b {
|
|
t.Error("deux forges partagent un miroir")
|
|
}
|
|
if !strings.HasSuffix(a, ".git") {
|
|
t.Errorf("le miroir n'est pas un dépôt nu : %s", a)
|
|
}
|
|
// « .git » écrit ou non par l'amont donne le même miroir.
|
|
c, _ := g.CheminMiroir("https://github.com/odoo/odoo")
|
|
if a != c {
|
|
t.Errorf("« .git » change le miroir : %s contre %s", a, c)
|
|
}
|
|
}
|
|
|
|
// Un chemin qui remonte écrirait hors de la racine.
|
|
func TestCheminMiroirRefuseCeQuiRemonte(t *testing.T) {
|
|
g := &GitMirror{Dir: "/var/x"}
|
|
for _, brut := range []string{
|
|
"https://h/../../etc/passwd",
|
|
"https://h/o/../../..",
|
|
"https://h/",
|
|
"pas une url",
|
|
} {
|
|
if chemin, err := g.CheminMiroir(brut); err == nil {
|
|
t.Errorf("%s accepté et rendu %q", brut, chemin)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMiroirEteintQuandRienNestConfigure(t *testing.T) {
|
|
if (&GitMirror{}).Actif() {
|
|
t.Error("un miroir sans répertoire se dit actif")
|
|
}
|
|
// Un répertoire sans le programme de git ne sert à rien : mieux vaut le
|
|
// relais vers l'amont, qui fonctionne.
|
|
g := &GitMirror{Dir: "/var/x", Backend: "/nexiste/pas"}
|
|
if g.Actif() {
|
|
t.Error("un miroir sans git-http-backend se dit actif")
|
|
}
|
|
}
|
|
|
|
// amontGit monte un dépôt nu ET le serveur qui le publie, comme le ferait une
|
|
// forge. L'URL rendue est celle qu'un client — ou le miroir — interroge.
|
|
func amontGit(t *testing.T) (string, *httptest.Server) {
|
|
t.Helper()
|
|
if TrouverBackend() == "" {
|
|
t.Skip("git-http-backend absent de cette machine")
|
|
}
|
|
nu := depotDEssai(t)
|
|
racine := filepath.Dir(nu)
|
|
h := &cgi.Handler{
|
|
Path: TrouverBackend(),
|
|
Dir: racine,
|
|
Env: []string{
|
|
"GIT_PROJECT_ROOT=" + racine,
|
|
"GIT_HTTP_EXPORT_ALL=1",
|
|
},
|
|
InheritEnv: []string{"PATH"},
|
|
}
|
|
srv := httptest.NewServer(h)
|
|
t.Cleanup(srv.Close)
|
|
return srv.URL + "/" + filepath.Base(nu), srv
|
|
}
|
|
|
|
// depotDEssai fabrique un dépôt nu local, servi comme s'il était l'amont.
|
|
func depotDEssai(t *testing.T) string {
|
|
t.Helper()
|
|
racine := t.TempDir()
|
|
nu := filepath.Join(racine, "amont.git")
|
|
travail := filepath.Join(racine, "travail")
|
|
lancer := func(dir string, args ...string) {
|
|
t.Helper()
|
|
c := exec.Command("git", args...)
|
|
c.Dir = dir
|
|
c.Env = append(os.Environ(),
|
|
"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@e",
|
|
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@e")
|
|
if out, err := c.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v : %v : %s", args, err, out)
|
|
}
|
|
}
|
|
lancer(racine, "init", "-q", "--bare", "--initial-branch=main", nu)
|
|
lancer(racine, "init", "-q", "--initial-branch=main", travail)
|
|
if err := os.WriteFile(
|
|
filepath.Join(travail, "f.txt"), []byte("bonjour"), 0o644,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lancer(travail, "add", "f.txt")
|
|
lancer(travail, "commit", "-qm", "premier")
|
|
lancer(travail, "push", "-q", nu, "HEAD:refs/heads/main")
|
|
return nu
|
|
}
|
|
|
|
// Le refus de place est la SEULE trace qu'un opérateur verra, et il nommait
|
|
// le champ PlancherLibre — laissé à zéro par tout appelant ordinaire — au
|
|
// lieu du plancher qui avait refusé. « moins de 0 o libres sur le disque »
|
|
// n'est ni vrai ni exploitable : il n'annonce aucun seuil et ne dit pas ce
|
|
// qu'il a mesuré.
|
|
func TestLeRefusConnaitLePlancherQuiARefuse(t *testing.T) {
|
|
g := &GitMirror{Dir: t.TempDir()}
|
|
libres, plancher, assez := g.placeSuffisante()
|
|
if plancher != PlancherParDefaut {
|
|
t.Fatalf("plancher rendu %d, le défaut est %d",
|
|
plancher, PlancherParDefaut)
|
|
}
|
|
if libres < 0 {
|
|
t.Skip("place non mesurable sur ce répertoire")
|
|
}
|
|
if assez != (libres > plancher) {
|
|
t.Fatalf("verdict %v pour %d libres sous un plancher de %d",
|
|
assez, libres, plancher)
|
|
}
|
|
}
|
|
|
|
// Une mesure impossible ne bloque pas — c'est le choix déjà écrit — mais elle
|
|
// doit se DISTINGUER d'un disque vide : zéro octet libre est une mesure, pas
|
|
// une absence de mesure.
|
|
func TestUnePlaceNonMesurableNeBloquePasEtSeDistingue(t *testing.T) {
|
|
g := &GitMirror{Dir: filepath.Join(t.TempDir(), "nexiste", "pas")}
|
|
libres, _, assez := g.placeSuffisante()
|
|
if !assez {
|
|
t.Fatal("une mesure impossible ne doit pas refuser le miroir")
|
|
}
|
|
if libres != -1 {
|
|
t.Fatalf("libres %d : une mesure absente doit se distinguer", libres)
|
|
}
|
|
}
|
|
|
|
// Le plancher des tests qui ne portent PAS sur le plancher.
|
|
//
|
|
// Le défaut de production — 10 Gio laissés au disque de l'orchestrateur — n'a
|
|
// rien à voir avec un miroir de trois commits dans un répertoire temporaire,
|
|
// et il fait échouer treize tests sur toute machine dont le répertoire
|
|
// temporaire offre moins que cela : un /tmp en tmpfs y suffit. Le refus était
|
|
// alors pris pour une panne du miroir.
|
|
//
|
|
// Le plancher lui-même est éprouvé plus bas, avec une valeur qu'aucun disque
|
|
// n'atteint.
|
|
const plancherDEssai int64 = 1
|
|
|
|
// L'épreuve qui compte : un client clone à travers le miroir, sans que rien
|
|
// dans l'invité soit configuré, et le dépôt arrive complet.
|
|
func TestClonerAuTraversDuMiroir(t *testing.T) {
|
|
if TrouverBackend() == "" {
|
|
t.Skip("git-http-backend absent de cette machine")
|
|
}
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 2 * time.Minute}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
// L'URL que absoluteURL rend au proxy : schéma et hôte compris.
|
|
u := &url.URL{Scheme: "http", Host: r.Host,
|
|
Path: r.URL.Path, RawQuery: r.URL.RawQuery}
|
|
_, reste, ok := DepotDeURL(u)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
// Tout chemin demandé désigne le dépôt qui joue l'amont : ce test
|
|
// mesure le miroir, pas le routage.
|
|
chemin, pret := g.Assurer(r.Context(), amont)
|
|
if !pret {
|
|
http.Error(w, "miroir indisponible", 502)
|
|
return
|
|
}
|
|
g.Servir(w, r, chemin, reste)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
for essai := 1; essai <= 2; essai++ {
|
|
dest := filepath.Join(t.TempDir(), "copie")
|
|
c := exec.Command("git", "clone", "-q", srv.URL+"/essai.git", dest)
|
|
c.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
|
|
if out, err := c.CombinedOutput(); err != nil {
|
|
t.Fatalf("clone %d : %v : %s", essai, err, out)
|
|
}
|
|
contenu, err := os.ReadFile(filepath.Join(dest, "f.txt"))
|
|
if err != nil || string(contenu) != "bonjour" {
|
|
t.Fatalf("clone %d : contenu %q, err %v", essai, contenu, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Un amont muet mais un miroir déjà là : le miroir sert. C'est ce qui rend un
|
|
// déploiement sans réseau possible pour git, ce qu'aucun cache de réponses ne
|
|
// peut faire de ce protocole.
|
|
func TestUnMiroirExistantSertQuandLAmontEstMuet(t *testing.T) {
|
|
amont, srvAmont := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: time.Minute}
|
|
if _, pret := g.Assurer(context.Background(), amont); !pret {
|
|
t.Fatal("le miroir n'a pas pu être créé")
|
|
}
|
|
// L'amont disparaît : le serveur qui le publiait est fermé, exactement ce
|
|
// que voit une VM quand le réseau tombe.
|
|
srvAmont.Close()
|
|
g.Frais = 0 // forcer une tentative de rafraîchissement
|
|
chemin, pret := g.Assurer(context.Background(), amont)
|
|
if !pret {
|
|
t.Error("le miroir refuse de servir alors qu'il existe")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(chemin, "HEAD")); err != nil {
|
|
t.Errorf("le miroir a été effacé : %v", err)
|
|
}
|
|
}
|
|
|
|
// Un amont muet SANS miroir doit rendre faux : l'appelant retombe alors sur le
|
|
// relais, qui donnera au client la vraie erreur du réseau plutôt qu'une erreur
|
|
// inventée ici.
|
|
func TestAucunMiroirEtAucunAmont(t *testing.T) {
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 20 * time.Second}
|
|
chemin, pret := g.Assurer(
|
|
context.Background(), "https://127.0.0.1:1/nexiste/pas.git")
|
|
if pret {
|
|
t.Errorf("se dit prêt avec %q", chemin)
|
|
}
|
|
// Aucun DÉPÔT ne doit rester derrière : un clonage à moitié fait serait
|
|
// pris pour un miroir valide à la requête suivante. Un répertoire vide,
|
|
// lui, ne trompe personne.
|
|
filepath.Walk(g.Dir, func(p string, info os.FileInfo, err error) error {
|
|
if err == nil && info != nil && info.Name() == "HEAD" {
|
|
t.Errorf("un miroir incomplet subsiste : %s", p)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestDepotsDuFichier(t *testing.T) {
|
|
f := filepath.Join(t.TempDir(), "liste")
|
|
contenu := strings.Join([]string{
|
|
"# un commentaire",
|
|
"",
|
|
"https://h/a.git",
|
|
" https://h/b.git ",
|
|
"https://h/a.git", // le même dépôt figure dans plusieurs manifestes
|
|
}, "\n")
|
|
if err := os.WriteFile(f, []byte(contenu), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := DepotsDuFichier(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
attendu := []string{"https://h/a.git", "https://h/b.git"}
|
|
if len(got) != len(attendu) {
|
|
t.Fatalf("%d dépôts, %d attendus : %v", len(got), len(attendu), got)
|
|
}
|
|
for i := range attendu {
|
|
if got[i] != attendu[i] {
|
|
t.Errorf("dépôt %d : %q, attendu %q", i, got[i], attendu[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Un dépôt qui échoue ne doit pas emporter les autres : sur une liste de
|
|
// trois cents, il y a toujours un dépôt privé, déplacé ou retiré, et tout
|
|
// arrêter pour lui perdrait le travail déjà fait.
|
|
func TestUnDepotEnEchecNEmportePasLesAutres(t *testing.T) {
|
|
bon, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 30 * time.Second}
|
|
depots := []string{
|
|
bon,
|
|
"https://127.0.0.1:1/absent.git",
|
|
bon + "-autre",
|
|
}
|
|
reussis, echoues := g.Prefetch(context.Background(), depots, 3, nil)
|
|
if reussis < 1 {
|
|
t.Errorf("%d réussite(s) : le dépôt joignable n'a pas été pris",
|
|
reussis)
|
|
}
|
|
if echoues < 1 {
|
|
t.Error("aucun échec compté alors qu'un dépôt est injoignable")
|
|
}
|
|
if reussis+echoues != len(depots) {
|
|
t.Errorf("%d + %d ne fait pas %d", reussis, echoues, len(depots))
|
|
}
|
|
}
|
|
|
|
// Le pré-remplissage rend le miroir prêt : la machine suivante n'a plus qu'à
|
|
// être servie, ce qui est tout l'objet de l'avance.
|
|
func TestApresPrefetchLeMiroirEstPret(t *testing.T) {
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 30 * time.Second}
|
|
if r, e := g.Prefetch(
|
|
context.Background(), []string{amont}, 2, nil,
|
|
); r != 1 || e != 0 {
|
|
t.Fatalf("pré-remplissage : %d réussis, %d échoués", r, e)
|
|
}
|
|
depots, octets := g.Occupation()
|
|
if depots != 1 || octets == 0 {
|
|
t.Errorf("occupation : %d dépôts, %d octets", depots, octets)
|
|
}
|
|
}
|
|
|
|
// Le plancher protège le disque de l'orchestrateur.
|
|
//
|
|
// Un miroir est COMPLET là où « repo sync » clone en profondeur un : le
|
|
// facteur entre les deux est celui de l'historique, et il ne se devine pas.
|
|
// Sans plancher, une seule installation qui tire trois cents dépôts peut
|
|
// remplir le disque de la machine qui héberge toutes les VM.
|
|
func TestLePlancherRefuseUnMiroirDeplus(t *testing.T) {
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
Dir: t.TempDir(),
|
|
Delai: 30 * time.Second,
|
|
// Plus que tout disque n'en offre : aucun miroir NEUF ne doit passer.
|
|
PlancherLibre: 1 << 62,
|
|
}
|
|
if chemin, pret := g.Assurer(context.Background(), amont); pret {
|
|
t.Errorf("un miroir a été créé sous le plancher : %s", chemin)
|
|
}
|
|
if depots, _ := g.Occupation(); depots != 0 {
|
|
t.Errorf("%d dépôt(s) créés malgré le plancher", depots)
|
|
}
|
|
}
|
|
|
|
// Un miroir DÉJÀ tenu continue d'être servi : une mise à jour ne coûte que ce
|
|
// qui a changé, et le refuser priverait de tout ce qui est déjà là.
|
|
func TestLePlancherNEmpechePasDeServirLexistant(t *testing.T) {
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 30 * time.Second}
|
|
if _, pret := g.Assurer(context.Background(), amont); !pret {
|
|
t.Fatal("le miroir n'a pas pu être créé")
|
|
}
|
|
g.PlancherLibre = 1 << 62
|
|
g.Frais = 0
|
|
if _, pret := g.Assurer(context.Background(), amont); !pret {
|
|
t.Error("un miroir existant est refusé à cause du plancher")
|
|
}
|
|
}
|
|
|
|
// Ce que le miroir sert doit être COMPTÉ.
|
|
//
|
|
// La passerelle CGI écrit directement dans la réponse : le journal notait zéro
|
|
// octet pour tout ce que le miroir servait, et c'est le chemin qui porte
|
|
// l'essentiel du trafic d'une installation. Un outil dont le journal EST la
|
|
// mesure ne peut pas avoir un chemin muet.
|
|
func TestCeQueLeMiroirSertEstCompte(t *testing.T) {
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 30 * time.Second}
|
|
chemin, pret := g.Assurer(context.Background(), amont)
|
|
if !pret {
|
|
t.Fatal("le miroir n'a pas pu être créé")
|
|
}
|
|
|
|
var pese int64
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
// L'URL que absoluteURL rend au proxy : schéma et hôte compris.
|
|
u := &url.URL{Scheme: "http", Host: r.Host,
|
|
Path: r.URL.Path, RawQuery: r.URL.RawQuery}
|
|
_, reste, ok := DepotDeURL(u)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
pese += g.Servir(w, r, chemin, reste)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dest := filepath.Join(t.TempDir(), "copie")
|
|
c := exec.Command("git", "clone", "-q", srv.URL+"/essai.git", dest)
|
|
c.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
|
|
if out, err := c.CombinedOutput(); err != nil {
|
|
t.Fatalf("clone : %v : %s", err, out)
|
|
}
|
|
if pese <= 0 {
|
|
t.Error("le miroir a servi un clone entier et le journal dirait zéro")
|
|
}
|
|
}
|
|
|
|
// La liste est triée par TAILLE : c'est ce qu'on cherche quand on surveille
|
|
// la place à la main, et trois dépôts font les trois quarts du total.
|
|
func TestLesDepotsSontTriesParTaille(t *testing.T) {
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir()}
|
|
for nom, poids := range map[string]int{
|
|
"h/petit.git": 10, "h/gros.git": 5000, "h/moyen.git": 500,
|
|
} {
|
|
d := filepath.Join(g.Dir, nom)
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(
|
|
filepath.Join(d, "HEAD"), make([]byte, poids), 0o644,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
depots := g.Depots()
|
|
if len(depots) != 3 {
|
|
t.Fatalf("%d dépôts vus", len(depots))
|
|
}
|
|
for i, attendu := range []string{"h/gros", "h/moyen", "h/petit"} {
|
|
if depots[i].Nom != attendu {
|
|
t.Errorf("rang %d : %q, attendu %q", i, depots[i].Nom, attendu)
|
|
}
|
|
}
|
|
if depots[0].Octets <= depots[2].Octets {
|
|
t.Error("les tailles ne sont pas mesurées")
|
|
}
|
|
}
|
|
|
|
// Effacer un miroir est sans danger : il se refait au prochain besoin. Mais
|
|
// l'effacement doit rester DANS les miroirs — un appel mal formé ne doit pas
|
|
// pouvoir emporter autre chose.
|
|
func TestRetirerRefuseCeQuiEstDehors(t *testing.T) {
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir()}
|
|
dehors := filepath.Join(t.TempDir(), "ailleurs.git")
|
|
if err := os.MkdirAll(dehors, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, cible := range []string{dehors, "/etc", g.Dir, g.Dir + "/x"} {
|
|
if err := g.Retirer(cible); err == nil {
|
|
t.Errorf("%s a été accepté", cible)
|
|
}
|
|
}
|
|
if _, err := os.Stat(dehors); err != nil {
|
|
t.Errorf("un répertoire hors des miroirs a été effacé : %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRetirerEffaceLeMiroir(t *testing.T) {
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 30 * time.Second}
|
|
chemin, pret := g.Assurer(context.Background(), amont)
|
|
if !pret {
|
|
t.Fatal("le miroir n'a pas pu être créé")
|
|
}
|
|
if err := g.Retirer(chemin); err != nil {
|
|
t.Fatalf("effacement : %v", err)
|
|
}
|
|
if depots, _ := g.Occupation(); depots != 0 {
|
|
t.Errorf("%d dépôt(s) subsistent", depots)
|
|
}
|
|
// Et il se refait : c'est ce qui rend l'effacement sans danger.
|
|
if _, pret := g.Assurer(context.Background(), amont); !pret {
|
|
t.Error("le miroir ne se refait pas après effacement")
|
|
}
|
|
}
|
|
|
|
// Un miroir qui existe est déjà servable : quand l'amont ne répond pas, il
|
|
// doit sortir TOUT DE SUITE, pas au terme du délai d'un clonage.
|
|
//
|
|
// Sans ce délai propre, un amont coupé faisait attendre chaque dépôt jusqu'à
|
|
// son terme : pour les trois cents dépôts d'une installation, des heures pour
|
|
// un déploiement que le disque pouvait servir en entier.
|
|
func TestUnRafraichissementNattendPasCommeUnClonage(t *testing.T) {
|
|
amont, srv := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai,
|
|
Dir: t.TempDir(),
|
|
Delai: 30 * time.Minute,
|
|
DelaiMaj: 2 * time.Second,
|
|
}
|
|
if _, pret := g.Assurer(context.Background(), amont); !pret {
|
|
t.Fatal("le miroir n'a pas pu être créé")
|
|
}
|
|
// L'amont devient MUET : il accepte la connexion et ne répond jamais,
|
|
// exactement ce que fait un paquet jeté sans être refusé.
|
|
srv.Close()
|
|
muet, err := net.Listen("tcp", hoteDe(t, amont))
|
|
if err != nil {
|
|
t.Skipf("le port de l'amont n'a pas pu être repris : %v", err)
|
|
}
|
|
defer muet.Close()
|
|
go func() {
|
|
for {
|
|
c, err := muet.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = c // accepté, jamais répondu
|
|
}
|
|
}()
|
|
|
|
g.Frais = 0
|
|
debut := time.Now()
|
|
chemin, pret := g.Assurer(context.Background(), amont)
|
|
ecoule := time.Since(debut)
|
|
if !pret || chemin == "" {
|
|
t.Fatal("le miroir existant n'a pas été servi")
|
|
}
|
|
if ecoule > 20*time.Second {
|
|
t.Errorf("le rafraîchissement a duré %v : le délai du clonage a"+
|
|
" été appliqué", ecoule)
|
|
}
|
|
}
|
|
|
|
func hoteDe(t *testing.T, brut string) string {
|
|
t.Helper()
|
|
u, err := url.Parse(brut)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return u.Host
|
|
}
|