@@ -7,19 +7,31 @@ 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"
31+ "github.com/siderolabs/go-pointer"
2132 "github.com/spf13/cobra"
2233 "github.com/spf13/pflag"
34+ "golang.org/x/sync/errgroup"
2335
2436 "github.com/siderolabs/talos/cmd/talosctl/pkg/talos/artifacts"
2537 "github.com/siderolabs/talos/cmd/talosctl/pkg/talos/helpers"
@@ -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,202 @@ 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 .Progress .SetOutput (os .Stderr )
402427
403- var imageDefaultCmdFlags = struct {
404- provisioner pflag.Value
405- }{
406- provisioner : helpers .StringChoice (provisionerInstaller , provisionerDocker , provisionerAll ),
428+ transport := remote .DefaultTransport .(* http.Transport ).Clone ()
429+ transport .TLSClientConfig = & tls.Config {
430+ InsecureSkipVerify : true , //nolint: gosec
431+ }
432+
433+ options = append (options ,
434+ crane .WithJobs (0 ),
435+ crane .Insecure ,
436+ crane .WithNondistributable (),
437+ crane .WithTransport (transport ),
438+ )
439+ },
440+ }
441+
442+ var options []crane.Option
443+
444+ var imageRegistryCreateCommand = & cobra.Command {
445+ Use : "create <path>" ,
446+ Short : "Create a local OCI from a list of images" ,
447+ Long : `Create a local OCI from a list of images` ,
448+ Example : fmt .Sprintf (
449+ `talosctl images registry create --images=ghcr.io/siderolabs/kubelet:v%s /tmp/registry
450+
451+ Alternatively, stdin can be piped to the command:
452+ talosctl images source-bundle | talosctl images registry create /tmp/registry --images=-
453+ ` ,
454+ constants .DefaultKubernetesVersion ,
455+ ),
456+ Args : cobra .ExactArgs (1 ),
457+ RunE : func (cmd * cobra.Command , args []string ) error {
458+ var err error
459+
460+ storagePath := args [0 ]
461+
462+ lis , err := net .Listen ("tcp" , "127.0.0.1:0" )
463+ if err != nil {
464+ return fmt .Errorf ("error finding free port: %w" , err )
465+ }
466+
467+ addr := lis .Addr ().String ()
468+
469+ lis .Close () //nolint:errcheck
470+
471+ log := slog .New (slog .DiscardHandler )
472+
473+ if imageRegistryCreateCmdFlags .debug {
474+ logs .Debug .SetOutput (os .Stderr )
475+ log = slog .Default ()
476+ }
477+
478+ reg := olareg .New (config.Config {
479+ HTTP : config.ConfigHTTP {
480+ Addr : addr ,
481+ },
482+ API : config.ConfigAPI {
483+ PushEnabled : pointer .To (true ),
484+ DeleteEnabled : pointer .To (false ),
485+ Blob : config.ConfigAPIBlob {
486+ DeleteEnabled : pointer .To (false ),
487+ },
488+ },
489+ Storage : config.ConfigStorage {
490+ StoreType : config .StoreDir ,
491+ RootDir : storagePath ,
492+ ReadOnly : pointer .To (false ),
493+ GC : config.ConfigGC {
494+ Frequency : - 1 , // disabled
495+ },
496+ },
497+ Log : log ,
498+ })
499+
500+ ctx , cancel := signal .NotifyContext (cmd .Context (), os .Interrupt )
501+ defer cancel ()
502+
503+ eg , ctx := errgroup .WithContext (ctx )
504+
505+ eg .Go (func () error {
506+ slog .Info ("starting registry" , "path" , storagePath )
507+
508+ return reg .Run (ctx )
509+ })
510+
511+ eg .Go (func () error {
512+ <- ctx .Done ()
513+
514+ ctx2 , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
515+ defer cancel ()
516+
517+ return reg .Shutdown (ctx2 )
518+ })
519+
520+ eg .Go (func () error {
521+ if imageRegistryCreateCmdFlags .images [0 ] == "-" {
522+ var imagesListData strings.Builder
523+
524+ if _ , err := io .Copy (& imagesListData , os .Stdin ); err != nil {
525+ return fmt .Errorf ("error reading from stdin: %w" , err )
526+ }
527+
528+ imageRegistryCreateCmdFlags .images = strings .Split (strings .Trim (imagesListData .String (), "\n " ), "\n " )
529+ }
530+
531+ slog .Info ("starting image mirror" , "images" , len (imageRegistryCreateCmdFlags .images ))
532+
533+ if err := artifacts .Mirror (ctx , options , imageRegistryCreateCmdFlags .images , addr ); err != nil {
534+ return fmt .Errorf ("error copying images: %w" , err )
535+ }
536+
537+ cancel ()
538+
539+ return nil
540+ })
541+
542+ if err := eg .Wait (); err != nil {
543+ return err
544+ }
545+
546+ return nil
547+ },
548+ }
549+
550+ var imageRegistryCreateCmdFlags struct {
551+ debug bool
552+ images []string
553+ }
554+
555+ var imageRegistryServeCommand = & cobra.Command {
556+ Use : "serve <path>" ,
557+ Short : "Serve images from a local storage" ,
558+ Long : `` ,
559+ Args : cobra .ExactArgs (1 ),
560+ RunE : func (cmd * cobra.Command , args []string ) error {
561+ storePath := args [0 ]
562+
563+ reg := olareg .New (config.Config {
564+ HTTP : config.ConfigHTTP {
565+ Addr : imageRegistryServeCmdFlags .address ,
566+ CertFile : imageRegistryServeCmdFlags .tlsCertFile ,
567+ KeyFile : imageRegistryServeCmdFlags .tlsKeyFile ,
568+ },
569+ API : config.ConfigAPI {
570+ PushEnabled : pointer .To (false ),
571+ DeleteEnabled : pointer .To (false ),
572+ Blob : config.ConfigAPIBlob {
573+ DeleteEnabled : pointer .To (false ),
574+ },
575+ },
576+ Storage : config.ConfigStorage {
577+ StoreType : config .StoreDir ,
578+ RootDir : storePath ,
579+ ReadOnly : pointer .To (true ),
580+ GC : config.ConfigGC {
581+ Frequency : - 1 , // disabled
582+ },
583+ },
584+ Log : slog .Default (),
585+ })
586+
587+ ctx , cancel := signal .NotifyContext (cmd .Context (), os .Interrupt )
588+ defer cancel ()
589+
590+ eg , ctx := errgroup .WithContext (ctx )
591+
592+ eg .Go (func () error {
593+ slog .Info ("Starting registry" , "addr" , imageRegistryServeCmdFlags .address , "path" , storePath )
594+
595+ return reg .Run (ctx )
596+ })
597+
598+ eg .Go (func () error {
599+ <- ctx .Done ()
600+
601+ slog .Info ("Shutting down" )
602+
603+ ctx2 , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
604+ defer cancel ()
605+
606+ return reg .Shutdown (ctx2 )
607+ })
608+
609+ return eg .Wait ()
610+ },
611+ }
612+
613+ var imageRegistryServeCmdFlags struct {
614+ address string
615+ tlsCertFile string
616+ tlsKeyFile string
407617}
408618
409619func init () {
@@ -415,10 +625,9 @@ func init() {
415625
416626 imageCmd .AddCommand (imageListCmd )
417627 imageCmd .AddCommand (imagePullCmd )
418- imageCmd .AddCommand (imageCacheCreateCmd )
419- imageCmd .AddCommand (imageIntegrationCmd )
420628 imageCmd .AddCommand (imageSourceBundleCmd )
421629
630+ imageCmd .AddCommand (imageCacheCreateCmd )
422631 imageCacheCreateCmd .PersistentFlags ().StringVar (& imageCacheCreateCmdFlags .imageCachePath , "image-cache-path" , "" , "directory to save the image cache in OCI format" )
423632 imageCacheCreateCmd .MarkPersistentFlagRequired ("image-cache-path" ) //nolint:errcheck
424633 imageCacheCreateCmd .PersistentFlags ().StringVar (& imageCacheCreateCmdFlags .imageLayerCachePath , "image-layer-cache-path" , "" , "directory to save the image layer cache" )
@@ -428,8 +637,19 @@ func init() {
428637 imageCacheCreateCmd .PersistentFlags ().BoolVar (& imageCacheCreateCmdFlags .insecure , "insecure" , false , "allow insecure registries" )
429638 imageCacheCreateCmd .PersistentFlags ().BoolVar (& imageCacheCreateCmdFlags .force , "force" , false , "force overwrite of existing image cache" )
430639
640+ imageCmd .AddCommand (imageIntegrationCmd )
431641 imageIntegrationCmd .PersistentFlags ().StringVar (& imageIntegrationCmdFlags .installerTag , "installer-tag" , "" , "tag of the installer image to use" )
432642 imageIntegrationCmd .MarkPersistentFlagRequired ("installer-tag" ) //nolint:errcheck
433643 imageIntegrationCmd .PersistentFlags ().StringVar (& imageIntegrationCmdFlags .registryAndUser , "registry-and-user" , "" , "registry and user to use for the images" )
434644 imageIntegrationCmd .MarkPersistentFlagRequired ("registry-and-user" ) //nolint:errcheck
645+
646+ imageCmd .AddCommand (imageRegistryCommand )
647+ imageRegistryCommand .AddCommand (imageRegistryCreateCommand )
648+ imageRegistryCreateCommand .PersistentFlags ().BoolVar (& imageRegistryCreateCmdFlags .debug , "debug" , false , "enable debug logging" )
649+ imageRegistryCreateCommand .PersistentFlags ().StringSliceVar (& imageRegistryCreateCmdFlags .images , "images" , nil , "images to cache" )
650+ imageRegistryCreateCommand .MarkPersistentFlagRequired ("images" ) //nolint:errcheck
651+ imageRegistryCommand .AddCommand (imageRegistryServeCommand )
652+ imageRegistryServeCommand .PersistentFlags ().StringVar (& imageRegistryServeCmdFlags .address , "addr" , ":5000" , "address to serve the registry on" )
653+ imageRegistryServeCommand .PersistentFlags ().StringVar (& imageRegistryServeCmdFlags .tlsCertFile , "tls-cert-file" , "" , "path to TLS certificate file" )
654+ imageRegistryServeCommand .PersistentFlags ().StringVar (& imageRegistryServeCmdFlags .tlsKeyFile , "tls-key-file" , "" , "path to TLS key file" )
435655}
0 commit comments