Skip to content

Commit dbdd2b2

Browse files
committed
feat: add static registry to talosctl
Fixes #11928 Fixes #11929 Signed-off-by: Mateusz Urbanek <mateusz.urbanek@siderolabs.com>
1 parent 77d8cc7 commit dbdd2b2

File tree

9 files changed

+414
-163
lines changed

9 files changed

+414
-163
lines changed

cmd/talosctl/cmd/talos/image.go

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"io"
1313
"os"
14+
"os/signal"
1415
"slices"
1516
"strings"
1617
"text/tabwriter"
@@ -20,9 +21,11 @@ import (
2021
"github.com/dustin/go-humanize"
2122
"github.com/spf13/cobra"
2223
"github.com/spf13/pflag"
24+
"go.uber.org/zap"
2325

2426
"github.com/siderolabs/talos/cmd/talosctl/pkg/talos/artifacts"
2527
"github.com/siderolabs/talos/cmd/talosctl/pkg/talos/helpers"
28+
"github.com/siderolabs/talos/internal/app/machined/pkg/system/services/registry"
2629
"github.com/siderolabs/talos/pkg/imager/cache"
2730
"github.com/siderolabs/talos/pkg/images"
2831
"github.com/siderolabs/talos/pkg/machinery/api/common"
@@ -168,14 +171,24 @@ var imageDefaultCmd = &cobra.Command{
168171
},
169172
}
170173

171-
var minimumVersion = semver.MustParse("1.11.0-alpha.0")
174+
const (
175+
provisionerDocker = "docker"
176+
provisionerInstaller = "installer"
177+
provisionerAll = "all"
178+
)
179+
180+
var imageDefaultCmdFlags = struct {
181+
provisioner pflag.Value
182+
}{
183+
provisioner: helpers.StringChoice(provisionerInstaller, provisionerDocker, provisionerAll),
184+
}
172185

