Skip to content
Permalink
Browse files
Ntate/feature/docr ea support (#783)
* Correctly utilize perPage variable in fetchPage

* Add displayers/BytesToHumanReadibleUnit utility

* Add support for docr/repo/List docrl/repo/ListTags

* Add support for docr/repo/DeleteTag docr/repo/DeleteManifest

* Add docr/registry/Repository.TagCount field

* Process as many deleteTag and deleteManifest arguments as possible when encountering errors
  • Loading branch information
nicktate committed Apr 23, 2020
1 parent 6e3f210 commit af0cac536acf293d8ffeb4a05a4ac74d2b3cebbe
@@ -57,3 +57,93 @@ func (r *Registry) KV() []map[string]interface{} {

return out
}

type Repository struct {
Repositories []do.Repository
}

var _ Displayable = &Repository{}

func (r *Repository) JSON(out io.Writer) error {
return writeJSON(r.Repositories, out)
}

func (r *Repository) Cols() []string {
return []string{
"Name",
"LatestTag",
"TagCount",
"UpdatedAt",
}
}

func (r *Repository) ColMap() map[string]string {
return map[string]string{
"Name": "Name",
"LatestTag": "Latest Tag",
"TagCount": "Tag Count",
"UpdatedAt": "Updated At",
}
}

func (r *Repository) KV() []map[string]interface{} {
out := []map[string]interface{}{}

for _, reg := range r.Repositories {
m := map[string]interface{}{
"Name": reg.Name,
"LatestTag": reg.LatestTag.Tag,
"TagCount": reg.TagCount,
"UpdatedAt": reg.LatestTag.UpdatedAt,
}

out = append(out, m)
}

return out
}

type RepositoryTag struct {
Tags []do.RepositoryTag
}

var _ Displayable = &RepositoryTag{}

func (r *RepositoryTag) JSON(out io.Writer) error {
return writeJSON(r.Tags, out)
}

func (r *RepositoryTag) Cols() []string {
return []string{
"Tag",
"CompressedSizeBytes",
"UpdatedAt",
"ManifestDigest",
}
}

func (r *RepositoryTag) ColMap() map[string]string {
return map[string]string{
"Tag": "Tag",
"CompressedSizeBytes": "Compressed Size",
"UpdatedAt": "Updated At",
"ManifestDigest": "Manifest Digest",
}
}

func (r *RepositoryTag) KV() []map[string]interface{} {
out := []map[string]interface{}{}

for _, tag := range r.Tags {
m := map[string]interface{}{
"Tag": tag.Tag,
"CompressedSizeBytes": BytesToHumanReadibleUnit(tag.CompressedSizeBytes),
"UpdatedAt": tag.UpdatedAt,
"ManifestDigest": tag.ManifestDigest,
}

out = append(out, m)
}

return out
}
@@ -0,0 +1,23 @@
package displayers

import "fmt"

const (
baseUnit = 1000
units = "kMGTPE"
)

// BytesToHumanReadibleUnit converts byte input to a human-readable
// form using the largest notation possible.
func BytesToHumanReadibleUnit(bytes uint64) string {
if bytes < baseUnit {
return fmt.Sprintf("%d B", bytes)
}

div, exp := uint64(baseUnit), 0
for n := bytes / baseUnit; n >= baseUnit; n /= baseUnit {
div *= baseUnit
exp++
}
return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), units[exp])
}
@@ -0,0 +1,59 @@
package displayers

import (
"math"
"testing"

"github.com/stretchr/testify/assert"
)

func Test_BytesToHumanReadibleUnit(t *testing.T) {
tests := map[string]struct {
bytes uint64
expected string
}{
"0 bytes": {
bytes: 0,
expected: "0 B",
},
"less than one kilobyte": {
bytes: uint64(math.Pow10(3)) - 1,
expected: "999 B",
},
"one kilobyte less than one megabyte": {
bytes: uint64(math.Pow10(6)) - uint64(math.Pow10(3)),
expected: "999.00 kB",
},
"one megabyte less than one gigabyte": {
bytes: uint64(math.Pow10(9)) - uint64(math.Pow10(6)),
expected: "999.00 MB",
},
"one gigabyte less than one terabyte": {
bytes: uint64(math.Pow10(12)) - uint64(math.Pow10(9)),
expected: "999.00 GB",
},
"one terabyte less than one petabyte": {
bytes: uint64(math.Pow10(15)) - uint64(math.Pow10(12)),
expected: "999.00 TB",
},
"one petabyte less than one exabyte": {
bytes: uint64(math.Pow10(18)) - uint64(math.Pow10(15)),
expected: "999.00 PB",
},
"one petabyte more than one exabyte": {
bytes: uint64(math.Pow10(18)) + uint64(math.Pow10(15)),
expected: "1.00 EB",
},
"1.25 GB": {
bytes: uint64(math.Pow10(9) * 1.25),
expected: "1.25 GB",
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
actual := BytesToHumanReadibleUnit(tt.bytes)
assert.Equal(t, tt.expected, actual)
})
}
}
@@ -48,6 +48,8 @@ func Registry() *Command {
},
}

cmd.AddCommand(Repository())

createRegDesc := "This command creates a new private container registry with the provided name."
CmdBuilder(cmd, RunRegistryCreate, "create <registry-name>",
"Create a private container registry", createRegDesc, Writer)
@@ -86,7 +88,75 @@ Redirect the command's output to a file to save the manifest for later use or pi
return cmd
}

