Repository: knative-sandbox/kn-plugin-func
Pull request #763 (2022-01-17 12:40:38)

feat: delete pipeline and resources with `func delete`
Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>

<!-- Thanks for sending a pull request! -->

# Changes

<!-- 
Describe your changes here- ideally you can get that description straight from
your descriptive commit message(s)! 

- :gift: Add new feature
- :bug: Fix bug
- :broom: Update or clean up current behavior
- :wastebasket: Remove feature or internal logic
-->

- 🎁 On cluster build: delete pipeline and all created resources with func delete

<!--
In addition, categorize the changes you're making using the "/kind" Prow command, example:

/kind <kind>

Supported kinds are: api-change, bug, cleanup, deprecation, removal, documentation, enhancement, performance

-->
/kind enhancement

Fixes: #754

knative-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative-sandbox/kn-plugin-func/pull/763#" title="Author self-approved">zroubalik</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative-sandbox%2Fkn-plugin-func).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/knative-sandbox/kn-plugin-func/blob/main/OWNERS)~~ [zroubalik]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) Report
> Merging [#763](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (4cd9369) into [main](https://codecov.io/gh/knative-sandbox/kn-plugin-func/commit/bddac30621e12cc5c0dc8432bd8064c26d4329eb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (bddac30) will **decrease** coverage by `0.00%`.
> The diff coverage is `34.42%`.

[![Impacted file tree graph](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763/graphs/tree.svg?width=650&height=150&src=pr&token=da1B9bMsQ9&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)

```diff
@@            Coverage Diff             @@
##             main     #763      +/-   ##
==========================================
- Coverage   41.39%   41.39%   -0.01%     
==========================================
  Files          44       44              
  Lines        4177     4223      +46     
==========================================
+ Hits         1729     1748      +19     
- Misses       2192     2218      +26     
- Partials      256      257       +1     
```


| [Impacted Files](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) | Coverage Δ | |
|---|---|---|
| [knative/remover.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-a25hdGl2ZS9yZW1vdmVyLmdv) | `0.00% <ø> (ø)` | |
| [cmd/delete.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL2RlbGV0ZS5nbw==) | `48.97% <11.11%> (-17.69%)` | :arrow_down: |
| [client.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y2xpZW50Lmdv) | `62.08% <68.00%> (+1.21%)` | :arrow_up: |
| [function\_git.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-ZnVuY3Rpb25fZ2l0Lmdv) | `100.00% <0.00%> (+16.66%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox). Last update [bddac30...4cd9369](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/763?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox).

knative-prow-robot: @zroubalik: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
knative-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative-sandbox/kn-plugin-func/pull/763#pullrequestreview-860883862" title="Approved">lkingland</a>*, *<a href="https://github.com/knative-sandbox/kn-plugin-func/pull/763#" title="Author self-approved">zroubalik</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative-sandbox%2Fkn-plugin-func).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/knative-sandbox/kn-plugin-func/blob/main/OWNERS)~~ [lkingland,zroubalik]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
knative-prow-robot: New changes are detected. LGTM label has been removed.

Patch:
diff --git a/client.go b/client.go
index 5ebc3600..95e0c09a 100644
--- a/client.go
+++ b/client.go
@@ -185,6 +185,7 @@ type DNSProvider interface {
 // PipelinesProvider manages lifecyle of CI/CD pipelines used by a Function
 type PipelinesProvider interface {
 	Run(context.Context, Function) error
+	Remove(context.Context, Function) error
 }
 
 // New client for Function management.
@@ -788,25 +789,47 @@ func (c *Client) List(ctx context.Context) ([]ListItem, error) {
 
 // Remove a Function.  Name takes precidence.  If no name is provided,
 // the Function defined at root is used if it exists.
-func (c *Client) Remove(ctx context.Context, cfg Function) error {
+func (c *Client) Remove(ctx context.Context, cfg Function, deleteAll bool) error {
 	go func() {
 		<-ctx.Done()
 		c.progressListener.Stopping()
 	}()
 	// If name is provided, it takes precidence.
-	// Otherwise load the Function deined at root.
-	if cfg.Name != "" {
-		return c.remover.Remove(ctx, cfg.Name)
+	// Otherwise load the Function defined at root.
+	functionName := cfg.Name
+	if cfg.Name == "" {
+		f, err := NewFunction(cfg.Root)
+		if err != nil {
+			return err
+		}
+		if !f.Initialized() {
+			return fmt.Errorf("Function at %v can not be removed unless initialized. Try removing by name", f.Root)
+		}
+		functionName = f.Name
+		cfg = f
 	}
 
-	f, err := NewFunction(cfg.Root)
-	if err != nil {
-		return err
+	// Delete Knative Service and dependent resources in parallel
+	c.progressListener.Increment(fmt.Sprintf("Removing Knative Service: %v", functionName))
+	errChan := make(chan error)
+	go func() {
+		errChan <- c.remover.Remove(ctx, functionName)
+	}()
+
+	var errResources error
+	if deleteAll {
+		c.progressListener.Increment(fmt.Sprintf("Removing Knative Service '%v' and all dependent resources", functionName))
+		errResources = c.pipelinesProvider.Remove(ctx, cfg)
 	}
-	if !f.Initialized() {
-		return fmt.Errorf("Function at %v can not be removed unless initialized.  Try removing by name", f.Root)
+
+	errService := <-errChan
+
+	if errService != nil && errResources != nil {
+		return fmt.Errorf("%s\n%s", errService, errResources)
+	} else if errResources != nil {
+		return errResources
 	}
-	return c.remover.Remove(ctx, f.Name)
+	return errService
 }
 
 // Invoke is a convenience method for triggering the execution of a Function
@@ -917,7 +940,8 @@ func (n *noopDescriber) Describe(context.Context, string) (Instance, error) {
 // PipelinesProvider
 type noopPipelinesProvider struct{}
 
-func (n *noopPipelinesProvider) Run(ctx context.Context, _ Function) error { return nil }
+func (n *noopPipelinesProvider) Run(ctx context.Context, _ Function) error    { return nil }
+func (n *noopPipelinesProvider) Remove(ctx context.Context, _ Function) error { return nil }
 
 // DNSProvider
 type noopDNSProvider struct{ output io.Writer }
diff --git a/cmd/delete.go b/cmd/delete.go
index 7de33a55..c718b23e 100644
--- a/cmd/delete.go
+++ b/cmd/delete.go
@@ -10,6 +10,8 @@ import (
 
 	fn "knative.dev/kn-plugin-func"
 	"knative.dev/kn-plugin-func/knative"
+	"knative.dev/kn-plugin-func/pipelines/tekton"
+	"knative.dev/kn-plugin-func/progress"
 )
 
 func init() {
@@ -23,15 +25,26 @@ func init() {
 // Testing note: This method is swapped out during testing to allow
 // mocking the remover or the client itself to fabricate test states.
 func newDeleteClient(cfg deleteConfig) (*fn.Client, error) {
+	listener := progress.New()
 	remover, err := knative.NewRemover(cfg.Namespace)
 	if err != nil {
 		return nil, err
 	}
 
+	pipelinesProvider, err := tekton.NewPipelinesProvider(
+		tekton.WithNamespace(cfg.Namespace))
+	if err != nil {
+		return nil, err
+	}
+
+	listener.Verbose = cfg.Verbose
 	remover.Verbose = cfg.Verbose
+	pipelinesProvider.Verbose = cfg.Verbose
 
 	return fn.New(
+		fn.WithProgressListener(listener),
 		fn.WithRemover(remover),
+		fn.WithPipelinesProvider(pipelinesProvider),
 		fn.WithVerbose(cfg.Verbose)), nil
 }
 
@@ -59,10 +72,11 @@ kn func delete -n apps myfunc
 `,
 		SuggestFor:        []string{"remove", "rm", "del"},
 		ValidArgsFunction: CompleteFunctionList,
-		PreRunE:           bindEnv("path", "confirm", "namespace"),
+		PreRunE:           bindEnv("path", "confirm", "namespace", "all"),
 	}
 
 	cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
+	cmd.Flags().StringP("all", "a", "true", "Delete all resources created for a function, eg. Pipelines, Secrets, etc. (Env: $FUNC_ALL) (allowed values: \"true\", \"false\")")
 	setNamespaceFlag(cmd)
 	setPathFlag(cmd)
 
@@ -117,13 +131,14 @@ func runDelete(cmd *cobra.Command, args []string, clientFn deleteClientFn) (err
 	}
 
 	// Invoke remove using the concrete client impl
-	return client.Remove(cmd.Context(), function)
+	return client.Remove(cmd.Context(), function, config.DeleteAll)
 }
 
 type deleteConfig struct {
 	Name      string
 	Namespace string
 	Path      string
+	DeleteAll bool
 	Verbose   bool
 }
 
