Skip to content

Commit 22531dd

Browse files
authored
refactor: remove (#3008)
Signed-off-by: Philip Laine <[email protected]>
1 parent 7ac8297 commit 22531dd

File tree

6 files changed

+294
-11
lines changed

6 files changed

+294
-11
lines changed

src/cmd/package.go

+14-10
Original file line numberDiff line numberDiff line change
@@ -270,19 +270,21 @@ var packageRemoveCmd = &cobra.Command{
270270
if err != nil {
271271
return err
272272
}
273-
pkgConfig.PkgOpts.PackageSource = packageSource
274-
src, err := identifyAndFallbackToClusterSource()
275-
if err != nil {
276-
return err
273+
filter := filters.Combine(
274+
filters.ByLocalOS(runtime.GOOS),
275+
filters.BySelectState(pkgConfig.PkgOpts.OptionalComponents),
276+
)
277+
cluster, _ := cluster.NewCluster()
278+
removeOpt := packager2.RemoveOptions{
279+
Source: packageSource,
280+
Cluster: cluster,
281+
Filter: filter,
282+
SkipSignatureValidation: pkgConfig.PkgOpts.SkipSignatureValidation,
277283
}
278-
pkgClient, err := packager.New(&pkgConfig, packager.WithSource(src))
284+
err = packager2.Remove(cmd.Context(), removeOpt)
279285
if err != nil {
280286
return err
281287
}
282-
defer pkgClient.ClearTempPaths()
283-
if err := pkgClient.Remove(cmd.Context()); err != nil {
284-
return fmt.Errorf("unable to remove the package with an error of: %w", err)
285-
}
286288
return nil
287289
},
288290
ValidArgsFunction: getPackageCompletionArgs,
@@ -382,7 +384,9 @@ func choosePackage(args []string) (string, error) {
382384
return path, nil
383385
}
384386

385-
// TODO: This code does not seem to do what it was intended.
387+
// NOTE: If the source is identified nil is returned because packager will create the source if it is nil.
388+
// If it can't be identified the cluster source is used causing packager to ignore the configured package source.
389+
// Use of cluster package source is limited to a few functions which is why this is not the default behavior.
386390
func identifyAndFallbackToClusterSource() (sources.PackageSource, error) {
387391
identifiedSrc := sources.Identify(pkgConfig.PkgOpts.PackageSource)
388392
if identifiedSrc == "" {

src/internal/packager2/load.go

+33
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import (
1919
"github.com/defenseunicorns/pkg/helpers/v2"
2020
"github.com/mholt/archiver/v3"
2121

22+
"github.com/zarf-dev/zarf/src/api/v1alpha1"
2223
"github.com/zarf-dev/zarf/src/config"
24+
"github.com/zarf-dev/zarf/src/pkg/cluster"
2325
"github.com/zarf-dev/zarf/src/pkg/layout"
2426
"github.com/zarf-dev/zarf/src/pkg/packager/filters"
2527
"github.com/zarf-dev/zarf/src/pkg/packager/sources"
@@ -162,6 +164,7 @@ func LoadPackage(ctx context.Context, opt LoadOptions) (*layout.PackagePaths, er
162164
return pkgPaths, nil
163165
}
164166

167+
// identifySource returns the source type for the given source.
165168
func identifySource(src string) (string, error) {
166169
parsed, err := url.Parse(src)
167170
if err == nil && parsed.Scheme != "" && parsed.Host != "" {
@@ -223,3 +226,33 @@ func assembleSplitTar(src, tarPath string) error {
223226
}
224227
return nil
225228
}
229+
230+
func packageFromSourceOrCluster(ctx context.Context, cluster *cluster.Cluster, src string, skipSignatureValidation bool) (v1alpha1.ZarfPackage, error) {
231+
_, err := identifySource(src)
232+
if err != nil {
233+
if cluster == nil {
234+
return v1alpha1.ZarfPackage{}, fmt.Errorf("cannot get Zarf package from Kubernetes without configuration")
235+
}
236+
depPkg, err := cluster.GetDeployedPackage(ctx, src)
237+
if err != nil {
238+
return v1alpha1.ZarfPackage{}, err
239+
}
240+
return depPkg.Data, nil
241+
}
242+
243+
loadOpt := LoadOptions{
244+
Source: src,
245+
SkipSignatureValidation: skipSignatureValidation,
246+
Filter: filters.Empty(),
247+
}
248+
pkgPaths, err := LoadPackage(ctx, loadOpt)
249+
if err != nil {
250+
return v1alpha1.ZarfPackage{}, err
251+
}
252+
defer os.RemoveAll(pkgPaths.Base)
253+
pkg, _, err := pkgPaths.ReadZarfYAML()
254+
if err != nil {
255+
return v1alpha1.ZarfPackage{}, err
256+
}
257+
return pkg, nil
258+
}

src/internal/packager2/load_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"testing"
99

1010
"github.com/stretchr/testify/require"
11+
"k8s.io/client-go/kubernetes/fake"
1112

13+
"github.com/zarf-dev/zarf/src/pkg/cluster"
1214
"github.com/zarf-dev/zarf/src/pkg/packager/filters"
1315
"github.com/zarf-dev/zarf/src/test/testutil"
1416
)
@@ -134,3 +136,25 @@ func TestIdentifySource(t *testing.T) {
134136
})
135137
}
136138
}
139+
140+
func TestPackageFromSourceOrCluster(t *testing.T) {
141+
t.Parallel()
142+
143+
ctx := testutil.TestContext(t)
144+
145+
_, err := packageFromSourceOrCluster(ctx, nil, "test", false)
146+
require.EqualError(t, err, "cannot get Zarf package from Kubernetes without configuration")
147+
148+
pkg, err := packageFromSourceOrCluster(ctx, nil, "./testdata/zarf-package-test-amd64-0.0.1.tar.zst", false)
149+
require.NoError(t, err)
150+
require.Equal(t, "test", pkg.Metadata.Name)
151+
152+
c := &cluster.Cluster{
153+
Clientset: fake.NewSimpleClientset(),
154+
}
155+
_, err = c.RecordPackageDeployment(ctx, pkg, nil, 1)
156+
require.NoError(t, err)
157+
pkg, err = packageFromSourceOrCluster(ctx, c, "test", false)
158+
require.NoError(t, err)
159+
require.Equal(t, "test", pkg.Metadata.Name)
160+
}

src/internal/packager2/remove.go

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors
3+
4+
package packager2
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"slices"
11+
12+
"helm.sh/helm/v3/pkg/action"
13+
"helm.sh/helm/v3/pkg/cli"
14+
"helm.sh/helm/v3/pkg/storage/driver"
15+
16+
"github.com/zarf-dev/zarf/src/api/v1alpha1"
17+
"github.com/zarf-dev/zarf/src/config"
18+
"github.com/zarf-dev/zarf/src/pkg/cluster"
19+
"github.com/zarf-dev/zarf/src/pkg/message"
20+
"github.com/zarf-dev/zarf/src/pkg/packager/actions"
21+
"github.com/zarf-dev/zarf/src/pkg/packager/filters"
22+
"github.com/zarf-dev/zarf/src/types"
23+
)
24+
25+
// RemoveOptions are the options for Remove.
26+
type RemoveOptions struct {
27+
Source string
28+
Cluster *cluster.Cluster
29+
Filter filters.ComponentFilterStrategy
30+
SkipSignatureValidation bool
31+
}
32+
33+
// Remove removes a package that was already deployed onto a cluster, uninstalling all installed helm charts.
34+
func Remove(ctx context.Context, opt RemoveOptions) error {
35+
pkg, err := packageFromSourceOrCluster(ctx, opt.Cluster, opt.Source, opt.SkipSignatureValidation)
36+
if err != nil {
37+
return err
38+
}
39+
40+
// If components were provided; just remove the things we were asked to remove
41+
components, err := opt.Filter.Apply(pkg)
42+
if err != nil {
43+
return err
44+
}
45+
// Check that cluster is configured if required.
46+
requiresCluster := false
47+
componentIdx := map[string]v1alpha1.ZarfComponent{}
48+
for _, component := range components {
49+
componentIdx[component.Name] = component
50+
if component.RequiresCluster() {
51+
if opt.Cluster == nil {
52+
return fmt.Errorf("component %s requires cluster access but none was configured", component.Name)
53+
}
54+
requiresCluster = true
55+
}
56+
}
57+
58+
// Get or build the secret for the deployed package
59+
depPkg := &types.DeployedPackage{}
60+
if requiresCluster {
61+
depPkg, err = opt.Cluster.GetDeployedPackage(ctx, pkg.Metadata.Name)
62+
if err != nil {
63+
return fmt.Errorf("unable to load the secret for the package we are attempting to remove: %s", err.Error())
64+
}
65+
} else {
66+
// If we do not need the cluster, create a deployed components object based on the info we have
67+
depPkg.Name = pkg.Metadata.Name
68+
depPkg.Data = pkg
69+
for _, component := range components {
70+
depPkg.DeployedComponents = append(depPkg.DeployedComponents, types.DeployedComponent{Name: component.Name})
71+
}
72+
}
73+
74+
reverseDepComps := slices.Clone(depPkg.DeployedComponents)
75+
slices.Reverse(reverseDepComps)
76+
for _, depComp := range reverseDepComps {
77+
// Only remove the component if it was requested or if we are removing the whole package.
78+
comp, ok := componentIdx[depComp.Name]
79+
if !ok {
80+
continue
81+
}
82+
83+
err := func() error {
84+
err := actions.Run(ctx, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.Before, nil)
85+
if err != nil {
86+
return fmt.Errorf("unable to run the before action: %w", err)
87+
}
88+
89+
reverseInstalledCharts := slices.Clone(depComp.InstalledCharts)
90+
slices.Reverse(reverseInstalledCharts)
91+
if opt.Cluster != nil {
92+
for _, chart := range reverseInstalledCharts {
93+
settings := cli.New()
94+
settings.SetNamespace(chart.Namespace)
95+
actionConfig := &action.Configuration{}
96+
// TODO (phillebaba): Get credentials from cluster instead of reading again.
97+
err := actionConfig.Init(settings.RESTClientGetter(), chart.Namespace, "", func(string, ...interface{}) {})
98+
if err != nil {
99+
return err
100+
}
101+
client := action.NewUninstall(actionConfig)
102+
client.KeepHistory = false
103+
client.Wait = true
104+
client.Timeout = config.ZarfDefaultTimeout
105+
_, err = client.Run(chart.ChartName)
106+
if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) {
107+
return fmt.Errorf("unable to uninstall the helm chart %s in the namespace %s: %w", chart.ChartName, chart.Namespace, err)
108+
}
109+
if errors.Is(err, driver.ErrReleaseNotFound) {
110+
message.Warnf("Helm release for helm chart '%s' in the namespace '%s' was not found. Was it already removed?", chart.ChartName, chart.Namespace)
111+
}
112+
113+
// Pop the removed helm chart from the installed charts slice.
114+
installedCharts := depPkg.DeployedComponents[len(depPkg.DeployedComponents)-1].InstalledCharts
115+
installedCharts = installedCharts[:len(installedCharts)-1]
116+
depPkg.DeployedComponents[len(depPkg.DeployedComponents)-1].InstalledCharts = installedCharts
117+
err = opt.Cluster.UpdateDeployedPackage(ctx, *depPkg)
118+
if err != nil {
119+
// We warn and ignore errors because we may have removed the cluster that this package was inside of
120+
message.Warnf("Unable to update the secret for package %s, this may be normal if the cluster was removed: %s", depPkg.Name, err.Error())
121+
}
122+
}
123+
}
124+
125+
err = actions.Run(ctx, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.After, nil)
126+
if err != nil {
127+
return fmt.Errorf("unable to run the after action: %w", err)
128+
}
129+
err = actions.Run(ctx, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.OnSuccess, nil)
130+
if err != nil {
131+
return fmt.Errorf("unable to run the success action: %w", err)
132+
}
133+
134+
// Pop the removed component from deploy components slice.
135+
if opt.Cluster != nil {
136+
depPkg.DeployedComponents = depPkg.DeployedComponents[:len(depPkg.DeployedComponents)-1]
137+
err = opt.Cluster.UpdateDeployedPackage(ctx, *depPkg)
138+
if err != nil {
139+
// We warn and ignore errors because we may have removed the cluster that this package was inside of
140+
message.Warnf("Unable to update the secret for package %s, this may be normal if the cluster was removed: %s", depPkg.Name, err.Error())
141+
}
142+
}
143+
return nil
144+
}()
145+
if err != nil {
146+
removeErr := actions.Run(ctx, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.OnFailure, nil)
147+
if removeErr != nil {
148+
return errors.Join(fmt.Errorf("unable to run the failure action: %w", err), removeErr)
149+
}
150+
return err
151+
}
152+
}
153+
154+
// All the installed components were deleted, therefore this package is no longer actually deployed
155+
if opt.Cluster != nil && len(depPkg.DeployedComponents) == 0 {
156+
err := opt.Cluster.DeleteDeployedPackage(ctx, depPkg.Name)
157+
if err != nil {
158+
message.Warnf("Unable to delete the secret for package %s, this may be normal if the cluster was removed: %s", depPkg.Name, err.Error())
159+
}
160+
}
161+
162+
return nil
163+
}