173186
// imageSourceBundleCmd represents the image source-bundle command.
174187
var imageSourceBundleCmd = &cobra.Command{
175188
Use: "source-bundle <talos-version>",
176189
Short: "List the source images used for building Talos",
177190
Long: ``,
178-
Args: helpers.ChainCobraPositionalArgs(
191+
Args: cobra.MatchAll(
179192
cobra.ExactArgs(1),
180193
func(cmd *cobra.Command, args []string) error {
181194
maximumVersion, err := semver.ParseTolerant(version.Tag)
@@ -245,6 +258,8 @@ var imageSourceBundleCmd = &cobra.Command{
245258
},
246259
}
247260

261+
var minimumVersion = semver.MustParse("1.11.0-alpha.0")
262+
248263
// imageIntegrationCmd represents the integration image command.
249264
var imageIntegrationCmd = &cobra.Command{
250265
Use: "integration",
@@ -374,6 +389,7 @@ talosctl images default | talosctl images cache-create --image-cache-path=/tmp/t
374389
imageCacheCreateCmdFlags.insecure,
375390
imageCacheCreateCmdFlags.imageLayerCachePath,
376391
imageCacheCreateCmdFlags.imageCachePath,
392+
imageCacheCreateCmdFlags.layout.String() == layoutFlat,
377393
)
378394
if err != nil {
379395
return fmt.Errorf("error generating cache: %w", err)
@@ -383,27 +399,65 @@ talosctl images default | talosctl images cache-create --image-cache-path=/tmp/t
383399
},
384400
}
385401

386-
var imageCacheCreateCmdFlags struct {
402+
const (
403+
layoutOCI = "oci"
404+
layoutFlat = "flat"
405+
)
406+
407+
var imageCacheCreateCmdFlags = struct {
387408
imageCachePath string
388409
imageLayerCachePath string
389-
platform string
410+
layout pflag.Value
411+
platform []string
390412

391413
images []string
392414

393415
insecure bool
394416
force bool
417+
}{
418+
layout: helpers.StringChoice(layoutOCI, layoutFlat),
395419
}
396420

397-
const (
398-
provisionerDocker = "docker"
399-
provisionerInstaller = "installer"
400-
provisionerAll = "all"
401-
)
421+
// imageCacheServeCmd represents the image cache serve command.
422+
var imageCacheServeCmd = &cobra.Command{
423+
Use: "cache-serve",
424+
Short: "Serve an OCI image cache directory over HTTP(S) as a container registry",
425+
Long: `Serve an OCI image cache directory over HTTP(S) as a container registry`,
426+
Example: ``,
427+
Args: cobra.NoArgs,
428+
RunE: func(cmd *cobra.Command, args []string) error {
429+
ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt)
430+
defer cancel()
402431

403-
var imageDefaultCmdFlags = struct {
404-
provisioner pflag.Value
405-
}{
406-
provisioner: helpers.StringChoice(provisionerInstaller, provisionerDocker, provisionerAll),
432+
development, err := zap.NewDevelopment()
433+
if err != nil {
434+
return fmt.Errorf("failed to create development logger: %w", err)
435+
}
436+
437+
it := func(yield func(string) bool) {
438+
for _, root := range []string{imageCacheServeCmdFlags.imageCachePath} {
439+
if !yield(root) {
440+
return
441+
}
442+
}
443+
}
444+
445+
return registry.NewService(registry.NewMultiPathFS(it), development).Run(
446+
ctx,
447+
registry.WithTLS(
448+
imageCacheServeCmdFlags.tlsCertFile,
449+
imageCacheServeCmdFlags.tlsKeyFile,
450+
),
451+
registry.WithAddress(imageCacheServeCmdFlags.address),
452+
)
453+
},
454+
}
455+
456+
var imageCacheServeCmdFlags struct {
457+
imageCachePath string
458+
address string
459+
tlsCertFile string
460+
tlsKeyFile string
407461
}
408462

409463
func init() {
@@ -415,19 +469,28 @@ func init() {
415469

416470
imageCmd.AddCommand(imageListCmd)
417471
imageCmd.AddCommand(imagePullCmd)
418-
imageCmd.AddCommand(imageCacheCreateCmd)
419-
imageCmd.AddCommand(imageIntegrationCmd)
420472
imageCmd.AddCommand(imageSourceBundleCmd)
421473

474+
imageCmd.AddCommand(imageCacheCreateCmd)
422475
imageCacheCreateCmd.PersistentFlags().StringVar(&imageCacheCreateCmdFlags.imageCachePath, "image-cache-path", "", "directory to save the image cache in OCI format")
423476
imageCacheCreateCmd.MarkPersistentFlagRequired("image-cache-path") //nolint:errcheck
424477
imageCacheCreateCmd.PersistentFlags().StringVar(&imageCacheCreateCmdFlags.imageLayerCachePath, "image-layer-cache-path", "", "directory to save the image layer cache")
425-
imageCacheCreateCmd.PersistentFlags().StringVar(&imageCacheCreateCmdFlags.platform, "platform", "linux/amd64", "platform to use for the cache")
478+
imageCacheCreateCmd.PersistentFlags().Var(imageCacheCreateCmdFlags.layout, "layout",
479+
"Specifies the cache layout format: \"oci\" for an OCI image layout directory, or \"flat\" for a registry-like flat file structure")
480+
imageCacheCreateCmd.PersistentFlags().StringSliceVar(&imageCacheCreateCmdFlags.platform, "platform", []string{"linux/amd64"}, "platform to use for the cache")
426481
imageCacheCreateCmd.PersistentFlags().StringSliceVar(&imageCacheCreateCmdFlags.images, "images", nil, "images to cache")
427482
imageCacheCreateCmd.MarkPersistentFlagRequired("images") //nolint:errcheck
428483
imageCacheCreateCmd.PersistentFlags().BoolVar(&imageCacheCreateCmdFlags.insecure, "insecure", false, "allow insecure registries")
429484
imageCacheCreateCmd.PersistentFlags().BoolVar(&imageCacheCreateCmdFlags.force, "force", false, "force overwrite of existing image cache")
430485

486+
imageCmd.AddCommand(imageCacheServeCmd)
487+
imageCacheServeCmd.PersistentFlags().StringVar(&imageCacheServeCmdFlags.imageCachePath, "image-cache-path", "", "directory to save the image cache in OCI format")
488+
imageCacheServeCmd.MarkPersistentFlagRequired("image-cache-path") //nolint:errcheck
489+
imageCacheServeCmd.PersistentFlags().StringVar(&imageCacheServeCmdFlags.address, "address", constants.RegistrydListenAddress, "address to serve the registry on")
490+
imageCacheServeCmd.PersistentFlags().StringVar(&imageCacheServeCmdFlags.tlsCertFile, "tls-cert-file", "", "TLS certificate file to use for serving")
491+
imageCacheServeCmd.PersistentFlags().StringVar(&imageCacheServeCmdFlags.tlsKeyFile, "tls-key-file", "", "TLS key file to use for serving")
492+
493+
imageCmd.AddCommand(imageIntegrationCmd)
431494
imageIntegrationCmd.PersistentFlags().StringVar(&imageIntegrationCmdFlags.installerTag, "installer-tag", "", "tag of the installer image to use")
432495
imageIntegrationCmd.MarkPersistentFlagRequired("installer-tag") //nolint:errcheck
433496
imageIntegrationCmd.PersistentFlags().StringVar(&imageIntegrationCmdFlags.registryAndUser, "registry-and-user", "", "registry and user to use for the images")

cmd/talosctl/pkg/talos/helpers/flags.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"fmt"
99
"slices"
1010

11-
"github.com/spf13/cobra"
1211
"github.com/spf13/pflag"
1312
)
1413

@@ -52,16 +51,3 @@ func StringChoice(defaultValue string, otherChoices ...string) pflag.Value {
5251
},
5352
}
5453
}
55-
56-
// ChainCobraPositionalArgs chains multiple cobra.PositionalArgs validators together.
57-
func ChainCobraPositionalArgs(validators ...cobra.PositionalArgs) cobra.PositionalArgs {
58-
return func(cmd *cobra.Command, args []string) error {
59-
for _, validator := range validators {
60-
if err := validator(cmd, args); err != nil {
61-
return err
62-
}
63-
}
64-
65-
return nil
66-
}
67-
}

hack/release.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,21 @@ These fields were removed from the default machine configuration schema in v1.12
103103
etcd container image is now pulled from `registry.k8s.io/etcd` instead of `gcr.io/etcd-development/etcd`.
104104
"""
105105

106+
[notes.talosctl]
107+
title = "talosctl image cache-serve"
108+
description = """\
109+
`talosctl` includes new subcommand `image cache-serve`.
110+
It allows serving the created OCI image registry over HTTP/HTTPS.
111+
It is a read-only registry, meaning images cannot be pushed to it, but the backing storage can be updated by re-running the `cache-create` command;
112+
113+
Additionally `talosctl image cache-create` has some changes:
114+
* new flag `--layout`: `oci` (_default_), `flat`:
115+
* `oci` preserves current behavior;
116+
* `flat` does not repack artifact layer, but moves it to a destination directory, allowing it to be served by `talosctl image cache-serve`;
117+
* changed flag `--platform`: now can accept multiple os/arch combinations:
118+
* comma separated (`--platform=linux/amd64,linux/arm64`);
119+
* multiple instances (`--platform=linux/amd64 --platform=linux/arm64`);
120+
"""
106121

107122
[make_deps]
108123

internal/app/machined/pkg/system/services/registry/params.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ import (
1515

1616
func extractParams(req *http.Request) (params, error) {
1717
registry := req.URL.Query().Get("ns")
18-
if registry == "" {
19-
return params{}, xerrors.NewTaggedf[badRequestTag]("missing ns")
20-
}
2118

2219
value := req.PathValue("args")
2320

internal/app/machined/pkg/system/services/registry/registry.go

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,32 @@ type Service struct {
4444
root fs.StatFS
4545
}
4646

47+
type config struct {
48+
addr string
49+
tlsKeyPath string
50+
tlsCertPath string
51+
}
52+
53+
// Option is a functional option for configuring the service.
54+
type Option func(*config)
55+
56+
// WithTLS enables TLS with the given certificate and key paths.
57+
func WithTLS(certPath, keyPath string) Option {
58+
return func(c *config) {
59+
c.tlsCertPath = certPath
60+
c.tlsKeyPath = keyPath
61+
}
62+
}
63+
64+
// WithAddress sets the address to listen on.
65+
func WithAddress(addr string) Option {
66+
return func(c *config) {
67+
c.addr = addr
68+
}
69+
}
70+
4771
// Run is an entrypoint to the API service.
48-
func (svc *Service) Run(ctx context.Context) error {
72+
func (svc *Service) Run(ctx context.Context, options ...Option) error {
4973
mux := http.NewServeMux()
5074

5175
mux.HandleFunc("GET /v2/{args...}", svc.serveHTTP)
@@ -56,7 +80,15 @@ func (svc *Service) Run(ctx context.Context) error {
5680
mux.HandleFunc("GET /"+p+"/{$}", giveOk)
5781
}
5882

59-
server := http.Server{Addr: constants.RegistrydListenAddress, Handler: mux}
83+
cfg := &config{
84+
addr: constants.RegistrydListenAddress,
85+
}
86+
87+
for _, option := range options {
88+
option(cfg)
89+
}
90+
91+
server := http.Server{Addr: cfg.addr, Handler: mux}
6092
errCh := make(chan error, 1)
6193

6294
ctx, cancel := context.WithCancel(ctx)
@@ -73,9 +105,18 @@ func (svc *Service) Run(ctx context.Context) error {
73105

74106
svc.logger.Info("starting registry server", zap.String("addr", server.Addr))
75107

76-
err := server.ListenAndServe()
77-
if errors.Is(err, http.ErrServerClosed) {
78-
err = nil
108+
var err error
109+
110+
if cfg.tlsCertPath != "" && cfg.tlsKeyPath != "" {
111+
err = server.ListenAndServeTLS(cfg.tlsCertPath, cfg.tlsKeyPath)
112+
if errors.Is(err, http.ErrServerClosed) {
113+
err = nil
114+
}
115+
} else {
116+
err = server.ListenAndServe()
117+
if errors.Is(err, http.ErrServerClosed) {
118+
err = nil
119+
}
79120
}
80121

81122
cancel()
@@ -94,6 +135,7 @@ func (svc *Service) serveHTTP(w http.ResponseWriter, req *http.Request) {
94135
}
95136
}
96137

138+
//nolint:gocyclo
97139
func (svc *Service) handler(w http.ResponseWriter, req *http.Request) error {
98140
logger := svc.logger.With(
99141
zap.String("method", req.Method),
@@ -114,6 +156,17 @@ func (svc *Service) handler(w http.ResponseWriter, req *http.Request) error {
114156
zap.String("registry", p.registry),
115157
)
116158

159+
if p.registry == "" {
160+
p.registry, err = svc.tryFindRegistry(p)
161+
if err != nil {
162+
return err
163+
}
164+
165+
if p.registry == "" {
166+
return fmt.Errorf("failed to extract params: %w", xerrors.NewTaggedf[badRequestTag]("missing ns"))
167+
}
168+
}
169+
117170
ref, err := svc.resolveCanonicalRef(p)
118171
if err != nil {
119172
return err
@@ -292,3 +345,29 @@ type (
292345
badRequestTag struct{}
293346
internalErrorTag struct{}
294347
)
348+
349+
func (svc *Service) tryFindRegistry(p params) (string, error) {
350+
entries, err := fs.ReadDir(svc.root, "manifests")
351+
if err != nil {
352+
return "", xerrors.NewTaggedf[internalErrorTag]("failed to read manifests directory: %w", err)
353+
}
354+
355+
for _, entry := range entries {
356+
p.registry = entry.Name()
357+
358+
ref, err := reference.ParseDockerRef(p.String())
359+
if err != nil {
360+
return "", xerrors.NewTaggedf[badRequestTag]("failed to parse docker ref: %w", err)
361+
}
362+
363+
namedTaggedName := handleRegistryWithPort(ref, p)
364+
365+
taggedFile := filepath.Join("manifests", namedTaggedName, "reference")
366+
367+
if _, err := fs.Stat(svc.root, taggedFile); err == nil {
368+
return entry.Name(), nil
369+
}
370+
}
371+
372+
return "", nil
373+
}

internal/app/machined/pkg/system/services/registry/registry_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func TestRegistry(t *testing.T) {
4848
platform, err := v1.ParsePlatform("linux/amd64")
4949
assert.NoError(t, err)
5050

51-
assert.NoError(t, cache.Generate(images, platform.String(), false, "", cacheDir))
51+
assert.NoError(t, cache.Generate(images, []string{platform.String()}, false, "", cacheDir, false))
5252

5353
l, err := layout.ImageIndexFromPath(cacheDir)
5454
assert.NoError(t, err)

0 commit comments

Comments
 (0)