diff --git a/packages/distros/k3s/common/zarf.yaml b/packages/distros/k3s/common/zarf.yaml index c11ef7e514..d58370ffc9 100644 --- a/packages/distros/k3s/common/zarf.yaml +++ b/packages/distros/k3s/common/zarf.yaml @@ -12,8 +12,8 @@ components: only: localOS: linux description: > - *** REQUIRES ROOT *** - Install K3s, certified Kubernetes distribution built for IoT & Edge computing. + *** REQUIRES ROOT (not sudo) *** + Install K3s, a certified Kubernetes distribution built for IoT & Edge computing. K3s provides the cluster need for Zarf running in Appliance Mode as well as can host a low-resource Gitops Service if not using an existing Kubernetes platform. actions: diff --git a/packages/zarf-registry/chart/templates/hpa.yaml b/packages/zarf-registry/chart/templates/hpa.yaml index 671b5e68d9..cdb4468872 100644 --- a/packages/zarf-registry/chart/templates/hpa.yaml +++ b/packages/zarf-registry/chart/templates/hpa.yaml @@ -26,14 +26,18 @@ spec: scaleDown: # Use 60 second stabilization window becuase zarf will freeze scale down during deploys stabilizationWindowSeconds: 60 + # Initially disable scale down - this gets set to Min later by Zarf (src/test/e2e/20_zarf_init_test.go) + selectPolicy: Disabled # Scale down one pod per minute policies: - type: Pods value: 1 - periodSeconds: 60 + periodSeconds: 60 scaleUp: # Delay initial checks by 30 seconds stabilizationWindowSeconds: 30 + # Scale up as much as is needed + selectPolicy: Max # Scale up one pod per minute policies: - type: Pods diff --git a/src/cmd/connect.go b/src/cmd/connect.go index fedde8606a..05a94eac8f 100644 --- a/src/cmd/connect.go +++ b/src/cmd/connect.go @@ -29,6 +29,7 @@ var ( if len(args) > 0 { target = args[0] } + spinner := message.NewProgressSpinner("Preparing a tunnel to connect to %s", target) tunnel, err := cluster.NewTunnel(connectNamespace, connectResourceType, connectResourceName, connectLocalPort, connectRemotePort) if err != nil { @@ -38,7 +39,10 @@ var ( if !cliOnly { tunnel.EnableAutoOpen() } + + tunnel.AddSpinner(spinner) tunnel.Connect(target, true) + spinner.Success() }, } diff --git a/src/cmd/destroy.go b/src/cmd/destroy.go index 1418d5f40a..512a576fb2 100644 --- a/src/cmd/destroy.go +++ b/src/cmd/destroy.go @@ -30,7 +30,7 @@ var destroyCmd = &cobra.Command{ Short: lang.CmdDestroyShort, Long: lang.CmdDestroyLong, Run: func(cmd *cobra.Command, args []string) { - c, err := cluster.NewClusterWithWait(30 * time.Second) + c, err := cluster.NewClusterWithWait(30*time.Second, true) if err != nil { message.Fatalf(err, lang.ErrNoClusterConnection) } diff --git a/src/cmd/package.go b/src/cmd/package.go index a7323a905a..7bc619dde2 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -89,6 +89,8 @@ var packageDeployCmd = &cobra.Command{ pkgClient := packager.NewOrDie(&pkgConfig) defer pkgClient.ClearTempPaths() + pterm.Println() + // Deploy the package if err := pkgClient.Deploy(); err != nil { message.Fatalf(err, "Failed to deploy package: %s", err.Error()) diff --git a/src/config/config.go b/src/config/config.go index f87d0b1514..4497e7ed00 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -134,6 +134,7 @@ func GetCraneOptions(insecure bool, archs ...string) []crane.Option { OS: "linux", Architecture: GetArch(archs...), }), + crane.WithUserAgent("zarf"), ) return options diff --git a/src/internal/api/cluster/cluster.go b/src/internal/api/cluster/cluster.go index 4e2624bde2..198358cf8f 100644 --- a/src/internal/api/cluster/cluster.go +++ b/src/internal/api/cluster/cluster.go @@ -26,8 +26,9 @@ func Summary(w http.ResponseWriter, _ *http.Request) { var hasZarf bool var k8sRevision string - c, err := cluster.NewClusterWithWait(5 * time.Second) + c, err := cluster.NewClusterWithWait(5*time.Second, false) rawConfig, _ := clientcmd.NewDefaultClientConfigLoadingRules().GetStartingConfig() + reachable = err == nil if reachable { distro, _ = c.Kube.DetectDistro() diff --git a/src/internal/cluster/common.go b/src/internal/cluster/common.go index ba6c67bdca..3c78c3236c 100644 --- a/src/internal/cluster/common.go +++ b/src/internal/cluster/common.go @@ -28,7 +28,7 @@ var labels = k8s.Labels{ // NewClusterOrDie creates a new cluster instance and waits up to 30 seconds for the cluster to be ready or throws a fatal error. func NewClusterOrDie() *Cluster { - c, err := NewClusterWithWait(defaultTimeout) + c, err := NewClusterWithWait(defaultTimeout, true) if err != nil { message.Fatalf(err, "Failed to connect to cluster") } @@ -37,19 +37,37 @@ func NewClusterOrDie() *Cluster { } // NewClusterWithWait creates a new cluster instance and waits for the given timeout for the cluster to be ready. -func NewClusterWithWait(timeout time.Duration) (*Cluster, error) { +func NewClusterWithWait(timeout time.Duration, withSpinner bool) (*Cluster, error) { + var spinner *message.Spinner + if withSpinner { + spinner = message.NewProgressSpinner("Waiting for cluster connection (%s timeout)", timeout.String()) + defer spinner.Stop() + } + c := &Cluster{} var err error + c.Kube, err = k8s.New(message.Debugf, labels) if err != nil { return c, err } - return c, c.Kube.WaitForHealthyCluster(timeout) + + err = c.Kube.WaitForHealthyCluster(timeout) + if err != nil { + return c, err + } + + if spinner != nil { + spinner.Success() + } + + return c, nil } // NewCluster creates a new cluster instance without waiting for the cluster to be ready. func NewCluster() (*Cluster, error) { + var err error c := &Cluster{} - c.Kube, _ = k8s.New(message.Debugf, labels) - return c, nil + c.Kube, err = k8s.New(message.Debugf, labels) + return c, err } diff --git a/src/internal/cluster/state.go b/src/internal/cluster/state.go index 928c39dcd3..6ea43f6c88 100644 --- a/src/internal/cluster/state.go +++ b/src/internal/cluster/state.go @@ -22,10 +22,11 @@ import ( // Zarf Cluster Constants. const ( - ZarfNamespace = "zarf" - ZarfStateSecretName = "zarf-state" - ZarfStateDataKey = "state" - ZarfPackageInfoLabel = "package-deploy-info" + ZarfNamespace = "zarf" + ZarfStateSecretName = "zarf-state" + ZarfStateDataKey = "state" + ZarfPackageInfoLabel = "package-deploy-info" + ZarfInitPackageInfoName = "zarf-package-init" ) // InitZarfState initializes the Zarf state with the given temporary directory and init configs. diff --git a/src/internal/cluster/tunnel.go b/src/internal/cluster/tunnel.go index ef81123edb..47c618226c 100644 --- a/src/internal/cluster/tunnel.go +++ b/src/internal/cluster/tunnel.go @@ -368,7 +368,6 @@ func (tunnel *Tunnel) establish() (string, error) { message.Debug("tunnel.Establish()") var err error - var spinner *message.Spinner // Track this locally as we may need to retry if the tunnel fails. localPort := tunnel.localPort @@ -390,6 +389,7 @@ func (tunnel *Tunnel) establish() (string, error) { defer globalMutex.Unlock() } + var spinner *message.Spinner spinnerMessage := fmt.Sprintf("Opening tunnel %d -> %d for %s/%s in namespace %s", localPort, tunnel.remotePort, @@ -402,8 +402,7 @@ func (tunnel *Tunnel) establish() (string, error) { spinner = tunnel.spinner spinner.Updatef(spinnerMessage) } else { - spinner = message.NewProgressSpinner(spinnerMessage) - defer spinner.Stop() + message.Debug(spinnerMessage) } kube, err := k8s.NewWithWait(message.Debugf, labels, defaultTimeout) @@ -455,19 +454,16 @@ func (tunnel *Tunnel) establish() (string, error) { // Wait for an error or the tunnel to be ready. select { case err = <-errChan: - if tunnel.spinner == nil { - spinner.Stop() - } return "", fmt.Errorf("unable to start the tunnel: %w", err) case <-portforwarder.Ready: // Store for endpoint output tunnel.localPort = localPort url := fmt.Sprintf("http://%s:%d%s", config.IPV4Localhost, localPort, tunnel.urlSuffix) msg := fmt.Sprintf("Creating port forwarding tunnel at %s", url) - if tunnel.spinner == nil { - spinner.Successf(msg) + if tunnel.spinner != nil { + spinner.Updatef("%s", msg) } else { - spinner.Updatef(msg) + message.Debug(msg) } return url, nil } diff --git a/src/internal/cluster/zarf.go b/src/internal/cluster/zarf.go index 28d509f850..0816a17e34 100644 --- a/src/internal/cluster/zarf.go +++ b/src/internal/cluster/zarf.go @@ -43,6 +43,21 @@ func (c *Cluster) GetDeployedZarfPackages() ([]types.DeployedPackage, error) { return deployedPackages, nil } +// GetDeployedPackage gets the metadata information about the package name provided (if it exists in the cluster). +// We determine what packages have been deployed to the cluster by looking for specific secrets in the Zarf namespace. +func (c *Cluster) GetDeployedPackage(packageName string) (types.DeployedPackage, error) { + var deployedPackage = types.DeployedPackage{} + + // Get the secret that describes the deployed init package + secret, err := c.Kube.GetSecret(ZarfNamespace, config.ZarfPackagePrefix+packageName) + if err != nil { + return deployedPackage, err + } + + err = json.Unmarshal(secret.Data["data"], &deployedPackage) + return deployedPackage, err +} + // StripZarfLabelsAndSecretsFromNamespaces removes metadata and secrets from existing namespaces no longer manged by Zarf. func (c *Cluster) StripZarfLabelsAndSecretsFromNamespaces() { spinner := message.NewProgressSpinner("Removing zarf metadata & secrets from existing namespaces not managed by Zarf") diff --git a/src/internal/packager/images/pull.go b/src/internal/packager/images/pull.go index c89b6b8950..32948059d4 100644 --- a/src/internal/packager/images/pull.go +++ b/src/internal/packager/images/pull.go @@ -48,11 +48,8 @@ func (i *ImgConfig) PullAll() error { spinner := message.NewProgressSpinner("Loading metadata for %d images. %s", imgCount, longer) defer spinner.Stop() - if message.GetLogLevel() >= message.DebugLevel { - spinner.EnablePreserveWrites() - logs.Warn.SetOutput(spinner) - logs.Progress.SetOutput(spinner) - } + logs.Warn.SetOutput(&message.DebugWriter{}) + logs.Progress.SetOutput(&message.DebugWriter{}) for idx, src := range i.ImgList { spinner.Updatef("Fetching image metadata (%d of %d): %s", idx+1, imgCount, src) diff --git a/src/internal/packager/images/push.go b/src/internal/packager/images/push.go index dbd9c9c239..655a46727f 100644 --- a/src/internal/packager/images/push.go +++ b/src/internal/packager/images/push.go @@ -5,12 +5,17 @@ package images import ( + "fmt" + "net/http" + "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/internal/cluster" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/transform" + "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/logs" + v1 "github.com/google/go-containerregistry/pkg/v1" ) // PushToZarfRegistry pushes a provided image into the configured Zarf registry @@ -18,58 +23,72 @@ import ( func (i *ImgConfig) PushToZarfRegistry() error { message.Debugf("images.PushToZarfRegistry(%#v)", i) - var ( - err error - tunnel *cluster.Tunnel - registryURL string - target string - ) + logs.Warn.SetOutput(&message.DebugWriter{}) + logs.Progress.SetOutput(&message.DebugWriter{}) - if i.RegInfo.InternalRegistry { - // Establish a registry tunnel to send the images to the zarf registry - if tunnel, err = cluster.NewZarfTunnel(); err != nil { + imageMap := map[string]v1.Image{} + var totalSize int64 + // Build an image list from the references + for _, src := range i.ImgList { + img, err := i.LoadImageFromPackage(src) + if err != nil { return err } - target = cluster.ZarfRegistry - } else { - svcInfo, err := cluster.ServiceInfoFromNodePortURL(i.RegInfo.Address) - - // If this is a service (no error getting svcInfo), create a port-forward tunnel to that resource - if err == nil { - if tunnel, err = cluster.NewTunnel(svcInfo.Namespace, cluster.SvcResource, svcInfo.Name, 0, svcInfo.Port); err != nil { - return err - } + imageMap[src] = img + imgSize, err := calcImgSize(img) + if err != nil { + return err } + totalSize += imgSize } - if tunnel != nil { - tunnel.Connect(target, false) - defer tunnel.Close() - registryURL = tunnel.Endpoint() - } else { - registryURL = i.RegInfo.Address + // If this is not a no checksum image push we will be pushing two images (the second will go faster as it checks the same layers) + if !i.NoChecksum { + totalSize = totalSize * 2 } - spinner := message.NewProgressSpinner("Storing images in the zarf registry") - defer spinner.Stop() - - if message.GetLogLevel() >= message.DebugLevel { - spinner.EnablePreserveWrites() - logs.Warn.SetOutput(spinner) - logs.Progress.SetOutput(spinner) - } + httpTransport := http.DefaultTransport.(*http.Transport).Clone() + httpTransport.TLSClientConfig.InsecureSkipVerify = i.Insecure + progressBar := message.NewProgressBar(totalSize, fmt.Sprintf("Pushing %d images to the zarf registry", len(i.ImgList))) + craneTransport := utils.NewTransport(httpTransport, progressBar) pushOptions := config.GetCraneOptions(i.Insecure, i.Architectures...) pushOptions = append(pushOptions, config.GetCraneAuthOption(i.RegInfo.PushUsername, i.RegInfo.PushPassword)) - + pushOptions = append(pushOptions, crane.WithTransport(craneTransport)) message.Debugf("crane pushOptions = %#v", pushOptions) - for _, src := range i.ImgList { - spinner.Updatef("Updating image %s", src) + for src, img := range imageMap { + progressBar.UpdateTitle(fmt.Sprintf("Pushing %s", src)) - img, err := i.LoadImageFromPackage(src) - if err != nil { - return err + var ( + err error + tunnel *cluster.Tunnel + registryURL string + target string + ) + + if i.RegInfo.InternalRegistry { + // Establish a registry tunnel to send the images to the zarf registry + if tunnel, err = cluster.NewZarfTunnel(); err != nil { + return err + } + target = cluster.ZarfRegistry + } else { + svcInfo, err := cluster.ServiceInfoFromNodePortURL(i.RegInfo.Address) + + // If this is a service (no error getting svcInfo), create a port-forward tunnel to that resource + if err == nil { + if tunnel, err = cluster.NewTunnel(svcInfo.Namespace, cluster.SvcResource, svcInfo.Name, 0, svcInfo.Port); err != nil { + return err + } + } + } + + if tunnel != nil { + tunnel.Connect(target, false) + registryURL = tunnel.Endpoint() + } else { + registryURL = i.RegInfo.Address } // If this is not a no checksum image push it for use with the Zarf agent @@ -98,8 +117,35 @@ func (i *ImgConfig) PushToZarfRegistry() error { if err = crane.Push(img, offlineName, pushOptions...); err != nil { return err } + + if tunnel != nil { + tunnel.Close() + } } - spinner.Success() + progressBar.Successf("Pushed %d images to the zarf registry", len(i.ImgList)) + return nil } + +func calcImgSize(img v1.Image) (int64, error) { + size, err := img.Size() + if err != nil { + return size, err + } + + layers, err := img.Layers() + if err != nil { + return size, err + } + + for _, layer := range layers { + ls, err := layer.Size() + if err != nil { + return size, err + } + size += ls + } + + return size, nil +} diff --git a/src/pkg/message/message.go b/src/pkg/message/message.go index 5b732cb089..459e6031ee 100644 --- a/src/pkg/message/message.go +++ b/src/pkg/message/message.go @@ -35,6 +35,9 @@ const ( // NoProgress tracks whether spinner/progress bars show updates. var NoProgress bool +// Separator is a string of 100 spaces to provide visual separation between elements. +var Separator = strings.Repeat(" ", 100) + var logLevel = InfoLevel // Write logs to stderr and a buffer for logFile generation. @@ -42,6 +45,13 @@ var logFile *os.File var useLogFile bool +type DebugWriter struct{} + +func (d *DebugWriter) Write(raw []byte) (int, error) { + Debug(raw) + return len(raw), nil +} + func init() { pterm.ThemeDefault.SuccessMessageStyle = *pterm.NewStyle(pterm.FgLightGreen) // Customize default error. @@ -100,18 +110,18 @@ func Debug(payload ...any) { // Debugf prints a debug message. func Debugf(format string, a ...any) { message := fmt.Sprintf(format, a...) - debugPrinter(3, message) + Debug(message) } // Error prints an error message. func Error(err any, message string) { - debugPrinter(2, err) + Debug(err) Warnf(message) } // ErrorWebf prints an error message and returns a web response. func ErrorWebf(err any, w http.ResponseWriter, format string, a ...any) { - debugPrinter(2, err) + Debug(err) message := fmt.Sprintf(format, a...) Warn(message) http.Error(w, message, http.StatusInternalServerError) @@ -119,74 +129,75 @@ func ErrorWebf(err any, w http.ResponseWriter, format string, a ...any) { // Errorf prints an error message. func Errorf(err any, format string, a ...any) { - debugPrinter(2, err) + Debug(err) Warnf(format, a...) } // Warn prints a warning message. func Warn(message string) { - Warnf(message) + Warnf("%s", message) } // Warnf prints a warning message. func Warnf(format string, a ...any) { - message := paragraph(format, a...) + message := Paragraph(format, a...) pterm.Warning.Println(message) } // Fatal prints a fatal error message and exits with a 1. func Fatal(err any, message string) { - debugPrinter(2, err) + Debug(err) errorPrinter(2).Println(message) - debugPrinter(2, string(debug.Stack())) + Debug(string(debug.Stack())) os.Exit(1) } // Fatalf prints a fatal error message and exits with a 1. func Fatalf(err any, format string, a ...any) { - debugPrinter(2, err) - message := paragraph(format, a...) - errorPrinter(2).Println(message) - debugPrinter(2, string(debug.Stack())) - os.Exit(1) + message := Paragraph(format, a...) + Fatal(err, message) } // Info prints an info message. func Info(message string) { - Infof(message) + Infof("%s", message) } // Infof prints an info message. func Infof(format string, a ...any) { if logLevel > 0 { - message := paragraph(format, a...) + message := Paragraph(format, a...) pterm.Info.Println(message) } } // Successf prints a success message. func Successf(format string, a ...any) { - message := paragraph(format, a...) + message := Paragraph(format, a...) pterm.Success.Println(message) } // Question prints a formatted message used in conjunction with a user prompt. func Question(text string) { - pterm.Println() - message := paragraph(text) - pterm.FgMagenta.Println(message) + Questionf("%s", text) } -// Notef prints a formatted yellow message. -func Notef(format string, a ...any) { - message := fmt.Sprintf(format, a...) - Note(message) +// Questionf prints a formatted message used in conjunction with a user prompt. +func Questionf(format string, a ...any) { + pterm.Println() + message := Paragraph(format, a...) + pterm.FgLightGreen.Println(message) } // Note prints a formatted yellow message. func Note(text string) { + Notef("%s", text) +} + +// Notef prints a formatted yellow message. +func Notef(format string, a ...any) { pterm.Println() - message := paragraph(text) + message := Paragraph(format, a...) pterm.FgYellow.Println(message) } @@ -203,6 +214,18 @@ func HeaderInfof(format string, a ...any) { Printfln(message + strings.Repeat(" ", padding)) } +// HorizontalRule prints a white horizontal rule to separate the terminal +func HorizontalRule() { + pterm.Println() + pterm.Println(strings.Repeat("━", 100)) +} + +// HorizontalRule prints a yellow horizontal rule to separate the terminal +func HorizontalNoteRule() { + pterm.Println() + pterm.FgYellow.Println(strings.Repeat("━", 100)) +} + // JSONValue prints any value as JSON. func JSONValue(value any) string { bytes, err := json.MarshalIndent(value, "", " ") @@ -212,8 +235,14 @@ func JSONValue(value any) string { return string(bytes) } -func paragraph(format string, a ...any) string { - return pterm.DefaultParagraph.WithMaxWidth(100).Sprintf(format, a...) +// Paragraph formats text into a 100 column paragraph +func Paragraph(format string, a ...any) string { + return Paragraphn(100, format, a...) +} + +// Paragraphn formats text into an n column paragraph +func Paragraphn(n int, format string, a ...any) string { + return pterm.DefaultParagraph.WithMaxWidth(n).Sprintf(format, a...) } func debugPrinter(offset int, a ...any) { diff --git a/src/pkg/message/progress.go b/src/pkg/message/progress.go index 482ccf4899..b3e27a485e 100644 --- a/src/pkg/message/progress.go +++ b/src/pkg/message/progress.go @@ -37,6 +37,7 @@ func NewProgressBar(total int64, text string) *ProgressBar { // Update updates the ProgressBar with completed progress and new text. func (p *ProgressBar) Update(complete int64, text string) { if NoProgress { + Debug(text) return } p.progress.UpdateTitle(" " + text) @@ -44,6 +45,15 @@ func (p *ProgressBar) Update(complete int64, text string) { p.Add(chunk) } +// UpdateTitle updates the ProgressBar with new text. +func (p *ProgressBar) UpdateTitle(text string) { + if NoProgress { + Debug(text) + return + } + p.progress.UpdateTitle(" " + text) +} + // Add updates the ProgressBar with completed progress. func (p *ProgressBar) Add(n int) { if p.progress != nil { diff --git a/src/pkg/packager/components.go b/src/pkg/packager/components.go index c88aa84a62..42c970621b 100644 --- a/src/pkg/packager/components.go +++ b/src/pkg/packager/components.go @@ -18,8 +18,6 @@ import ( "github.com/pterm/pterm" ) -const horizontalRule = "───────────────────────────────────────────────────────────────────────────────────────" - func (p *Packager) getValidComponents() []types.ZarfComponent { message.Debugf("packager.getValidComponents()") @@ -182,13 +180,12 @@ func (p *Packager) confirmOptionalComponent(component types.ZarfComponent) (conf return component.Default } - pterm.Println(horizontalRule) + message.HorizontalRule() displayComponent := component displayComponent.Description = "" utils.ColorPrintYAML(displayComponent) if component.Description != "" { - pterm.Println() message.Question(component.Description) } @@ -221,7 +218,7 @@ func (p *Packager) confirmChoiceGroup(componentGroup []types.ZarfComponent) type message.Fatalf(nil, "You must specify at least one component from the group %#v when using the --confirm flag.", componentNames) } - pterm.Println(horizontalRule) + message.HorizontalRule() var chosen int var options []string @@ -236,6 +233,8 @@ func (p *Packager) confirmChoiceGroup(componentGroup []types.ZarfComponent) type Options: options, } + pterm.Println() + if err := survey.AskOne(prompt, &chosen); err != nil { message.Fatalf(nil, "Component selection canceled: %s", err.Error()) } diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index e1ea4e43aa..62f0175a77 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -64,7 +64,9 @@ func (p *Packager) Deploy() error { // Reset registry HPA scale down whether an error occurs or not defer func() { if p.cluster != nil && hpaModified { - p.cluster.EnableRegHPAScaleDown() + if err := p.cluster.EnableRegHPAScaleDown(); err != nil { + message.Debugf("unable to reenable the registry HPA scale down: %s", err.Error()) + } } }() @@ -144,13 +146,19 @@ func (p *Packager) deployInitComponent(component types.ZarfComponent) (charts [] // Always init the state on the seed registry component if isSeedRegistry { - p.cluster, err = cluster.NewClusterWithWait(5 * time.Minute) + p.cluster, err = cluster.NewClusterWithWait(5*time.Minute, true) if err != nil { return charts, fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err) } + p.cluster.InitZarfState(p.cfg.InitOpts) } + if isRegistry { + // If we are deploying the registry then mark the HPA as "modifed" to set it to Min later + hpaModified = true + } + if hasExternalRegistry && (isSeedRegistry || isInjector || isRegistry) { message.Notef("Not deploying the component (%s) since external registry information was provided during `zarf init`", component.Name) return charts, nil @@ -209,25 +217,26 @@ func (p *Packager) deployComponent(component types.ZarfComponent, noImgChecksum // Make sure we have access to the cluster if p.cluster == nil { - p.cluster, err = cluster.NewClusterWithWait(30 * time.Second) + p.cluster, err = cluster.NewClusterWithWait(30*time.Second, true) if err != nil { return charts, fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err) } } + // Setup the state in the config and get the valuesTemplate + valueTemplate, err = p.setupStateValuesTemplate(component) + if err != nil { + return charts, fmt.Errorf("unable to get the updated value template: %w", err) + } + // Disable the registry HPA scale down if we are deploying images and it is not already disabled if hasImages && !hpaModified && p.cfg.State.RegistryInfo.InternalRegistry { if err := p.cluster.DisableRegHPAScaleDown(); err != nil { - message.Debugf("unable to toggle the registry HPA scale down: %s", err.Error()) + message.Debugf("unable to disable the registry HPA scale down: %s", err.Error()) } else { hpaModified = true } } - - valueTemplate, err = p.getUpdatedValueTemplate(component) - if err != nil { - return charts, fmt.Errorf("unable to get the updated value template: %w", err) - } } if hasImages { @@ -331,7 +340,7 @@ func (p *Packager) processComponentFiles(component types.ZarfComponent, sourceLo } // Fetch the current ZarfState from the k8s cluster and generate a valueTemplate from the state values. -func (p *Packager) getUpdatedValueTemplate(component types.ZarfComponent) (values template.Values, err error) { +func (p *Packager) setupStateValuesTemplate(component types.ZarfComponent) (values template.Values, err error) { // If we are touching K8s, make sure we can talk to it once per deployment spinner := message.NewProgressSpinner("Loading the Zarf State from the Kubernetes cluster") defer spinner.Stop() diff --git a/src/pkg/packager/deprecated/common.go b/src/pkg/packager/deprecated/common.go index 125fad34b3..5e7e5ffbdb 100644 --- a/src/pkg/packager/deprecated/common.go +++ b/src/pkg/packager/deprecated/common.go @@ -5,16 +5,37 @@ package deprecated import ( + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/defenseunicorns/zarf/src/config" + "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/types" + "github.com/pterm/pterm" ) +type BreakingChange struct { + version *semver.Version + title string + mitigation string +} + // List of migrations tracked in the zarf.yaml build data. const ( ScriptsToActionsMigrated = "scripts-to-actions" PluralizeSetVariable = "pluralize-set-variable" ) +// List of breaking changes to warn the user of. +var breakingChanges = []BreakingChange{ + { + version: semver.New(0, 26, 0, "", ""), + title: "Zarf container images are now mutated based on tag instead of repository name.", + mitigation: "Reinitialize the cluster using v0.26.0 or later and redeploy existing packages to update the image references (you can view existing packages with 'zarf package list' and view cluster images with 'zarf tools registry catalog').", + }, +} + // MigrateComponent runs all migrations on a component. // Build should be empty on package create, but include just in case someone copied a zarf.yaml from a zarf package. func MigrateComponent(build types.ZarfBuildData, c types.ZarfComponent) types.ZarfComponent { @@ -37,3 +58,49 @@ func MigrateComponent(build types.ZarfBuildData, c types.ZarfComponent) types.Za // Future migrations here. return c } + +// PrintBreakingChanges prints the breaking changes between the provided version and the current CLIVersion +func PrintBreakingChanges(deployedZarfVersion string) { + deployedSemver, err := semver.NewVersion(deployedZarfVersion) + if err != nil { + message.HorizontalNoteRule() + pterm.Println() + message.Warnf("Unable to determine init-package version from %s. There is potential for breaking changes.", deployedZarfVersion) + return + } + + applicableBreakingChanges := []BreakingChange{} + + // Calculate the applicable breaking changes + for _, breakingChange := range breakingChanges { + if deployedSemver.LessThan(breakingChange.version) { + applicableBreakingChanges = append(applicableBreakingChanges, breakingChange) + } + } + + if len(applicableBreakingChanges) > 0 { + // Print header information + message.HorizontalNoteRule() + message.Warn(pterm.Bold.Sprint("Potential Breaking Changes Detected Between Versions")) + + // Print information about the versions + format := pterm.FgYellow.Sprint("CLI version ") + "%s" + pterm.FgYellow.Sprint(" is being used to deploy to a cluster that was initialized with ") + + "%s" + pterm.FgYellow.Sprint(". Between these versions there are the following breaking changes to consider:") + cliVersion := pterm.Bold.Sprintf(config.CLIVersion) + deployedVersion := pterm.Bold.Sprintf(deployedZarfVersion) + pterm.Printfln("\n%s", message.Paragraphn(120, format, cliVersion, deployedVersion)) + + // Print each applicable breaking change + for idx, applicableBreakingChange := range applicableBreakingChanges { + titleFormat := pterm.Bold.Sprintf("\n %d. ", idx+1) + "%s" + title := pterm.FgYellow.Sprint(applicableBreakingChange.title) + + pterm.Printfln(titleFormat, title) + + mitigationText := message.Paragraphn(96, "%s", pterm.FgLightCyan.Sprint(applicableBreakingChange.mitigation)) + + pterm.Printfln("\n - %s", pterm.Bold.Sprint("Mitigation:")) + pterm.Printfln(" %s", strings.ReplaceAll(mitigationText, "\n", "\n ")) + } + } +} diff --git a/src/pkg/packager/interactive.go b/src/pkg/packager/interactive.go index 6acba9bdec..664498e714 100644 --- a/src/pkg/packager/interactive.go +++ b/src/pkg/packager/interactive.go @@ -12,7 +12,9 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/defenseunicorns/zarf/src/config" + "github.com/defenseunicorns/zarf/src/internal/cluster" "github.com/defenseunicorns/zarf/src/pkg/message" + "github.com/defenseunicorns/zarf/src/pkg/packager/deprecated" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/types" "github.com/pterm/pterm" @@ -20,18 +22,30 @@ import ( func (p *Packager) confirmAction(userMessage string, sbomViewFiles []string) (confirm bool) { - pterm.Println() + message.HorizontalRule() utils.ColorPrintYAML(p.cfg.Pkg) + // Print the location that the user can view the package SBOMs from if len(sbomViewFiles) > 0 { cwd, _ := os.Getwd() - link := filepath.Join(cwd, config.ZarfSBOMDir, filepath.Base(sbomViewFiles[0])) + link := pterm.FgWhite.Sprint(pterm.Bold.Sprint(filepath.Join(cwd, config.ZarfSBOMDir, filepath.Base(sbomViewFiles[0])))) msg := fmt.Sprintf("This package has %d artifacts with software bill-of-materials (SBOM) included. You can view them now in the zarf-sbom folder in this directory or to go directly to one, open this in your browser: %s", len(sbomViewFiles), link) + message.HorizontalNoteRule() message.Note(msg) message.Note(" * This directory will be removed after package deployment.") } - pterm.Println() + // Print any potential breaking changes (if this is a Deploy confirm) between this CLI version and the deployed init package + if userMessage == "Deploy" { + if cluster, err := cluster.NewCluster(); err == nil { + if initPackage, err := cluster.GetDeployedPackage("init"); err == nil { + // We use the build.version for now because it is the most reliable way to get this version info pre v0.26.0 + deprecated.PrintBreakingChanges(initPackage.Data.Build.Version) + } + } + + message.HorizontalNoteRule() + } // Display prompt if not auto-confirmed if config.CommonOptions.Confirm { @@ -43,6 +57,8 @@ func (p *Packager) confirmAction(userMessage string, sbomViewFiles []string) (co Message: userMessage + " this Zarf package?", } + pterm.Println() + // Prompt the user for confirmation, on abort return false if err := survey.AskOne(prompt, &confirm); err != nil || !confirm { // User aborted or declined, cancel the action diff --git a/src/pkg/packager/remove.go b/src/pkg/packager/remove.go index e7fb7ba191..ba815fd0e8 100644 --- a/src/pkg/packager/remove.go +++ b/src/pkg/packager/remove.go @@ -60,24 +60,17 @@ func (p *Packager) Remove(packageName string) (err error) { // Get the secret for the deployed package deployedPackage := types.DeployedPackage{} - secretName := config.ZarfPackagePrefix + packageName if requiresCluster { // If we need the cluster, connect to it and pull the package secret if p.cluster == nil { - p.cluster, err = cluster.NewClusterWithWait(30 * time.Second) + p.cluster, err = cluster.NewClusterWithWait(30*time.Second, true) if err != nil { return fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err) } } - packageSecret, err := p.cluster.Kube.GetSecret(cluster.ZarfNamespace, secretName) - if err != nil { - return fmt.Errorf("unable to get the secret for the package we are attempting to remove: %w", err) - } - - // Get the list of components the package had deployed - err = json.Unmarshal(packageSecret.Data["data"], &deployedPackage) + deployedPackage, err = p.cluster.GetDeployedPackage(packageName) if err != nil { return fmt.Errorf("unable to load the secret for the package we are attempting to remove: %w", err) } @@ -102,7 +95,7 @@ func (p *Packager) Remove(packageName string) (err error) { continue } - if deployedPackage, err = p.removeComponent(deployedPackage, c, secretName, spinner); err != nil { + if deployedPackage, err = p.removeComponent(deployedPackage, c, spinner); err != nil { return fmt.Errorf("unable to remove the component (%s): %w", c.Name, err) } } @@ -110,9 +103,11 @@ func (p *Packager) Remove(packageName string) (err error) { return nil } -func (p *Packager) updatePackageSecret(deployedPackage types.DeployedPackage, secretName string) { +func (p *Packager) updatePackageSecret(deployedPackage types.DeployedPackage, packageName string) { // Only attempt to update the package secret if we are actually connected to a cluster if p.cluster != nil { + secretName := config.ZarfPackagePrefix + packageName + // Save the new secret with the removed components removed from the secret newPackageSecret := p.cluster.Kube.GenerateSecret(cluster.ZarfNamespace, secretName, corev1.SecretTypeOpaque) newPackageSecret.Labels[cluster.ZarfPackageInfoLabel] = p.cfg.Pkg.Metadata.Name @@ -127,7 +122,7 @@ func (p *Packager) updatePackageSecret(deployedPackage types.DeployedPackage, se } } -func (p *Packager) removeComponent(deployedPackage types.DeployedPackage, deployedComponent types.DeployedComponent, secretName string, spinner *message.Spinner) (types.DeployedPackage, error) { +func (p *Packager) removeComponent(deployedPackage types.DeployedPackage, deployedComponent types.DeployedComponent, spinner *message.Spinner) (types.DeployedPackage, error) { components := deployedPackage.Data.Components c := utils.Find(components, func(t types.ZarfComponent) bool { @@ -163,7 +158,7 @@ func (p *Packager) removeComponent(deployedPackage types.DeployedPackage, deploy deployedComponent.InstalledCharts = utils.RemoveMatches(deployedComponent.InstalledCharts, func(t types.InstalledChart) bool { return t.ChartName == chart.ChartName }) - p.updatePackageSecret(deployedPackage, secretName) + p.updatePackageSecret(deployedPackage, deployedPackage.Name) } if err := p.runActions(onRemove.Defaults, onRemove.After, nil); err != nil { @@ -183,13 +178,13 @@ func (p *Packager) removeComponent(deployedPackage types.DeployedPackage, deploy if len(deployedPackage.DeployedComponents) == 0 && p.cluster != nil { // All the installed components were deleted, therefore this package is no longer actually deployed - packageSecret, err := p.cluster.Kube.GetSecret(cluster.ZarfNamespace, secretName) + packageSecret, err := p.cluster.Kube.GetSecret(cluster.ZarfNamespace, config.ZarfPackagePrefix+deployedPackage.Name) if err != nil { return deployedPackage, fmt.Errorf("unable to get the secret for the package we are attempting to remove: %w", err) } _ = p.cluster.Kube.DeleteSecret(packageSecret) } else { - p.updatePackageSecret(deployedPackage, secretName) + p.updatePackageSecret(deployedPackage, deployedPackage.Name) } return deployedPackage, nil diff --git a/src/pkg/transform/image.go b/src/pkg/transform/image.go index 7b7825f608..d461c13587 100644 --- a/src/pkg/transform/image.go +++ b/src/pkg/transform/image.go @@ -32,7 +32,12 @@ func ImageTransformHost(targetHost, srcReference string) (string, error) { // Generate a crc32 hash of the image host + name checksum := utils.GetCRCHash(image.Name) - return fmt.Sprintf("%s/%s-%d%s", targetHost, image.Path, checksum, image.TagOrDigest), nil + // If this image is specified by digest then don't add a checksum it as it will already be a specific SHA + if image.Digest != "" { + return fmt.Sprintf("%s/%s@%s", targetHost, image.Path, image.Digest), nil + } else { + return fmt.Sprintf("%s/%s:%s-zarf-%d", targetHost, image.Path, image.Tag, checksum), nil + } } // ImageTransformHostWithoutChecksum replaces the base url for an image but avoids adding a checksum of the original url (note image refs are not full URLs). @@ -73,5 +78,12 @@ func ParseImageRef(srcReference string) (out Image, err error) { out.TagOrDigest = fmt.Sprintf("@%s", digested.Digest().String()) } + // If no tag or digest was provided use the default tag (latest) + if out.TagOrDigest == "" { + out.Tag = "latest" + out.TagOrDigest = ":latest" + out.Reference += ":latest" + } + return out, nil } diff --git a/src/pkg/transform/image_test.go b/src/pkg/transform/image_test.go index 196a0ab0db..b00fca1fbc 100644 --- a/src/pkg/transform/image_test.go +++ b/src/pkg/transform/image_test.go @@ -11,6 +11,7 @@ import ( ) var imageRefs = []string{ + "nginx", "nginx:1.23.3", "defenseunicorns/zarf-agent:v0.22.1", "defenseunicorns/zarf-agent@sha256:84605f731c6a18194794c51e70021c671ab064654b751aa57e905bce55be13de", @@ -27,11 +28,12 @@ var badImageRefs = []string{ func TestImageTransformHost(t *testing.T) { var expectedResult = []string{ // Normal git repos and references for pushing/pulling - "gitlab.com/project/library/nginx-3793515731:1.23.3", - "gitlab.com/project/defenseunicorns/zarf-agent-4283503412:v0.22.1", - "gitlab.com/project/defenseunicorns/zarf-agent-4283503412@sha256:84605f731c6a18194794c51e70021c671ab064654b751aa57e905bce55be13de", - "gitlab.com/project/stefanprodan/podinfo-2985051089:6.3.3", - "gitlab.com/project/ironbank/opensource/defenseunicorns/zarf/zarf-agent-2003217571:v0.25.0", + "gitlab.com/project/library/nginx:latest-zarf-3793515731", + "gitlab.com/project/library/nginx:1.23.3-zarf-3793515731", + "gitlab.com/project/defenseunicorns/zarf-agent:v0.22.1-zarf-4283503412", + "gitlab.com/project/defenseunicorns/zarf-agent@sha256:84605f731c6a18194794c51e70021c671ab064654b751aa57e905bce55be13de", + "gitlab.com/project/stefanprodan/podinfo:6.3.3-zarf-2985051089", + "gitlab.com/project/ironbank/opensource/defenseunicorns/zarf/zarf-agent:v0.25.0-zarf-2003217571", } for idx, ref := range imageRefs { @@ -48,6 +50,7 @@ func TestImageTransformHost(t *testing.T) { func TestImageTransformHostWithoutChecksum(t *testing.T) { var expectedResult = []string{ + "gitlab.com/project/library/nginx:latest", "gitlab.com/project/library/nginx:1.23.3", "gitlab.com/project/defenseunicorns/zarf-agent:v0.22.1", "gitlab.com/project/defenseunicorns/zarf-agent@sha256:84605f731c6a18194794c51e70021c671ab064654b751aa57e905bce55be13de", @@ -69,6 +72,7 @@ func TestImageTransformHostWithoutChecksum(t *testing.T) { func TestParseImageRef(t *testing.T) { var expectedResult = [][]string{ + {"docker.io/", "library/nginx", "latest", ""}, {"docker.io/", "library/nginx", "1.23.3", ""}, {"docker.io/", "defenseunicorns/zarf-agent", "v0.22.1", ""}, {"docker.io/", "defenseunicorns/zarf-agent", "", "sha256:84605f731c6a18194794c51e70021c671ab064654b751aa57e905bce55be13de"}, diff --git a/src/pkg/utils/network.go b/src/pkg/utils/network.go index 04ed94cb56..4087ef338c 100644 --- a/src/pkg/utils/network.go +++ b/src/pkg/utils/network.go @@ -154,7 +154,6 @@ func httpGetFile(url string, destinationFile *os.File) { } // Writer the body to file - text := fmt.Sprintf("Downloading %s", url) title := fmt.Sprintf("Downloading %s", path.Base(url)) progressBar := message.NewProgressBar(resp.ContentLength, title) @@ -162,7 +161,8 @@ func httpGetFile(url string, destinationFile *os.File) { progressBar.Fatalf(err, "Unable to save the file %s", destinationFile.Name()) } - progressBar.Successf("%s", text) + title = fmt.Sprintf("Downloaded %s", url) + progressBar.Successf("%s", title) } func sgetFile(url string, destinationFile *os.File, cosignKeyPath string) { diff --git a/src/pkg/utils/yaml.go b/src/pkg/utils/yaml.go index 58c1f61929..5cb7080aad 100644 --- a/src/pkg/utils/yaml.go +++ b/src/pkg/utils/yaml.go @@ -76,6 +76,7 @@ func ColorPrintYAML(data any) { } } + pterm.Println() pterm.Println(p.PrintTokens(tokens)) } diff --git a/src/test/e2e/20_zarf_init_test.go b/src/test/e2e/20_zarf_init_test.go index 0eb016768e..cabbb49afe 100644 --- a/src/test/e2e/20_zarf_init_test.go +++ b/src/test/e2e/20_zarf_init_test.go @@ -53,6 +53,11 @@ func TestZarfInit(t *testing.T) { require.NoError(t, err) require.Contains(t, stdOut, "31337") + // Check that the registry is running with the correct scale down policy + stdOut, _, err = e2e.ExecZarfCommand("tools", "kubectl", "get", "hpa", "-n", "zarf", "zarf-docker-registry", "-o=jsonpath='{.spec.behavior.scaleDown.selectPolicy}'") + require.NoError(t, err) + require.Contains(t, stdOut, "Min") + // Special sizing-hacking for reducing resources where Kind + CI eats a lot of free cycles (ignore errors) _, _, _ = e2e.ExecZarfCommand("tools", "kubectl", "scale", "deploy", "-n", "kube-system", "coredns", "--replicas=1") _, _, _ = e2e.ExecZarfCommand("tools", "kubectl", "scale", "deploy", "-n", "zarf", "agent-hook", "--replicas=1") diff --git a/src/test/e2e/21_connect_test.go b/src/test/e2e/21_connect_test.go index 8dda47592f..74e7d5f73e 100644 --- a/src/test/e2e/21_connect_test.go +++ b/src/test/e2e/21_connect_test.go @@ -30,9 +30,8 @@ func TestConnect(t *testing.T) { // We assert greater than or equal to since the base init has 12 images // HOWEVER during an upgrade we could have mismatched versions/names resulting in more images - assert.GreaterOrEqual(t, len(registryList), 12) + assert.GreaterOrEqual(t, len(registryList), 7) assert.Contains(t, stdOut, "gitea/gitea") - assert.Contains(t, stdOut, "gitea/gitea-3431384023") // Connect to Gitea tunnelGit, err := cluster.NewZarfTunnel() diff --git a/src/test/ui/02_initialize_cluster.spec.ts b/src/test/ui/02_initialize_cluster.spec.ts index a252dea146..e25d39fe37 100644 --- a/src/test/ui/02_initialize_cluster.spec.ts +++ b/src/test/ui/02_initialize_cluster.spec.ts @@ -52,7 +52,7 @@ test.describe('initialize a zarf cluster', () => { await k3s.locator('text=Deploy').click(); await expect(k3s.locator('.deploy-component-toggle')).toHaveAttribute('aria-pressed', 'true'); await expect( - page.locator('.component-accordion-header:has-text("*** REQUIRES ROOT *** Install K3s")') + page.locator('.component-accordion-header:has-text("*** REQUIRES ROOT (not sudo) *** Install K3s")') ).toBeVisible(); await expect(k3s.locator('code')).toBeHidden(); await k3s.locator('.accordion-toggle').click();