diff --git a/cmd/config.go b/cmd/config.go index 28d53492..a6fe37f4 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -127,6 +127,8 @@ func runConfigCmd(cmd *cobra.Command, args []string) (err error) { err = runRemoveEnvsPrompt(function) } else if answers.SelectedConfig == "Labels" { err = runRemoveLabelsPrompt(function, defaultLoaderSaver) + } else if answers.SelectedConfig == "Git" { + err = runConfigGitRemoveCmd(cmd, NewClient) } case "List": if answers.SelectedConfig == "Volumes" { diff --git a/cmd/config_git.go b/cmd/config_git.go index 9b0bf670..7321cf4f 100644 --- a/cmd/config_git.go +++ b/cmd/config_git.go @@ -37,11 +37,13 @@ the current directory or from the directory specified with --path. } configGitSetCmd := NewConfigGitSetCmd(newClient) + configGitRemoveCmd := NewConfigGitRemoveCmd(newClient) addPathFlag(cmd) addVerboseFlag(cmd, cfg.Verbose) cmd.AddCommand(configGitSetCmd) + cmd.AddCommand(configGitRemoveCmd) return cmd } diff --git a/cmd/config_git_remove.go b/cmd/config_git_remove.go new file mode 100644 index 00000000..5965eae0 --- /dev/null +++ b/cmd/config_git_remove.go @@ -0,0 +1,188 @@ +package cmd + +import ( + "fmt" + + "github.com/AlecAivazis/survey/v2" + "github.com/ory/viper" + "github.com/spf13/cobra" + + "knative.dev/func/pkg/config" + fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/pipelines" +) + +func NewConfigGitRemoveCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove", + Short: "Remove Git settings from the function configuration", + Long: `Remove Git settings from the function configuration + + Interactive prompt to remove Git settings from the function project in the current + directory or from the directory specified with --path. + + It also removes any generated resources that are used for Git based build and deployment, + such as local generated Pipelines resources and any resources generated on the cluster. + `, + SuggestFor: []string{"rem", "rmeove", "del", "dle"}, + PreRunE: bindEnv("path", "namespace", "delete-local", "delete-cluster"), + RunE: func(cmd *cobra.Command, args []string) (err error) { + return runConfigGitRemoveCmd(cmd, newClient) + }, + } + + // Global Config + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + + // Function Context + f, _ := fn.NewFunction(effectivePath()) + if f.Initialized() { + cfg = cfg.Apply(f) + } + + // Flags + // + // Globally-Configurable Flags: + // Options whose value may be defined globally may also exist on the + // contextually relevant function; but sets are flattened via cfg.Apply(f) + cmd.Flags().StringP("namespace", "n", cfg.Namespace, + "Deploy into a specific namespace. Will use function's current namespace by default if already deployed, and the currently active namespace if it can be determined. (Env: $FUNC_NAMESPACE)") + + // Resources generated related Flags: + cmd.Flags().Bool("delete-local", false, "Delete local resources (pipeline templates).") + cmd.Flags().Bool("delete-cluster", false, "Delete cluster resources (credentials and config on the cluster).") + + addPathFlag(cmd) + addVerboseFlag(cmd, cfg.Verbose) + + return cmd +} + +type configGitRemoveConfig struct { + // Globals (builder, confirm, registry, verbose) + config.Global + + // Path of the function implementation on local disk. Defaults to current + // working directory of the process. + Path string + + Namespace string + + // informs whether any specific flag for deleting only a subset of resources has been set + flagSet bool + + metadata pipelines.PacMetadata +} + +// newConfigGitRemoveConfig creates a configGitRemoveConfig populated from command flags +func newConfigGitRemoveConfig(cmd *cobra.Command) (c configGitRemoveConfig) { + flagSet := false + + // decide what resources we should delete: + // - by default all resources + // - if any parameter is explicitly specified then get value from parameters + deleteLocal := true + deleteCluster := true + if viper.HasChanged("delete-local") || viper.HasChanged("delete-cluster") { + deleteLocal = viper.GetBool("delete-local") + deleteCluster = viper.GetBool("delete-cluster") + flagSet = true + } + + c = configGitRemoveConfig{ + Namespace: viper.GetString("namespace"), + + flagSet: flagSet, + + metadata: pipelines.PacMetadata{ + ConfigureLocalResources: deleteLocal, + ConfigureClusterResources: deleteCluster, + }, + } + + return c +} + +func (c configGitRemoveConfig) Prompt(f fn.Function) (configGitRemoveConfig, error) { + deleteAll := true + // prompt if any flag hasn't been set yet + if !c.flagSet { + if err := survey.AskOne(&survey.Confirm{ + Message: "Do you want to delete all Git related resources?", + Help: "Delete Git config, local Pipeline resourdces and on the cluster resources.", + Default: deleteAll, + }, &deleteAll, survey.WithValidator(survey.Required)); err != nil { + return c, err + } + } + + if !deleteAll { + deleteLocal := true + if err := survey.AskOne(&survey.Confirm{ + Message: "Do you want to delete all local Git related resources (Pipelines)?", + Help: "Delete local Pipeline resources created in the function project directory.", + Default: deleteLocal, + }, &deleteLocal, survey.WithValidator(survey.Required)); err != nil { + return c, err + } + c.metadata.ConfigureLocalResources = deleteLocal + + deleteCluster := true + if err := survey.AskOne(&survey.Confirm{ + Message: "Do you want to delete all Git related resources present on the cluster?", + Help: "Delete all Pipeline resources that were created on the cluster.", + Default: deleteCluster, + }, &deleteCluster, survey.WithValidator(survey.Required)); err != nil { + return c, err + } + c.metadata.ConfigureClusterResources = deleteCluster + } + + return c, nil +} + +// Configure the given function. Updates a function struct with all +// configurable values. Note that the config already includes function's +// current values, as they were passed through via flag defaults. +func (c configGitRemoveConfig) Configure(f fn.Function) (fn.Function, error) { + var err error + + if c.metadata.ConfigureLocalResources { + f.Build.Git = fn.Git{} + } + + // Save the function which has now been updated with flags/config + if err = f.Write(); err != nil { // TODO: remove when client API uses 'f' + return f, err + } + + return f, nil +} + +func runConfigGitRemoveCmd(cmd *cobra.Command, newClient ClientFactory) (err error) { + var ( + cfg configGitRemoveConfig + f fn.Function + ) + if err = config.CreatePaths(); err != nil { // for possible auth.json usage + return + } + cfg = newConfigGitRemoveConfig(cmd) + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if cfg, err = cfg.Prompt(f); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { // Updates f with deploy cfg + return + } + + client, done := newClient(ClientConfig{Namespace: cfg.Namespace, Verbose: cfg.Verbose}) + defer done() + + return client.RemovePAC(cmd.Context(), f, cfg.metadata) +} diff --git a/cmd/config_git_set.go b/cmd/config_git_set.go index a76d7d88..f95275d0 100644 --- a/cmd/config_git_set.go +++ b/cmd/config_git_set.go @@ -6,6 +6,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/ory/viper" "github.com/spf13/cobra" + "knative.dev/func/pkg/config" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/pipelines" @@ -75,7 +76,7 @@ func NewConfigGitSetCmd(newClient ClientFactory) *cobra.Command { // Resources generated related Flags: cmd.Flags().Bool("config-local", false, "Configure local resources (pipeline templates).") - cmd.Flags().Bool("config-cluster", false, "Configure cluster resources (credentials and config on cluster).") + cmd.Flags().Bool("config-cluster", false, "Configure cluster resources (credentials and config on the cluster).") cmd.Flags().Bool("config-remote", false, "Configure remote resources (webhook on the Git provider side).") addPathFlag(cmd) diff --git a/pkg/functions/client.go b/pkg/functions/client.go index e5073b6c..7dac080a 100644 --- a/pkg/functions/client.go +++ b/pkg/functions/client.go @@ -192,6 +192,7 @@ type PipelinesProvider interface { Run(context.Context, Function) error Remove(context.Context, Function) error ConfigurePAC(context.Context, Function, any) error + RemovePAC(context.Context, Function, any) error } // New client for function management. @@ -853,6 +854,21 @@ func (c *Client) ConfigurePAC(ctx context.Context, f Function, metadata any) err return nil } +// RemovePAC deletes generated Pipeline as Code resources on the local filesystem and on the cluster +func (c *Client) RemovePAC(ctx context.Context, f Function, metadata any) error { + go func() { + <-ctx.Done() + c.progressListener.Stopping() + }() + + // Build and deploy function using Pipeline + if err := c.pipelinesProvider.RemovePAC(ctx, f, metadata); err != nil { + return fmt.Errorf("failed to remove git related resources: %w", err) + } + + return nil +} + // Route returns the current primary route to the function at root. // // Note that local instances of the Function created by the .Run @@ -1197,6 +1213,9 @@ func (n *noopPipelinesProvider) Remove(ctx context.Context, _ Function) error { func (n *noopPipelinesProvider) ConfigurePAC(ctx context.Context, _ Function, _ any) error { return nil } +func (n *noopPipelinesProvider) RemovePAC(ctx context.Context, _ Function, _ any) error { + return nil +} // DNSProvider type noopDNSProvider struct{ output io.Writer } diff --git a/pkg/mock/pipelines_provider.go b/pkg/mock/pipelines_provider.go index 1db52960..2837d24d 100644 --- a/pkg/mock/pipelines_provider.go +++ b/pkg/mock/pipelines_provider.go @@ -13,6 +13,8 @@ type PipelinesProvider struct { RemoveFn func(fn.Function) error ConfigurePACInvoked bool ConfigurePACFn func(fn.Function) error + RemovePACInvoked bool + RemovePACFn func(fn.Function) error } func NewPipelinesProvider() *PipelinesProvider { @@ -20,6 +22,7 @@ func NewPipelinesProvider() *PipelinesProvider { RunFn: func(fn.Function) error { return nil }, RemoveFn: func(fn.Function) error { return nil }, ConfigurePACFn: func(fn.Function) error { return nil }, + RemovePACFn: func(fn.Function) error { return nil }, } } @@ -32,7 +35,13 @@ func (p *PipelinesProvider) Remove(ctx context.Context, f fn.Function) error { p.RemoveInvoked = true return p.RemoveFn(f) } + func (p *PipelinesProvider) ConfigurePAC(ctx context.Context, f fn.Function, metadata any) error { p.ConfigurePACInvoked = true return p.ConfigurePACFn(f) } + +func (p *PipelinesProvider) RemovePAC(ctx context.Context, f fn.Function, metadata any) error { + p.RemovePACInvoked = true + return p.RemovePACFn(f) +} diff --git a/pkg/pipelines/tekton/pipelines_pac_provider.go b/pkg/pipelines/tekton/pipelines_pac_provider.go index 93c9fc39..aaf823f0 100644 --- a/pkg/pipelines/tekton/pipelines_pac_provider.go +++ b/pkg/pipelines/tekton/pipelines_pac_provider.go @@ -76,6 +76,34 @@ func (pp *PipelinesProvider) ConfigurePAC(ctx context.Context, f fn.Function, me return nil } +// RemovePAC tries to remove all local and remote resources that were created for PAC. +// Resources on the remote GitHub repo are not removed, we would need to store webhook id somewhere locally. +func (pp *PipelinesProvider) RemovePAC(ctx context.Context, f fn.Function, metadata any) error { + data, ok := metadata.(pipelines.PacMetadata) + if !ok { + return fmt.Errorf("incorrect type of pipelines metadata: %T", metadata) + } + + compoundErrMsg := "" + + if data.ConfigureLocalResources { + errMsg := deleteAllPipelineTemplates(f) + compoundErrMsg += errMsg + } + + if data.ConfigureClusterResources { + errMsg := pp.removeClusterResources(ctx, f) + compoundErrMsg += errMsg + + } + + if compoundErrMsg != "" { + return fmt.Errorf("%s", compoundErrMsg) + } + + return nil +} + // createLocalResources creates necessary local resources in .tekton directory: // Pipeline and PipelineRun templates func (pp *PipelinesProvider) createLocalResources(ctx context.Context, f fn.Function) error { diff --git a/pkg/pipelines/tekton/pipelines_pac_provider_test.go b/pkg/pipelines/tekton/pipelines_pac_provider_test.go index c22443da..4ff6c2ca 100644 --- a/pkg/pipelines/tekton/pipelines_pac_provider_test.go +++ b/pkg/pipelines/tekton/pipelines_pac_provider_test.go @@ -2,6 +2,7 @@ package tekton import ( "context" + "path/filepath" "testing" "knative.dev/func/pkg/builders" @@ -59,3 +60,38 @@ func Test_createLocalResources(t *testing.T) { }) } } + +func Test_deleteAllPipelineTemplates(t *testing.T) { + root := "testdata/deleteAllPipelineTemplates" + defer Using(t, root)() + + f, err := fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + + f.Build.Builder = builders.Pack + f.Build.Git.URL = "https://foo.bar/repo/function" + f.Image = "docker.io/alice/" + f.Name + f.Registry = TestRegistry + + pp := NewPipelinesProvider() + err = pp.createLocalResources(context.Background(), f) + if err != nil { + t.Errorf("unexpected error while running pp.createLocalResources() error = %v", err) + } + + errMsg := deleteAllPipelineTemplates(f) + if errMsg != "" { + t.Errorf("unexpected error while running deleteAllPipelineTemplates() error message = %s", errMsg) + } + + fp := filepath.Join(root, resourcesDirectory) + exists, err := FileExists(t, fp) + if err != nil { + t.Fatal(err) + } + if exists { + t.Errorf("directory with pipeline resources shouldn't exist on path = %s", fp) + } +} diff --git a/pkg/pipelines/tekton/pipeplines_provider.go b/pkg/pipelines/tekton/pipeplines_provider.go index 33ebcae3..8b85b1a2 100644 --- a/pkg/pipelines/tekton/pipeplines_provider.go +++ b/pkg/pipelines/tekton/pipeplines_provider.go @@ -302,8 +302,22 @@ func sourcesAsTarStream(f fn.Function) *io.PipeReader { return pr } +// Remove tries to remove all resources that are present on the cluster and belongs to the input function and it's pipelines func (pp *PipelinesProvider) Remove(ctx context.Context, f fn.Function) error { + var err error + errMsg := pp.removeClusterResources(ctx, f) + if errMsg != "" { + err = fmt.Errorf("%s", errMsg) + } + + return err +} + +// removeClusterResources tries to remove all resources that are present on the cluster and belongs to the input function and it's pipelines +// if there are any errors during the removal, string with error messages is returned +// if there are no error the returned string is empty +func (pp *PipelinesProvider) removeClusterResources(ctx context.Context, f fn.Function) string { l := k8slabels.SelectorFromSet(k8slabels.Set(map[string]string{fnlabels.FunctionNameKey: f.Name})) listOptions := metav1.ListOptions{ LabelSelector: l.String(), @@ -316,6 +330,7 @@ func (pp *PipelinesProvider) Remove(ctx context.Context, f fn.Function) error { deletePipelineRuns, k8s.DeleteSecrets, k8s.DeletePersistentVolumeClaims, + deletePACRepositories, } wg.Add(len(deleteFunctions)) @@ -334,8 +349,7 @@ func (pp *PipelinesProvider) Remove(ctx context.Context, f fn.Function) error { wg.Wait() close(errChan) - // collect all errors and print them - var err error + // collect all errors and return them as a string errMsg := "" anyError := false for e := range errChan { @@ -346,11 +360,7 @@ func (pp *PipelinesProvider) Remove(ctx context.Context, f fn.Function) error { errMsg += fmt.Sprintf("\n %v", e) } - if anyError { - err = fmt.Errorf("%s", errMsg) - } - - return err + return errMsg } // watchPipelineRunProgress watches the progress of the input PipelineRun diff --git a/pkg/pipelines/tekton/resources_pac.go b/pkg/pipelines/tekton/resources_pac.go index 86942720..d44ddc46 100644 --- a/pkg/pipelines/tekton/resources_pac.go +++ b/pkg/pipelines/tekton/resources_pac.go @@ -92,6 +92,16 @@ func ensurePACRepositoryExists(ctx context.Context, f fn.Function, namespace str return nil } +// deletePACRepositories deletes all Repository resources present on the cluster that match input list options +func deletePACRepositories(ctx context.Context, namespaceOverride string, listOptions metav1.ListOptions) error { + client, namespace, err := pac.NewTektonPacClientAndResolvedNamespace(namespaceOverride) + if err != nil { + return err + } + + return client.Repositories(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions) +} + // getPipelineRepositoryName generates name for Repository CR func getPipelineRepositoryName(f fn.Function) string { return fmt.Sprintf("%s-repo", getPipelineName(f)) diff --git a/pkg/pipelines/tekton/templates.go b/pkg/pipelines/tekton/templates.go index 37dceb18..77b215bc 100644 --- a/pkg/pipelines/tekton/templates.go +++ b/pkg/pipelines/tekton/templates.go @@ -136,6 +136,17 @@ func createPipelineRunTemplate(f fn.Function) error { return fmt.Errorf("builder %q is not supported", f.Build.Builder) } +// deleteAllPipelineTemplates deletes all templates and pipeline resources that exists for a function +// and are stored in the .tekton directory +func deleteAllPipelineTemplates(f fn.Function) string { + err := os.RemoveAll(path.Join(f.Root, resourcesDirectory)) + if err != nil { + return fmt.Sprintf("\n %v", err) + } + + return "" +} + func createResource(projectRoot, fileName, fileTemplate string, data interface{}) error { tmpl, err := template.New(fileName).Parse(fileTemplate) if err != nil {