Skip to content

Commit acc75fd

Browse files
committed
feat: add static registry to talosctl
Fixes #11928 Fixes #11929 Signed-off-by: Mateusz Urbanek <[email protected]>
1 parent 8b041a7 commit acc75fd

File tree

8 files changed

+489
-27
lines changed

8 files changed

+489
-27
lines changed

cmd/talosctl/cmd/talos/image.go

Lines changed: 255 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,32 @@ package talos
77
import (
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.
174196
var 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.
249273
var 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

409641
func 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

Comments
 (0)