Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: remove #3008

Merged
merged 1 commit into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions src/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,19 +270,21 @@ var packageRemoveCmd = &cobra.Command{
if err != nil {
return err
}
pkgConfig.PkgOpts.PackageSource = packageSource
src, err := identifyAndFallbackToClusterSource()
if err != nil {
return err
filter := filters.Combine(
filters.ByLocalOS(runtime.GOOS),
filters.BySelectState(pkgConfig.PkgOpts.OptionalComponents),
)
cluster, _ := cluster.NewCluster()
removeOpt := packager2.RemoveOptions{
Source: packageSource,
Cluster: cluster,
Filter: filter,
SkipSignatureValidation: pkgConfig.PkgOpts.SkipSignatureValidation,
}
pkgClient, err := packager.New(&pkgConfig, packager.WithSource(src))
err = packager2.Remove(cmd.Context(), removeOpt)
if err != nil {
return err
}
defer pkgClient.ClearTempPaths()
if err := pkgClient.Remove(cmd.Context()); err != nil {
return fmt.Errorf("unable to remove the package with an error of: %w", err)
}
return nil
},
ValidArgsFunction: getPackageCompletionArgs,
Expand Down Expand Up @@ -382,7 +384,9 @@ func choosePackage(args []string) (string, error) {
return path, nil
}

// TODO: This code does not seem to do what it was intended.
// NOTE: If the source is identified nil is returned because packager will create the source if it is nil.
// If it can't be identified the cluster source is used causing packager to ignore the configured package source.
// Use of cluster package source is limited to a few functions which is why this is not the default behavior.
func identifyAndFallbackToClusterSource() (sources.PackageSource, error) {
identifiedSrc := sources.Identify(pkgConfig.PkgOpts.PackageSource)
if identifiedSrc == "" {
Expand Down
33 changes: 33 additions & 0 deletions src/internal/packager2/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import (
"github.com/defenseunicorns/pkg/helpers/v2"
"github.com/mholt/archiver/v3"

"github.com/zarf-dev/zarf/src/api/v1alpha1"
"github.com/zarf-dev/zarf/src/config"
"github.com/zarf-dev/zarf/src/pkg/cluster"
"github.com/zarf-dev/zarf/src/pkg/layout"
"github.com/zarf-dev/zarf/src/pkg/packager/filters"
"github.com/zarf-dev/zarf/src/pkg/packager/sources"
Expand Down Expand Up @@ -162,6 +164,7 @@ func LoadPackage(ctx context.Context, opt LoadOptions) (*layout.PackagePaths, er
return pkgPaths, nil
}

// identifySource returns the source type for the given source.
func identifySource(src string) (string, error) {
parsed, err := url.Parse(src)
if err == nil && parsed.Scheme != "" && parsed.Host != "" {
Expand Down Expand Up @@ -223,3 +226,33 @@ func assembleSplitTar(src, tarPath string) error {
}
return nil
}

func packageFromSourceOrCluster(ctx context.Context, cluster *cluster.Cluster, src string, skipSignatureValidation bool) (v1alpha1.ZarfPackage, error) {
_, err := identifySource(src)
if err != nil {
if cluster == nil {
return v1alpha1.ZarfPackage{}, fmt.Errorf("cannot get Zarf package from Kubernetes without configuration")
}
depPkg, err := cluster.GetDeployedPackage(ctx, src)
if err != nil {
return v1alpha1.ZarfPackage{}, err
}
return depPkg.Data, nil
}

loadOpt := LoadOptions{
Source: src,
SkipSignatureValidation: skipSignatureValidation,
Filter: filters.Empty(),
}
pkgPaths, err := LoadPackage(ctx, loadOpt)
if err != nil {
return v1alpha1.ZarfPackage{}, err
}
defer os.RemoveAll(pkgPaths.Base)
pkg, _, err := pkgPaths.ReadZarfYAML()
if err != nil {
return v1alpha1.ZarfPackage{}, err
}
return pkg, nil
}
24 changes: 24 additions & 0 deletions src/internal/packager2/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"testing"

"github.com/stretchr/testify/require"
"k8s.io/client-go/kubernetes/fake"

"github.com/zarf-dev/zarf/src/pkg/cluster"
"github.com/zarf-dev/zarf/src/pkg/packager/filters"
"github.com/zarf-dev/zarf/src/test/testutil"
)
Expand Down Expand Up @@ -134,3 +136,25 @@ func TestIdentifySource(t *testing.T) {
})
}
}

func TestPackageFromSourceOrCluster(t *testing.T) {
t.Parallel()

ctx := testutil.TestContext(t)

_, err := packageFromSourceOrCluster(ctx, nil, "test", false)
require.EqualError(t, err, "cannot get Zarf package from Kubernetes without configuration")

pkg, err := packageFromSourceOrCluster(ctx, nil, "./testdata/zarf-package-test-amd64-0.0.1.tar.zst", false)
require.NoError(t, err)
require.Equal(t, "test", pkg.Metadata.Name)

c := &cluster.Cluster{
Clientset: fake.NewSimpleClientset(),
}
_, err = c.RecordPackageDeployment(ctx, pkg, nil, 1)
require.NoError(t, err)
pkg, err = packageFromSourceOrCluster(ctx, c, "test", false)
require.NoError(t, err)
require.Equal(t, "test", pkg.Metadata.Name)
}
163 changes: 163 additions & 0 deletions src/internal/packager2/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

package packager2

import (
"context"
"errors"
"fmt"
"slices"

"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/storage/driver"

"github.com/zarf-dev/zarf/src/api/v1alpha1"
"github.com/zarf-dev/zarf/src/config"
"github.com/zarf-dev/zarf/src/pkg/cluster"
"github.com/zarf-dev/zarf/src/pkg/message"
"github.com/zarf-dev/zarf/src/pkg/packager/actions"
"github.com/zarf-dev/zarf/src/pkg/packager/filters"
"github.com/zarf-dev/zarf/src/types"
)

// RemoveOptions are the options for Remove.
type RemoveOptions struct {
Source string
Cluster *cluster.Cluster
Filter filters.ComponentFilterStrategy
SkipSignatureValidation bool
}

// Remove removes a package that was already deployed onto a cluster, uninstalling all installed helm charts.
func Remove(ctx context.Context, opt RemoveOptions) error {
pkg, err := packageFromSourceOrCluster(ctx, opt.Cluster, opt.Source, opt.SkipSignatureValidation)
if err != nil {
return err
}

// If components were provided; just remove the things we were asked to remove
components, err := opt.Filter.Apply(pkg)
if err != nil {
return err
}
// Check that cluster is configured if required.
requiresCluster := false
componentIdx := map[string]v1alpha1.ZarfComponent{}
for _, component := range components {
componentIdx[component.Name] = component
if component.RequiresCluster() {
if opt.Cluster == nil {
return fmt.Errorf("component %s requires cluster access but none was configured", component.Name)
}
requiresCluster = true
}
}

// Get or build the secret for the deployed package
depPkg := &types.DeployedPackage{}
if requiresCluster {
depPkg, err = opt.Cluster.GetDeployedPackage(ctx, pkg.Metadata.Name)
if err != nil {
return fmt.Errorf("unable to load the secret for the package we are attempting to remove: %s", err.Error())
}
} else {
phillebaba marked this conversation as resolved.
Show resolved Hide resolved
// If we do not need the cluster, create a deployed components object based on the info we have
depPkg.Name = pkg.Metadata.Name
depPkg.Data = pkg
for _, component := range components {
depPkg.DeployedComponents = append(depPkg.DeployedComponents, types.DeployedComponent{Name: component.Name})
}
}

reverseDepComps := slices.Clone(depPkg.DeployedComponents)
slices.Reverse(reverseDepComps)
for _, depComp := range reverseDepComps {
// Only remove the component if it was requested or if we are removing the whole package.
comp, ok := componentIdx[depComp.Name]
if !ok {
continue
}

err := func() error {
err := actions.Run(ctx, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.Before, nil)
if err != nil {
return fmt.Errorf("unable to run the before action: %w", err)
}

reverseInstalledCharts := slices.Clone(depComp.InstalledCharts)
slices.Reverse(reverseInstalledCharts)
if opt.Cluster != nil {
for _, chart := range reverseInstalledCharts {
settings := cli.New()
settings.SetNamespace(chart.Namespace)
actionConfig := &action.Configuration{}
// TODO (phillebaba): Get credentials from cluster instead of reading again.
err := actionConfig.Init(settings.RESTClientGetter(), chart.Namespace, "", func(string, ...interface{}) {})
if err != nil {
return err
}
client := action.NewUninstall(actionConfig)
client.KeepHistory = false
client.Wait = true
client.Timeout = config.ZarfDefaultTimeout
_, err = client.Run(chart.ChartName)
if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) {
return fmt.Errorf("unable to uninstall the helm chart %s in the namespace %s: %w", chart.ChartName, chart.Namespace, err)
}
if errors.Is(err, driver.ErrReleaseNotFound) {
message.Warnf("Helm release for helm chart '%s' in the namespace '%s' was not found. Was it already removed?", chart.ChartName, chart.Namespace)
}

// Pop the removed helm chart from the installed charts slice.
installedCharts := depPkg.DeployedComponents[len(depPkg.DeployedComponents)-1].InstalledCharts
installedCharts = installedCharts[:len(installedCharts)-1]
depPkg.DeployedComponents[len(depPkg.DeployedComponents)-1].InstalledCharts = installedCharts
err = opt.Cluster.UpdateDeployedPackage(ctx, *depPkg)
if err != nil {
// We warn and ignore errors because we may have removed the cluster that this package was inside of
message.Warnf("Unable to update the secret for package %s, this may be normal if the cluster was removed: %s", depPkg.Name, err.Error())
}
}
}

err = actions.Run(ctx, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.After, nil)
if err != nil {
return fmt.Errorf("unable to run the after action: %w", err)
}
err = actions.Run(ctx, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.OnSuccess, nil)
if err != nil {
return fmt.Errorf("unable to run the success action: %w", err)
}

// Pop the removed component from deploy components slice.
if opt.Cluster != nil {
depPkg.DeployedComponents = depPkg.DeployedComponents[:len(depPkg.DeployedComponents)-1]
err = opt.Cluster.UpdateDeployedPackage(ctx, *depPkg)
if err != nil {
// We warn and ignore errors because we may have removed the cluster that this package was inside of
message.Warnf("Unable to update the secret for package %s, this may be normal if the cluster was removed: %s", depPkg.Name, err.Error())
}
}
return nil
}()
if err != nil {
removeErr := actions.Run(ctx, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.OnFailure, nil)
if removeErr != nil {
return errors.Join(fmt.Errorf("unable to run the failure action: %w", err), removeErr)
}
return err
}
}

// All the installed components were deleted, therefore this package is no longer actually deployed
if opt.Cluster != nil && len(depPkg.DeployedComponents) == 0 {
err := opt.Cluster.DeleteDeployedPackage(ctx, depPkg.Name)
if err != nil {
message.Warnf("Unable to delete the secret for package %s, this may be normal if the cluster was removed: %s", depPkg.Name, err.Error())
}
}

return nil
}
59 changes: 59 additions & 0 deletions src/pkg/cluster/zarf.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,65 @@ func (c *Cluster) GetDeployedPackage(ctx context.Context, packageName string) (*
return deployedPackage, nil
}