// Registry
// Repository creates the repository sub-command
func Repository() *Command {
cmd := &Command{
Command: &cobra.Command{
Use: "repository",
Aliases: []string{"repo", "r"},
Short: "[Beta] Display commands for working with repositories in a container registry",
Long: "[Beta] The subcommands of `doctl registry repository` help you command actions related to a repository.",
Hidden: true,
},
}

listRepositoriesDesc := `
This command retrieves information about repositories in a registry, including:
- The repository name
- The latest tag of the repository
- The compressed size for the latest tag
- The manifest digest for the latest tag
- The last updated timestamp
`
CmdBuilder(
cmd,
RunListRepositories, "list",
"List repositories for a container registry", listRepositoriesDesc,
Writer, aliasOpt("ls"), displayerType(&displayers.Repository{}),
)

listRepositoryTagsDesc := `
This command retrieves information about tags in a repository, including:
- The tag name
- The compressed size
- The manifest digest
- The last updated timestamp
`
CmdBuilder(
cmd,
RunListRepositoryTags, "list-tags <repository>",
"List tags for a repository in a container registry", listRepositoryTagsDesc,
Writer, aliasOpt("lt"), displayerType(&displayers.RepositoryTag{}),
)

deleteTagDesc := "This command permanently deletes one or more repository tags."
cmdRunRepositoryDeleteTag := CmdBuilder(
cmd,
RunRepositoryDeleteTag,
"delete-tag <repository> <tag>...",
"Delete one or more container repository tags",
deleteTagDesc,
Writer,
aliasOpt("dt"),
)
AddBoolFlag(cmdRunRepositoryDeleteTag, doctl.ArgForce, doctl.ArgShortForce, false, "Force tag deletion")

deleteManifestDesc := "This command permanently deletes one or more repository manifests by digest."
cmdRunRepositoryDeleteManifest := CmdBuilder(
cmd,
RunRepositoryDeleteManifest,
"delete-manifest <repository> <manifest-digest>...",
"Delete one or more container repository manifests by digest",
deleteManifestDesc,
Writer,
aliasOpt("dm"),
)
AddBoolFlag(cmdRunRepositoryDeleteManifest, doctl.ArgForce, doctl.ArgShortForce, false, "Force manifest deletion")

return cmd
}

// Registry Run Commands

// RunRegistryCreate creates a registry
func RunRegistryCreate(c *CmdConfig) error {
@@ -259,9 +329,133 @@ func RunRegistryLogout(c *CmdConfig) error {
return cmd.Run()
}

// Repository Run Commands

// RunListRepositories lists repositories for the registry
func RunListRepositories(c *CmdConfig) error {
registry, err := c.Registry().Get()
if err != nil {
return fmt.Errorf("failed to get registry: %w", err)
}

repositories, err := c.Registry().ListRepositories(registry.Name)
if err != nil {
return err
}

return displayRepositories(c, repositories...)
}

// RunListRepositoryTags lists tags for the repository in a registry
func RunListRepositoryTags(c *CmdConfig) error {
if len(c.Args) != 1 {
return doctl.NewMissingArgsErr(c.NS)
}

registry, err := c.Registry().Get()
if err != nil {
return fmt.Errorf("failed to get registry: %w", err)
}

tags, err := c.Registry().ListRepositoryTags(registry.Name, c.Args[0])
if err != nil {
return err
}

return displayRepositoryTags(c, tags...)
}

// RunRepositoryDeleteTag deletes one or more repository tags
func RunRepositoryDeleteTag(c *CmdConfig) error {
force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
if err != nil {
return err
}

if len(c.Args) < 2 {
return doctl.NewMissingArgsErr(c.NS)
}

registry, err := c.Registry().Get()
if err != nil {
return fmt.Errorf("failed to get registry: %w", err)
}

repository := c.Args[0]
tags := c.Args[1:]

if !force && AskForConfirm(fmt.Sprintf("delete %d repository tag(s)", len(tags))) != nil {
return fmt.Errorf("operation aborted")
}

var errors []string
for _, tag := range tags {
if err := c.Registry().DeleteTag(registry.Name, repository, tag); err != nil {
errors = append(errors, err.Error())
}
}

if len(errors) > 0 {
return fmt.Errorf("failed to delete all repository tags: \n%s", strings.Join(errors, "\n"))
}

return nil
}

// RunRepositoryDeleteManifest deletes one or more repository manifests by digest
func RunRepositoryDeleteManifest(c *CmdConfig) error {
force, err := c.Doit.GetBool(c.NS, doctl.ArgForce)
if err != nil {
return err
}

if len(c.Args) < 2 {
return doctl.NewMissingArgsErr(c.NS)
}

registry, err := c.Registry().Get()
if err != nil {
return fmt.Errorf("failed to get registry: %w", err)
}

repository := c.Args[0]
digests := c.Args[1:]

if !force && AskForConfirm(fmt.Sprintf("delete %d repository manifest(s) by digest (including associated tags)", len(digests))) != nil {
return fmt.Errorf("operation aborted")
}

var errors []string
for _, digest := range digests {
if err := c.Registry().DeleteManifest(registry.Name, repository, digest); err != nil {
errors = append(errors, err.Error())
}
}

if len(errors) > 0 {
return fmt.Errorf("failed to delete all repository manifests: \n%s", strings.Join(errors, "\n"))
}

return nil
}

func displayRegistries(c *CmdConfig, registries ...do.Registry) error {
item := &displayers.Registry{
Registries: registries,
}
return c.Display(item)
}

func displayRepositories(c *CmdConfig, repositories ...do.Repository) error {
item := &displayers.Repository{
Repositories: repositories,
}
return c.Display(item)
}

func displayRepositoryTags(c *CmdConfig, tags ...do.RepositoryTag) error {
item := &displayers.RepositoryTag{
Tags: tags,
}
return c.Display(item)
}

0 comments on commit af0cac5

Please sign in to comment.