diff --git a/internal/catalogapi/cleanup.go b/internal/catalogapi/cleanup.go new file mode 100644 index 0000000..66cb33d --- /dev/null +++ b/internal/catalogapi/cleanup.go @@ -0,0 +1,67 @@ +package catalogapi + +import ( + "context" + + "github.com/Khan/genqlient/graphql" + + "github.com/opdev/productctl/internal/genpyxis" + "github.com/opdev/productctl/internal/logger" + "github.com/opdev/productctl/internal/resource" +) + +// CleanupProduct will, if able, detach and archive all components on a product +// listing. Then, it will archive the product listing, and sanitize the listing. +func CleanupProduct( + ctx context.Context, + client graphql.Client, + declaration *resource.ProductListingDeclaration, +) (*resource.ProductListingDeclaration, error) { + L := logger.FromContextOrDiscard(ctx) + listingExists := declaration.Spec.ID != "" + + if listingExists { + L.Info("detaching any and all components from product listing", "productListingID", declaration.Spec.ID, "productListingName", declaration.Spec.Name) + resp, err := genpyxis.SetComponentsForProduct(ctx, client, declaration.Spec.ID, []string{}) + if err != nil { + return nil, err + } + + if gqlErr := resp.Update_product_listing.GetError(); gqlErr != nil { + return nil, ParseGraphQLResponseError(gqlErr) + } + } + + for _, component := range declaration.With.Components { + if component.ID == "" { + continue + } + + L.Info("archiving component", "id", component.ID, "name", component.Name, "type", component.Type) + resp, err := genpyxis.ArchiveComponent(ctx, client, component.ID) + if err != nil { + return nil, err + } + + if gqlErr := resp.Update_certification_project.GetError(); gqlErr != nil { + return nil, ParseGraphQLResponseError(gqlErr) + } + } + + if listingExists { + L.Info("deleting product listing") + resp, err := genpyxis.DeleteProduct(ctx, client, declaration.Spec.ID) + if err != nil { + return nil, err + } + + if gqlErr := resp.Update_product_listing.GetError(); gqlErr != nil { + return nil, ParseGraphQLResponseError(gqlErr) + } + } + + L.Info("cleanup API calls completed") + declaration.Sanitize() + + return declaration, nil +} diff --git a/internal/cmd/productctl/cmd/cleanup/cleanup.go b/internal/cmd/productctl/cmd/cleanup/cleanup.go new file mode 100644 index 0000000..01f9629 --- /dev/null +++ b/internal/cmd/productctl/cmd/cleanup/cleanup.go @@ -0,0 +1,105 @@ +package cleanup + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/Khan/genqlient/graphql" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" + + "github.com/opdev/productctl/internal/catalogapi" + "github.com/opdev/productctl/internal/cli" + "github.com/opdev/productctl/internal/file" + "github.com/opdev/productctl/internal/logger" + "github.com/opdev/productctl/internal/resource" +) + +func Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "cleanup [my.product.yaml]", + Short: "Detaches and archives components. Deletes the product listing. This is destructive. Use with caution.", + Args: cobra.MinimumNArgs(1), // The product declaration + RunE: runE, + } + + cmd.Flags().Bool(cli.FlagIDCreateBackupOnOverwrite, false, "Create a backup of the declaration on overwrite. Note that this backups the on-disk declaration that was created/applied before overwriting it with new content.") + + return cmd +} + +func runE(cmd *cobra.Command, args []string) error { + L := logger.FromContextOrDiscard(cmd.Context()) + _, token, err := cli.EnsureEnv() + if err != nil { + return err + } + + var endpoint string + if cmd.Flags().Changed(cli.FlagIDCustomEndpoint) { + endpoint, _ = cmd.Flags().GetString(cli.FlagIDCustomEndpoint) + L.Debug("custom endpoint set, using it over env value", "endpoint", endpoint) + } else { + env, _ := cmd.Flags().GetString(cli.FlagIDEndpoint) + endpoint, err = cli.ResolveAPIEndpoint(env) + if err != nil { + return err + } + L.Debug("endpoint resolved", "endpoint", endpoint) + } + + if args[0] == "-" { + return runCleanup(cmd.Context(), os.Stdin, os.Stdout, token, endpoint) + } + + // This is a read-only open. + f, err := os.Open(args[0]) + if err != nil { + return err + } + + backupOnOverwrite, _ := cmd.Flags().GetBool(cli.FlagIDCreateBackupOnOverwrite) + + updateFileOnSuccess := file.LazyOverwriter{ + Filename: args[0], + DoBackup: backupOnOverwrite, + OptionalLogger: L.With("name", "fileIO"), + } + + defer f.Close() + return runCleanup(cmd.Context(), f, &updateFileOnSuccess, token, endpoint) +} + +func runCleanup(ctx context.Context, in io.Reader, outOnCompletion io.Writer, token string, endpoint catalogapi.APIEndpoint) error { + L := logger.FromContextOrDiscard(ctx) + + L.Info("reading in product listing") + declaration, err := resource.ReadProductListing(in) + if err != nil { + return err + } + + L.Debug("building graphql client") + httpClient := catalogapi.TokenAuthenticatedHTTPClient(token, L.With("name", "httpclient")) + client := graphql.NewClient(endpoint, httpClient) + + L.Debug("starting cleanup") + cleaned, err := catalogapi.CleanupProduct(ctx, client, declaration) + if err != nil { + return err + } + + L.Info("Updating provided resource declaration.") + b, err := yaml.Marshal(cleaned) + if err != nil { + return err + } + _, err = fmt.Fprint(outOnCompletion, string(b)) + if err != nil { + return err + } + + return nil +} diff --git a/internal/cmd/productctl/cmd/cmd.go b/internal/cmd/productctl/cmd/cmd.go index 58a86c1..85c86da 100644 --- a/internal/cmd/productctl/cmd/cmd.go +++ b/internal/cmd/productctl/cmd/cmd.go @@ -14,6 +14,7 @@ import ( "github.com/opdev/productctl/internal/cmd/productctl/cmd/certifyhelmcharts" "github.com/opdev/productctl/internal/cmd/productctl/cmd/certifyoperators" "github.com/opdev/productctl/internal/cmd/productctl/cmd/certtargets" + "github.com/opdev/productctl/internal/cmd/productctl/cmd/cleanup" "github.com/opdev/productctl/internal/cmd/productctl/cmd/create" "github.com/opdev/productctl/internal/cmd/productctl/cmd/fetch" "github.com/opdev/productctl/internal/cmd/productctl/cmd/lsp" @@ -47,6 +48,7 @@ func rootCmd() *cobra.Command { product.AddCommand(apply.Command()) product.AddCommand(fetch.Command()) product.AddCommand(sanitize.Command()) + product.AddCommand(cleanup.Command()) cmd.AddCommand(product)