Skip to content

Commit

Permalink
Merge pull request #452 from CircleCI-Public/orb-categories
Browse files Browse the repository at this point in the history
[CIRCLE-28341] Add new commands for orb categorization
  • Loading branch information
lokst authored Aug 24, 2020
2 parents 308499e + 4c104ca commit 908cfd5
Show file tree
Hide file tree
Showing 6 changed files with 883 additions and 1 deletion.
192 changes: 192 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ import (
"gopkg.in/yaml.v3"
)

type UpdateOrbCategorizationRequestType int

const (
Add UpdateOrbCategorizationRequestType = iota
Remove
)

// GQLErrorsCollection is a slice of errors returned by the GraphQL server.
// Each error is made up of a GQLResponseError type.
type GQLErrorsCollection []GQLResponseError
Expand Down Expand Up @@ -218,6 +225,43 @@ type OrbsForListing struct {
Namespace string `json:"namespace,omitempty"`
}

// OrbCategoryIDResponse matches the GQL response for fetching an Orb category's id
type OrbCategoryIDResponse struct {
OrbCategoryByName struct {
ID string
}
}

// AddOrRemoveOrbCategorizationResponse type matches the data shape of the GQL response for
// adding or removing an orb categorization
type AddOrRemoveOrbCategorizationResponse map[string]AddOrRemoveOrbCategorizationData

type AddOrRemoveOrbCategorizationData struct {
CategoryId string
OrbId string
Errors GQLErrorsCollection
}

// OrbCategoryListResponse type matches the result from GQL.
// So that we can use mapstructure to convert from nested maps to a strongly typed struct.
type OrbCategoryListResponse struct {
OrbCategories struct {
TotalCount int
Edges []struct {
Cursor string
Node OrbCategory
}
PageInfo struct {
HasNextPage bool
}
}
}

// OrbCategoriesForListing is a container type for multiple orb categories for deserializing back into JSON.
type OrbCategoriesForListing struct {
OrbCategories []OrbCategory `json:"orbCategories"`
}

