2023-01-24 15:47:27 +01:00
package main
import (
"context"
2023-01-24 16:40:11 +01:00
"errors"
2023-01-24 15:47:27 +01:00
"fmt"
2024-10-07 22:00:59 +02:00
"log/slog"
2023-06-27 17:53:47 +02:00
"net"
2023-01-24 16:40:11 +01:00
"net/http"
2024-04-12 11:23:11 +02:00
"net/http/pprof"
2023-01-24 15:47:27 +01:00
"net/url"
"os"
"os/signal"
2025-03-05 12:37:46 +01:00
"path/filepath"
2023-01-24 16:56:17 +01:00
"syscall"
2023-01-24 16:40:11 +01:00
"time"
2023-01-24 15:47:27 +01:00
"github.com/alexflint/go-arg"
"github.com/go-logr/logr"
2023-01-24 16:40:11 +01:00
"github.com/prometheus/client_golang/prometheus/promhttp"
2023-01-24 15:47:27 +01:00
"github.com/spf13/afero"
"golang.org/x/sync/errgroup"
2023-10-27 16:50:48 +02:00
"k8s.io/klog/v2"
2023-01-24 15:47:27 +01:00
2024-04-10 11:03:47 +02:00
"github.com/spegel-org/spegel/internal/kubernetes"
2024-04-01 18:59:26 +02:00
"github.com/spegel-org/spegel/pkg/metrics"
"github.com/spegel-org/spegel/pkg/oci"
"github.com/spegel-org/spegel/pkg/registry"
"github.com/spegel-org/spegel/pkg/routing"
"github.com/spegel-org/spegel/pkg/state"
2023-01-24 15:47:27 +01:00
)
2023-03-15 23:10:18 +01:00
type ConfigurationCmd struct {
2024-03-21 15:10:43 +00:00
ContainerdRegistryConfigPath string ` arg:"--containerd-registry-config-path,env:CONTAINERD_REGISTRY_CONFIG_PATH" default:"/etc/containerd/certs.d" help:"Directory where mirror configuration is written." `
2024-02-29 10:59:50 +01:00
MirroredRegistries [ ] url . URL ` arg:"--mirrored-registries,env:MIRRORED_REGISTRIES" help:"Registries that are configured to be mirrored, if slice is empty all registires are mirrored." `
MirrorTargets [ ] url . URL ` arg:"--mirror-targets,env:MIRROR_TARGETS,required" help:"registries that are configured to act as mirrors." `
2024-03-21 15:10:43 +00:00
ResolveTags bool ` arg:"--resolve-tags,env:RESOLVE_TAGS" default:"true" help:"When true Spegel will resolve tags to digests." `
2025-02-25 13:08:27 +01:00
PrependExisting bool ` arg:"--prepend-existing,env:PREPEND_EXISTING" default:"false" help:"When true existing mirror configuration will be kept and Spegel will prepend it's configuration." `
2023-03-15 23:10:18 +01:00
}
2024-02-01 11:43:43 +01:00
type BootstrapConfig struct {
2024-03-21 15:10:43 +00:00
BootstrapKind string ` arg:"--bootstrap-kind,env:BOOTSTRAP_KIND" help:"Kind of bootsrapper to use." `
KubeconfigPath string ` arg:"--kubeconfig-path,env:KUBECONFIG_PATH" help:"Path to the kubeconfig file." `
LeaderElectionName string ` arg:"--leader-election-name,env:LEADER_ELECTION_NAME" default:"spegel-leader-election" help:"Name of leader election." `
LeaderElectionNamespace string ` arg:"--leader-election-namespace,env:LEADER_ELECTION_NAMESPACE" default:"spegel" help:"Kubernetes namespace to write leader election data." `
2025-01-07 22:22:22 +01:00
DNSBootstrapDomain string ` arg:"--dns-bootstrap-domain,env:DNS_BOOTSTRAP_DOMAIN" help:"Domain to use when bootstrapping using DNS." `
HTTPBootstrapAddr string ` arg:"--http-bootstrap-addr,env:HTTP_BOOTSTRAP_ADDR" help:"Address to serve for HTTP bootstrap." `
HTTPBootstrapPeer string ` arg:"--http-bootstrap-peer,env:HTTP_BOOTSTRAP_PEER" help:"Peer to HTTP bootstrap with." `
2024-02-01 11:43:43 +01:00
}
2023-03-15 23:10:18 +01:00
type RegistryCmd struct {
2024-02-01 11:43:43 +01:00
BootstrapConfig
2024-10-07 22:00:59 +02:00
ContainerdRegistryConfigPath string ` arg:"--containerd-registry-config-path,env:CONTAINERD_REGISTRY_CONFIG_PATH" default:"/etc/containerd/certs.d" help:"Directory where mirror configuration is written." `
MetricsAddr string ` arg:"--metrics-addr,required,env:METRICS_ADDR" help:"address to serve metrics." `
LocalAddr string ` arg:"--local-addr,required,env:LOCAL_ADDR" help:"Address that the local Spegel instance will be reached at." `
ContainerdSock string ` arg:"--containerd-sock,env:CONTAINERD_SOCK" default:"/run/containerd/containerd.sock" help:"Endpoint of containerd service." `
ContainerdNamespace string ` arg:"--containerd-namespace,env:CONTAINERD_NAMESPACE" default:"k8s.io" help:"Containerd namespace to fetch images from." `
ContainerdContentPath string ` arg:"--containerd-content-path,env:CONTAINERD_CONTENT_PATH" default:"/var/lib/containerd/io.containerd.content.v1.content" help:"Path to Containerd content store" `
RouterAddr string ` arg:"--router-addr,env:ROUTER_ADDR,required" help:"address to serve router." `
RegistryAddr string ` arg:"--registry-addr,env:REGISTRY_ADDR,required" help:"address to server image registry." `
2024-02-29 10:59:50 +01:00
MirroredRegistries [ ] url . URL ` arg:"--mirrored-registries,env:MIRRORED_REGISTRIES" help:"Registries that are configured to be mirrored, if slice is empty all registires are mirrored." `
2024-10-07 22:00:59 +02:00
MirrorResolveTimeout time . Duration ` arg:"--mirror-resolve-timeout,env:MIRROR_RESOLVE_TIMEOUT" default:"20ms" help:"Max duration spent finding a mirror." `
MirrorResolveRetries int ` arg:"--mirror-resolve-retries,env:MIRROR_RESOLVE_RETRIES" default:"3" help:"Max amount of mirrors to attempt." `
ResolveLatestTag bool ` arg:"--resolve-latest-tag,env:RESOLVE_LATEST_TAG" default:"true" help:"When true latest tags will be resolved to digests." `
2023-03-15 23:10:18 +01:00
}
2023-03-17 13:41:43 +01:00
type Arguments struct {
2023-03-15 23:10:18 +01:00
Configuration * ConfigurationCmd ` arg:"subcommand:configuration" `
Registry * RegistryCmd ` arg:"subcommand:registry" `
2024-04-20 11:23:49 +02:00
LogLevel slog . Level ` arg:"--log-level,env:LOG_LEVEL" default:"INFO" help:"Minimum log level to output. Value should be DEBUG, INFO, WARN, or ERROR." `
2023-01-24 15:47:27 +01:00
}
func main ( ) {
2023-03-17 13:41:43 +01:00
args := & Arguments { }
2023-01-24 15:47:27 +01:00
arg . MustParse ( args )
2024-04-20 11:23:49 +02:00
opts := slog . HandlerOptions {
AddSource : true ,
Level : args . LogLevel ,
2023-01-24 15:47:27 +01:00
}
2024-04-20 11:23:49 +02:00
handler := slog . NewJSONHandler ( os . Stderr , & opts )
log := logr . FromSlogHandler ( handler )
2023-10-27 16:50:48 +02:00
klog . SetLogger ( log )
2023-03-15 23:10:18 +01:00
ctx := logr . NewContext ( context . Background ( ) , log )
2023-01-24 15:47:27 +01:00
2024-04-20 11:23:49 +02:00
err := run ( ctx , args )
2023-01-24 15:47:27 +01:00
if err != nil {
2024-04-20 11:23:49 +02:00
log . Error ( err , "run exit with error" )
2023-01-24 15:47:27 +01:00
os . Exit ( 1 )
}
2023-01-31 23:17:06 +01:00
log . Info ( "gracefully shutdown" )
}
2023-01-24 15:47:27 +01:00
2023-03-17 13:41:43 +01:00
func run ( ctx context . Context , args * Arguments ) error {
2023-03-15 23:10:18 +01:00
ctx , cancel := signal . NotifyContext ( ctx , syscall . SIGTERM )
defer cancel ( )
switch {
case args . Configuration != nil :
return configurationCommand ( ctx , args . Configuration )
case args . Registry != nil :
return registryCommand ( ctx , args . Registry )
default :
2024-05-14 23:14:04 +02:00
return errors . New ( "unknown subcommand" )
2023-03-15 23:10:18 +01:00
}
}
2023-03-17 13:41:43 +01:00
func configurationCommand ( ctx context . Context , args * ConfigurationCmd ) error {
2025-03-05 12:37:46 +01:00
username , password , err := loadBasicAuth ( )
if err != nil {
return err
}
2023-03-15 23:10:18 +01:00
fs := afero . NewOsFs ( )
2025-03-05 12:37:46 +01:00
err = oci . AddMirrorConfiguration ( ctx , fs , args . ContainerdRegistryConfigPath , args . MirroredRegistries , args . MirrorTargets , args . ResolveTags , args . PrependExisting , username , password )
2023-03-15 23:10:18 +01:00
if err != nil {
return err
}
return nil
}
2023-03-17 13:41:43 +01:00
func registryCommand ( ctx context . Context , args * RegistryCmd ) ( err error ) {
2023-03-15 23:10:18 +01:00
log := logr . FromContextOrDiscard ( ctx )
g , ctx := errgroup . WithContext ( ctx )
2025-03-05 12:37:46 +01:00
username , password , err := loadBasicAuth ( )
if err != nil {
return err
}
2024-02-01 11:43:43 +01:00
// OCI Client
2024-02-29 10:59:50 +01:00
ociClient , err := oci . NewContainerd ( args . ContainerdSock , args . ContainerdNamespace , args . ContainerdRegistryConfigPath , args . MirroredRegistries , oci . WithContentPath ( args . ContainerdContentPath ) )
2023-07-31 22:33:56 +02:00
if err != nil {
return err
}
err = ociClient . Verify ( ctx )
2023-01-31 23:17:06 +01:00
if err != nil {
2023-05-16 09:10:41 +02:00
return err
2023-01-26 18:48:02 +01:00
}
2023-01-31 23:17:06 +01:00
2024-01-19 18:25:48 +01:00
// Metrics
2024-01-18 11:33:11 +01:00
metrics . Register ( )
2023-01-24 16:40:11 +01:00
mux := http . NewServeMux ( )
2024-01-18 11:33:11 +01:00
mux . Handle ( "/metrics" , promhttp . HandlerFor ( metrics . DefaultGatherer , promhttp . HandlerOpts { } ) )
2024-04-12 11:23:11 +02:00
mux . Handle ( "/debug/pprof/" , http . HandlerFunc ( pprof . Index ) )
mux . Handle ( "/debug/pprof/profile" , http . HandlerFunc ( pprof . Profile ) )
mux . Handle ( "/debug/pprof/trace" , http . HandlerFunc ( pprof . Trace ) )
mux . Handle ( "/debug/pprof/symbol" , http . HandlerFunc ( pprof . Symbol ) )
mux . Handle ( "/debug/pprof/heap" , pprof . Handler ( "heap" ) )
2024-12-06 13:45:34 +01:00
mux . Handle ( "/debug/pprof/allocs" , pprof . Handler ( "allocs" ) )
2024-04-12 11:23:11 +02:00
mux . Handle ( "/debug/pprof/goroutine" , pprof . Handler ( "goroutine" ) )
mux . Handle ( "/debug/pprof/threadcreate" , pprof . Handler ( "threadcreate" ) )
mux . Handle ( "/debug/pprof/block" , pprof . Handler ( "block" ) )
mux . Handle ( "/debug/pprof/mutex" , pprof . Handler ( "mutex" ) )
2023-06-28 21:52:39 +02:00
metricsSrv := & http . Server {
2023-03-17 13:41:43 +01:00
Addr : args . MetricsAddr ,
2023-01-24 16:40:11 +01:00
Handler : mux ,
}
g . Go ( func ( ) error {
2023-06-28 21:52:39 +02:00
if err := metricsSrv . ListenAndServe ( ) ; err != nil && ! errors . Is ( err , http . ErrServerClosed ) {
2023-01-24 16:40:11 +01:00
return err
}
return nil
} )
g . Go ( func ( ) error {
<- ctx . Done ( )
2023-01-25 23:52:50 +01:00
shutdownCtx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
2023-01-24 16:40:11 +01:00
defer cancel ( )
2023-06-28 21:52:39 +02:00
return metricsSrv . Shutdown ( shutdownCtx )
2023-01-24 16:40:11 +01:00
} )
2024-01-19 18:25:48 +01:00
// Router
2023-06-27 17:53:47 +02:00
_ , registryPort , err := net . SplitHostPort ( args . RegistryAddr )
if err != nil {
return err
}
2024-02-01 11:43:43 +01:00
bootstrapper , err := getBootstrapper ( args . BootstrapConfig )
if err != nil {
return err
}
2023-06-27 17:53:47 +02:00
router , err := routing . NewP2PRouter ( ctx , args . RouterAddr , bootstrapper , registryPort )
2023-01-24 15:47:27 +01:00
if err != nil {
2023-01-31 23:17:06 +01:00
return err
2023-01-24 15:47:27 +01:00
}
2024-01-19 18:25:48 +01:00
g . Go ( func ( ) error {
return router . Run ( ctx )
} )
// State tracking
2023-01-24 15:47:27 +01:00
g . Go ( func ( ) error {
2024-02-29 11:41:53 +01:00
err := state . Track ( ctx , ociClient , router , args . ResolveLatestTag )
if err != nil {
return err
}
2023-08-17 21:51:35 +02:00
return nil
2023-01-24 15:47:27 +01:00
} )
2024-01-19 18:25:48 +01:00
// Registry
2024-02-05 14:32:56 +01:00
registryOpts := [ ] registry . Option {
registry . WithResolveLatestTag ( args . ResolveLatestTag ) ,
registry . WithResolveRetries ( args . MirrorResolveRetries ) ,
registry . WithResolveTimeout ( args . MirrorResolveTimeout ) ,
registry . WithLocalAddress ( args . LocalAddr ) ,
2024-03-15 10:21:00 +01:00
registry . WithLogger ( log ) ,
2025-03-05 12:37:46 +01:00
registry . WithBasicAuth ( username , password ) ,
2024-02-05 14:32:56 +01:00
}
reg := registry . NewRegistry ( ociClient , router , registryOpts ... )
2024-06-19 16:37:43 +02:00
regSrv , err := reg . Server ( args . RegistryAddr )
if err != nil {
return err
}
2023-01-24 15:47:27 +01:00
g . Go ( func ( ) error {
2023-06-28 21:52:39 +02:00
if err := regSrv . ListenAndServe ( ) ; err != nil && ! errors . Is ( err , http . ErrServerClosed ) {
return err
}
return nil
2023-01-24 15:47:27 +01:00
} )
g . Go ( func ( ) error {
<- ctx . Done ( )
2023-06-28 21:52:39 +02:00
shutdownCtx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
defer cancel ( )
return regSrv . Shutdown ( shutdownCtx )
2023-01-24 15:47:27 +01:00
} )
2024-01-19 18:25:48 +01:00
log . Info ( "running Spegel" , "registry" , args . RegistryAddr , "router" , args . RouterAddr )
2023-01-24 15:47:27 +01:00
err = g . Wait ( )
if err != nil {
2023-01-31 23:17:06 +01:00
return err
2023-01-24 15:47:27 +01:00
}
2023-01-31 23:17:06 +01:00
return nil
2023-01-24 15:47:27 +01:00
}
2024-02-01 11:43:43 +01:00
2024-05-14 22:52:54 +02:00
func getBootstrapper ( cfg BootstrapConfig ) ( routing . Bootstrapper , error ) { //nolint: ireturn // Return type can be different structs.
2024-02-01 11:43:43 +01:00
switch cfg . BootstrapKind {
case "kubernetes" :
2024-04-10 11:03:47 +02:00
cs , err := kubernetes . GetClientset ( cfg . KubeconfigPath )
2024-02-01 11:43:43 +01:00
if err != nil {
return nil , err
}
return routing . NewKubernetesBootstrapper ( cs , cfg . LeaderElectionNamespace , cfg . LeaderElectionName ) , nil
2025-01-07 22:22:22 +01:00
case "dns" :
2025-01-15 19:12:00 +01:00
return routing . NewDNSBootstrapper ( cfg . DNSBootstrapDomain , 10 ) , nil
2025-01-07 22:22:22 +01:00
case "http" :
return routing . NewHTTPBootstrapper ( cfg . HTTPBootstrapAddr , cfg . HTTPBootstrapPeer ) , nil
2024-02-01 11:43:43 +01:00
default :
return nil , fmt . Errorf ( "unknown bootstrap kind %s" , cfg . BootstrapKind )
}
}
2025-03-05 12:37:46 +01:00
func loadBasicAuth ( ) ( string , string , error ) {
dirPath := "/etc/secrets/basic-auth"
username , err := os . ReadFile ( filepath . Join ( dirPath , "username" ) )
if err != nil && ! errors . Is ( err , os . ErrNotExist ) {
return "" , "" , err
}
password , err := os . ReadFile ( filepath . Join ( dirPath , "password" ) )
if err != nil && ! errors . Is ( err , os . ErrNotExist ) {
return "" , "" , err
}
return string ( username ) , string ( password ) , nil
}