1// Copyright 2015 CoreOS, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package flags implements command-line flag parsing.
16package flags
17
18import (
19	"flag"
20	"fmt"
21	"net/url"
22	"os"
23	"strings"
24
25	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/pkg/capnslog"
26	"github.com/coreos/etcd/pkg/transport"
27)
28
29var (
30	plog = capnslog.NewPackageLogger("github.com/coreos/etcd/pkg", "flags")
31)
32
33// DeprecatedFlag encapsulates a flag that may have been previously valid but
34// is now deprecated. If a DeprecatedFlag is set, an error occurs.
35type DeprecatedFlag struct {
36	Name string
37}
38
39func (f *DeprecatedFlag) Set(_ string) error {
40	return fmt.Errorf(`flag "-%s" is no longer supported.`, f.Name)
41}
42
43func (f *DeprecatedFlag) String() string {
44	return ""
45}
46
47// IgnoredFlag encapsulates a flag that may have been previously valid but is
48// now ignored. If an IgnoredFlag is set, a warning is printed and
49// operation continues.
50type IgnoredFlag struct {
51	Name string
52}
53
54// IsBoolFlag is defined to allow the flag to be defined without an argument
55func (f *IgnoredFlag) IsBoolFlag() bool {
56	return true
57}
58
59func (f *IgnoredFlag) Set(s string) error {
60	plog.Warningf(`flag "-%s" is no longer supported - ignoring.`, f.Name)
61	return nil
62}
63
64func (f *IgnoredFlag) String() string {
65	return ""
66}
67
68// SetFlagsFromEnv parses all registered flags in the given flagset,
69// and if they are not already set it attempts to set their values from
70// environment variables. Environment variables take the name of the flag but
71// are UPPERCASE, have the prefix "ETCD_", and any dashes are replaced by
72// underscores - for example: some-flag => ETCD_SOME_FLAG
73func SetFlagsFromEnv(fs *flag.FlagSet) error {
74	var err error
75	alreadySet := make(map[string]bool)
76	fs.Visit(func(f *flag.Flag) {
77		alreadySet[flagToEnv(f.Name)] = true
78	})
79	usedEnvKey := make(map[string]bool)
80	fs.VisitAll(func(f *flag.Flag) {
81		key := flagToEnv(f.Name)
82		if !alreadySet[key] {
83			val := os.Getenv(key)
84			if val != "" {
85				usedEnvKey[key] = true
86				if serr := fs.Set(f.Name, val); serr != nil {
87					err = fmt.Errorf("invalid value %q for %s: %v", val, key, serr)
88				}
89				plog.Infof("recognized and used environment variable %s=%s", key, val)
90			}
91		}
92	})
93
94	for _, env := range os.Environ() {
95		kv := strings.SplitN(env, "=", 2)
96		if len(kv) != 2 {
97			plog.Warningf("found invalid env %s", env)
98		}
99		if usedEnvKey[kv[0]] {
100			continue
101		}
102		if alreadySet[kv[0]] {
103			plog.Infof("recognized environment variable %s, but unused: shadowed by corresponding flag ", kv[0])
104			continue
105		}
106		if strings.HasPrefix(env, "ETCD_") {
107			plog.Warningf("unrecognized environment variable %s", env)
108		}
109	}
110
111	return err
112}
113
114func flagToEnv(name string) string {
115	return "ETCD_" + strings.ToUpper(strings.Replace(name, "-", "_", -1))
116}
117
118// SetBindAddrFromAddr sets the value of bindAddr flag from the value
119// of addr flag. Both flags' Value must be of type IPAddressPort. If the
120// bindAddr flag is set and the addr flag is unset, it will set bindAddr to
121// [::]:port of addr. Otherwise, it keeps the original values.
122func SetBindAddrFromAddr(fs *flag.FlagSet, bindAddrFlagName, addrFlagName string) {
123	if IsSet(fs, bindAddrFlagName) || !IsSet(fs, addrFlagName) {
124		return
125	}
126	addr := *fs.Lookup(addrFlagName).Value.(*IPAddressPort)
127	addr.IP = "::"
128	if err := fs.Set(bindAddrFlagName, addr.String()); err != nil {
129		plog.Panicf("unexpected flags set error: %v", err)
130	}
131}
132
133// URLsFromFlags decides what URLs should be using two different flags
134// as datasources. The first flag's Value must be of type URLs, while
135// the second must be of type IPAddressPort. If both of these flags
136// are set, an error will be returned. If only the first flag is set,
137// the underlying url.URL objects will be returned unmodified. If the
138// second flag happens to be set, the underlying IPAddressPort will be
139// converted to a url.URL and returned. The Scheme of the returned
140// url.URL will be http unless the provided TLSInfo object is non-empty.
141// If neither of the flags have been explicitly set, the default value
142// of the first flag will be returned unmodified.
143func URLsFromFlags(fs *flag.FlagSet, urlsFlagName string, addrFlagName string, tlsInfo transport.TLSInfo) ([]url.URL, error) {
144	visited := make(map[string]struct{})
145	fs.Visit(func(f *flag.Flag) {
146		visited[f.Name] = struct{}{}
147	})
148
149	_, urlsFlagIsSet := visited[urlsFlagName]
150	_, addrFlagIsSet := visited[addrFlagName]
151
152	if addrFlagIsSet {
153		if urlsFlagIsSet {
154			return nil, fmt.Errorf("Set only one of flags -%s and -%s", urlsFlagName, addrFlagName)
155		}
156
157		addr := *fs.Lookup(addrFlagName).Value.(*IPAddressPort)
158		addrURL := url.URL{Scheme: "http", Host: addr.String()}
159		if !tlsInfo.Empty() {
160			addrURL.Scheme = "https"
161		}
162		return []url.URL{addrURL}, nil
163	}
164
165	return []url.URL(*fs.Lookup(urlsFlagName).Value.(*URLsValue)), nil
166}
167
168func IsSet(fs *flag.FlagSet, name string) bool {
169	set := false
170	fs.Visit(func(f *flag.Flag) {
171		if f.Name == name {
172			set = true
173		}
174	})
175	return set
176}
177