@@ -137,6 +152,7 @@ func newDeleteConfig(args []string) deleteConfig {
 	return deleteConfig{
 		Path:      viper.GetString("path"),
 		Namespace: viper.GetString("namespace"),
+		DeleteAll: viper.GetBool("all"),
 		Name:      deriveName(name, viper.GetString("path")), // args[0] or derived
 		Verbose:   viper.GetBool("verbose"),                  // defined on root
 	}
@@ -150,11 +166,35 @@ func (c deleteConfig) Prompt() (deleteConfig, error) {
 		return c, nil
 	}
 
-	dc := deleteConfig{}
+	dc := c
+	var qs = []*survey.Question{
+		{
+			Name: "name",
+			Prompt: &survey.Input{
+				Message: "Function to remove:",
+				Default: deriveName(c.Name, c.Path)},
+			Validate: survey.Required,
+		},
+		{
+			Name: "all",
+			Prompt: &survey.Confirm{
+				Message: "Do you want to delete all resources?",
+				Default: c.DeleteAll,
+			},
+		},
+	}
+	answers := struct {
+		Name string
+		All  bool
+	}{}
+
+	err := survey.Ask(qs, &answers)
+	if err != nil {
+		return dc, err
+	}
+
+	dc.Name = answers.Name
+	dc.DeleteAll = answers.All
 
-	return dc, survey.AskOne(
-		&survey.Input{
-			Message: "Function to remove:",
-			Default: deriveName(c.Name, c.Path)},
-		&dc.Name, survey.WithValidator(survey.Required))
+	return dc, err
 }
