Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions internal/catalogapi/cleanup.go
Original file line number Diff line number Diff line change
@@ -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
}
105 changes: 105 additions & 0 deletions internal/cmd/productctl/cmd/cleanup/cleanup.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions internal/cmd/productctl/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand Down