package cleanup
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/go-logr/logr"
"golang.org/x/sync/errgroup"
"clyde/internal/channel"
"clyde/pkg/oci"
)
func Run(ctx context.Context, addr, configPath string) error {
log := logr.FromContextOrDiscard(ctx)
err := oci.CleanupMirrorConfiguration(ctx, configPath)
if err != nil {
return err
}
g, gCtx := errgroup.WithContext(ctx)
mux := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet && req.URL.Path != "/healthz" {
log.Error(errors.New("unknown request"), "unsupported probe request", "path", req.URL.Path, "method", req.Method)
rw.WriteHeader(http.StatusNotFound)
return
}
rw.WriteHeader(http.StatusOK)
})
srv := &http.Server{
Addr: addr,
Handler: mux,
}
g.Go(func() error {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
g.Go(func() error {
<-gCtx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return srv.Shutdown(shutdownCtx)
})
log.Info("waiting to be shutdown")
err = g.Wait()
if err != nil {
return err
}
return nil
}
func Wait(ctx context.Context, probeEndpoint string, period time.Duration, threshold int) error {
log := logr.FromContextOrDiscard(ctx)
resolver := &net.Resolver{}
client := &http.Client{}
addr, port, err := net.SplitHostPort(probeEndpoint)
if err != nil {
return err
}
immediateCh := make(chan time.Time, 1)
immediateCh <- time.Now()
close(immediateCh)
ticker := time.NewTicker(period)
defer ticker.Stop()
tickerCh := channel.Merge(immediateCh, ticker.C)
thresholdCount := 0
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-tickerCh:
start := time.Now()
log.Info("running probe lookup", "host", addr)
ips, err := resolver.LookupIPAddr(ctx, addr)
if err != nil {
log.Error(err, "cleanup probe lookup failed")
thresholdCount = 0
continue
}
log.Info("running probe request", "endpoints", len(ips))
err = probeIPs(ctx, client, ips, port)
if err != nil {
log.Error(err, "cleanup probe request failed")
thresholdCount = 0
continue
}
thresholdCount += 1
log.Info("probe ran successfully", "threshold", thresholdCount, "duration", time.Since(start).String())
if thresholdCount == threshold {
log.Info("probe threshold reached")
return nil
}
}
}
}
func probeIPs(ctx context.Context, client *http.Client, ips []net.IPAddr, port string) error {
g, gCtx := errgroup.WithContext(ctx)
g.SetLimit(10)
for _, ip := range ips {
g.Go(func() error {
u := url.URL{
Scheme: "http",
Host: net.JoinHostPort(ip.String(), port),
Path: "/healthz",
}
reqCtx, cancel := context.WithTimeout(gCtx, 1*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, u.String(), nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, err = io.Copy(io.Discard, resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code %s", resp.Status)
}
return nil
})
}
err := g.Wait()
if err != nil {
return err
}
return nil
}