From 4d9dc7dcdb19ffaa97d17f1b4e91b6617dc2c586 Mon Sep 17 00:00:00 2001 From: Ricardo N Feliciano Date: Mon, 15 Nov 2021 00:35:04 -0500 Subject: [PATCH] Feat: show available & status of local tags (#119) New command: sonar tags status Shows the local tags for an image, and their status as compared to Docker Hub. Status can be "local-only", synced, newer, or older. --- go.mod | 8 ++- go.sum | 6 ++ sonar/cmd/tags_list.go | 2 +- sonar/cmd/tags_status.go | 124 +++++++++++++++++++++++++++++++++++++++ sonar/docker/tag.go | 4 +- 5 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 sonar/cmd/tags_status.go diff --git a/go.mod b/go.mod index 9caa1ba..5a26414 100644 --- a/go.mod +++ b/go.mod @@ -20,12 +20,14 @@ require ( github.com/arduino/go-apt-client v0.0.0-20190812130613-5613f843fdc8 // indirect github.com/containerd/cgroups v1.0.1 // indirect github.com/containerd/containerd v1.5.7 // indirect - github.com/docker/docker v20.10.8+incompatible // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/docker v20.10.10+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/magiconair/properties v1.8.5 // indirect @@ -48,9 +50,13 @@ require ( github.com/subosito/gotenv v1.2.0 // indirect go.opencensus.io v0.23.0 // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 // indirect golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect golang.org/x/text v0.3.6 // indirect + google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71 // indirect + google.golang.org/grpc v1.40.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index a73074a..3ec7232 100644 --- a/go.sum +++ b/go.sum @@ -258,9 +258,12 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.8+incompatible h1:RVqD337BgQicVCzYrrlhLDWhq6OAD2PJDUg2LsEUvKM= github.com/docker/docker v20.10.8+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.10+incompatible h1:GKkP0T7U4ks6X3lmmHKC2QDprnpRJor2Z5a8m62R9ZM= +github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= @@ -886,6 +889,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1178,6 +1182,7 @@ google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKr google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71 h1:z+ErRPu0+KS02Td3fOAgdX+lnPDh/VyaABEJPD4JRQs= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1207,6 +1212,7 @@ google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/sonar/cmd/tags_list.go b/sonar/cmd/tags_list.go index 4f40bdf..8d6a43c 100644 --- a/sonar/cmd/tags_list.go +++ b/sonar/cmd/tags_list.go @@ -14,7 +14,7 @@ var ( tagsListCmd = &cobra.Command{ Use: "list ", - Short: "Displays tags for a given Docker image name", + Short: "Displays tags on Docker Hub for a given Docker image name", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/sonar/cmd/tags_status.go b/sonar/cmd/tags_status.go new file mode 100644 index 0000000..531292d --- /dev/null +++ b/sonar/cmd/tags_status.go @@ -0,0 +1,124 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/felicianotech/sonar/sonar/docker" + "github.com/spf13/cobra" +) + +var statusCmd = &cobra.Command{ + Use: "status ", + Short: "Displays the push/pull status of local tags", + Long: `Displays the tags for a particular image that you have locally in a table. Provides info on if the tag also exists on Docker Hub, and if it does, is the local or Hub version newer.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + + dCLI, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + + images, err := dCLI.ImageList(context.Background(), types.ImageListOptions{}) + if err != nil { + return err + } + + var localTags []docker.Tag + + // Loop through all images + for _, image := range images { + + // Look through each image's tags + for _, tag := range image.RepoTags { + + if !strings.HasPrefix(tag, args[0]) { + break + } + + // this next section is to get the digest since while the image + // ID is available the digest is the preferred identifer. + var digestOrID string + if image.RepoDigests != nil { + + digestOrID = strings.Split(image.RepoDigests[0], "@")[1] + } else { + digestOrID = image.ID + } + + localTags = append(localTags, docker.Tag{ + Name: strings.Split(tag, ":")[1], + Size: image.Size, + Date: time.Unix(image.Created, 0).UTC(), + Digest: digestOrID, + }) + } + } + + dCLI.Close() + + // output data + + if len(localTags) == 0 { + fmt.Println("The image doesn't have any tags local.") + return nil + } + + hubTags, hubTagsErr := docker.GetAllTags(args[0]) + + fmt.Printf("Local tags for %s:\n\n", args[0]) + fmt.Println(" Tag Docker Hub Status") + fmt.Println("========== ===================") + for _, tag := range localTags { + + status := "local-only" + + if hubTagsErr == nil { + + for _, hubTag := range hubTags { + if tag.Name == hubTag.Name { + + if hubTag.Digest == "" { + status = "can't check" + } else if tag.Digest == hubTag.Digest { + status = "synced" + } else if tag.Date.After(hubTag.Date) { + status = "newer" + } else if tag.Date.Equal(hubTag.Date) { + status = "synced" + } else if tag.Date.Before(hubTag.Date) { + status = "older" + } + + break + } + } + } + + fmt.Printf(" %-10s %5s\n", tag.Name, status) + } + + // compare digest and.... creation time? + + return nil + }, +} + +func init() { + tagsCmd.AddCommand(statusCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // statusCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // statusCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/sonar/docker/tag.go b/sonar/docker/tag.go index 053aed2..e77e2ae 100644 --- a/sonar/docker/tag.go +++ b/sonar/docker/tag.go @@ -8,7 +8,7 @@ import ( type Tag struct { Name string - Size uint64 + Size int64 Date time.Time Digest string } @@ -40,7 +40,7 @@ func GetAllTags(image string) ([]Tag, error) { var aTag Tag aTag.Name = v.(map[string]interface{})["name"].(string) - aTag.Size = uint64(v.(map[string]interface{})["full_size"].(float64)) + aTag.Size = int64(v.(map[string]interface{})["full_size"].(float64)) aTag.Date, err = time.Parse(time.RFC3339, v.(map[string]interface{})["last_updated"].(string)) anImage := v.(map[string]interface{})["images"].([]interface{})[0]