Skip to content

Commit

Permalink
refactor: remove
Browse files Browse the repository at this point in the history
Signed-off-by: Philip Laine <philip.laine@gmail.com>
  • Loading branch information
phillebaba committed Sep 20, 2024
1 parent 669777a commit 78ce3dc
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 11 deletions.
51 changes: 43 additions & 8 deletions src/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/spf13/viper"
"oras.land/oras-go/v2/registry"

"github.com/zarf-dev/zarf/src/api/v1alpha1"
"github.com/zarf-dev/zarf/src/cmd/common"
"github.com/zarf-dev/zarf/src/config"
"github.com/zarf-dev/zarf/src/config/lang"
Expand Down Expand Up @@ -270,19 +271,21 @@ var packageRemoveCmd = &cobra.Command{
if err != nil {
return err
}
pkgConfig.PkgOpts.PackageSource = packageSource
src, err := identifyAndFallbackToClusterSource()

cluster, _ := cluster.NewCluster()
pkg, err := packageFromSourceOrCluster(cmd.Context(), cluster, packageSource)
if err != nil {
return err
}
pkgClient, err := packager.New(&pkgConfig, packager.WithSource(src))
removeOpt := packager2.RemoveOptions{
Cluster: cluster,
OptionalComponents: pkgConfig.PkgOpts.OptionalComponents,
Pkg: pkg,
}
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 +385,39 @@ func choosePackage(args []string) (string, error) {
return path, nil
}

// TODO: This code does not seem to do what it was intended.
func packageFromSourceOrCluster(ctx context.Context, cluster *cluster.Cluster, src string) (v1alpha1.ZarfPackage, error) {
_, err := packager2.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 := packager2.LoadOptions{
Source: src,
SkipSignatureValidation: pkgConfig.PkgOpts.SkipSignatureValidation,
Filter: filters.Empty(),
}
pkgPaths, err := packager2.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
}

// 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
5 changes: 3 additions & 2 deletions src/internal/packager2/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type LoadOptions struct {

// LoadPackage optionally fetches and loads the package from the given source.
func LoadPackage(ctx context.Context, opt LoadOptions) (*layout.PackagePaths, error) {
srcType, err := identifySource(opt.Source)
srcType, err := IdentifySource(opt.Source)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -162,7 +162,8 @@ func LoadPackage(ctx context.Context, opt LoadOptions) (*layout.PackagePaths, er
return pkgPaths, nil
}

func identifySource(src string) (string, error) {
// 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 != "" {
return parsed.Scheme, nil
Expand Down
2 changes: 1 addition & 1 deletion src/internal/packager2/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func TestIdentifySource(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

srcType, err := identifySource(tt.src)
srcType, err := IdentifySource(tt.src)
require.NoError(t, err)
require.Equal(t, tt.expectedSrcType, srcType)
})
Expand Down
160 changes: 160 additions & 0 deletions src/internal/packager2/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

package packager2

import (
"context"
"errors"
"fmt"
"runtime"
"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 {
Cluster *cluster.Cluster
OptionalComponents string
Pkg v1alpha1.ZarfPackage
}

// Remove removes a package that was already deployed onto a cluster, uninstalling all installed helm charts.
func Remove(ctx context.Context, opt RemoveOptions) error {
// If components were provided; just remove the things we were asked to remove
filter := filters.Combine(
filters.ByLocalOS(runtime.GOOS),
filters.BySelectState(opt.OptionalComponents),
)
components, err := filter.Apply(opt.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, opt.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 = opt.Pkg.Metadata.Name
depPkg.Data = opt.Pkg
for _, component := range components {
depPkg.DeployedComponents = append(depPkg.DeployedComponents, types.DeployedComponent{Name: component.Name})
}
}

reverseDepComps := depPkg.DeployedComponents
slices.Reverse(reverseDepComps)
for i, 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 := depComp.InstalledCharts
slices.Reverse(reverseInstalledCharts)
if opt.Cluster != nil {
for _, chart := range reverseInstalledCharts {
settings := cli.New()
actionConfig := &action.Configuration{}
// TODO (phillebaba): Get credentials from cluster instead of reading again.
err := actionConfig.Init(settings.RESTClientGetter(), chart.Namespace, "", nil)
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.
depPkg.DeployedComponents[len(depPkg.DeployedComponents)-i].InstalledCharts = depComp.InstalledCharts[:len(depComp.InstalledCharts)-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())
}
}
}

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 {
err := actions.Run(ctx, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.OnFailure, nil)
if err != nil {
// TODO: I think we should return an error if failure action fails. Does this break existing use cases?
return fmt.Errorf("unable to run the failure action: %w", err)
}
return nil
}
}

// 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, tis 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 zarf state 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 zarf state 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

0 comments on commit 78ce3dc

Please sign in to comment.