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/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") } } 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 + }{} - return dc, survey.AskOne( - &survey.Input{ - Message: "Function to remove:", - Default: deriveName(c.Name, c.Path)}, - &dc.Name, survey.WithValidator(survey.Required)) + err := survey.Ask(qs, &answers) + if err != nil { + return dc, err + } + + dc.Name = answers.Name + dc.DeleteAll = answers.All + + return dc, err } 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)