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
19 changes: 19 additions & 0 deletions internal/catalogapi/catalogapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,10 @@ func PopulateProduct(
return nil, err
}

attachedIDs := make([]string, len(associatedComponents))
newListing.With.Components = make([]*resource.Component, 0, len(associatedComponents))
for _, v := range associatedComponents {
attachedIDs = append(attachedIDs, v.Id)
converted, err := resource.JSONConvert[resource.Component](v)
if err != nil {
return nil, err
Expand All @@ -276,6 +278,23 @@ func PopulateProduct(
}

slices.SortStableFunc(newListing.With.Components, func(a, b *resource.Component) int { return strings.Compare(a.ID, b.ID) })

// The cert_projects field in the product listing will contain
// attached-but-archived components. ComponentsForListing (above) only
// returns active components, so we'll true this up on the client side.
slices.Sort(attachedIDs)
slices.Sort(newListing.Spec.CertProjects)

L.Info("foo")
if !slices.Equal(attachedIDs, newListing.Spec.CertProjects) {
L.Debug("product listing has attached components that are not marked as active.")
L.Debug(
"replacing product_listing.spec.cert_projects with only attached IDs",
"original", newListing.Spec.CertProjects,
"replacement", attachedIDs,
)
newListing.Spec.CertProjects = attachedIDs
}
}

return &newListing, nil
Expand Down
70 changes: 70 additions & 0 deletions internal/cmd/productctl/cmd/archivecomponent/archivecomponent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package archivecomponent

import (
"context"

"github.com/Khan/genqlient/graphql"
"github.com/spf13/cobra"

"github.com/opdev/productctl/internal/catalogapi"
"github.com/opdev/productctl/internal/cli"
"github.com/opdev/productctl/internal/genpyxis"
"github.com/opdev/productctl/internal/logger"
)

func Command() *cobra.Command {
cmd := &cobra.Command{
Use: "archive-component <component-id>",
Short: "Archives the component with the specified component ID",
Long: `Archives the component with the specified component ID

This should be considered a destructive operation. Note that there are various reasons why the API may reject this operation. Those reasons may need to be handled directly via the Partner Connect UI.`,
Args: cobra.MinimumNArgs(1), // The component ID
RunE: runE,
}

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

return run(cmd.Context(), args[0], token, endpoint)
}

func run(ctx context.Context, componentID string, token string, endpoint catalogapi.APIEndpoint) error {
L := logger.FromContextOrDiscard(ctx)
L.Info("archiving component", "_id", componentID)

L.Debug("building graphql client")
httpClient := catalogapi.TokenAuthenticatedHTTPClient(token, L.With("name", "httpclient"))
client := graphql.NewClient(endpoint, httpClient)

resp, err := genpyxis.ArchiveComponent(ctx, client, componentID)
if err != nil {
return err
}

if gqlErr := resp.Update_certification_project.GetError(); gqlErr != nil {
return catalogapi.ParseGraphQLResponseError(gqlErr)
}

return nil
}
9 changes: 9 additions & 0 deletions internal/cmd/productctl/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (

"github.com/opdev/productctl/internal/cli"
"github.com/opdev/productctl/internal/cmd/productctl/cmd/apply"
"github.com/opdev/productctl/internal/cmd/productctl/cmd/archivecomponent"
"github.com/opdev/productctl/internal/cmd/productctl/cmd/bridge"
"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/deleteproductlisting"
"github.com/opdev/productctl/internal/cmd/productctl/cmd/fetch"
"github.com/opdev/productctl/internal/cmd/productctl/cmd/jsonschema"
"github.com/opdev/productctl/internal/cmd/productctl/cmd/sanitize"
Expand All @@ -36,6 +38,13 @@ func RootCmd() *cobra.Command {
cmd.AddCommand(version.Command())
cmd.PersistentFlags().String(cli.FlagIDLogLevel, "info", "The verbosity of the tool itself. Ex. error, warn, info, debug")

util := bridge.Command("util", "Utilities for the management of your Partner Connect account")
util.PersistentFlags().String(cli.FlagIDEndpoint, "prod", "The catalog API environment to use. Choose from stage, prod")
util.PersistentFlags().String(cli.FlagIDCustomEndpoint, "", "Define a custom API endpoint. Supersedes predefined environment values like \"prod\" if set")
util.AddCommand(archivecomponent.Command())
util.AddCommand(deleteproductlisting.Command())
cmd.AddCommand(util)

// Build the product management command tree.
product := bridge.Command("product", "Manage your Product Listing")
product.PersistentFlags().String(cli.FlagIDEndpoint, "prod", "The catalog API environment to use. Choose from stage, prod")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package deleteproductlisting

import (
"context"

"github.com/Khan/genqlient/graphql"
"github.com/spf13/cobra"

"github.com/opdev/productctl/internal/catalogapi"
"github.com/opdev/productctl/internal/cli"
"github.com/opdev/productctl/internal/genpyxis"
"github.com/opdev/productctl/internal/logger"
)

func Command() *cobra.Command {
cmd := &cobra.Command{
Use: "delete-productlisting <productID>",
Short: "Deletes the product listingwith the specified product ID.",
Long: `Deletes the product listing with the specified product ID

This should be considered a destructive operation. Note that there are various reasons why the API may reject this operation. Those reasons may need to be handled directly via the Partner Connect UI.
`,
Args: cobra.MinimumNArgs(1), // The product ID
RunE: runE,
}

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

return run(cmd.Context(), args[0], token, endpoint)
}

func run(ctx context.Context, listingID string, token string, endpoint catalogapi.APIEndpoint) error {
L := logger.FromContextOrDiscard(ctx)
L.Info("deleting product listing", "_id", listingID)

L.Debug("building graphql client")
httpClient := catalogapi.TokenAuthenticatedHTTPClient(token, L.With("name", "httpclient"))
client := graphql.NewClient(endpoint, httpClient)

resp, err := genpyxis.DeleteProduct(ctx, client, listingID)
if err != nil {
return err
}

if gqlErr := resp.Update_product_listing.GetError(); gqlErr != nil {
return catalogapi.ParseGraphQLResponseError(gqlErr)
}

L.Info("done")
return nil
}
15 changes: 9 additions & 6 deletions internal/cmd/productctl/cmd/fetch/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
package fetch

import (
"context"
"fmt"
"os"

Expand All @@ -19,9 +18,14 @@ func Command() *cobra.Command {
cmd := &cobra.Command{
Use: "fetch <productID>",
Short: "Get a pre-existing product listing",
Long: "Get data about a pre-existing product listing by its ID and generate its declaration for storage on disk.",
Args: cobra.MinimumNArgs(1),
RunE: getProductListingRunE,
Long: `Get data about a pre-existing product listing by its ID and generate its declaration for storage on disk.

Only components attached to this product listing with an "active" status will be returned.

This command does not overwrite an existing file, and relies in output redirection to store the contents to disk at any location you would prefer.
`,
Args: cobra.MinimumNArgs(1),
RunE: getProductListingRunE,
}

return cmd
Expand Down Expand Up @@ -49,11 +53,10 @@ func getProductListingRunE(cmd *cobra.Command, args []string) error {
L.Debug("endpoint resolved", "endpoint", endpoint)
}

ctx := context.Background()
httpClient := catalogapi.TokenAuthenticatedHTTPClient(token, L.With("name", "httpclient"))
client := graphql.NewClient(endpoint, httpClient)

newListing, err := catalogapi.PopulateProduct(ctx, client, productID)
newListing, err := catalogapi.PopulateProduct(cmd.Context(), client, productID)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/genpyxis/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/genpyxis/queries.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ query ComponentsForListing(
id: $productID
page: $page
page_size: $pageSize
filter: {project_status: {eq: "active"}}
) {
# @genqlient(flatten: true)
data {
Expand Down