// UpdateDeployedPackage updates the deployed package metadata.
func (c *Cluster) UpdateDeployedPackage(ctx context.Context, depPkg types.DeployedPackage) error {
secretName := config.ZarfPackagePrefix + depPkg.Name
packageSecretData, err := json.Marshal(depPkg)
if err != nil {
return err
}
packageSecret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: ZarfNamespaceName,
Labels: map[string]string{
ZarfManagedByLabel: "zarf",
ZarfPackageInfoLabel: depPkg.Name,
},
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"data": packageSecretData,
},
}
err = func() error {
_, err := c.Clientset.CoreV1().Secrets(packageSecret.Namespace).Get(ctx, packageSecret.Name, metav1.GetOptions{})
if err != nil && !kerrors.IsNotFound(err) {
return err
}
if kerrors.IsNotFound(err) {
_, err = c.Clientset.CoreV1().Secrets(packageSecret.Namespace).Create(ctx, packageSecret, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("unable to create the deployed package secret: %w", err)
}
return nil
}
_, err = c.Clientset.CoreV1().Secrets(packageSecret.Namespace).Update(ctx, packageSecret, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("unable to update the deployed package secret: %w", err)
}
return nil
}()
if err != nil {
return err
}
return nil
}

// DeleteDeployedPackage removes the metadata for the deployed package.
func (c *Cluster) DeleteDeployedPackage(ctx context.Context, packageName string) error {
secretName := config.ZarfPackagePrefix + packageName
err := c.Clientset.CoreV1().Secrets(ZarfNamespaceName).Delete(ctx, secretName, metav1.DeleteOptions{})
if err != nil {
return err
}
return nil
}

// StripZarfLabelsAndSecretsFromNamespaces removes metadata and secrets from existing namespaces no longer manged by Zarf.
func (c *Cluster) StripZarfLabelsAndSecretsFromNamespaces(ctx context.Context) {
spinner := message.NewProgressSpinner("Removing zarf metadata & secrets from existing namespaces not managed by Zarf")
Expand Down
2 changes: 1 addition & 1 deletion src/test/e2e/25_helm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func testHelmEscaping(t *testing.T) {
require.NoError(t, err, stdOut, stdErr)

// Verify the configmap was deployed, escaped, and contains all of its data
kubectlOut, _ := exec.Command("kubectl", "describe", "cm", "dont-template-me").Output()
kubectlOut, _ := exec.Command("kubectl", "-n", "default", "describe", "cm", "dont-template-me").Output()
require.Contains(t, string(kubectlOut), `alert: OOMKilled {{ "{{ \"random.Values\" }}" }}`)
require.Contains(t, string(kubectlOut), "backtick1: \"content with backticks `some random things`\"")
require.Contains(t, string(kubectlOut), "backtick2: \"nested templating with backticks {{` random.Values `}}\"")
Expand Down