feat: delete pipeline and resources with `func delete` (#763)

* feat: delete pipeline and resources with `func delete`

Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>

* fix test and prompt

Signed-off-by: Zbynek Roubalik <zroubali@redhat.com>
This commit is contained in:
Zbynek Roubalik 2022-01-24 21:39:29 +01:00 committed by GitHub
parent 6649a71f4d
commit d478f555cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 318 additions and 28 deletions

View File

@ -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 }

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)