diff --git a/go.mod b/go.mod index 1275140145..137284437c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ go 1.21.8 replace github.com/xeipuuv/gojsonschema => github.com/defenseunicorns/gojsonschema v0.0.0-20231116163348-e00f069122d6 require ( - cuelang.org/go v0.7.0 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Masterminds/semver/v3 v3.2.1 github.com/agnivade/levenshtein v1.1.1 @@ -62,6 +61,8 @@ require ( sigs.k8s.io/yaml v1.4.0 ) +require cuelang.org/go v0.7.0 // indirect + require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect diff --git a/src/internal/packager/git/gitea.go b/src/internal/packager/git/gitea.go index d243337d3c..c6644c6b1a 100644 --- a/src/internal/packager/git/gitea.go +++ b/src/internal/packager/git/gitea.go @@ -10,17 +10,17 @@ import ( "encoding/json" "fmt" "io" + netHttp "net/http" "os" "time" - netHttp "net/http" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/cluster" "github.com/defenseunicorns/zarf/src/pkg/k8s" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/types" - "k8s.io/apimachinery/pkg/runtime/schema" ) // CreateTokenResponse is the response given from creating a token in Gitea @@ -253,24 +253,38 @@ func UpdateGiteaPVC(ctx context.Context, shouldRollBack bool) (string, error) { } pvcName := os.Getenv("ZARF_VAR_GIT_SERVER_EXISTING_PVC") - groupKind := schema.GroupKind{ - Group: "", - Kind: "PersistentVolumeClaim", - } - labels := map[string]string{"app.kubernetes.io/managed-by": "Helm"} - annotations := map[string]string{"meta.helm.sh/release-name": "zarf-gitea", "meta.helm.sh/release-namespace": "zarf"} if shouldRollBack { - err = c.K8s.RemoveLabelsAndAnnotations(ctx, cluster.ZarfNamespaceName, pvcName, groupKind, labels, annotations) - return "false", err + pvc, err := c.Clientset.CoreV1().PersistentVolumeClaims(cluster.ZarfNamespaceName).Get(ctx, pvcName, metav1.GetOptions{}) + if err != nil { + return "false", err + } + delete(pvc.Labels, "app.kubernetes.io/managed-by") + delete(pvc.Annotations, "meta.helm.sh/release-name") + delete(pvc.Annotations, "meta.helm.sh/release-namespace") + _, err = c.Clientset.CoreV1().PersistentVolumeClaims(cluster.ZarfNamespaceName).Update(ctx, pvc, metav1.UpdateOptions{}) + if err != nil { + return "false", err + } + return "false", nil } if pvcName == "data-zarf-gitea-0" { - err = c.K8s.AddLabelsAndAnnotations(ctx, cluster.ZarfNamespaceName, pvcName, groupKind, labels, annotations) - return "true", err + pvc, err := c.Clientset.CoreV1().PersistentVolumeClaims(cluster.ZarfNamespaceName).Get(ctx, pvcName, metav1.GetOptions{}) + if err != nil { + return "true", err + } + pvc.Labels["app.kubernetes.io/managed-by"] = "Helm" + pvc.Annotations["meta.helm.sh/release-name"] = "zarf-gitea" + pvc.Annotations["meta.helm.sh/release-namespace"] = "zarf" + _, err = c.Clientset.CoreV1().PersistentVolumeClaims(cluster.ZarfNamespaceName).Update(ctx, pvc, metav1.UpdateOptions{}) + if err != nil { + return "true", err + } + return "true", nil } - return "false", err + return "false", nil } // DoHTTPThings adds http request boilerplate and perform the request, checking for a successful response. diff --git a/src/internal/packager/helm/post-render.go b/src/internal/packager/helm/post-render.go index 4be00a1b11..f4789fdcca 100644 --- a/src/internal/packager/helm/post-render.go +++ b/src/internal/packager/helm/post-render.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "github.com/defenseunicorns/pkg/helpers" "github.com/defenseunicorns/zarf/src/config" @@ -20,6 +21,8 @@ import ( "github.com/defenseunicorns/zarf/src/types" "helm.sh/helm/v3/pkg/releaseutil" corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/restmapper" "sigs.k8s.io/yaml" kerrors "k8s.io/apimachinery/pkg/api/errors" @@ -117,12 +120,15 @@ func (r *renderer) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) { func (r *renderer) adoptAndUpdateNamespaces(ctx context.Context) error { c := r.cluster - existingNamespaces, _ := c.GetNamespaces(ctx) + namespaceList, err := r.cluster.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + if err != nil { + return err + } for name, namespace := range r.namespaces { // Check to see if this namespace already exists var existingNamespace bool - for _, serverNamespace := range existingNamespaces.Items { + for _, serverNamespace := range namespaceList.Items { if serverNamespace.Name == name { existingNamespace = true } @@ -130,16 +136,19 @@ func (r *renderer) adoptAndUpdateNamespaces(ctx context.Context) error { if !existingNamespace { // This is a new namespace, add it - if _, err := c.CreateNamespace(ctx, namespace); err != nil { + _, err := c.Clientset.CoreV1().Namespaces().Create(ctx, namespace, metav1.CreateOptions{}) + if err != nil { return fmt.Errorf("unable to create the missing namespace %s", name) } } else if r.cfg.DeployOpts.AdoptExistingResources { - if r.cluster.IsInitialNamespace(name) { - // If this is a K8s initial namespace, refuse to adopt it + // Refuse to adopt namespace if it is one of four initial Kubernetes namespaces. + // https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#initial-namespaces + if slices.Contains([]string{"default", "kube-node-lease", "kube-public", "kube-system"}, name) { message.Warnf("Refusing to adopt the initial namespace: %s", name) } else { // This is an existing namespace to adopt - if _, err := c.UpdateNamespace(ctx, namespace); err != nil { + _, err := c.Clientset.CoreV1().Namespaces().Update(ctx, namespace, metav1.UpdateOptions{}) + if err != nil { return fmt.Errorf("unable to adopt the existing namespace %s", name) } } @@ -174,6 +183,16 @@ func (r *renderer) adoptAndUpdateNamespaces(ctx context.Context) error { } func (r *renderer) editHelmResources(ctx context.Context, resources []releaseutil.Manifest, finalManifestsOutput *bytes.Buffer) error { + dc, err := dynamic.NewForConfig(r.cluster.RestConfig) + if err != nil { + return err + } + groupResources, err := restmapper.GetAPIGroupResources(r.cluster.Clientset.Discovery()) + if err != nil { + return err + } + mapper := restmapper.NewDiscoveryRESTMapper(groupResources) + for _, resource := range resources { // parse to unstructured to have access to more data than just the name rawData := &unstructured.Unstructured{} @@ -199,8 +218,13 @@ func (r *renderer) editHelmResources(ctx context.Context, resources []releaseuti case "Service": // Check service resources for the zarf-connect label labels := rawData.GetLabels() + if labels == nil { + labels = map[string]string{} + } annotations := rawData.GetAnnotations() - + if annotations == nil { + annotations = map[string]string{} + } if key, keyExists := labels[config.ZarfConnectLabelName]; keyExists { // If there is a zarf-connect label message.Debugf("Match helm service %s for zarf connection %s", rawData.GetName(), key) @@ -226,14 +250,35 @@ func (r *renderer) editHelmResources(ctx context.Context, resources []releaseuti deployedNamespace = r.chart.Namespace } - helmLabels := map[string]string{"app.kubernetes.io/managed-by": "Helm"} - helmAnnotations := map[string]string{ - "meta.helm.sh/release-name": r.chart.ReleaseName, - "meta.helm.sh/release-namespace": r.chart.Namespace, - } - - if err := r.cluster.AddLabelsAndAnnotations(ctx, deployedNamespace, rawData.GetName(), rawData.GroupVersionKind().GroupKind(), helmLabels, helmAnnotations); err != nil { - // Print a debug message since this could just be because the resource doesn't exist + err := func() error { + mapping, err := mapper.RESTMapping(rawData.GroupVersionKind().GroupKind()) + if err != nil { + return err + } + resource, err := dc.Resource(mapping.Resource).Namespace(deployedNamespace).Get(ctx, rawData.GetName(), metav1.GetOptions{}) + if err != nil { + return err + } + labels := resource.GetLabels() + if labels == nil { + labels = map[string]string{} + } + labels["app.kubernetes.io/managed-by"] = "Helm" + resource.SetLabels(labels) + annotations := resource.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations["meta.helm.sh/release-name"] = r.chart.ReleaseName + annotations["meta.helm.sh/release-namespace"] = r.chart.Namespace + resource.SetAnnotations(annotations) + _, err = dc.Resource(mapping.Resource).Namespace(deployedNamespace).Update(ctx, resource, metav1.UpdateOptions{}) + if err != nil { + return err + } + return nil + }() + if err != nil { message.Debugf("Unable to adopt resource %s: %s", rawData.GetName(), err.Error()) } } diff --git a/src/pkg/cluster/namespace.go b/src/pkg/cluster/namespace.go index 56a8566834..f7e776146d 100644 --- a/src/pkg/cluster/namespace.go +++ b/src/pkg/cluster/namespace.go @@ -6,10 +6,12 @@ package cluster import ( "context" + "time" "github.com/defenseunicorns/zarf/src/pkg/k8s" "github.com/defenseunicorns/zarf/src/pkg/message" corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -18,7 +20,30 @@ func (c *Cluster) DeleteZarfNamespace(ctx context.Context) error { spinner := message.NewProgressSpinner("Deleting the zarf namespace from this cluster") defer spinner.Stop() - return c.DeleteNamespace(ctx, ZarfNamespaceName) + err := c.Clientset.CoreV1().Namespaces().Delete(ctx, ZarfNamespaceName, metav1.DeleteOptions{}) + if kerrors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + timer := time.NewTimer(0) + defer timer.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + _, err := c.Clientset.CoreV1().Namespaces().Get(ctx, ZarfNamespaceName, metav1.GetOptions{}) + if kerrors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + timer.Reset(1 * time.Second) + } + } } // NewZarfManagedNamespace returns a corev1.Namespace with Zarf-managed labels diff --git a/src/pkg/cluster/secrets.go b/src/pkg/cluster/secrets.go index 8bd604250c..a7190c8037 100644 --- a/src/pkg/cluster/secrets.go +++ b/src/pkg/cluster/secrets.go @@ -11,6 +11,7 @@ import ( "reflect" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/k8s" @@ -80,11 +81,12 @@ func (c *Cluster) UpdateZarfManagedImageSecrets(ctx context.Context, state *type spinner := message.NewProgressSpinner("Updating existing Zarf-managed image secrets") defer spinner.Stop() - if namespaces, err := c.GetNamespaces(ctx); err != nil { + namespaceList, err := c.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + if err != nil { spinner.Errorf(err, "Unable to get k8s namespaces") } else { // Update all image pull secrets - for _, namespace := range namespaces.Items { + for _, namespace := range namespaceList.Items { currentRegistrySecret, err := c.GetSecret(ctx, namespace.Name, config.ZarfImagePullSecretName) if err != nil { continue @@ -115,11 +117,12 @@ func (c *Cluster) UpdateZarfManagedGitSecrets(ctx context.Context, state *types. spinner := message.NewProgressSpinner("Updating existing Zarf-managed git secrets") defer spinner.Stop() - if namespaces, err := c.GetNamespaces(ctx); err != nil { + namespaceList, err := c.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + if err != nil { spinner.Errorf(err, "Unable to get k8s namespaces") } else { // Update all git pull secrets - for _, namespace := range namespaces.Items { + for _, namespace := range namespaceList.Items { currentGitSecret, err := c.GetSecret(ctx, namespace.Name, config.ZarfGitServerSecretName) if err != nil { continue diff --git a/src/pkg/cluster/state.go b/src/pkg/cluster/state.go index fbcccf7989..03ac17e32c 100644 --- a/src/pkg/cluster/state.go +++ b/src/pkg/cluster/state.go @@ -87,16 +87,12 @@ func (c *Cluster) InitZarfState(ctx context.Context, initOptions types.ZarfInitO // Setup zarf agent PKI state.AgentTLS = pki.GeneratePKI(config.ZarfAgentHost) - namespaces, err := c.GetNamespaces(ctx) + namespaceList, err := c.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) if err != nil { return fmt.Errorf("unable to get the Kubernetes namespaces: %w", err) } // Mark existing namespaces as ignored for the zarf agent to prevent mutating resources we don't own. - for _, namespace := range namespaces.Items { - // Skip Zarf namespace if it already exists. - if namespace.Name == ZarfNamespaceName { - continue - } + for _, namespace := range namespaceList.Items { spinner.Updatef("Marking existing namespace %s as ignored by Zarf Agent", namespace.Name) if namespace.Labels == nil { // Ensure label map exists to avoid nil panic @@ -105,7 +101,8 @@ func (c *Cluster) InitZarfState(ctx context.Context, initOptions types.ZarfInitO // This label will tell the Zarf Agent to ignore this namespace. namespace.Labels[k8s.AgentLabel] = "ignore" namespaceCopy := namespace - if _, err = c.UpdateNamespace(ctx, &namespaceCopy); err != nil { + _, err := c.Clientset.CoreV1().Namespaces().Update(ctx, &namespaceCopy, metav1.UpdateOptions{}) + if err != nil { // This is not a hard failure, but we should log it. message.WarnErrf(err, "Unable to mark the namespace %s as ignored by Zarf Agent", namespace.Name) } diff --git a/src/pkg/cluster/zarf.go b/src/pkg/cluster/zarf.go index e37810269c..921268e6a6 100644 --- a/src/pkg/cluster/zarf.go +++ b/src/pkg/cluster/zarf.go @@ -72,15 +72,17 @@ func (c *Cluster) StripZarfLabelsAndSecretsFromNamespaces(ctx context.Context) { LabelSelector: k8s.ZarfManagedByLabel + "=zarf", } - if namespaces, err := c.GetNamespaces(ctx); err != nil { + namespaceList, err := c.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + if err != nil { spinner.Errorf(err, "Unable to get k8s namespaces") } else { - for _, namespace := range namespaces.Items { + for _, namespace := range namespaceList.Items { if _, ok := namespace.Labels[k8s.AgentLabel]; ok { spinner.Updatef("Removing Zarf Agent label for namespace %s", namespace.Name) delete(namespace.Labels, k8s.AgentLabel) namespaceCopy := namespace - if _, err = c.UpdateNamespace(ctx, &namespaceCopy); err != nil { + _, err := c.Clientset.CoreV1().Namespaces().Update(ctx, &namespaceCopy, metav1.UpdateOptions{}) + if err != nil { // This is not a hard failure, but we should log it spinner.Errorf(err, "Unable to update the namespace labels for %s", namespace.Name) } diff --git a/src/pkg/k8s/dynamic.go b/src/pkg/k8s/dynamic.go deleted file mode 100644 index 59f295f26b..0000000000 --- a/src/pkg/k8s/dynamic.go +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2021-Present The Zarf Authors - -// Package k8s provides a client for interacting with a Kubernetes cluster. -package k8s - -import ( - "context" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/restmapper" -) - -// AddLabelsAndAnnotations adds the provided labels and annotations to the specified K8s resource -func (k *K8s) AddLabelsAndAnnotations(ctx context.Context, resourceNamespace, resourceName string, groupKind schema.GroupKind, labels, annotations map[string]string) error { - return k.updateLabelsAndAnnotations(ctx, resourceNamespace, resourceName, groupKind, labels, annotations, false) -} - -// RemoveLabelsAndAnnotations removes the provided labels and annotations to the specified K8s resource -func (k *K8s) RemoveLabelsAndAnnotations(ctx context.Context, resourceNamespace, resourceName string, groupKind schema.GroupKind, labels, annotations map[string]string) error { - return k.updateLabelsAndAnnotations(ctx, resourceNamespace, resourceName, groupKind, labels, annotations, true) -} - -// updateLabelsAndAnnotations updates the provided labels and annotations to the specified K8s resource -func (k *K8s) updateLabelsAndAnnotations(ctx context.Context, resourceNamespace, resourceName string, groupKind schema.GroupKind, labels, annotations map[string]string, isRemove bool) error { - dynamicClient := dynamic.NewForConfigOrDie(k.RestConfig) - - discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(k.RestConfig) - - groupResources, err := restmapper.GetAPIGroupResources(discoveryClient) - if err != nil { - return err - } - mapper := restmapper.NewDiscoveryRESTMapper(groupResources) - - mapping, err := mapper.RESTMapping(groupKind) - if err != nil { - return err - } - - deployedResource, err := dynamicClient.Resource(mapping.Resource).Namespace(resourceNamespace).Get(ctx, resourceName, metav1.GetOptions{}) - if err != nil { - return err - } - - // Pull the existing labels from the rendered resource - deployedLabels := deployedResource.GetLabels() - if deployedLabels == nil { - // Ensure label map exists to avoid nil panic - deployedLabels = make(map[string]string) - } - for key, value := range labels { - if isRemove { - delete(deployedLabels, key) - } else { - deployedLabels[key] = value - } - } - - deployedResource.SetLabels(deployedLabels) - - // Pull the existing annotations from the rendered resource - deployedAnnotations := deployedResource.GetAnnotations() - if deployedAnnotations == nil { - // Ensure label map exists to avoid nil panic - deployedAnnotations = make(map[string]string) - } - for key, value := range annotations { - if isRemove { - delete(deployedAnnotations, key) - } else { - deployedAnnotations[key] = value - } - } - - deployedResource.SetAnnotations(deployedAnnotations) - - _, err = dynamicClient.Resource(mapping.Resource).Namespace(resourceNamespace).Update(ctx, deployedResource, metav1.UpdateOptions{}) - return err -} diff --git a/src/pkg/k8s/namespace.go b/src/pkg/k8s/namespace.go deleted file mode 100644 index 2f1d43d019..0000000000 --- a/src/pkg/k8s/namespace.go +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2021-Present The Zarf Authors - -// Package k8s provides a client for interacting with a Kubernetes cluster. -package k8s - -import ( - "context" - "time" - - "cuelang.org/go/pkg/strings" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// GetNamespaces returns a list of namespaces in the cluster. -func (k *K8s) GetNamespaces(ctx context.Context) (*corev1.NamespaceList, error) { - metaOptions := metav1.ListOptions{} - return k.Clientset.CoreV1().Namespaces().List(ctx, metaOptions) -} - -// UpdateNamespace updates the given namespace in the cluster. -func (k *K8s) UpdateNamespace(ctx context.Context, namespace *corev1.Namespace) (*corev1.Namespace, error) { - updateOptions := metav1.UpdateOptions{} - return k.Clientset.CoreV1().Namespaces().Update(ctx, namespace, updateOptions) -} - -// CreateNamespace creates the given namespace or returns it if it already exists in the cluster. -func (k *K8s) CreateNamespace(ctx context.Context, namespace *corev1.Namespace) (*corev1.Namespace, error) { - metaOptions := metav1.GetOptions{} - createOptions := metav1.CreateOptions{} - - match, err := k.Clientset.CoreV1().Namespaces().Get(ctx, namespace.Name, metaOptions) - - if err != nil || match.Name != namespace.Name { - return k.Clientset.CoreV1().Namespaces().Create(ctx, namespace, createOptions) - } - - return match, err -} - -// DeleteNamespace deletes the given namespace from the cluster. -func (k *K8s) DeleteNamespace(ctx context.Context, name string) error { - // Attempt to delete the namespace immediately - gracePeriod := int64(0) - err := k.Clientset.CoreV1().Namespaces().Delete(ctx, name, metav1.DeleteOptions{GracePeriodSeconds: &gracePeriod}) - if err != nil && !errors.IsNotFound(err) { - return err - } - - timer := time.NewTimer(0) - defer timer.Stop() - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-timer.C: - _, err := k.Clientset.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{}) - if errors.IsNotFound(err) { - return nil - } - - timer.Reset(1 * time.Second) - } - } -} - -// IsInitialNamespace returns true if the given namespace name is an initial k8s namespace: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#initial-namespaces -func (k *K8s) IsInitialNamespace(name string) bool { - if name == "default" { - return true - } else if strings.HasPrefix(name, "kube-") { - return true - } - - return false -} diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index 2805c96620..b7268bb8ca 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -17,7 +17,12 @@ import ( "sync" "time" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/defenseunicorns/pkg/helpers" + "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/config/lang" "github.com/defenseunicorns/zarf/src/internal/packager/git" @@ -32,7 +37,6 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/packager/filters" "github.com/defenseunicorns/zarf/src/pkg/transform" "github.com/defenseunicorns/zarf/src/types" - corev1 "k8s.io/api/core/v1" ) func (p *Packager) resetRegistryHPA(ctx context.Context) { @@ -451,7 +455,21 @@ func (p *Packager) setupState(ctx context.Context) (err error) { // Try to create the zarf namespace spinner.Updatef("Creating the Zarf namespace") zarfNamespace := cluster.NewZarfManagedNamespace(cluster.ZarfNamespaceName) - if _, err := p.cluster.CreateNamespace(ctx, zarfNamespace); err != nil { + err := func() error { + _, err := p.cluster.Clientset.CoreV1().Namespaces().Create(ctx, zarfNamespace, metav1.CreateOptions{}) + if err != nil && !kerrors.IsAlreadyExists(err) { + return err + } + if err == nil { + return nil + } + _, err = p.cluster.Clientset.CoreV1().Namespaces().Update(ctx, zarfNamespace, metav1.UpdateOptions{}) + if err != nil { + return err + } + return nil + }() + if err != nil { spinner.Fatalf(err, "Unable to create the zarf namespace") } }