зеркало из https://github.com/docker/hub-tool.git
Improve account info command to check status about an organization
Show current consumption (seats, private repos and teams) Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
This commit is contained in:
Родитель
3633a9001a
Коммит
a54d65065e
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/hub-tool/internal/ansi"
|
||||
"github.com/docker/hub-tool/internal/format"
|
||||
|
@ -43,9 +44,9 @@ type infoOptions struct {
|
|||
func newInfoCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
|
||||
var opts infoOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: infoName + " [OPTIONS]",
|
||||
Use: infoName + " [OPTIONS] [ORGANIZATION]",
|
||||
Short: "Print the account information",
|
||||
Args: cli.NoArgs,
|
||||
Args: cli.RequiresMaxArgs(1),
|
||||
DisableFlagsInUseLine: true,
|
||||
Annotations: map[string]string{
|
||||
"sudo": "true",
|
||||
|
@ -54,55 +55,107 @@ func newInfoCmd(streams command.Streams, hubClient *hub.Client, parent string) *
|
|||
metrics.Send(parent, infoName)
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runInfo(streams, hubClient, opts)
|
||||
if len(args) > 0 {
|
||||
return runOrgInfo(streams, hubClient, opts, args[0])
|
||||
}
|
||||
return runUserInfo(streams, hubClient, opts)
|
||||
},
|
||||
}
|
||||
opts.AddFormatFlag(cmd.Flags())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInfo(streams command.Streams, hubClient *hub.Client, opts infoOptions) error {
|
||||
func runOrgInfo(streams command.Streams, hubClient *hub.Client, opts infoOptions, orgName string) error {
|
||||
var (
|
||||
org *hub.Account
|
||||
consumption *hub.Consumption
|
||||
)
|
||||
|
||||
g := errgroup.Group{}
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
org, err = hubClient.GetOrganizationInfo(orgName)
|
||||
return checkForbiddenError(err)
|
||||
})
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
consumption, err = hubClient.GetOrgConsumption(orgName)
|
||||
return checkForbiddenError(err)
|
||||
})
|
||||
if err := g.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plan, err := hubClient.GetHubPlan(org.ID)
|
||||
if err != nil {
|
||||
return checkForbiddenError(err)
|
||||
}
|
||||
|
||||
return opts.Print(streams.Out(), account{org, plan, consumption}, printAccount)
|
||||
}
|
||||
|
||||
func runUserInfo(streams command.Streams, hubClient *hub.Client, opts infoOptions) error {
|
||||
user, err := hubClient.GetUserInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
return checkForbiddenError(err)
|
||||
}
|
||||
consumption, err := hubClient.GetUserConsumption(user.Name)
|
||||
if err != nil {
|
||||
return checkForbiddenError(err)
|
||||
}
|
||||
plan, err := hubClient.GetHubPlan(user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return checkForbiddenError(err)
|
||||
}
|
||||
return opts.Print(streams.Out(), account{user, plan}, printAccount)
|
||||
|
||||
return opts.Print(streams.Out(), account{user, plan, consumption}, printAccount)
|
||||
}
|
||||
|
||||
func checkForbiddenError(err error) error {
|
||||
if hub.IsForbiddenError(err) {
|
||||
return fmt.Errorf(ansi.Error("failed to get organization information, you need to be the organization Owner"))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func printAccount(out io.Writer, value interface{}) error {
|
||||
account := value.(account)
|
||||
|
||||
// print user info
|
||||
fmt.Fprintf(out, ansi.Key("Username:")+"\t%s\n", account.User.UserName)
|
||||
fmt.Fprintf(out, ansi.Key("Full name:")+"\t%s\n", account.User.FullName)
|
||||
fmt.Fprintf(out, ansi.Key("Company:")+"\t%s\n", account.User.Company)
|
||||
fmt.Fprintf(out, ansi.Key("Location:")+"\t%s\n", account.User.Location)
|
||||
fmt.Fprintf(out, ansi.Key("Joined:")+"\t\t%s ago\n", units.HumanDuration(time.Since(account.User.Joined)))
|
||||
fmt.Fprintf(out, ansi.Key("Name:")+"\t\t%s\n", account.Account.Name)
|
||||
fmt.Fprintf(out, ansi.Key("Full name:")+"\t%s\n", account.Account.FullName)
|
||||
fmt.Fprintf(out, ansi.Key("Company:")+"\t%s\n", account.Account.Company)
|
||||
fmt.Fprintf(out, ansi.Key("Location:")+"\t%s\n", account.Account.Location)
|
||||
fmt.Fprintf(out, ansi.Key("Joined:")+"\t\t%s ago\n", units.HumanDuration(time.Since(account.Account.Joined)))
|
||||
fmt.Fprintf(out, ansi.Key("Plan:")+"\t\t%s\n", ansi.Emphasise(account.Plan.Name))
|
||||
|
||||
// print plan info
|
||||
fmt.Fprintf(out, ansi.Key("Limits:")+"\n")
|
||||
fmt.Fprintf(out, ansi.Key(" Seats:")+"\t\t%v\n", account.Plan.Limits.Seats)
|
||||
fmt.Fprintf(out, ansi.Key(" Private repositories:")+"\t%v\n", account.Plan.Limits.PrivateRepos)
|
||||
fmt.Fprintf(out, ansi.Key(" Parallel builds:")+"\t%v\n", account.Plan.Limits.ParallelBuilds)
|
||||
fmt.Fprintf(out, ansi.Key(" Seats:")+"\t\t%v\n", getCurrentLimit(account.Consumption.Seats, account.Plan.Limits.Seats))
|
||||
fmt.Fprintf(out, ansi.Key(" Private repositories:")+"\t%v\n", getCurrentLimit(account.Consumption.PrivateRepositories, account.Plan.Limits.PrivateRepos))
|
||||
fmt.Fprintf(out, ansi.Key(" Teams:")+"\t\t%v\n", getCurrentLimit(account.Consumption.Teams, account.Plan.Limits.Teams))
|
||||
fmt.Fprintf(out, ansi.Key(" Collaborators:")+"\t%v\n", getLimit(account.Plan.Limits.Collaborators))
|
||||
fmt.Fprintf(out, ansi.Key(" Teams:")+"\t\t%v\n", getLimit(account.Plan.Limits.Teams))
|
||||
fmt.Fprintf(out, ansi.Key(" Parallel builds:")+"\t%v\n", getLimit(account.Plan.Limits.ParallelBuilds))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLimit(number int) string {
|
||||
if number == 9999 {
|
||||
func getCurrentLimit(current, limit int) string {
|
||||
if limit == 9999 {
|
||||
return ansi.Emphasise("unlimited")
|
||||
}
|
||||
return fmt.Sprintf("%v", number)
|
||||
return fmt.Sprintf("%v/%v", current, limit)
|
||||
}
|
||||
|
||||
func getLimit(limit int) string {
|
||||
if limit == 9999 {
|
||||
return ansi.Emphasise("unlimited")
|
||||
}
|
||||
return fmt.Sprintf("%v", limit)
|
||||
}
|
||||
|
||||
type account struct {
|
||||
User *hub.User
|
||||
Plan *hub.Plan
|
||||
Account *hub.Account
|
||||
Plan *hub.Plan
|
||||
Consumption *hub.Consumption
|
||||
}
|
||||
|
|
|
@ -29,9 +29,9 @@ import (
|
|||
|
||||
func TestInfoOutput(t *testing.T) {
|
||||
account := account{
|
||||
User: &hub.User{
|
||||
Account: &hub.Account{
|
||||
ID: "id",
|
||||
UserName: "my-user-name",
|
||||
Name: "my-user-name",
|
||||
FullName: "My Full Name",
|
||||
Location: "MyLocation",
|
||||
Company: "My Company",
|
||||
|
@ -47,6 +47,11 @@ func TestInfoOutput(t *testing.T) {
|
|||
ParallelBuilds: 3,
|
||||
},
|
||||
},
|
||||
Consumption: &hub.Consumption{
|
||||
Seats: 0,
|
||||
PrivateRepositories: 1,
|
||||
Teams: 2,
|
||||
},
|
||||
}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err := printAccount(buf, account)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
Username: my-user-name
|
||||
Name: my-user-name
|
||||
Full name: My Full Name
|
||||
Company: My Company
|
||||
Location: MyLocation
|
||||
Joined: Less than a second ago
|
||||
Plan: free
|
||||
Limits:
|
||||
Seats: 1
|
||||
Private repositories: 2
|
||||
Parallel builds: 3
|
||||
Collaborators: unlimited
|
||||
Seats: 0/1
|
||||
Private repositories: 1/2
|
||||
Teams: unlimited
|
||||
Collaborators: unlimited
|
||||
Parallel builds: 3
|
||||
|
|
|
@ -78,7 +78,7 @@ type listOptions struct {
|
|||
func newListCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
|
||||
var opts listOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: listName + " [ORGANIZATION]",
|
||||
Use: listName + " [OPTIONS] [ORGANIZATION]",
|
||||
Aliases: []string{"list"},
|
||||
Short: "List all the repositories from your account or an organization",
|
||||
Args: cli.RequiresMaxArgs(1),
|
||||
|
|
|
@ -300,6 +300,9 @@ func (c *Client) doRequest(req *http.Request, reqOps ...RequestOp) ([]byte, erro
|
|||
}
|
||||
log.Tracef("HTTP response: %+v", resp)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
if resp.StatusCode == http.StatusForbidden {
|
||||
return nil, &forbiddenError{}
|
||||
}
|
||||
buf, err := ioutil.ReadAll(resp.Body)
|
||||
log.Debugf("bad status code %q: %s", resp.Status, buf)
|
||||
if err == nil {
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
Copyright 2020 Docker Hub Tool authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package hub
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
//Consumption represents current user or org consumption
|
||||
type Consumption struct {
|
||||
Seats int
|
||||
PrivateRepositories int
|
||||
Teams int
|
||||
}
|
||||
|
||||
//GetOrgConsumption return the current organization consumption
|
||||
func (c *Client) GetOrgConsumption(org string) (*Consumption, error) {
|
||||
var (
|
||||
members int
|
||||
privateRepos int
|
||||
teams int
|
||||
)
|
||||
c.fetchAllElements = true
|
||||
eg, _ := errgroup.WithContext(context.Background())
|
||||
eg.Go(func() error {
|
||||
count, err := c.GetMembersCount(org)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
members = count
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
count, err := c.GetTeamsCount(org)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
teams = count
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
repos, _, err := c.GetRepositories(org)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range repos {
|
||||
if r.IsPrivate {
|
||||
privateRepos++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Consumption{
|
||||
Seats: members,
|
||||
PrivateRepositories: privateRepos,
|
||||
Teams: teams,
|
||||
}, nil
|
||||
}
|
||||
|
||||
//GetUserConsumption return the current user consumption
|
||||
func (c *Client) GetUserConsumption(user string) (*Consumption, error) {
|
||||
c.fetchAllElements = true
|
||||
privateRepos := 0
|
||||
repos, _, err := c.GetRepositories(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range repos {
|
||||
if r.IsPrivate {
|
||||
privateRepos++
|
||||
}
|
||||
}
|
||||
return &Consumption{
|
||||
Seats: 1,
|
||||
PrivateRepositories: privateRepos,
|
||||
Teams: 0,
|
||||
}, nil
|
||||
}
|
|
@ -44,3 +44,15 @@ func IsInvalidTokenError(err error) bool {
|
|||
_, ok := err.(*invalidTokenError)
|
||||
return ok
|
||||
}
|
||||
|
||||
type forbiddenError struct{}
|
||||
|
||||
func (f forbiddenError) Error() string {
|
||||
return "operation not permitted"
|
||||
}
|
||||
|
||||
// IsForbiddenError check if the error type is a forbidden error
|
||||
func IsForbiddenError(err error) bool {
|
||||
_, ok := err.(*forbiddenError)
|
||||
return ok
|
||||
}
|
||||
|
|
|
@ -32,3 +32,8 @@ func TestIsInvalidTokenError(t *testing.T) {
|
|||
assert.Assert(t, IsInvalidTokenError(&invalidTokenError{}))
|
||||
assert.Assert(t, !IsInvalidTokenError(errors.New("")))
|
||||
}
|
||||
|
||||
func TestIsForbiddenError(t *testing.T) {
|
||||
assert.Assert(t, IsForbiddenError(&forbiddenError{}))
|
||||
assert.Assert(t, !IsForbiddenError(errors.New("")))
|
||||
}
|
||||
|
|
|
@ -65,6 +65,32 @@ func (c *Client) GetMembers(organization string) ([]Member, error) {
|
|||
return members, nil
|
||||
}
|
||||
|
||||
// GetMembersCount return the number of members in an organization
|
||||
func (c *Client) GetMembersCount(organization string) (int, error) {
|
||||
u, err := url.Parse(c.domain + fmt.Sprintf(MembersURL, organization))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Add("page_size", "1")
|
||||
q.Add("page", "1")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
response, err := c.doRequest(req, withHubToken(c.token))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var hubResponse hubMemberResponse
|
||||
if err := json.Unmarshal(response, &hubResponse); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return hubResponse.Count, nil
|
||||
}
|
||||
|
||||
// GetMembersPerTeam returns the members of a team in an organization
|
||||
func (c *Client) GetMembersPerTeam(organization, team string) ([]Member, error) {
|
||||
u := c.domain + fmt.Sprintf(MembersPerTeamURL, organization, team)
|
||||
|
|
|
@ -31,6 +31,8 @@ import (
|
|||
const (
|
||||
// OrganizationsURL path to the Hub API listing the organizations
|
||||
OrganizationsURL = "/v2/user/orgs/"
|
||||
// OrganizationInfoURL path to the Hub API returning organization info
|
||||
OrganizationInfoURL = "/v2/orgs/%s"
|
||||
)
|
||||
|
||||
//Organization represents a Docker Hub organization
|
||||
|
@ -70,6 +72,35 @@ func (c *Client) GetOrganizations(ctx context.Context) ([]Organization, error) {
|
|||
return organizations, nil
|
||||
}
|
||||
|
||||
//GetOrganizationInfo returns organization info
|
||||
func (c *Client) GetOrganizationInfo(orgname string) (*Account, error) {
|
||||
u, err := url.Parse(c.domain + fmt.Sprintf(OrganizationInfoURL, orgname))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := c.doRequest(req, withHubToken(c.token))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var hubResponse hubOrgInfoResponse
|
||||
if err := json.Unmarshal(response, &hubResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Account{
|
||||
ID: hubResponse.ID,
|
||||
Name: hubResponse.OrgName,
|
||||
FullName: hubResponse.FullName,
|
||||
Location: hubResponse.Location,
|
||||
Company: hubResponse.Company,
|
||||
Joined: hubResponse.DateJoined,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) getOrganizationsPage(ctx context.Context, url string) ([]Organization, string, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
|
@ -160,3 +191,16 @@ type hubOrganizationResult struct {
|
|||
ProfileURL string `json:"profile_url"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type hubOrgInfoResponse struct {
|
||||
ID string `json:"id"`
|
||||
OrgName string `json:"orgname"`
|
||||
FullName string `json:"full_name"`
|
||||
Location string `json:"location"`
|
||||
Company string `json:"company"`
|
||||
GravatarEmail string `json:"gravatar_email"`
|
||||
GravatarURL string `json:"gravatar_url"`
|
||||
ProfileURL string `json:"profile_url"`
|
||||
DateJoined time.Time `json:"date_joined"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
|
|
@ -67,6 +67,32 @@ func (c *Client) GetTeams(organization string) ([]Team, error) {
|
|||
return teams, nil
|
||||
}
|
||||
|
||||
//GetTeamsCount returns the number of teams in an organization
|
||||
func (c *Client) GetTeamsCount(organization string) (int, error) {
|
||||
u, err := url.Parse(c.domain + fmt.Sprintf(GroupsURL, organization))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Add("page_size", "1")
|
||||
q.Add("page", "1")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
response, err := c.doRequest(req, withHubToken(c.token))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var hubResponse hubGroupResponse
|
||||
if err := json.Unmarshal(response, &hubResponse); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return hubResponse.Count, nil
|
||||
}
|
||||
|
||||
func (c *Client) getTeamsPage(url, organization string) ([]Team, string, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
|
|
|
@ -28,10 +28,10 @@ const (
|
|||
UserURL = "/v2/user/"
|
||||
)
|
||||
|
||||
//User represents the user information
|
||||
type User struct {
|
||||
//Account represents a user or organization information
|
||||
type Account struct {
|
||||
ID string
|
||||
UserName string
|
||||
Name string
|
||||
FullName string
|
||||
Location string
|
||||
Company string
|
||||
|
@ -39,7 +39,7 @@ type User struct {
|
|||
}
|
||||
|
||||
//GetUserInfo returns the information on the user retrieved from Hub
|
||||
func (c *Client) GetUserInfo() (*User, error) {
|
||||
func (c *Client) GetUserInfo() (*Account, error) {
|
||||
u, err := url.Parse(c.domain + UserURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -56,9 +56,9 @@ func (c *Client) GetUserInfo() (*User, error) {
|
|||
if err := json.Unmarshal(response, &hubResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &User{
|
||||
return &Account{
|
||||
ID: hubResponse.ID,
|
||||
UserName: hubResponse.UserName,
|
||||
Name: hubResponse.UserName,
|
||||
FullName: hubResponse.FullName,
|
||||
Location: hubResponse.Location,
|
||||
Company: hubResponse.Company,
|
||||
|
|
Загрузка…
Ссылка в новой задаче