mirror of https://github.com/docker/cli.git
Merge pull request #48 from cyli/root-rotation-cli
Synchronous CLI command for root CA rotation
This commit is contained in:
commit
c17acee8cf
|
@ -2,6 +2,7 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/service/progress"
|
"github.com/docker/cli/cli/command/service/progress"
|
||||||
|
@ -20,14 +21,7 @@ func waitOnService(ctx context.Context, dockerCli *command.DockerCli, serviceID
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if opts.quiet {
|
if opts.quiet {
|
||||||
go func() {
|
go io.Copy(ioutil.Discard, pipeReader)
|
||||||
for {
|
|
||||||
var buf [1024]byte
|
|
||||||
if _, err := pipeReader.Read(buf[:]); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return <-errChan
|
return <-errChan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
package swarm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli"
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/swarm/progress"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type caOptions struct {
|
||||||
|
swarmOptions
|
||||||
|
rootCACert PEMFile
|
||||||
|
rootCAKey PEMFile
|
||||||
|
rotate bool
|
||||||
|
detach bool
|
||||||
|
quiet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRotateCACommand(dockerCli command.Cli) *cobra.Command {
|
||||||
|
opts := caOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ca [OPTIONS]",
|
||||||
|
Short: "Manage root CA",
|
||||||
|
Args: cli.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runRotateCA(dockerCli, cmd.Flags(), opts)
|
||||||
|
},
|
||||||
|
Tags: map[string]string{"version": "1.30"},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmd.Flags()
|
||||||
|
addSwarmCAFlags(flags, &opts.swarmOptions)
|
||||||
|
flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate the swarm CA - if no certificate or key are provided, new ones will be generated")
|
||||||
|
flags.Var(&opts.rootCACert, flagCACert, "Path to the PEM-formatted root CA certificate to use for the new cluster")
|
||||||
|
flags.Var(&opts.rootCAKey, flagCAKey, "Path to the PEM-formatted root CA key to use for the new cluster")
|
||||||
|
|
||||||
|
flags.BoolVarP(&opts.detach, "detach", "d", false, "Exit immediately instead of waiting for the root rotation to converge")
|
||||||
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRotateCA(dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
swarmInspect, err := client.SwarmInspect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.rotate {
|
||||||
|
if swarmInspect.ClusterInfo.TLSInfo.TrustRoot == "" {
|
||||||
|
fmt.Fprintln(dockerCli.Out(), "No CA information available")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(dockerCli.Out(), strings.TrimSpace(swarmInspect.ClusterInfo.TLSInfo.TrustRoot))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
genRootCA := true
|
||||||
|
spec := &swarmInspect.Spec
|
||||||
|
opts.mergeSwarmSpec(spec, flags)
|
||||||
|
if flags.Changed(flagCACert) {
|
||||||
|
spec.CAConfig.SigningCACert = opts.rootCACert.Contents()
|
||||||
|
genRootCA = false
|
||||||
|
}
|
||||||
|
if flags.Changed(flagCAKey) {
|
||||||
|
spec.CAConfig.SigningCAKey = opts.rootCAKey.Contents()
|
||||||
|
genRootCA = false
|
||||||
|
}
|
||||||
|
if genRootCA {
|
||||||
|
spec.CAConfig.ForceRotate++
|
||||||
|
spec.CAConfig.SigningCACert = ""
|
||||||
|
spec.CAConfig.SigningCAKey = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, swarm.UpdateFlags{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.detach {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
errChan <- progress.RootRotationProgress(ctx, client, pipeWriter)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if opts.quiet {
|
||||||
|
go io.Copy(ioutil.Discard, pipeReader)
|
||||||
|
return <-errChan
|
||||||
|
}
|
||||||
|
|
||||||
|
err = jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil)
|
||||||
|
if err == nil {
|
||||||
|
err = <-errChan
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
swarmInspect, err = client.SwarmInspect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if swarmInspect.ClusterInfo.TLSInfo.TrustRoot == "" {
|
||||||
|
fmt.Fprintln(dockerCli.Out(), "No CA information available")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(dockerCli.Out(), strings.TrimSpace(swarmInspect.ClusterInfo.TLSInfo.TrustRoot))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ func NewSwarmCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
newUpdateCommand(dockerCli),
|
newUpdateCommand(dockerCli),
|
||||||
newLeaveCommand(dockerCli),
|
newLeaveCommand(dockerCli),
|
||||||
newUnlockCommand(dockerCli),
|
newUnlockCommand(dockerCli),
|
||||||
|
newRotateCACommand(dockerCli),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ const (
|
||||||
flagSnapshotInterval = "snapshot-interval"
|
flagSnapshotInterval = "snapshot-interval"
|
||||||
flagAutolock = "autolock"
|
flagAutolock = "autolock"
|
||||||
flagAvailability = "availability"
|
flagAvailability = "availability"
|
||||||
|
flagCACert = "ca-cert"
|
||||||
|
flagCAKey = "ca-key"
|
||||||
)
|
)
|
||||||
|
|
||||||
type swarmOptions struct {
|
type swarmOptions struct {
|
||||||
|
@ -119,6 +121,39 @@ func (m *ExternalCAOption) Value() []*swarm.ExternalCA {
|
||||||
return m.values
|
return m.values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PEMFile represents the path to a pem-formatted file
|
||||||
|
type PEMFile struct {
|
||||||
|
path, contents string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the type of this option.
|
||||||
|
func (p *PEMFile) Type() string {
|
||||||
|
return "pem-file"
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the path to the pem file
|
||||||
|
func (p *PEMFile) String() string {
|
||||||
|
return p.path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set parses a root rotation option
|
||||||
|
func (p *PEMFile) Set(value string) error {
|
||||||
|
contents, err := ioutil.ReadFile(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pemBlock, _ := pem.Decode(contents); pemBlock == nil {
|
||||||
|
return errors.New("file contents must be in PEM format")
|
||||||
|
}
|
||||||
|
p.contents, p.path = string(contents), value
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contents returns the contents of the PEM file
|
||||||
|
func (p *PEMFile) Contents() string {
|
||||||
|
return p.contents
|
||||||
|
}
|
||||||
|
|
||||||
// parseExternalCA parses an external CA specification from the command line,
|
// parseExternalCA parses an external CA specification from the command line,
|
||||||
// such as protocol=cfssl,url=https://example.com.
|
// such as protocol=cfssl,url=https://example.com.
|
||||||
func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) {
|
func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) {
|
||||||
|
@ -181,15 +216,19 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) {
|
||||||
return &externalCA, nil
|
return &externalCA, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addSwarmCAFlags(flags *pflag.FlagSet, opts *swarmOptions) {
|
||||||
|
flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates (ns|us|ms|s|m|h)")
|
||||||
|
flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints")
|
||||||
|
}
|
||||||
|
|
||||||
func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) {
|
func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) {
|
||||||
flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 5, "Task history retention limit")
|
flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 5, "Task history retention limit")
|
||||||
flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period (ns|us|ms|s|m|h)")
|
flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period (ns|us|ms|s|m|h)")
|
||||||
flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates (ns|us|ms|s|m|h)")
|
|
||||||
flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints")
|
|
||||||
flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain")
|
flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain")
|
||||||
flags.SetAnnotation(flagMaxSnapshots, "version", []string{"1.25"})
|
flags.SetAnnotation(flagMaxSnapshots, "version", []string{"1.25"})
|
||||||
flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots")
|
flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots")
|
||||||
flags.SetAnnotation(flagSnapshotInterval, "version", []string{"1.25"})
|
flags.SetAnnotation(flagSnapshotInterval, "version", []string{"1.25"})
|
||||||
|
addSwarmCAFlags(flags, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) {
|
func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) {
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
package progress
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/pkg/progress"
|
||||||
|
"github.com/docker/docker/pkg/streamformatter"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certsRotatedStr = " rotated TLS certificates"
|
||||||
|
rootsRotatedStr = " rotated CA certificates"
|
||||||
|
// rootsAction has a single space because rootsRotatedStr is one character shorter than certsRotatedStr.
|
||||||
|
// This makes sure the progress bar are aligned.
|
||||||
|
certsAction = ""
|
||||||
|
rootsAction = " "
|
||||||
|
)
|
||||||
|
|
||||||
|
// RootRotationProgress outputs progress information for convergence of a root rotation.
|
||||||
|
func RootRotationProgress(ctx context.Context, dclient client.APIClient, progressWriter io.WriteCloser) error {
|
||||||
|
defer progressWriter.Close()
|
||||||
|
|
||||||
|
progressOut := streamformatter.NewJSONProgressOutput(progressWriter, false)
|
||||||
|
|
||||||
|
sigint := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigint, os.Interrupt)
|
||||||
|
defer signal.Stop(sigint)
|
||||||
|
|
||||||
|
// draw 2 progress bars, 1 for nodes with the correct cert, 1 for nodes with the correct trust root
|
||||||
|
progress.Update(progressOut, "desired root digest", "")
|
||||||
|
progress.Update(progressOut, certsRotatedStr, certsAction)
|
||||||
|
progress.Update(progressOut, rootsRotatedStr, rootsAction)
|
||||||
|
|
||||||
|
var done bool
|
||||||
|
|
||||||
|
for {
|
||||||
|
info, err := dclient.SwarmInspect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if done {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes, err := dclient.NodeList(ctx, types.NodeListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
done = updateProgress(progressOut, info.ClusterInfo.TLSInfo, nodes, info.ClusterInfo.RootRotationInProgress)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
case <-sigint:
|
||||||
|
if !done {
|
||||||
|
progress.Message(progressOut, "", "Operation continuing in background.")
|
||||||
|
progress.Message(progressOut, "", "Use `swarmctl cluster inspect default` to check progress.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateProgress(progressOut progress.Output, desiredTLSInfo swarm.TLSInfo, nodes []swarm.Node, rootRotationInProgress bool) bool {
|
||||||
|
// write the current desired root cert's digest, because the desired root certs might be too long
|
||||||
|
progressOut.WriteProgress(progress.Progress{
|
||||||
|
ID: "desired root digest",
|
||||||
|
Action: digest.FromBytes([]byte(desiredTLSInfo.TrustRoot)).String(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// If we had reached a converged state, check if we are still converged.
|
||||||
|
var certsRight, trustRootsRight int64
|
||||||
|
for _, n := range nodes {
|
||||||
|
if bytes.Equal(n.Description.TLSInfo.CertIssuerPublicKey, desiredTLSInfo.CertIssuerPublicKey) &&
|
||||||
|
bytes.Equal(n.Description.TLSInfo.CertIssuerSubject, desiredTLSInfo.CertIssuerSubject) {
|
||||||
|
certsRight++
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Description.TLSInfo.TrustRoot == desiredTLSInfo.TrustRoot {
|
||||||
|
trustRootsRight++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
total := int64(len(nodes))
|
||||||
|
progressOut.WriteProgress(progress.Progress{
|
||||||
|
ID: certsRotatedStr,
|
||||||
|
Action: certsAction,
|
||||||
|
Current: certsRight,
|
||||||
|
Total: total,
|
||||||
|
Units: "nodes",
|
||||||
|
})
|
||||||
|
|
||||||
|
rootsProgress := progress.Progress{
|
||||||
|
ID: rootsRotatedStr,
|
||||||
|
Action: rootsAction,
|
||||||
|
Current: trustRootsRight,
|
||||||
|
Total: total,
|
||||||
|
Units: "nodes",
|
||||||
|
}
|
||||||
|
|
||||||
|
if certsRight == total && !rootRotationInProgress {
|
||||||
|
progressOut.WriteProgress(rootsProgress)
|
||||||
|
return certsRight == total && trustRootsRight == total
|
||||||
|
}
|
||||||
|
|
||||||
|
// we still have certs that need renewing, so display that there are zero roots rotated yet
|
||||||
|
rootsProgress.Current = 0
|
||||||
|
progressOut.WriteProgress(rootsProgress)
|
||||||
|
return false
|
||||||
|
}
|
|
@ -123,6 +123,8 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error {
|
||||||
fmt.Fprintf(dockerCli.Out(), " Heartbeat Period: %s\n", units.HumanDuration(time.Duration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod)))
|
fmt.Fprintf(dockerCli.Out(), " Heartbeat Period: %s\n", units.HumanDuration(time.Duration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod)))
|
||||||
fmt.Fprintf(dockerCli.Out(), " CA Configuration:\n")
|
fmt.Fprintf(dockerCli.Out(), " CA Configuration:\n")
|
||||||
fmt.Fprintf(dockerCli.Out(), " Expiry Duration: %s\n", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry))
|
fmt.Fprintf(dockerCli.Out(), " Expiry Duration: %s\n", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry))
|
||||||
|
fmt.Fprintf(dockerCli.Out(), " Force Rotate: %d\n", info.Swarm.Cluster.Spec.CAConfig.ForceRotate)
|
||||||
|
fprintfIfNotEmpty(dockerCli.Out(), " Signing CA Certificate: \n%s\n\n", strings.TrimSpace(info.Swarm.Cluster.Spec.CAConfig.SigningCACert))
|
||||||
if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 {
|
if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 {
|
||||||
fmt.Fprintf(dockerCli.Out(), " External CAs:\n")
|
fmt.Fprintf(dockerCli.Out(), " External CAs:\n")
|
||||||
for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs {
|
for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs {
|
||||||
|
|
|
@ -6,7 +6,7 @@ github.com/agl/ed25519 d2b94fd789ea21d12fac1a4443dd3a3f79cda72c
|
||||||
github.com/coreos/etcd 824277cb3a577a0e8c829ca9ec557b973fe06d20
|
github.com/coreos/etcd 824277cb3a577a0e8c829ca9ec557b973fe06d20
|
||||||
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
|
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
|
||||||
github.com/docker/distribution b38e5838b7b2f2ad48e06ec4b500011976080621
|
github.com/docker/distribution b38e5838b7b2f2ad48e06ec4b500011976080621
|
||||||
github.com/docker/docker 69c35dad8e7ec21de32d42b9dd606d3416ae1566
|
github.com/docker/docker eb8abc95985bf3882a4a177c409a96e36e25f5b7
|
||||||
github.com/docker/docker-credential-helpers v0.5.0
|
github.com/docker/docker-credential-helpers v0.5.0
|
||||||
github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06
|
github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06
|
||||||
github.com/docker/go-connections e15c02316c12de00874640cd76311849de2aeed5
|
github.com/docker/go-connections e15c02316c12de00874640cd76311849de2aeed5
|
||||||
|
|
|
@ -109,6 +109,16 @@ type CAConfig struct {
|
||||||
// ExternalCAs is a list of CAs to which a manager node will make
|
// ExternalCAs is a list of CAs to which a manager node will make
|
||||||
// certificate signing requests for node certificates.
|
// certificate signing requests for node certificates.
|
||||||
ExternalCAs []*ExternalCA `json:",omitempty"`
|
ExternalCAs []*ExternalCA `json:",omitempty"`
|
||||||
|
|
||||||
|
// SigningCACert and SigningCAKey specify the desired signing root CA and
|
||||||
|
// root CA key for the swarm. When inspecting the cluster, the key will
|
||||||
|
// be redacted.
|
||||||
|
SigningCACert string `json:",omitempty"`
|
||||||
|
SigningCAKey string `json:",omitempty"`
|
||||||
|
|
||||||
|
// If this value changes, and there is no specified signing cert and key,
|
||||||
|
// then the swarm is forced to generate a new root certificate ane key.
|
||||||
|
ForceRotate uint64 `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalCAProtocol represents type of external CA.
|
// ExternalCAProtocol represents type of external CA.
|
||||||
|
|
|
@ -37,6 +37,7 @@ type JSONProgress struct {
|
||||||
Start int64 `json:"start,omitempty"`
|
Start int64 `json:"start,omitempty"`
|
||||||
// If true, don't show xB/yB
|
// If true, don't show xB/yB
|
||||||
HideCounts bool `json:"hidecounts,omitempty"`
|
HideCounts bool `json:"hidecounts,omitempty"`
|
||||||
|
Units string `json:"units,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *JSONProgress) String() string {
|
func (p *JSONProgress) String() string {
|
||||||
|
@ -55,11 +56,16 @@ func (p *JSONProgress) String() string {
|
||||||
if p.Current <= 0 && p.Total <= 0 {
|
if p.Current <= 0 && p.Total <= 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
current := units.HumanSize(float64(p.Current))
|
|
||||||
if p.Total <= 0 {
|
if p.Total <= 0 {
|
||||||
|
switch p.Units {
|
||||||
|
case "":
|
||||||
|
current := units.HumanSize(float64(p.Current))
|
||||||
return fmt.Sprintf("%8v", current)
|
return fmt.Sprintf("%8v", current)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d %s", p.Current, p.Units)
|
||||||
}
|
}
|
||||||
total := units.HumanSize(float64(p.Total))
|
}
|
||||||
|
|
||||||
percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
|
percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
|
||||||
if percentage > 50 {
|
if percentage > 50 {
|
||||||
percentage = 50
|
percentage = 50
|
||||||
|
@ -73,13 +79,25 @@ func (p *JSONProgress) String() string {
|
||||||
pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
|
pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.HideCounts {
|
switch {
|
||||||
|
case p.HideCounts:
|
||||||
|
case p.Units == "": // no units, use bytes
|
||||||
|
current := units.HumanSize(float64(p.Current))
|
||||||
|
total := units.HumanSize(float64(p.Total))
|
||||||
|
|
||||||
numbersBox = fmt.Sprintf("%8v/%v", current, total)
|
numbersBox = fmt.Sprintf("%8v/%v", current, total)
|
||||||
|
|
||||||
if p.Current > p.Total {
|
if p.Current > p.Total {
|
||||||
// remove total display if the reported current is wonky.
|
// remove total display if the reported current is wonky.
|
||||||
numbersBox = fmt.Sprintf("%8v", current)
|
numbersBox = fmt.Sprintf("%8v", current)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units)
|
||||||
|
|
||||||
|
if p.Current > p.Total {
|
||||||
|
// remove total display if the reported current is wonky.
|
||||||
|
numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Current > 0 && p.Start > 0 && percentage < 50 {
|
if p.Current > 0 && p.Start > 0 && percentage < 50 {
|
||||||
|
|
|
@ -18,6 +18,8 @@ type Progress struct {
|
||||||
|
|
||||||
// If true, don't show xB/yB
|
// If true, don't show xB/yB
|
||||||
HideCounts bool
|
HideCounts bool
|
||||||
|
// If not empty, use units instead of bytes for counts
|
||||||
|
Units string
|
||||||
|
|
||||||
// Aux contains extra information not presented to the user, such as
|
// Aux contains extra information not presented to the user, such as
|
||||||
// digests for push signing.
|
// digests for push signing.
|
||||||
|
|
|
@ -117,7 +117,7 @@ func (out *progressOutput) WriteProgress(prog progress.Progress) error {
|
||||||
if prog.Message != "" {
|
if prog.Message != "" {
|
||||||
formatted = out.sf.formatStatus(prog.ID, prog.Message)
|
formatted = out.sf.formatStatus(prog.ID, prog.Message)
|
||||||
} else {
|
} else {
|
||||||
jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total, HideCounts: prog.HideCounts}
|
jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total, HideCounts: prog.HideCounts, Units: prog.Units}
|
||||||
formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
|
formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
|
||||||
}
|
}
|
||||||
_, err := out.out.Write(formatted)
|
_, err := out.out.Write(formatted)
|
||||||
|
|
Loading…
Reference in New Issue