From 08af75f371592ea609d3f0b048ad482e34cd2cd0 Mon Sep 17 00:00:00 2001 From: Deepak Dahiya <59823596+t-dedah@users.noreply.github.com> Date: Wed, 22 Jun 2022 15:23:55 +0530 Subject: [PATCH] E2E List cmd and added API calls (#3) * Completed List cmd and added API calls * Minor comments and add delete code to pass linting * Typo in descriptions * Minor comments * Validations * Validations-1 * improved branch flag validation * removed build * Minor comments and Readme --- .gitignore | 3 +- README.md | 29 ++++++++++++++++- cmd/client.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/delete.go | 16 +++++++--- cmd/list.go | 65 +++++++++++++++++++++++++++++++------ cmd/root.go | 4 ++- cmd/utils.go | 69 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 257 insertions(+), 17 deletions(-) create mode 100644 cmd/client.go create mode 100644 cmd/utils.go diff --git a/.gitignore b/.gitignore index 496ee2c..84264c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.DS_Store \ No newline at end of file +.DS_Store +gh-actions-cache \ No newline at end of file diff --git a/README.md b/README.md index beeae46..555b970 100644 --- a/README.md +++ b/README.md @@ -1 +1,28 @@ -# gh-actions-cache \ No newline at end of file +# gh-actions-cache + +## Local Development + +1. Build the extension using + + go build + +2. Install the extension + + + gh extension install + + + If you are already in the same directory use this `gh extension install .` + +3. Run the command + + gh actions-cache [Flags] + + +## Troubleshooting + +1. `symlink /Users/.../gh-actions-cache /Users/.../share/gh/extensions/gh-actions-cache: file exists` + +Uninstall the current version of extension using + + gh extension remove gh-actions-cache \ No newline at end of file diff --git a/cmd/client.go b/cmd/client.go new file mode 100644 index 0000000..51c8000 --- /dev/null +++ b/cmd/client.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "fmt" + "log" + "net/url" + + gh "github.com/cli/go-gh" + "github.com/cli/go-gh/pkg/api" + ghRepo "github.com/cli/go-gh/pkg/repository" +) + +type cacheInfo struct { + Key string + Ref string + LastAccessedAt string + Size float64 +} + +func getCacheUsage(repo ghRepo.Repository) float64 { + client, err := getRestClient(repo) + if err != nil { + log.Fatal(err) + } + pathComponent := fmt.Sprintf("repos/%s/%s/actions/cache/usage", repo.Owner(), repo.Name()) + var apiResults map[string]interface{} + err = client.Get(pathComponent, &apiResults) + if err != nil { + log.Fatal(err) + } + + cacheSizeResult := apiResults["active_caches_size_in_bytes"].(float64) + return cacheSizeResult +} + +func listCaches(repo ghRepo.Repository, queryParams url.Values) []cacheInfo { + client, err := getRestClient(repo) + if err != nil { + log.Fatal(err) + } + pathComponent := fmt.Sprintf("repos/%s/%s/actions/caches", repo.Owner(), repo.Name()) + var apiResults map[string]interface{} + err = client.Get(pathComponent+"?"+queryParams.Encode(), &apiResults) + if err != nil { + log.Fatal(err) + } + + actionsCachesResult := apiResults["actions_caches"].([]interface{}) + + var caches []cacheInfo + for _, item := range actionsCachesResult { + caches = append(caches, cacheInfo{ + Key: item.(map[string]interface{})["key"].(string), + Ref: item.(map[string]interface{})["ref"].(string), + LastAccessedAt: item.(map[string]interface{})["last_accessed_at"].(string), + Size: item.(map[string]interface{})["size_in_bytes"].(float64), + }) + } + return caches +} + +func deleteCaches(repo ghRepo.Repository, queryParams url.Values) float64 { + client, err := getRestClient(repo) + if err != nil { + log.Fatal(err) + } + pathComponent := fmt.Sprintf("repos/%s/%s/actions/caches", repo.Owner(), repo.Name()) + var apiResults map[string]interface{} + err = client.Delete(pathComponent+"?"+queryParams.Encode(), &apiResults) + if err != nil { + log.Fatal(err) + } + + totalDeletedCachesResult := apiResults["total_count"].(float64) + return totalDeletedCachesResult +} + +func getRestClient(repo ghRepo.Repository) (api.RESTClient, error) { + opts := api.ClientOptions{ + Host: repo.Host(), + Headers: map[string]string{"User-Agent": fmt.Sprintf("gh-actions-cache/%s/%s", VERSION, COMMAND) }, + } + client, err := gh.RESTClient(&opts) + if err != nil { + return nil, err + } + return client, nil +} diff --git a/cmd/delete.go b/cmd/delete.go index 6b4d354..f428536 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -1,7 +1,7 @@ package cmd import ( - "fmt" + "log" "github.com/spf13/cobra" ) @@ -18,11 +18,17 @@ var deleteCmd = &cobra.Command{ Short: "Delete cache by key", Long: `Delete cache by key`, Run: func(cmd *cobra.Command, args []string) { - repo, _ := cmd.Flags().GetString("repo") + COMMAND = "delete" + r, _ := cmd.Flags().GetString("repo") branch, _ := cmd.Flags().GetString("branch") - fmt.Println("DELETE") - fmt.Println(repo) - fmt.Println(branch) + + repo, err := getRepo(r) + if err != nil { + log.Fatal(err) + } + + queryParams := generateQueryParams(branch, 30, "", "", "") + deleteCaches(repo, queryParams) }, } diff --git a/cmd/list.go b/cmd/list.go index 39ef63b..48f2862 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "log" "github.com/spf13/cobra" ) @@ -10,9 +11,10 @@ func init() { rootCmd.AddCommand(listCmd) listCmd.Flags().StringP("repo", "R", "", "Select another repository for finding actions cache.") listCmd.Flags().StringP("branch", "B", "", "Filter by branch") + listCmd.Flags().IntP("limit", "", 30, "Maximum number of items to fetch (default is 30, max limit is 100)") listCmd.Flags().StringP("key", "", "", "Filter by key") listCmd.Flags().StringP("order", "", "", "Order of caches returned (asc/desc)") - listCmd.Flags().StringP("sort", "", "", "Sort fetched caches (used/size/created)") + listCmd.Flags().StringP("sort", "", "", "Sort fetched caches (last-used/size/created-at)") listCmd.SetHelpTemplate(getListHelp()) } @@ -21,20 +23,64 @@ var listCmd = &cobra.Command{ Short: "Lists the actions cache", Long: `Lists the actions cache`, Run: func(cmd *cobra.Command, args []string) { - repo, _ := cmd.Flags().GetString("repo") + COMMAND = "list" + + if len(args) != 0 { + fmt.Printf("Invalid argument(s). Expected 0 received %d\n", len(args)) + fmt.Println(getListHelp()) + return + } + + r, _ := cmd.Flags().GetString("repo") branch, _ := cmd.Flags().GetString("branch") + limit, _ := cmd.Flags().GetInt("limit") key, _ := cmd.Flags().GetString("key") order, _ := cmd.Flags().GetString("order") sort, _ := cmd.Flags().GetString("sort") - fmt.Println("LIST") - fmt.Println(repo) - fmt.Println(branch) - fmt.Println(key) - fmt.Println(order) - fmt.Println(sort) + + repo, err := getRepo(r) + if err != nil { + log.Fatal(err) + } + + validateInputs(sort, order, limit) + + if branch == "" && key == "" { + totalCacheSize := getCacheUsage(repo) + fmt.Printf("Total caches size %s\n\n", formatCacheSize(totalCacheSize)) + } + + queryParams := generateQueryParams(branch, limit, key, order, sort) + caches := listCaches(repo, queryParams) + + fmt.Printf("Showing %d of %d cache entries in %s/%s\n\n", displayedEntriesCount(len(caches), limit), len(caches), repo.Owner(), repo.Name()) + for _, cache := range caches { + fmt.Printf("%s\t [%s]\t %s\t %s\n", cache.Key, formatCacheSize(cache.Size), cache.Ref, cache.LastAccessedAt) + } }, } +func displayedEntriesCount(totalCaches int, limit int) int { + if totalCaches < limit { + return totalCaches + } + return limit +} + +func validateInputs(sort string, order string, limit int){ + if order != "" && order != "asc" && order != "desc"{ + log.Fatal(fmt.Errorf(fmt.Sprintf("%s is not a valid value for order flag. Allowed values: asc/desc", order))) + } + + if sort != "" && sort != "last-used" && sort != "size" && sort != "created-at"{ + log.Fatal(fmt.Errorf(fmt.Sprintf("%s is not a valid value for sort flag. Allowed values: last-used/size/created-at", sort))) + } + + if limit < 1{ + log.Fatal(fmt.Errorf(fmt.Sprintf("%d is not a valid value for limit flag. Allowed values: > 1", limit))) + } +} + func getListHelp() string { return ` gh-actions-cache: Works with GitHub Actions Cache. @@ -48,9 +94,10 @@ ARGUMENTS: FLAGS: -R, --repo <[HOST/]owner/repo> Select another repository using the [HOST/]OWNER/REPO format -B, --branch Filter by branch + -L, --limit Maximum number of items to fetch (default is 30, max limit is 100) --key Filter by key --order Order of caches returned (asc/desc) - --sort Sort fetched caches (used/size/created) + --sort Sort fetched caches (last-used/size/created-at) INHERITED FLAGS --help Show help for command diff --git a/cmd/root.go b/cmd/root.go index eb3344b..bbb8736 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,11 +6,13 @@ import ( "github.com/spf13/cobra" ) +const VERSION = "0.0.1" +var COMMAND string = "" + var rootCmd = &cobra.Command{ Use: "gh-actions-cache", Short: "Works with GitHub Actions Cache. ", Long: `Works with GitHub Actions Cache.`, - // Run: func(cmd *cobra.Command, args []string) {}, } func Execute() { diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 0000000..fdc0b61 --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "fmt" + "net/url" + "strconv" + "strings" + + gh "github.com/cli/go-gh" + ghRepo "github.com/cli/go-gh/pkg/repository" +) + +const MB_IN_BYTES = 1024 * 1024 +const GB_IN_BYTES = 1024 * 1024 * 1024 + +var SORT_INPUT_TO_QUERY_MAP = map[string]string{ + "created-at": "created_at", + "last-used": "last_accessed_at", + "size": "size_in_bytes", +} + +func generateQueryParams(branch string, limit int, key string, order string, sort string) url.Values { + query := url.Values{} + if branch != "" { + if strings.HasPrefix(branch, "refs/"){ + query.Add("ref", branch) + } else { + query.Add("ref", fmt.Sprintf("refs/heads/%s", branch)) + } + } + if limit != 30 { + query.Add("per_page", strconv.Itoa(limit)) + } + if key != "" { + query.Add("key", key) + } + if order != "" { + query.Add("direction", order) + } + if sort != "" { + query.Add("sort", SORT_INPUT_TO_QUERY_MAP[sort]) + } + + return query +} + +func getRepo(r string) (ghRepo.Repository, error) { + if r != "" { + return ghRepo.Parse(r) + } + + return gh.CurrentRepository() +} + +func formatCacheSize(size_in_bytes float64) string { + if size_in_bytes < 1024 { + return fmt.Sprintf("%.2f B", size_in_bytes) + } + + if size_in_bytes < MB_IN_BYTES { + return fmt.Sprintf("%.2f KB", size_in_bytes/1024) + } + + if size_in_bytes < GB_IN_BYTES { + return fmt.Sprintf("%.2f MB", size_in_bytes/MB_IN_BYTES) + } + + return fmt.Sprintf("%.2f GB", size_in_bytes/GB_IN_BYTES) +}