src/pkg/cluster/zarf.go

+59
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,65 @@ func (c *Cluster) GetDeployedPackage(ctx context.Context, packageName string) (*
7474
return deployedPackage, nil
7575
}
7676

77+
// UpdateDeployedPackage updates the deployed package metadata.
78+
func (c *Cluster) UpdateDeployedPackage(ctx context.Context, depPkg types.DeployedPackage) error {
79+
secretName := config.ZarfPackagePrefix + depPkg.Name
80+
packageSecretData, err := json.Marshal(depPkg)
81+
if err != nil {
82+
return err
83+
}
84+
packageSecret := &corev1.Secret{
85+
TypeMeta: metav1.TypeMeta{
86+
APIVersion: corev1.SchemeGroupVersion.String(),
87+
Kind: "Secret",
88+
},
89+
ObjectMeta: metav1.ObjectMeta{
90+
Name: secretName,
91+
Namespace: ZarfNamespaceName,
92+
Labels: map[string]string{
93+
ZarfManagedByLabel: "zarf",
94+
ZarfPackageInfoLabel: depPkg.Name,
95+
},
96+
},
97+
Type: corev1.SecretTypeOpaque,
98+
Data: map[string][]byte{
99+
"data": packageSecretData,
100+
},
101+
}
102+
err = func() error {
103+
_, err := c.Clientset.CoreV1().Secrets(packageSecret.Namespace).Get(ctx, packageSecret.Name, metav1.GetOptions{})
104+
if err != nil && !kerrors.IsNotFound(err) {
105+
return err
106+
}
107+
if kerrors.IsNotFound(err) {
108+
_, err = c.Clientset.CoreV1().Secrets(packageSecret.Namespace).Create(ctx, packageSecret, metav1.CreateOptions{})
109+
if err != nil {
110+
return fmt.Errorf("unable to create the deployed package secret: %w", err)
111+
}
112+
return nil
113+
}
114+
_, err = c.Clientset.CoreV1().Secrets(packageSecret.Namespace).Update(ctx, packageSecret, metav1.UpdateOptions{})
115+
if err != nil {
116+
return fmt.Errorf("unable to update the deployed package secret: %w", err)
117+
}
118+
return nil
119+
}()
120+
if err != nil {
121+
return err
122+
}
123+
return nil
124+
}
125+
126+
// DeleteDeployedPackage removes the metadata for the deployed package.
127+
func (c *Cluster) DeleteDeployedPackage(ctx context.Context, packageName string) error {
128+
secretName := config.ZarfPackagePrefix + packageName
129+
err := c.Clientset.CoreV1().Secrets(ZarfNamespaceName).Delete(ctx, secretName, metav1.DeleteOptions{})
130+
if err != nil {
131+
return err
132+
}
133+
return nil
134+
}
135+
77136
// StripZarfLabelsAndSecretsFromNamespaces removes metadata and secrets from existing namespaces no longer manged by Zarf.
78137
func (c *Cluster) StripZarfLabelsAndSecretsFromNamespaces(ctx context.Context) {
79138
spinner := message.NewProgressSpinner("Removing zarf metadata & secrets from existing namespaces not managed by Zarf")

src/test/e2e/25_helm_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func testHelmEscaping(t *testing.T) {
9999
require.NoError(t, err, stdOut, stdErr)
100100

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

0 commit comments

Comments
 (0)