github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/swarm/opts.go (about) 1 package swarm 2 3 import ( 4 "encoding/csv" 5 "encoding/pem" 6 "fmt" 7 "os" 8 "strings" 9 "time" 10 11 "github.com/docker/cli/opts" 12 "github.com/docker/docker/api/types/swarm" 13 "github.com/pkg/errors" 14 "github.com/spf13/pflag" 15 ) 16 17 const ( 18 defaultListenAddr = "0.0.0.0:2377" 19 20 flagCertExpiry = "cert-expiry" 21 flagDispatcherHeartbeat = "dispatcher-heartbeat" 22 flagListenAddr = "listen-addr" 23 flagAdvertiseAddr = "advertise-addr" 24 flagDataPathAddr = "data-path-addr" 25 flagDataPathPort = "data-path-port" 26 flagDefaultAddrPool = "default-addr-pool" 27 flagDefaultAddrPoolMaskLength = "default-addr-pool-mask-length" 28 flagQuiet = "quiet" 29 flagRotate = "rotate" 30 flagToken = "token" 31 flagTaskHistoryLimit = "task-history-limit" 32 flagExternalCA = "external-ca" 33 flagMaxSnapshots = "max-snapshots" 34 flagSnapshotInterval = "snapshot-interval" 35 flagAutolock = "autolock" 36 flagAvailability = "availability" 37 flagCACert = "ca-cert" 38 flagCAKey = "ca-key" 39 ) 40 41 type swarmOptions struct { 42 swarmCAOptions 43 taskHistoryLimit int64 44 dispatcherHeartbeat time.Duration 45 maxSnapshots uint64 46 snapshotInterval uint64 47 autolock bool 48 } 49 50 // NodeAddrOption is a pflag.Value for listening addresses 51 type NodeAddrOption struct { 52 addr string 53 } 54 55 // String prints the representation of this flag 56 func (a *NodeAddrOption) String() string { 57 return a.Value() 58 } 59 60 // Set the value for this flag 61 func (a *NodeAddrOption) Set(value string) error { 62 addr, err := opts.ParseTCPAddr(value, a.addr) 63 if err != nil { 64 return err 65 } 66 a.addr = addr 67 return nil 68 } 69 70 // Type returns the type of this flag 71 func (a *NodeAddrOption) Type() string { 72 return "node-addr" 73 } 74 75 // Value returns the value of this option as addr:port 76 func (a *NodeAddrOption) Value() string { 77 return strings.TrimPrefix(a.addr, "tcp://") 78 } 79 80 // NewNodeAddrOption returns a new node address option 81 func NewNodeAddrOption(addr string) NodeAddrOption { 82 return NodeAddrOption{addr} 83 } 84 85 // NewListenAddrOption returns a NodeAddrOption with default values 86 func NewListenAddrOption() NodeAddrOption { 87 return NewNodeAddrOption(defaultListenAddr) 88 } 89 90 // ExternalCAOption is a Value type for parsing external CA specifications. 91 type ExternalCAOption struct { 92 values []*swarm.ExternalCA 93 } 94 95 // Set parses an external CA option. 96 func (m *ExternalCAOption) Set(value string) error { 97 parsed, err := parseExternalCA(value) 98 if err != nil { 99 return err 100 } 101 102 m.values = append(m.values, parsed) 103 return nil 104 } 105 106 // Type returns the type of this option. 107 func (m *ExternalCAOption) Type() string { 108 return "external-ca" 109 } 110 111 // String returns a string repr of this option. 112 func (m *ExternalCAOption) String() string { 113 externalCAs := []string{} 114 for _, externalCA := range m.values { 115 repr := fmt.Sprintf("%s: %s", externalCA.Protocol, externalCA.URL) 116 externalCAs = append(externalCAs, repr) 117 } 118 return strings.Join(externalCAs, ", ") 119 } 120 121 // Value returns the external CAs 122 func (m *ExternalCAOption) Value() []*swarm.ExternalCA { 123 return m.values 124 } 125 126 // PEMFile represents the path to a pem-formatted file 127 type PEMFile struct { 128 path, contents string 129 } 130 131 // Type returns the type of this option. 132 func (p *PEMFile) Type() string { 133 return "pem-file" 134 } 135 136 // String returns the path to the pem file 137 func (p *PEMFile) String() string { 138 return p.path 139 } 140 141 // Set parses a root rotation option 142 func (p *PEMFile) Set(value string) error { 143 contents, err := os.ReadFile(value) 144 if err != nil { 145 return err 146 } 147 if pemBlock, _ := pem.Decode(contents); pemBlock == nil { 148 return errors.New("file contents must be in PEM format") 149 } 150 p.contents, p.path = string(contents), value 151 return nil 152 } 153 154 // Contents returns the contents of the PEM file 155 func (p *PEMFile) Contents() string { 156 return p.contents 157 } 158 159 // parseExternalCA parses an external CA specification from the command line, 160 // such as protocol=cfssl,url=https://example.com. 161 func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { 162 csvReader := csv.NewReader(strings.NewReader(caSpec)) 163 fields, err := csvReader.Read() 164 if err != nil { 165 return nil, err 166 } 167 168 externalCA := swarm.ExternalCA{ 169 Options: make(map[string]string), 170 } 171 172 var ( 173 hasProtocol bool 174 hasURL bool 175 ) 176 177 for _, field := range fields { 178 key, value, ok := strings.Cut(field, "=") 179 if !ok { 180 return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field) 181 } 182 183 // TODO(thaJeztah): these options should not be case-insensitive. 184 switch strings.ToLower(key) { 185 case "protocol": 186 hasProtocol = true 187 if strings.ToLower(value) == string(swarm.ExternalCAProtocolCFSSL) { 188 externalCA.Protocol = swarm.ExternalCAProtocolCFSSL 189 } else { 190 return nil, errors.Errorf("unrecognized external CA protocol %s", value) 191 } 192 case "url": 193 hasURL = true 194 externalCA.URL = value 195 case "cacert": 196 cacontents, err := os.ReadFile(value) 197 if err != nil { 198 return nil, errors.Wrap(err, "unable to read CA cert for external CA") 199 } 200 if pemBlock, _ := pem.Decode(cacontents); pemBlock == nil { 201 return nil, errors.New("CA cert for external CA must be in PEM format") 202 } 203 externalCA.CACert = string(cacontents) 204 default: 205 externalCA.Options[key] = value 206 } 207 } 208 209 if !hasProtocol { 210 return nil, errors.New("the external-ca option needs a protocol= parameter") 211 } 212 if !hasURL { 213 return nil, errors.New("the external-ca option needs a url= parameter") 214 } 215 216 return &externalCA, nil 217 } 218 219 func addSwarmCAFlags(flags *pflag.FlagSet, options *swarmCAOptions) { 220 flags.DurationVar(&options.nodeCertExpiry, flagCertExpiry, 90*24*time.Hour, "Validity period for node certificates (ns|us|ms|s|m|h)") 221 flags.Var(&options.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") 222 } 223 224 func addSwarmFlags(flags *pflag.FlagSet, options *swarmOptions) { 225 flags.Int64Var(&options.taskHistoryLimit, flagTaskHistoryLimit, 5, "Task history retention limit") 226 flags.DurationVar(&options.dispatcherHeartbeat, flagDispatcherHeartbeat, 5*time.Second, "Dispatcher heartbeat period (ns|us|ms|s|m|h)") 227 flags.Uint64Var(&options.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain") 228 flags.SetAnnotation(flagMaxSnapshots, "version", []string{"1.25"}) 229 flags.Uint64Var(&options.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") 230 flags.SetAnnotation(flagSnapshotInterval, "version", []string{"1.25"}) 231 addSwarmCAFlags(flags, &options.swarmCAOptions) 232 } 233 234 func (o *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet, caCert string) { 235 if flags.Changed(flagTaskHistoryLimit) { 236 spec.Orchestration.TaskHistoryRetentionLimit = &o.taskHistoryLimit 237 } 238 if flags.Changed(flagDispatcherHeartbeat) { 239 spec.Dispatcher.HeartbeatPeriod = o.dispatcherHeartbeat 240 } 241 if flags.Changed(flagMaxSnapshots) { 242 spec.Raft.KeepOldSnapshots = &o.maxSnapshots 243 } 244 if flags.Changed(flagSnapshotInterval) { 245 spec.Raft.SnapshotInterval = o.snapshotInterval 246 } 247 if flags.Changed(flagAutolock) { 248 spec.EncryptionConfig.AutoLockManagers = o.autolock 249 } 250 o.mergeSwarmSpecCAFlags(spec, flags, caCert) 251 } 252 253 type swarmCAOptions struct { 254 nodeCertExpiry time.Duration 255 externalCA ExternalCAOption 256 } 257 258 func (o *swarmCAOptions) mergeSwarmSpecCAFlags(spec *swarm.Spec, flags *pflag.FlagSet, caCert string) { 259 if flags.Changed(flagCertExpiry) { 260 spec.CAConfig.NodeCertExpiry = o.nodeCertExpiry 261 } 262 if flags.Changed(flagExternalCA) { 263 spec.CAConfig.ExternalCAs = o.externalCA.Value() 264 for _, ca := range spec.CAConfig.ExternalCAs { 265 ca.CACert = caCert 266 } 267 } 268 } 269 270 func (o *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { 271 var spec swarm.Spec 272 o.mergeSwarmSpec(&spec, flags, "") 273 return spec 274 }