Skip to content

Commit f56a88e

Browse files
committed
refactor: remove
Signed-off-by: Philip Laine <[email protected]>
1 parent 7ac8297 commit f56a88e

File tree

6 files changed

+295
-11
lines changed

6 files changed

+295
-11
lines changed

src/cmd/package.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -270,19 +270,17 @@ 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+
cluster, _ := cluster.NewCluster()
274+
removeOpt := packager2.RemoveOptions{
275+
Source: packageSource,
276+
Cluster: cluster,
277+
OptionalComponents: pkgConfig.PkgOpts.OptionalComponents,
278+
SkipSignatureValidation: pkgConfig.PkgOpts.SkipSignatureValidation,
277279
}
278-
pkgClient, err := packager.New(&pkgConfig, packager.WithSource(src))
280+
err = packager2.Remove(cmd.Context(), removeOpt)
279281
if err != nil {
280282
return err
281283
}
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-
}
286284
return nil
287285
},
288286
ValidArgsFunction: getPackageCompletionArgs,
@@ -382,7 +380,9 @@ func choosePackage(args []string) (string, error) {
382380
return path, nil
383381
}
384382

385-
// TODO: This code does not seem to do what it was intended.
383+
// NOTE: If the source is identified nil is returned because packager will create the source if it is nil.
384+
// If it can't be identified the cluster source is used causing packager to ignore the configured package source.
385+
// Use of cluster package source is limited to a few functions which is why this is not the default behavior.
386386
func identifyAndFallbackToClusterSource() (sources.PackageSource, error) {
387387
identifiedSrc := sources.Identify(pkgConfig.PkgOpts.PackageSource)
388388
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

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

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 zarf state 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 zarf state 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)