From 0f799b0d5adfc45e2306b2486555abfdb273f82f Mon Sep 17 00:00:00 2001 From: Mikhail Klementev Date: Mon, 15 May 2023 07:29:40 +0000 Subject: [PATCH] feat: implement part of metasnap api --- distro/debian/snapshot/metasnap/metasnap.go | 158 ++++++++++++++++++ .../debian/snapshot/metasnap/metasnap_test.go | 28 ++++ 2 files changed, 186 insertions(+) create mode 100644 distro/debian/snapshot/metasnap/metasnap.go create mode 100644 distro/debian/snapshot/metasnap/metasnap_test.go diff --git a/distro/debian/snapshot/metasnap/metasnap.go b/distro/debian/snapshot/metasnap/metasnap.go new file mode 100644 index 0000000..596c0f5 --- /dev/null +++ b/distro/debian/snapshot/metasnap/metasnap.go @@ -0,0 +1,158 @@ +package metasnap + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/rs/zerolog/log" + "golang.org/x/time/rate" +) + +const apiURL = "http://metasnap.debian.net/cgi-bin/api?" + +var archives = []string{ + "debian", + "debian-security", + "debian-backports", + // "debian-archive", + // "debian-debug", + // "debian-ports", + // "debian-volatile", +} + +var ( + limiterTimeout time.Duration = time.Second + limiterBurst int = 3 + + Limiter = rate.NewLimiter(rate.Every(limiterTimeout), limiterBurst) +) + +func lowerLimit() { + limiterTimeout = limiterTimeout * 2 + log.Info().Msgf("limiter timeout set to %v", limiterTimeout) + Limiter.SetLimit(rate.Every(limiterTimeout)) +} + +// Retries in case of 5xx errors +var Retries = 10 + +var ErrNotFound = errors.New("404 not found") + +func query(q string) (result string, err error) { + flog := log.With().Str("url", q).Logger() + + var resp *http.Response + for i := Retries; i > 0; i-- { + flog.Trace().Msg("wait") + Limiter.Wait(context.Background()) + + flog.Trace().Msg("start") + resp, err = http.Get(q) + if err != nil { + if strings.Contains(err.Error(), "reset by peer") { + flog.Debug().Err(err).Msg("") + lowerLimit() + continue + } + flog.Error().Err(err).Msg("") + return + } + defer resp.Body.Close() + + flog.Debug().Msgf("%s", resp.Status) + + if resp.StatusCode == 404 { + err = ErrNotFound + return + } + + if resp.StatusCode < 500 { + break + } + + flog.Debug().Msgf("retry (%d left)", i) + } + + if resp.StatusCode >= 400 { + err = fmt.Errorf("%d (%s)", resp.StatusCode, q) + } + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + result = string(buf) + return +} + +func queryAPIf(f string, s ...interface{}) (result string, err error) { + return query(apiURL + fmt.Sprintf(f, s...)) +} + +type Snapshot struct { + First string + Last string +} + +type PackageInfo struct { + Suite string + Component string + Snapshot Snapshot +} + +func GetPackageInfo(pkg, arch, version string) (infos []PackageInfo, err error) { + var result string + + for _, archive := range archives { + result, err = queryAPIf("archive=%s&pkg=%s&arch=%s&ver=%s", + archive, pkg, arch, version) + if err == nil { + break + } + } + + if err != nil { + return + } + + if result == "" { + err = ErrNotFound + return + } + + for _, line := range strings.Split(result, "\n") { + if line == "" { + break + } + + fields := strings.Split(line, " ") + if len(fields) != 4 { + err = fmt.Errorf("metasnap api returned %s", result) + return + } + + info := PackageInfo{ + Suite: fields[0], + Component: fields[1], + Snapshot: Snapshot{ + First: fields[2], + Last: fields[3], + }, + } + + infos = append(infos, info) + } + + if len(infos) == 0 { + err = ErrNotFound + return + } + + return +} diff --git a/distro/debian/snapshot/metasnap/metasnap_test.go b/distro/debian/snapshot/metasnap/metasnap_test.go new file mode 100644 index 0000000..264a3f4 --- /dev/null +++ b/distro/debian/snapshot/metasnap/metasnap_test.go @@ -0,0 +1,28 @@ +package metasnap + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" +) + +func TestGetPackageInfo(t *testing.T) { + // existing + infos, err := GetPackageInfo("linux-image-3.8-trunk-amd64", + "amd64", "3.8.2-1~experimental.1") + if err != nil { + t.Fatal(err) + } + + t.Log(spew.Sdump(infos)) + + // non-existing + infos, err = GetPackageInfo("meh", "amd64", "meh") + if err == nil { + t.Fatalf("should not be ok, result: %s", spew.Sdump(infos)) + } + + if err != ErrNotFound { + t.Fatal("wrong error type") + } +}