diff --git a/hack/allocate.sh b/hack/allocate.sh
index 3a234b78..8c44c700 100755
--- a/hack/allocate.sh
+++ b/hack/allocate.sh
@@ -47,7 +47,7 @@ main() {
 
 kubernetes() {
   echo "${em}① Kubernetes${me}"
-  cat <<EOF | kind create cluster --wait=60s --config=-
+  cat <<EOF | kind create cluster --name=func --wait=60s --config=-
 kind: Cluster
 apiVersion: kind.x-k8s.io/v1alpha4
 nodes:
@@ -63,7 +63,7 @@ nodes:
 containerdConfigPatches:
 - |-
   [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:50000"]
-    endpoint = ["http://kind-registry:5000"]
+    endpoint = ["http://func-registry:5000"]
 EOF
   sleep 10
   kubectl wait pod --for=condition=Ready -l '!job-name' -n kube-system --timeout=5m
@@ -179,8 +179,8 @@ registry() {
   # see https://kind.sigs.k8s.io/docs/user/local-registry/
 
   echo "${em}⑥ Registry${me}"
-  docker run -d --restart=always -p "127.0.0.1:50000:5000" --name "kind-registry" registry:2
-  docker network connect "kind" "kind-registry"
+  docker run -d --restart=always -p "127.0.0.1:50000:5000" --name "func-registry" registry:2
+  docker network connect "kind" "func-registry"
   kubectl apply -f - <<EOF
 apiVersion: v1
 kind: ConfigMap
diff --git a/hack/delete.sh b/hack/delete.sh
index 1784a3e3..180a27e5 100755
--- a/hack/delete.sh
+++ b/hack/delete.sh
@@ -14,12 +14,11 @@ main() {
   local red=$(tput bold)$(tput setaf 2)
   local reset=$(tput sgr0)
 
-  echo "${green}Deleting Cluster, Registry and Network${reset}"
-  kind delete cluster --name "kind"
-  docker stop kind-registry && docker rm kind-registry
-  docker network rm kind
+  echo "${green}Deleting Cluster and Registry${reset}"
+  kind delete cluster --name "func"
+  docker stop func-registry && docker rm func-registry
   echo "${red}NOTE:${reset}  The following changes have not been undone:"
-  echo " - Manual docker config registering registry at localhost:50000 (kind-registry) as insecure"
+  echo " - Config setting registry localhost:50000 (func-registry) as insecure"
   echo " - Downloaded container images were not removed"
   echo "${green}DONE${reset}"
 }
diff --git a/k8s/persistent_volumes.go b/k8s/persistent_volumes.go
index 465d05a7..c0a3a61c 100644
--- a/k8s/persistent_volumes.go
+++ b/k8s/persistent_volumes.go
@@ -41,3 +41,12 @@ func CreatePersistentVolumeClaim(ctx context.Context, name, namespaceOverride st
 	_, err = client.CoreV1().PersistentVolumeClaims(namespace).Create(ctx, pvc, metav1.CreateOptions{})
 	return
 }
+
+func DeletePersistentVolumeClaims(ctx context.Context, namespaceOverride string, listOptions metav1.ListOptions) (err error) {
+	client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
+	if err != nil {
+		return
+	}
+
+	return client.CoreV1().PersistentVolumeClaims(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions)
+}
diff --git a/k8s/role_bidings.go b/k8s/role_bidings.go
index 290ddd08..99abdb37 100644
--- a/k8s/role_bidings.go
+++ b/k8s/role_bidings.go
@@ -35,3 +35,12 @@ func CreateRoleBindingForServiceAccount(ctx context.Context, name, namespaceOver
 	_, err = client.RbacV1().RoleBindings(namespace).Create(ctx, rb, metav1.CreateOptions{})
 	return
 }
+
+func DeleteRoleBindings(ctx context.Context, namespaceOverride string, listOptions metav1.ListOptions) (err error) {
+	client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
+	if err != nil {
+		return
+	}
+
+	return client.RbacV1().RoleBindings(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions)
+}
diff --git a/k8s/secrets.go b/k8s/secrets.go
index 04500d5e..aa82160b 100644
--- a/k8s/secrets.go
+++ b/k8s/secrets.go
@@ -36,6 +36,15 @@ func ListSecretsNames(ctx context.Context, namespaceOverride string) (names []st
 	return
 }
 
+func DeleteSecrets(ctx context.Context, namespaceOverride string, listOptions metav1.ListOptions) (err error) {
+	client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
+	if err != nil {
+		return
+	}
+
+	return client.CoreV1().Secrets(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions)
+}
+
 func CreateDockerRegistrySecret(ctx context.Context, name, namespaceOverride string, labels map[string]string, username, password, server string) (err error) {
 	client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
 	if err != nil {
diff --git a/k8s/service_accounts.go b/k8s/service_accounts.go
index 5f9a676b..19719cd5 100644
--- a/k8s/service_accounts.go
+++ b/k8s/service_accounts.go
@@ -31,3 +31,12 @@ func CreateServiceAccountWithSecret(ctx context.Context, name, namespaceOverride
 	_, err = client.CoreV1().ServiceAccounts(namespace).Create(ctx, sa, metav1.CreateOptions{})
 	return
 }
+
+func DeleteServiceAccounts(ctx context.Context, namespaceOverride string, listOptions metav1.ListOptions) (err error) {
+	client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
+	if err != nil {
+		return
+	}
+
+	return client.CoreV1().ServiceAccounts(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions)
+}
diff --git a/knative/remover.go b/knative/remover.go
index ac42e27b..a32f3262 100644
--- a/knative/remover.go
+++ b/knative/remover.go
@@ -33,8 +33,6 @@ func (remover *Remover) Remove(ctx context.Context, name string) (err error) {
 		return
 	}
 
-	fmt.Printf("Removing Knative Service: %v\n", name)
-
 	err = client.DeleteService(ctx, name, RemoveTimeout)
 	if err != nil {
 		err = fmt.Errorf("knative remover failed to delete the service: %v", err)
diff --git a/mock/pipelines_provider.go b/mock/pipelines_provider.go
new file mode 100644
index 00000000..a5d674c6
--- /dev/null
+++ b/mock/pipelines_provider.go
@@ -0,0 +1,31 @@
+package mock
+
+import (
+	"context"
+
+	fn "knative.dev/kn-plugin-func"
+)
+
+type PipelinesProvider struct {
+	RunInvoked    bool
+	RunFn         func(fn.Function) error
+	RemoveInvoked bool
+	RemoveFn      func(fn.Function) error
+}
+
+func NewPipelinesProvider() *PipelinesProvider {
+	return &PipelinesProvider{
+		RunFn:    func(fn.Function) error { return nil },
+		RemoveFn: func(fn.Function) error { return nil },
+	}
+}
+
+func (p *PipelinesProvider) Run(ctx context.Context, f fn.Function) error {
+	p.RunInvoked = true
+	return p.RunFn(f)
+}
+
+func (p *PipelinesProvider) Remove(ctx context.Context, f fn.Function) error {
+	p.RemoveInvoked = true
+	return p.RemoveFn(f)
+}
diff --git a/pipelines/tekton/pipeplines_provider.go b/pipelines/tekton/pipeplines_provider.go
index b8a4969d..96a72e2a 100644
--- a/pipelines/tekton/pipeplines_provider.go
+++ b/pipelines/tekton/pipeplines_provider.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/google/go-containerregistry/pkg/authn"
 	"github.com/google/go-containerregistry/pkg/name"
-
 	"github.com/tektoncd/cli/pkg/pipelinerun"
 	"github.com/tektoncd/cli/pkg/taskrun"
 	"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
@@ -15,6 +14,7 @@ import (
 	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	k8slabels "k8s.io/apimachinery/pkg/labels"
 
 	fn "knative.dev/kn-plugin-func"
 	"knative.dev/kn-plugin-func/docker"
@@ -184,6 +184,59 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) error {
 	return nil
 }
 
+func (pp *PipelinesProvider) Remove(ctx context.Context, f fn.Function) error {
+
+	l := k8slabels.SelectorFromSet(k8slabels.Set(map[string]string{labels.FunctionNameKey: f.Name}))
+	listOptions := metav1.ListOptions{
+		LabelSelector: l.String(),
+	}
+
+	// let's try to delete all resources in parallel, so the operation doesn't take long
+	wg := sync.WaitGroup{}
+	deleteFunctions := []func(context.Context, string, metav1.ListOptions) error{
+		deletePipelines,
+		deletePipelineRuns,
+		k8s.DeleteRoleBindings,
+		k8s.DeleteServiceAccounts,
+		k8s.DeleteSecrets,
+		k8s.DeletePersistentVolumeClaims,
+	}
+
+	wg.Add(len(deleteFunctions))
+	errChan := make(chan error, len(deleteFunctions))
+
+	for i := range deleteFunctions {
+		df := deleteFunctions[i]
+		go func() {
+			defer wg.Done()
+			err := df(ctx, pp.namespace, listOptions)
+			if err != nil && !errors.IsNotFound(err) {
+				errChan <- err
+			}
+		}()
+	}
+	wg.Wait()
+	close(errChan)
+
+	// collect all errors and print them
+	var err error
+	errMsg := ""
+	anyError := false
+	for e := range errChan {
+		if !anyError {
+			anyError = true
+			errMsg = "error deleting resources:"
+		}
+		errMsg += fmt.Sprintf("\n %v", e)
+	}
+
+	if anyError {
+		err = fmt.Errorf("%s", errMsg)
+	}
+
+	return err
+}
+
 // watchPipelineRunProgress watches the progress of the input PipelineRun
 // and prints detailed description of the currently executed Tekton Task.
 func (pp *PipelinesProvider) watchPipelineRunProgress(pr *v1beta1.PipelineRun) error {
diff --git a/pipelines/tekton/resources.go b/pipelines/tekton/resources.go
index ad41184a..2e519b48 100644
--- a/pipelines/tekton/resources.go
+++ b/pipelines/tekton/resources.go
@@ -1,15 +1,35 @@
 package tekton
 
 import (
+	"context"
 	"fmt"
 
 	pplnv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	fn "knative.dev/kn-plugin-func"
 )
 
+func deletePipelines(ctx context.Context, namespace string, listOptions metav1.ListOptions) (err error) {
+	client, err := NewTektonClient()
+	if err != nil {
+		return
+	}
+
+	return client.Pipelines(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions)
+}
+
+func deletePipelineRuns(ctx context.Context, namespace string, listOptions metav1.ListOptions) (err error) {
+	client, err := NewTektonClient()
+	if err != nil {
+		return
+	}
+
+	return client.PipelineRuns(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions)
+}
+
 func generatePipeline(f fn.Function, labels map[string]string) *pplnv1beta1.Pipeline {
 	pipelineName := getPipelineName(f)
 


Test patch:
diff --git a/client_int_test.go b/client_int_test.go
index 5738f2b9..912b8562 100644
--- a/client_int_test.go
+++ b/client_int_test.go
@@ -135,7 +135,7 @@ func TestRemove(t *testing.T) {
 	}
 	waitFor(t, client, "remove")
 
-	if err := client.Remove(context.Background(), fn.Function{Name: "remove"}); err != nil {
+	if err := client.Remove(context.Background(), fn.Function{Name: "remove"}, false); err != nil {
 		t.Fatal(err)
 	}
 
@@ -256,7 +256,7 @@ func newClient(verbose bool) *fn.Client {
 func del(t *testing.T, c *fn.Client, name string) {
 	t.Helper()
 	waitFor(t, c, name)
-	if err := c.Remove(context.Background(), fn.Function{Name: name}); err != nil {
+	if err := c.Remove(context.Background(), fn.Function{Name: name}, false); err != nil {
 		t.Fatal(err)
 	}
 }
diff --git a/client_test.go b/client_test.go
index 276d23f8..94026d34 100644
--- a/client_test.go
+++ b/client_test.go
@@ -673,7 +673,7 @@ func TestClient_Remove_ByPath(t *testing.T) {
 		return nil
 	}
 
-	if err := client.Remove(context.Background(), fn.Function{Root: root}); err != nil {
+	if err := client.Remove(context.Background(), fn.Function{Root: root}, false); err != nil {
 		t.Fatal(err)
 	}
 
@@ -683,6 +683,94 @@ func TestClient_Remove_ByPath(t *testing.T) {
 
 }
 
+// TestClient_Remove_DeleteAll ensures that the remover is invoked to remove
+// and that dependent resources are removed as well -> pipeline provider is invoked
+// the Function with the name of the function at the provided root.
+func TestClient_Remove_DeleteAll(t *testing.T) {
+	var (
+		root              = "testdata/example.com/testRemoveDeleteAll"
+		expectedName      = "testRemoveDeleteAll"
+		remover           = mock.NewRemover()
+		pipelinesProvider = mock.NewPipelinesProvider()
+		deleteAll         = true
+	)
+
+	defer Using(t, root)()
+
+	client := fn.New(
+		fn.WithRegistry(TestRegistry),
+		fn.WithRemover(remover),
+		fn.WithPipelinesProvider(pipelinesProvider))
+
+	if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil {
+		t.Fatal(err)
+	}
+
+	remover.RemoveFn = func(name string) error {
+		if name != expectedName {
+			t.Fatalf("Expected to remove '%v', got '%v'", expectedName, name)
+		}
+		return nil
+	}
+
+	if err := client.Remove(context.Background(), fn.Function{Root: root}, deleteAll); err != nil {
+		t.Fatal(err)
+	}
+
+	if !remover.RemoveInvoked {
+		t.Fatal("remover was not invoked")
+	}
+
+	if !pipelinesProvider.RemoveInvoked {
+		t.Fatal("pipelinesprovider was not invoked")
+	}
+
+}
+
+// TestClient_Remove_Dont_DeleteAll ensures that the remover is invoked to remove
+// and that dependent resources are not removed as well -> pipeline provider not is invoked
+// the Function with the name of the function at the provided root.
+func TestClient_Remove_Dont_DeleteAll(t *testing.T) {
+	var (
+		root              = "testdata/example.com/testRemoveDontDeleteAll"
+		expectedName      = "testRemoveDontDeleteAll"
+		remover           = mock.NewRemover()
+		pipelinesProvider = mock.NewPipelinesProvider()
+		deleteAll         = false
+	)
+
+	defer Using(t, root)()
+
+	client := fn.New(
+		fn.WithRegistry(TestRegistry),
+		fn.WithRemover(remover),
+		fn.WithPipelinesProvider(pipelinesProvider))
+
+	if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil {
+		t.Fatal(err)
+	}
+
+	remover.RemoveFn = func(name string) error {
+		if name != expectedName {
+			t.Fatalf("Expected to remove '%v', got '%v'", expectedName, name)
+		}
+		return nil
+	}
+
+	if err := client.Remove(context.Background(), fn.Function{Root: root}, deleteAll); err != nil {
+		t.Fatal(err)
+	}
+
+	if !remover.RemoveInvoked {
+		t.Fatal("remover was not invoked")
+	}
+
+	if pipelinesProvider.RemoveInvoked {
+		t.Fatal("pipelinesprovider was invoked, but should not")
+	}
+
+}
+
 // TestClient_Remove_ByName ensures that the remover is invoked to remove the function
 // of the name provided, with precidence over a provided root path.
 func TestClient_Remove_ByName(t *testing.T) {
@@ -710,12 +798,12 @@ func TestClient_Remove_ByName(t *testing.T) {
 	}
 
 	// Run remove with only a name
-	if err := client.Remove(context.Background(), fn.Function{Name: expectedName}); err != nil {
+	if err := client.Remove(context.Background(), fn.Function{Name: expectedName}, false); err != nil {
 		t.Fatal(err)
 	}
 
 	// Run remove with a name and a root, which should be ignored in favor of the name.
-	if err := client.Remove(context.Background(), fn.Function{Name: expectedName, Root: root}); err != nil {
+	if err := client.Remove(context.Background(), fn.Function{Name: expectedName, Root: root}, false); err != nil {
 		t.Fatal(err)
 	}
 
@@ -746,7 +834,7 @@ func TestClient_Remove_UninitializedFails(t *testing.T) {
 		fn.WithRemover(remover))
 
 	// Attempt to remove by path (uninitialized), expecting an error.
-	if err := client.Remove(context.Background(), fn.Function{Root: root}); err == nil {
+	if err := client.Remove(context.Background(), fn.Function{Root: root}, false); err == nil {
 		t.Fatalf("did not received expeced error removing an uninitialized func")
 	}
 }