@@ -7,20 +7,32 @@ package talos
77import (
88 "bufio"
99 "context"
10+ "crypto/tls"
1011 "errors"
1112 "fmt"
1213 "io"
14+ "log/slog"
15+ "net"
16+ "net/http"
1317 "os"
18+ "os/signal"
1419 "slices"
1520 "strings"
1621 "text/tabwriter"
1722 "time"
1823
1924 "github.com/blang/semver/v4"
2025 "github.com/dustin/go-humanize"
26+ "github.com/google/go-containerregistry/pkg/crane"
27+ "github.com/google/go-containerregistry/pkg/logs"
28+ "github.com/google/go-containerregistry/pkg/v1/remote"
29+ "github.com/olareg/olareg"
30+ "github.com/olareg/olareg/config"
2131 "github.com/spf13/cobra"
2232 "github.com/spf13/pflag"
33+ "golang.org/x/sync/errgroup"
2334
35+ "github.com/siderolabs/go-pointer"
2436 "github.com/siderolabs/talos/cmd/talosctl/pkg/talos/artifacts"
2537 "github.com/siderolabs/talos/cmd/talosctl/pkg/talos/helpers"
2638 "github.com/siderolabs/talos/pkg/imager/cache"
@@ -168,14 +180,24 @@ var imageDefaultCmd = &cobra.Command{
168180 },
169181}
170182
171- var minimumVersion = semver .MustParse ("1.11.0-alpha.0" )
183+ const (
184+ provisionerDocker = "docker"
185+ provisionerInstaller = "installer"
186+ provisionerAll = "all"
187+ )
188+
189+ var imageDefaultCmdFlags = struct {
190+ provisioner pflag.Value
191+ }{
192+ provisioner : helpers .StringChoice (provisionerInstaller , provisionerDocker , provisionerAll ),
193+ }
172194
173195// imageSourceBundleCmd represents the image source-bundle command.
174196var imageSourceBundleCmd = & cobra.Command {
175197 Use : "source-bundle <talos-version>" ,
176198 Short : "List the source images used for building Talos" ,
177199 Long : `` ,
178- Args : helpers . ChainCobraPositionalArgs (
200+ Args : cobra . MatchAll (
179201 cobra .ExactArgs (1 ),
180202 func (cmd * cobra.Command , args []string ) error {
181203 maximumVersion , err := semver .ParseTolerant (version .Tag )
@@ -245,6 +267,8 @@ var imageSourceBundleCmd = &cobra.Command{
245267 },
246268}
247269
270+ var minimumVersion = semver .MustParse ("1.11.0-alpha.0" )
271+
248272// imageIntegrationCmd represents the integration image command.
249273var imageIntegrationCmd = & cobra.Command {
250274 Use : "integration" ,
@@ -394,16 +418,224 @@ var imageCacheCreateCmdFlags struct {
394418 force bool
395419}
396420
397- const (
398- provisionerDocker = "docker"
399- provisionerInstaller = "installer"
400- provisionerAll = "all"
401- )
421+ var imageRegistryCommand = & cobra.Command {
422+ Use : "registry" ,
423+ Short : "Commands for working with a local image registry" ,
424+ Long : `` ,
425+ PersistentPreRun : func (cmd * cobra.Command , _ []string ) {
426+ logs .Warn .SetOutput (os .Stderr )
427+ logs .Progress .SetOutput (os .Stderr )
428+
429+ options = append (options ,
430+ crane .WithContext (cmd .Context ()),
431+ crane .WithJobs (0 ),
432+ crane .Insecure ,
433+ crane .WithNondistributable (),
434+ )
402435
403- var imageDefaultCmdFlags = struct {
404- provisioner pflag.Value
405- }{
406- provisioner : helpers .StringChoice (provisionerInstaller , provisionerDocker , provisionerAll ),
436+ transport := remote .DefaultTransport .(* http.Transport ).Clone ()
437+ transport .TLSClientConfig = & tls.Config {
438+ InsecureSkipVerify : true , //nolint: gosec
439+ }
440+
441+ options = append (options , crane .WithTransport (transport ))
442+ },
443+ }
444+
445+ // headerTransport sets headers on outgoing requests.
446+ type headerTransport struct {
447+ httpHeaders map [string ]string
448+ inner http.RoundTripper
449+ }
450+
451+ // RoundTrip implements http.RoundTripper.
452+ func (ht * headerTransport ) RoundTrip (in * http.Request ) (* http.Response , error ) {
453+ for k , v := range ht .httpHeaders {
454+ if http .CanonicalHeaderKey (k ) == "User-Agent" {
455+ // Docker sets this, which is annoying, since we're not docker.
456+ // We might want to revisit completely ignoring this.
457+ continue
458+ }
459+ in .Header .Set (k , v )
460+ }
461+ return ht .inner .RoundTrip (in )
462+ }
463+
464+ var options []crane.Option
465+
466+ var imageRegistryCreateCommand = & cobra.Command {
467+ Use : "create <path>" ,
468+ Short : "Create a local OCI from a list of images" ,
469+ Long : `Create a local OCI from a list of images` ,
470+ Example : fmt .Sprintf (
471+ `talosctl images registry create --images=ghcr.io/siderolabs/kubelet:v%s /tmp/registry
472+
473+ Alternatively, stdin can be piped to the command:
474+ talosctl images source-bundle | talosctl images registry create /tmp/registry --images=-
475+ ` ,
476+ constants .DefaultKubernetesVersion ,
477+ ),
478+ Args : cobra .ExactArgs (1 ),
479+ RunE : func (cmd * cobra.Command , args []string ) error {
480+ var err error
481+
482+ storagePath := args [0 ]
483+
484+ lis , err := net .Listen ("tcp" , "127.0.0.1:0" )
485+ if err != nil {
486+ return fmt .Errorf ("error finding free port: %w" , err )
487+ }
488+
489+ addr := lis .Addr ().String ()
490+
491+ lis .Close () //nolint:errcheck
492+
493+ log := slog .New (slog .DiscardHandler )
494+
495+ if imageRegistryCreateCmdFlags .debug {
496+ logs .Debug .SetOutput (os .Stderr )
497+ log = slog .Default ()
498+ }
499+
500+ reg := olareg .New (config.Config {
501+ HTTP : config.ConfigHTTP {
502+ Addr : addr ,
503+ },
504+ API : config.ConfigAPI {
505+ PushEnabled : pointer .To (true ),
506+ DeleteEnabled : pointer .To (false ),
507+ Blob : config.ConfigAPIBlob {
508+ DeleteEnabled : pointer .To (false ),
509+ },
510+ },
511+ Storage : config.ConfigStorage {
512+ StoreType : config .StoreDir ,
513+ RootDir : storagePath ,
514+ ReadOnly : pointer .To (false ),
515+ GC : config.ConfigGC {
516+ Frequency : - 1 , // disabled
517+ },
518+ },
519+ Log : log ,
520+ })
521+
522+ ctx , cancel := signal .NotifyContext (cmd .Context (), os .Interrupt )
523+ defer cancel ()
524+
525+ eg , ctx := errgroup .WithContext (ctx )
526+
527+ eg .Go (func () error {
528+ slog .Info ("starting registry" , "path" , storagePath )
529+
530+ return reg .Run (ctx )
531+ })
532+
533+ eg .Go (func () error {
534+ <- ctx .Done ()
535+
536+ ctx2 , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
537+ defer cancel ()
538+
539+ return reg .Shutdown (ctx2 )
540+ })
541+
542+ eg .Go (func () error {
543+ if imageRegistryCreateCmdFlags .images [0 ] == "-" {
544+ var imagesListData strings.Builder
545+
546+ if _ , err := io .Copy (& imagesListData , os .Stdin ); err != nil {
547+ return fmt .Errorf ("error reading from stdin: %w" , err )
548+ }
549+
550+ imageRegistryCreateCmdFlags .images = strings .Split (strings .Trim (imagesListData .String (), "\n " ), "\n " )
551+ }
552+
553+ slog .Info ("starting image mirror" , "images" , len (imageRegistryCreateCmdFlags .images ))
554+
555+ if err := artifacts .Mirror (ctx , options , imageRegistryCreateCmdFlags .images , addr ); err != nil {
556+ return fmt .Errorf ("error copying images: %w" , err )
557+ }
558+
559+ cancel ()
560+
561+ return nil
562+ })
563+
564+ if err := eg .Wait (); err != nil {
565+ return err
566+ }
567+
568+ return nil
569+ },
570+ }
571+
572+ var imageRegistryCreateCmdFlags struct {
573+ debug bool
574+ images []string
575+ }
576+
577+ var imageRegistryServeCommand = & cobra.Command {
578+ Use : "serve <path>" ,
579+ Short : "Serve images from a local storage" ,
580+ Long : `` ,
581+ Args : cobra .ExactArgs (1 ),
582+ RunE : func (cmd * cobra.Command , args []string ) error {
583+ storePath := args [0 ]
584+
585+ reg := olareg .New (config.Config {
586+ HTTP : config.ConfigHTTP {
587+ Addr : imageRegistryServeCmdFlags .address ,
588+ CertFile : imageRegistryServeCmdFlags .tlsCertFile ,
589+ KeyFile : imageRegistryServeCmdFlags .tlsKeyFile ,
590+ },
591+ API : config.ConfigAPI {
592+ PushEnabled : pointer .To (false ),
593+ DeleteEnabled : pointer .To (false ),
594+ Blob : config.ConfigAPIBlob {
595+ DeleteEnabled : pointer .To (false ),
596+ },
597+ },
598+ Storage : config.ConfigStorage {
599+ StoreType : config .StoreDir ,
600+ RootDir : storePath ,
601+ ReadOnly : pointer .To (true ),
602+ GC : config.ConfigGC {
603+ Frequency : - 1 , // disabled
604+ },
605+ },
606+ Log : slog .Default (),
607+ })
608+
609+ ctx , cancel := signal .NotifyContext (cmd .Context (), os .Interrupt )
610+ defer cancel ()
611+
612+ eg , ctx := errgroup .WithContext (ctx )
613+
614+ eg .Go (func () error {
615+ slog .Info ("Starting registry" , "addr" , imageRegistryServeCmdFlags .address , "path" , storePath )
616+
617+ return reg .Run (ctx )
618+ })
619+
620+ eg .Go (func () error {
621+ <- ctx .Done ()
622+
623+ slog .Info ("Shutting down" )
624+
625+ ctx2 , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
626+ defer cancel ()
627+
628+ return reg .Shutdown (ctx2 )
629+ })
630+
631+ return eg .Wait ()
632+ },
633+ }
634+
635+ var imageRegistryServeCmdFlags struct {
636+ address string
637+ tlsCertFile string
638+ tlsKeyFile string
407639}
408640
409641func init () {
@@ -415,10 +647,9 @@ func init() {
415647
416648 imageCmd .AddCommand (imageListCmd )
417649 imageCmd .AddCommand (imagePullCmd )
418- imageCmd .AddCommand (imageCacheCreateCmd )
419- imageCmd .AddCommand (imageIntegrationCmd )
420650 imageCmd .AddCommand (imageSourceBundleCmd )
421651
652+ imageCmd .AddCommand (imageCacheCreateCmd )
422653 imageCacheCreateCmd .PersistentFlags ().StringVar (& imageCacheCreateCmdFlags .imageCachePath , "image-cache-path" , "" , "directory to save the image cache in OCI format" )
423654 imageCacheCreateCmd .MarkPersistentFlagRequired ("image-cache-path" ) //nolint:errcheck
424655 imageCacheCreateCmd .PersistentFlags ().StringVar (& imageCacheCreateCmdFlags .imageLayerCachePath , "image-layer-cache-path" , "" , "directory to save the image layer cache" )
@@ -428,8 +659,19 @@ func init() {
428659 imageCacheCreateCmd .PersistentFlags ().BoolVar (& imageCacheCreateCmdFlags .insecure , "insecure" , false , "allow insecure registries" )
429660 imageCacheCreateCmd .PersistentFlags ().BoolVar (& imageCacheCreateCmdFlags .force , "force" , false , "force overwrite of existing image cache" )
430661
662+ imageCmd .AddCommand (imageIntegrationCmd )
431663 imageIntegrationCmd .PersistentFlags ().StringVar (& imageIntegrationCmdFlags .installerTag , "installer-tag" , "" , "tag of the installer image to use" )
432664 imageIntegrationCmd .MarkPersistentFlagRequired ("installer-tag" ) //nolint:errcheck
433665 imageIntegrationCmd .PersistentFlags ().StringVar (& imageIntegrationCmdFlags .registryAndUser , "registry-and-user" , "" , "registry and user to use for the images" )
434666 imageIntegrationCmd .MarkPersistentFlagRequired ("registry-and-user" ) //nolint:errcheck
667+
668+ imageCmd .AddCommand (imageRegistryCommand )
669+ imageRegistryCommand .AddCommand (imageRegistryCreateCommand )
670+ imageRegistryCreateCommand .PersistentFlags ().BoolVar (& imageRegistryCreateCmdFlags .debug , "debug" , false , "enable debug logging" )
671+ imageRegistryCreateCommand .PersistentFlags ().StringSliceVar (& imageRegistryCreateCmdFlags .images , "images" , nil , "images to cache" )
672+ imageRegistryCreateCommand .MarkPersistentFlagRequired ("images" ) //nolint:errcheck
673+ imageRegistryCommand .AddCommand (imageRegistryServeCommand )
674+ imageRegistryServeCommand .PersistentFlags ().StringVar (& imageRegistryServeCmdFlags .address , "addr" , ":5000" , "address to serve the registry on" )
675+ imageRegistryServeCommand .PersistentFlags ().StringVar (& imageRegistryServeCmdFlags .tlsCertFile , "tls-cert-file" , "" , "path to TLS certificate file" )
676+ imageRegistryServeCommand .PersistentFlags ().StringVar (& imageRegistryServeCmdFlags .tlsKeyFile , "tls-key-file" , "" , "path to TLS key file" )
435677}
0 commit comments