Skip to content

Commit

Permalink
refactor: remove
Browse files Browse the repository at this point in the history
Signed-off-by: Philip Laine <[email protected]>
  • Loading branch information
phillebaba committed Sep 23, 2024
1 parent 7ac8297 commit 7fa1a55
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 11 deletions.
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 {
// 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

0 comments on commit 7fa1a55

Please sign in to comment.