// SortBy allows us to sort a collection of orbs by builds, projects, or orgs from the last 30 days of data.
func (orbs *OrbsForListing) SortBy(sortBy string) {
switch sortBy {
Expand Down Expand Up @@ -336,6 +380,8 @@ type Orb struct {
Jobs map[string]OrbElement
Executors map[string]OrbElement
Versions []OrbVersion

Categories []OrbCategory
}

// OrbVersion wraps the GQL result used by OrbSource and OrbInfo
Expand All @@ -347,6 +393,11 @@ type OrbVersion struct {
CreatedAt string
}

type OrbCategory struct {
ID string `json:"id"`
Name string `json:"name"`
}

// #nosec
func loadYaml(path string) (string, error) {
var err error
Expand Down Expand Up @@ -951,6 +1002,10 @@ func OrbInfo(cl *graphql.Client, orbRef string) (*OrbVersion, error) {
id
createdAt
name
categories {
id
name
}
statistics {
last30DaysBuildCount,
last30DaysProjectCount,
Expand Down Expand Up @@ -1185,3 +1240,140 @@ func IntrospectionQuery(cl *graphql.Client) (*IntrospectionResponse, error) {

return &response, err
}

// OrbCategoryID fetches an orb returning the ID
func OrbCategoryID(cl *graphql.Client, name string) (*OrbCategoryIDResponse, error) {
var response OrbCategoryIDResponse

query := `
query ($name: String!) {
orbCategoryByName(name: $name) {
id
}
}`

request := graphql.NewRequest(query)

request.Var("name", name)

err := cl.Run(request, &response)

// If there is an error, or the request was successful, return now.
if err != nil || response.OrbCategoryByName.ID != "" {
return &response, err
}

return nil, fmt.Errorf("the '%s' category does not exist. Did you misspell the category name? To see the list of category names, please run 'circleci orb list-categories'.", name)
}

// AddOrRemoveOrbCategorization adds or removes an orb categorization
func AddOrRemoveOrbCategorization(cl *graphql.Client, namespace string, orb string, categoryName string, updateType UpdateOrbCategorizationRequestType) error {
orbId, err := OrbID(cl, namespace, orb)
if err != nil {
return err
}

categoryId, err := OrbCategoryID(cl, categoryName)
if err != nil {
return err
}

var response AddOrRemoveOrbCategorizationResponse

var mutationName string
if updateType == Add {
mutationName = "addCategorizationToOrb"
} else if updateType == Remove {
mutationName = "removeCategorizationFromOrb"
}

if mutationName == "" {
return fmt.Errorf("Internal error - invalid update type %d", updateType)
}

query := fmt.Sprintf(`
mutation($orbId: UUID!, $categoryId: UUID!) {
%s(
orbId: $orbId,
categoryId: $categoryId
) {
orbId
categoryId
errors {
message
type
}
}
}
`, mutationName)

request := graphql.NewRequest(query)
request.SetToken(cl.Token)

request.Var("orbId", orbId.Orb.ID)
request.Var("categoryId", categoryId.OrbCategoryByName.ID)

err = cl.Run(request, &response)

responseData := response[mutationName]

if len(responseData.Errors) > 0 {
return &responseData.Errors
}

if err != nil {
return errors.Wrap(err, "Unable to add/remove orb categorization")
}

return nil
}

// ListOrbCategories queries the API to find all categories.
// Returns a collection of OrbCategory objects containing their relevant data.
func ListOrbCategories(cl *graphql.Client) (*OrbCategoriesForListing, error) {

query := `
query ListOrbCategories($after: String!) {
orbCategories(first: 20, after: $after) {
totalCount
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
}
}
}
`

var orbCategories OrbCategoriesForListing

var result OrbCategoryListResponse
currentCursor := ""

for {
request := graphql.NewRequest(query)
request.Var("after", currentCursor)

err := cl.Run(request, &result)
if err != nil {
return nil, errors.Wrap(err, "GraphQL query failed")
}

for i := range result.OrbCategories.Edges {
edge := result.OrbCategories.Edges[i]
currentCursor = edge.Cursor
orbCategories.OrbCategories = append(orbCategories.OrbCategories, edge.Node)
}

if !result.OrbCategories.PageInfo.HasNextPage {
break
}

}
return &orbCategories, nil
}
112 changes: 112 additions & 0 deletions cmd/orb.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,38 @@ Please note that at this time all orbs created in the registry are world-readabl
Args: cobra.ExactArgs(1),
}

listCategoriesCommand := &cobra.Command{
Use: "list-categories",
Short: "List orb categories",
Args: cobra.ExactArgs(0),
RunE: func(_ *cobra.Command, _ []string) error {
return listOrbCategories(opts)
},
}

listCategoriesCommand.PersistentFlags().BoolVar(&opts.listJSON, "json", false, "print output as json instead of human-readable")
if err := listCategoriesCommand.PersistentFlags().MarkHidden("json"); err != nil {
panic(err)
}

addCategorizationToOrbCommand := &cobra.Command{
Use: "add-to-category <namespace>/<orb> \"<category-name>\"",
Short: "Add an orb to a category",
Args: cobra.ExactArgs(2),
RunE: func(_ *cobra.Command, _ []string) error {
return addOrRemoveOrbCategorization(opts, api.Add)
},
}

removeCategorizationFromOrbCommand := &cobra.Command{
Use: "remove-from-category <namespace>/<orb> \"<category-name>\"",
Short: "Remove an orb from a category",
Args: cobra.ExactArgs(2),
RunE: func(_ *cobra.Command, _ []string) error {
return addOrRemoveOrbCategorization(opts, api.Remove)
},
}

orbCreate.Flags().BoolVar(&opts.integrationTesting, "integration-testing", false, "Enable test mode to bypass interactive UI.")
if err := orbCreate.Flags().MarkHidden("integration-testing"); err != nil {
panic(err)
Expand Down Expand Up @@ -286,6 +318,9 @@ Please note that at this time all orbs created in the registry are world-readabl
orbCommand.AddCommand(sourceCommand)
orbCommand.AddCommand(orbInfoCmd)
orbCommand.AddCommand(orbPack)
orbCommand.AddCommand(addCategorizationToOrbCommand)
orbCommand.AddCommand(removeCategorizationFromOrbCommand)
orbCommand.AddCommand(listCategoriesCommand)

return orbCommand
}
Expand Down Expand Up @@ -464,6 +499,37 @@ func logOrbs(orbCollection *api.OrbsForListing, opts orbOptions) error {
return nil
}

func orbCategoryCollectionToString(orbCategoryCollection *api.OrbCategoriesForListing, opts orbOptions) (string, error) {
var result string

if opts.listJSON {
orbCategoriesJSON, err := json.MarshalIndent(orbCategoryCollection, "", " ")
if err != nil {
return "", errors.Wrapf(err, "Failed to convert to JSON")
}
result = string(orbCategoriesJSON)
} else {
var categories []string = make([]string, 0, len(orbCategoryCollection.OrbCategories))
for _, orbCategory := range orbCategoryCollection.OrbCategories {
categories = append(categories, orbCategory.Name)
}
result = strings.Join(categories, "\n")
}

return result, nil
}

func logOrbCategories(orbCategoryCollection *api.OrbCategoriesForListing, opts orbOptions) error {
result, err := orbCategoryCollectionToString(orbCategoryCollection, opts)
if err != nil {
return err
}

fmt.Println(result)

return nil
}

var validSortFlag = map[string]bool{
"builds": true,
"projects": true,
Expand Down Expand Up @@ -748,6 +814,14 @@ func orbInfo(opts orbOptions) error {
fmt.Printf("Projects: %d\n", info.Orb.Statistics.Last30DaysProjectCount)
fmt.Printf("Orgs: %d\n", info.Orb.Statistics.Last30DaysOrganizationCount)

if len(info.Orb.Categories) > 0 {
fmt.Println("")
fmt.Println("## Categories:")
for _, category := range info.Orb.Categories {
fmt.Printf("%s\n", category.Name)
}
}

orbVersionSplit := strings.Split(ref, "@")
orbRef := orbVersionSplit[0]
fmt.Printf(`
Expand All @@ -758,6 +832,44 @@ https://circleci.com/orbs/registry/orb/%s
return nil
}

func listOrbCategories(opts orbOptions) error {
orbCategories, err := api.ListOrbCategories(opts.cl)
if err != nil {
return errors.Wrapf(err, "Failed to list orb categories")
}

return logOrbCategories(orbCategories, opts)
}

func addOrRemoveOrbCategorization(opts orbOptions, updateType api.UpdateOrbCategorizationRequestType) error {
var err error

namespace, orb, err := references.SplitIntoOrbAndNamespace(opts.args[0])

if err != nil {
return err
}

err = api.AddOrRemoveOrbCategorization(opts.cl, namespace, orb, opts.args[1], updateType)

if err != nil {
var errorString = "Failed to add orb %s to category %s"
if updateType == api.Remove {
errorString = "Failed to remove orb %s from category %s"
}
return errors.Wrapf(err, errorString, opts.args[0], opts.args[1])
}

var output = `%s is successfully added to the "%s" category.` + "\n"
if updateType == api.Remove {
output = `%s is successfully removed from the "%s" category.` + "\n"
}

fmt.Printf(output, opts.args[0], opts.args[1])

return nil
}

type OrbSchema struct {
Version float32 `yaml:"version,omitempty"`
Description string `yaml:"description,omitempty"`
Expand Down
Loading

0 comments on commit 908cfd5

Please sign in to comment.