diff --git a/build/dependencies.yaml b/build/dependencies.yaml index c8446d6f778..011942e84fe 100644 --- a/build/dependencies.yaml +++ b/build/dependencies.yaml @@ -65,8 +65,7 @@ dependencies: refPaths: - path: cluster/images/etcd/Makefile match: BUNDLED_ETCD_VERSIONS\?|LATEST_ETCD_VERSION\? - - path: cluster/images/etcd/migrate-if-needed.sh - match: BUNDLED_VERSIONS= + - path: cluster/images/etcd/migrate/options.go - name: "golang" version: 1.13.9 diff --git a/cluster/images/etcd/migrate-if-needed.sh b/cluster/images/etcd/migrate-if-needed.sh index d9599a974da..75002700c31 100755 --- a/cluster/images/etcd/migrate-if-needed.sh +++ b/cluster/images/etcd/migrate-if-needed.sh @@ -14,95 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -# NOTES -# This script performs etcd upgrade based on the following environmental -# variables: -# TARGET_STORAGE - API of etcd to be used (supported: 'etcd3') -# TARGET_VERSION - etcd release to be used (supported: '3.0.17', '3.1.12', -# '3.2.24', "3.3.17", "3.4.9") -# DATA_DIRECTORY - directory with etcd data -# -# The current etcd version and storage format is detected based on the -# contents of "${DATA_DIRECTORY}/version.txt" file (if the file doesn't -# exist, we default it to "3.0.17/etcd2". -# -# The update workflow support the following upgrade steps: -# - 3.0.17/etcd3 -> 3.1.12/etcd3 -# - 3.1.12/etcd3 -> 3.2.24/etcd3 -# - 3.2.24/etcd3 -> 3.3.17/etcd3 -# - 3.3.17/etcd3 -> 3.4.9/etcd3 -# -# NOTE: The releases supported in this script has to match release binaries -# present in the etcd image (to make this script work correctly). -# -# Based on the current etcd version and storage format we detect what -# upgrade step from this should be done to get reach target configuration + +# DEPRECATED: +# The functionality has been moved to migrate binary and this script +# if left for backward compatibility with previous manifests. It will be +# removed in the future. set -o errexit set -o nounset -# NOTE: BUNDLED_VERSION has to match release binaries present in the -# etcd image (to make this script work correctly). -BUNDLED_VERSIONS="3.0.17, 3.1.12, 3.2.24, 3.3.17, 3.4.9" - -# shellcheck disable=SC2039 -# NOTE: Make sure the resulted ETCD_NAME agrees with --name in etcd.manifest: https://github.com/kubernetes/kubernetes/blob/e7ca64fbe16d0c4b6c7b36aecde9cd75042b2828/cluster/gce/manifests/etcd.manifest#L27 -ETCD_NAME="${ETCD_NAME:-etcd-${ETCD_HOSTNAME:-$HOSTNAME}}" -if [ -z "${DATA_DIRECTORY:-}" ]; then - echo "DATA_DIRECTORY variable unset - unexpected failure" - exit 1 -fi - -case "${DATA_DIRECTORY}" in - *event*) - ETCD_PEER_PORT=2381 - ETCD_CLIENT_PORT=18631 - ;; - *) - ETCD_PEER_PORT=2380 - ETCD_CLIENT_PORT=18629 - ;; -esac - -if [ -z "${INITIAL_CLUSTER:-}" ]; then - echo "Warn: INITIAL_CLUSTER variable unset - defaulting to ${ETCD_NAME}=http://localhost:${ETCD_PEER_PORT}" - INITIAL_CLUSTER="${ETCD_NAME}=http://localhost:${ETCD_PEER_PORT}" -fi -if [ -z "${LISTEN_PEER_URLS:-}" ]; then - echo "Warn: LISTEN_PEER_URLS variable unset - defaulting to http://localhost:${ETCD_PEER_PORT}" - LISTEN_PEER_URLS="http://localhost:${ETCD_PEER_PORT}" -fi -if [ -z "${INITIAL_ADVERTISE_PEER_URLS:-}" ]; then - echo "Warn: INITIAL_ADVERTISE_PEER_URLS variable unset - defaulting to http://localhost:${ETCD_PEER_PORT}" - INITIAL_ADVERTISE_PEER_URLS="http://localhost:${ETCD_PEER_PORT}" -fi -if [ -z "${TARGET_VERSION:-}" ]; then - echo "TARGET_VERSION variable unset - unexpected failure" - exit 1 -fi -if [ -z "${TARGET_STORAGE:-}" ]; then - echo "TARGET_STORAGE variable unset - unexpected failure" - exit 1 -fi -ETCD_DATA_PREFIX="${ETCD_DATA_PREFIX:-/registry}" -ETCD_CREDS="${ETCD_CREDS:-}" - -# Correctly support upgrade and rollback to non-default version. -if [ "${DO_NOT_MOVE_BINARIES:-}" != "true" ]; then - /bin/cp "/usr/local/bin/etcd-${TARGET_VERSION}" "/usr/local/bin/etcd" - /bin/cp "/usr/local/bin/etcdctl-${TARGET_VERSION}" "/usr/local/bin/etcdctl" -fi - -/usr/local/bin/migrate \ - --name "${ETCD_NAME}" \ - --port "${ETCD_CLIENT_PORT}" \ - --listen-peer-urls "${LISTEN_PEER_URLS}" \ - --initial-advertise-peer-urls "${INITIAL_ADVERTISE_PEER_URLS}" \ - --data-dir "${DATA_DIRECTORY}" \ - --bundled-versions "${BUNDLED_VERSIONS}" \ - --initial-cluster "${INITIAL_CLUSTER}" \ - --target-version "${TARGET_VERSION}" \ - --target-storage "${TARGET_STORAGE}" \ - --etcd-data-prefix "${ETCD_DATA_PREFIX}" \ - --ttl-keys-directory "${TTL_KEYS_DIRECTORY:-${ETCD_DATA_PREFIX}/events}" \ - --etcd-server-extra-args "${ETCD_CREDS}" +/usr/local/bin/migrate diff --git a/cluster/images/etcd/migrate/BUILD b/cluster/images/etcd/migrate/BUILD index 9d39ea3b507..c226fc58e9c 100644 --- a/cluster/images/etcd/migrate/BUILD +++ b/cluster/images/etcd/migrate/BUILD @@ -3,11 +3,13 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ + "copy_file.go", "data_dir.go", "migrate.go", "migrate_client.go", "migrate_server.go", "migrator.go", + "options.go", "util_others.go", "utils_windows.go", "versions.go", @@ -17,6 +19,7 @@ go_library( deps = [ "//vendor/github.com/blang/semver:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", + "//vendor/github.com/spf13/pflag:go_default_library", "//vendor/go.etcd.io/etcd/client:go_default_library", "//vendor/go.etcd.io/etcd/clientv3:go_default_library", "//vendor/google.golang.org/grpc:go_default_library", @@ -69,6 +72,7 @@ go_test( name = "go_default_test", srcs = [ "data_dir_test.go", + "options_test.go", "versions_test.go", ], data = glob(["testdata/**"]), diff --git a/cluster/images/etcd/migrate/copy_file.go b/cluster/images/etcd/migrate/copy_file.go new file mode 100644 index 00000000000..7a8421237b4 --- /dev/null +++ b/cluster/images/etcd/migrate/copy_file.go @@ -0,0 +1,56 @@ +/* +Copyright 2020 The Kubernetes 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 main + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +func copyFile(source, dest string) error { + sf, err := os.Open(source) + if err != nil { + return fmt.Errorf("unable to open source file [%s]: %q", source, err) + } + defer sf.Close() + fi, err := sf.Stat() + if err != nil { + return fmt.Errorf("unable to stat source file [%s]: %q", source, err) + } + + dir := filepath.Dir(dest) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("unable to create directory [%s]: %q", dir, err) + } + df, err := os.Create(dest) + if err != nil { + return fmt.Errorf("unable to create destination file [%s]: %q", dest, err) + } + defer df.Close() + + _, err = io.Copy(df, sf) + if err != nil { + return fmt.Errorf("unable to copy [%s] to [%s]: %q", source, dest, err) + } + + if err := os.Chmod(dest, fi.Mode()); err != nil { + return fmt.Errorf("unable to close destination file: %q", err) + } + return nil +} diff --git a/cluster/images/etcd/migrate/integration_test.go b/cluster/images/etcd/migrate/integration_test.go index e63b54b3a5d..258be69dc0c 100644 --- a/cluster/images/etcd/migrate/integration_test.go +++ b/cluster/images/etcd/migrate/integration_test.go @@ -42,7 +42,7 @@ import ( ) var ( - testSupportedVersions = MustParseSupportedVersions("3.0.17, 3.1.12") + testSupportedVersions = MustParseSupportedVersions([]string{"3.0.17", "3.1.12"}) testVersionPrevious = &EtcdVersion{semver.MustParse("3.0.17")} testVersionLatest = &EtcdVersion{semver.MustParse("3.1.12")} ) diff --git a/cluster/images/etcd/migrate/migrate.go b/cluster/images/etcd/migrate/migrate.go index 9b9c06bfb85..8dd3a33c56e 100644 --- a/cluster/images/etcd/migrate/migrate.go +++ b/cluster/images/etcd/migrate/migrate.go @@ -18,7 +18,6 @@ package main import ( "fmt" - "os" "path/filepath" "github.com/spf13/cobra" @@ -26,8 +25,7 @@ import ( ) const ( - versionFilename = "version.txt" - defaultPort uint64 = 18629 + versionFilename = "version.txt" ) var ( @@ -46,94 +44,43 @@ a target etcd version, this tool will upgrade or downgrade the etcd data from th opts = migrateOpts{} ) -type migrateOpts struct { - name string - port uint64 - peerListenUrls string - peerAdvertiseUrls string - binDir string - dataDir string - bundledVersionString string - etcdDataPrefix string - ttlKeysDirectory string - initialCluster string - targetVersion string - targetStorage string - etcdServerArgs string -} - func main() { - flags := migrateCmd.Flags() - flags.StringVar(&opts.name, "name", "", "etcd cluster member name. Defaults to etcd-{hostname}") - flags.Uint64Var(&opts.port, "port", defaultPort, "etcd client port to use during migration operations. This should be a different port than typically used by etcd to avoid clients accidentally connecting during upgrade/downgrade operations.") - flags.StringVar(&opts.peerListenUrls, "listen-peer-urls", "", "etcd --listen-peer-urls flag, required for HA clusters") - flags.StringVar(&opts.peerAdvertiseUrls, "initial-advertise-peer-urls", "", "etcd --initial-advertise-peer-urls flag, required for HA clusters") - flags.StringVar(&opts.binDir, "bin-dir", "/usr/local/bin", "directory of etcd and etcdctl binaries, must contain etcd- and etcdctl- for each version listed in bindled-versions") - flags.StringVar(&opts.dataDir, "data-dir", "", "etcd data directory of etcd server to migrate") - flags.StringVar(&opts.bundledVersionString, "bundled-versions", "", "comma separated list of etcd binary versions present under the bin-dir") - flags.StringVar(&opts.etcdDataPrefix, "etcd-data-prefix", "/registry", "etcd key prefix under which all objects are kept") - flags.StringVar(&opts.ttlKeysDirectory, "ttl-keys-directory", "", "etcd key prefix under which all keys with TTLs are kept. Defaults to {etcd-data-prefix}/events") - flags.StringVar(&opts.initialCluster, "initial-cluster", "", "comma separated list of name=endpoint pairs. Defaults to etcd-{hostname}=http://localhost:2380") - flags.StringVar(&opts.targetVersion, "target-version", "", "version of etcd to migrate to. Format must be '..'") - flags.StringVar(&opts.targetStorage, "target-storage", "", "storage version of etcd to migrate to, one of: etcd2, etcd3") - flags.StringVar(&opts.etcdServerArgs, "etcd-server-extra-args", "", "additional etcd server args for starting etcd servers during migration steps, --peer-* TLS cert flags should be added for etcd clusters with more than 1 member that use mutual TLS for peer communication.") + registerFlags(migrateCmd.Flags(), &opts) err := migrateCmd.Execute() if err != nil { - fmt.Printf("Failed to execute migratecmd: %s", err) + klog.Errorf("Failed to execute migratecmd: %s", err) } } -// runMigrate validates the command line flags and starts the migration. +// runMigrate starts the migration. func runMigrate() { - if opts.name == "" { - hostname, err := os.Hostname() - if err != nil { - klog.Errorf("Error while getting hostname to supply default --name: %v", err) - os.Exit(1) - } - opts.name = fmt.Sprintf("etcd-%s", hostname) - } - - if opts.ttlKeysDirectory == "" { - opts.ttlKeysDirectory = fmt.Sprintf("%s/events", opts.etcdDataPrefix) - } - if opts.initialCluster == "" { - opts.initialCluster = fmt.Sprintf("%s=http://localhost:2380", opts.name) - } - if opts.targetStorage == "" { - klog.Errorf("--target-storage is required") - os.Exit(1) - } - if opts.targetVersion == "" { - klog.Errorf("--target-version is required") - os.Exit(1) - } - if opts.dataDir == "" { - klog.Errorf("--data-dir is required") - os.Exit(1) - } - if opts.bundledVersionString == "" { - klog.Errorf("--bundled-versions is required") - os.Exit(1) - } - - bundledVersions, err := ParseSupportedVersions(opts.bundledVersionString) - if err != nil { - klog.Errorf("Failed to parse --supported-versions: %v", err) - } - err = validateBundledVersions(bundledVersions, opts.binDir) - if err != nil { - klog.Errorf("Failed to validate that 'etcd-' and 'etcdctl-' binaries exist in --bin-dir '%s' for all --bundled-versions '%s': %v", - opts.binDir, opts.bundledVersionString, err) - os.Exit(1) + if err := opts.validateAndDefault(); err != nil { + klog.Fatalf("%v", err) } + copyBinaries() target := &EtcdVersionPair{ version: MustParseEtcdVersion(opts.targetVersion), storageVersion: MustParseEtcdStorageVersion(opts.targetStorage), } - migrate(opts.name, opts.port, opts.peerListenUrls, opts.peerAdvertiseUrls, opts.binDir, opts.dataDir, opts.etcdDataPrefix, opts.ttlKeysDirectory, opts.initialCluster, target, bundledVersions, opts.etcdServerArgs) + migrate( + opts.name, opts.port, opts.peerListenUrls, opts.peerAdvertiseUrls, opts.binDir, + opts.dataDir, opts.etcdDataPrefix, opts.ttlKeysDirectory, opts.initialCluster, + target, opts.supportedVersions, opts.etcdServerArgs) +} + +func copyBinaries() { + if val, err := lookupEnv("DO_NOT_MOVE_BINARIES"); err != nil || val != "true" { + etcdVersioned := fmt.Sprintf("etcd-%s", opts.targetVersion) + etcdctlVersioned := fmt.Sprintf("etcdctl-%s", opts.targetVersion) + if err := copyFile(filepath.Join(opts.binDir, etcdVersioned), filepath.Join(opts.binDir, "etcd")); err != nil { + klog.Fatalf("Failed to copy %s: %v", etcdVersioned, err) + } + if err := copyFile(filepath.Join(opts.binDir, etcdctlVersioned), filepath.Join(opts.binDir, "etcdctl")); err != nil { + klog.Fatalf("Failed to copy %s: %v", etcdctlVersioned, err) + } + } } // migrate opens or initializes the etcd data directory, configures the migrator, and starts the migration. @@ -142,8 +89,7 @@ func migrate(name string, port uint64, peerListenUrls string, peerAdvertiseUrls dataDir, err := OpenOrCreateDataDirectory(dataDirPath) if err != nil { - klog.Errorf("Error opening or creating data directory %s: %v", dataDirPath, err) - os.Exit(1) + klog.Fatalf("Error opening or creating data directory %s: %v", dataDirPath, err) } cfg := &EtcdMigrateCfg{ @@ -161,8 +107,7 @@ func migrate(name string, port uint64, peerListenUrls string, peerAdvertiseUrls } client, err := NewEtcdMigrateClient(cfg) if err != nil { - klog.Errorf("Migration failed: %v", err) - os.Exit(1) + klog.Fatalf("Migration failed: %v", err) } defer client.Close() @@ -170,22 +115,6 @@ func migrate(name string, port uint64, peerListenUrls string, peerAdvertiseUrls err = migrator.MigrateIfNeeded(target) if err != nil { - klog.Errorf("Migration failed: %v", err) - os.Exit(1) + klog.Fatalf("Migration failed: %v", err) } } - -// validateBundledVersions checks that 'etcd-' and 'etcdctl-' binaries exist in the binDir -// for each version in the bundledVersions list. -func validateBundledVersions(bundledVersions SupportedVersions, binDir string) error { - for _, v := range bundledVersions { - for _, binaryName := range []string{"etcd", "etcdctl"} { - fn := filepath.Join(binDir, fmt.Sprintf("%s-%s", binaryName, v)) - if _, err := os.Stat(fn); err != nil { - return fmt.Errorf("failed to validate '%s' binary exists for bundled-version '%s': %v", fn, v, err) - } - - } - } - return nil -} diff --git a/cluster/images/etcd/migrate/options.go b/cluster/images/etcd/migrate/options.go new file mode 100644 index 00000000000..9581c9d6fdc --- /dev/null +++ b/cluster/images/etcd/migrate/options.go @@ -0,0 +1,239 @@ +/* +Copyright 2018 The Kubernetes 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 main + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + flag "github.com/spf13/pflag" + "k8s.io/klog/v2" +) + +var ( + supportedEtcdVersions = []string{"3.0.17", "3.1.12", "3.2.24", "3.3.17", "3.4.9"} +) + +const ( + etcdNameEnv = "ETCD_NAME" + etcdHostnameEnv = "ETCD_HOSTNAME" + hostnameEnv = "HOSTNAME" + dataDirEnv = "DATA_DIRECTORY" + initialClusterEnv = "INITIAL_CLUSTER" + initialClusterFmt = "%s=http://localhost:%d" + peerListenUrlsEnv = "LISTEN_PEER_URLS" + peerListenUrlsFmt = "http://localhost:%d" + peerAdvertiseUrlsEnv = "INITIAL_ADVERTISE_PEER_URLS" + peerAdvertiseUrlsFmt = "http://localhost:%d" + targetVersionEnv = "TARGET_VERSION" + targetStorageEnv = "TARGET_STORAGE" + etcdDataPrefixEnv = "ETCD_DATA_PREFIX" + etcdDataPrefixDefault = "/registry" + ttlKeysDirectoryFmt = "%s/events" + etcdServerArgsEnv = "ETCD_CREDS" +) + +type migrateOpts struct { + name string + port uint64 + peerPort uint64 + peerListenUrls string + peerAdvertiseUrls string + binDir string + dataDir string + bundledVersions []string + supportedVersions SupportedVersions + etcdDataPrefix string + ttlKeysDirectory string + initialCluster string + targetVersion string + targetStorage string + etcdServerArgs string +} + +func registerFlags(flags *flag.FlagSet, opt *migrateOpts) { + flags.StringVar(&opts.name, "name", "", + "etcd cluster member name. If unset fallbacks to defaults to ETCD_NAME env, if unset defaults to etcd- env, if unset defaults to etcd- env.") + flags.Uint64Var(&opts.port, "port", 0, + "etcd client port to use during migration operations. "+ + "This should be a different port than typically used by etcd to avoid clients accidentally connecting during upgrade/downgrade operations. "+ + "If unset default to 18629 or 18631 depenging on .") + flags.Uint64Var(&opts.peerPort, "peer-port", 0, + "etcd peer port to use during migration operations. If unset defaults to 2380 or 2381 depending on .") + flags.StringVar(&opts.peerListenUrls, "listen-peer-urls", "", + "etcd --listen-peer-urls flag. If unset, fallbacks to LISTEN_PEER_URLS env and if unset defaults to http://localhost:.") + flags.StringVar(&opts.peerAdvertiseUrls, "initial-advertise-peer-urls", "", + "etcd --initial-advertise-peer-urls flag. If unset fallbacks to INITIAL_ADVERTISE_PEER_URLS env and if unset defaults to http://localhost:.") + flags.StringVar(&opts.binDir, "bin-dir", "/usr/local/bin", + "directory of etcd and etcdctl binaries, must contain etcd- and etcdctl- for each version listed in .") + flags.StringVar(&opts.dataDir, "data-dir", "", + "etcd data directory of etcd server to migrate. If unset fallbacks to DATA_DIRECTORY env.") + flags.StringSliceVar(&opts.bundledVersions, "bundled-versions", supportedEtcdVersions, + "comma separated list of etcd binary versions present under the bin-dir.") + flags.StringVar(&opts.etcdDataPrefix, "etcd-data-prefix", "", + "etcd key prefix under which all objects are kept. If unset fallbacks to ETCD_DATA_PREFIX env and if unset defaults to /registry.") + flags.StringVar(&opts.ttlKeysDirectory, "ttl-keys-directory", "", + "etcd key prefix under which all keys with TTLs are kept. Defaults to /events") + flags.StringVar(&opts.initialCluster, "initial-cluster", "", + "comma separated list of name=endpoint pairs. If unset fallbacks to INITIAL_CLUSTER and if unset defaults to =https://localhost:.") + flags.StringVar(&opts.targetVersion, "target-version", "", + "version of etcd to migrate to. Format must be ... If unset fallbacks to TARGET_VERSION env.") + flags.StringVar(&opts.targetStorage, "target-storage", "", + "storage version of etcd to migrate to, one of: etcd2, etcd3. If unset fallbacks to TARGET_STORAGE env.") + flags.StringVar(&opts.etcdServerArgs, "etcd-server-extra-args", "", + "additional etcd server args for starting etcd servers during migration steps, need to set TLS certs flags for multi-member clusters using mTLS for communication. "+ + "If unset fallbacks to ETCD_CREDS env.") +} + +func lookupEnv(env string) (string, error) { + result, ok := os.LookupEnv(env) + if !ok || len(result) == 0 { + return result, fmt.Errorf("%s variable unset - expected failure", env) + } + return result, nil +} + +func fallbackToEnv(flag, env string) (string, error) { + klog.Infof("--%s unset - falling back to %s variable", flag, env) + return lookupEnv(env) +} + +func fallbackToEnvWithDefault(flag, env, def string) string { + if value, err := lookupEnv(env); err == nil { + return value + } + klog.Warningf("%s variable unset - defaulting to %s", env, def) + return def +} + +func defaultName() (string, error) { + if etcdName, err := lookupEnv(etcdNameEnv); err == nil { + return etcdName, nil + } + klog.Warningf("%s variable unset - falling back to etcd-%s variable", etcdNameEnv, etcdHostnameEnv) + if etcdHostname, err := lookupEnv(etcdHostnameEnv); err == nil { + return fmt.Sprintf("etcd-%s", etcdHostname), nil + } + klog.Warningf("%s variable unset - falling back to etcd-%s variable", etcdHostnameEnv, hostnameEnv) + if hostname, err := lookupEnv(hostnameEnv); err == nil { + return fmt.Sprintf("etcd-%s", hostname), nil + } + return "", fmt.Errorf("defaulting --name failed due to all ETCD_NAME, ETCD_HOSTNAME and HOSTNAME unset") +} + +func (opts *migrateOpts) validateAndDefault() error { + var err error + + if opts.name == "" { + klog.Infof("--name unset - falling back to %s variable", etcdNameEnv) + if opts.name, err = defaultName(); err != nil { + return err + } + } + + if opts.dataDir == "" { + if opts.dataDir, err = fallbackToEnv("data-dir", dataDirEnv); err != nil { + return err + } + } + + etcdEventsRE := regexp.MustCompile("event") + if opts.port == 0 { + if etcdEventsRE.MatchString(opts.dataDir) { + opts.port = 18631 + } else { + opts.port = 18629 + } + klog.Infof("--port unset - defaulting to %d", opts.port) + } + if opts.peerPort == 0 { + if etcdEventsRE.MatchString(opts.dataDir) { + opts.peerPort = 2381 + } else { + opts.peerPort = 2380 + } + klog.Infof("--peer-port unset - defaulting to %d", opts.peerPort) + } + + if opts.initialCluster == "" { + def := fmt.Sprintf(initialClusterFmt, opts.name, opts.peerPort) + opts.initialCluster = fallbackToEnvWithDefault("initial-cluster", initialClusterEnv, def) + } + + if opts.peerListenUrls == "" { + def := fmt.Sprintf(peerListenUrlsFmt, opts.peerPort) + opts.peerListenUrls = fallbackToEnvWithDefault("listen-peer-urls", peerListenUrlsEnv, def) + } + + if opts.peerAdvertiseUrls == "" { + def := fmt.Sprintf(peerAdvertiseUrlsFmt, opts.peerPort) + opts.peerAdvertiseUrls = fallbackToEnvWithDefault("initial-advertise-peer-urls", peerAdvertiseUrlsEnv, def) + } + + if opts.targetVersion == "" { + if opts.targetVersion, err = fallbackToEnv("target-version", targetVersionEnv); err != nil { + return err + } + } + + if opts.targetStorage == "" { + if opts.targetStorage, err = fallbackToEnv("target-storage", targetStorageEnv); err != nil { + return err + } + } + + if opts.etcdDataPrefix == "" { + opts.etcdDataPrefix = fallbackToEnvWithDefault("etcd-data-prefix", etcdDataPrefixEnv, etcdDataPrefixDefault) + } + + if opts.ttlKeysDirectory == "" { + opts.ttlKeysDirectory = fmt.Sprintf(ttlKeysDirectoryFmt, opts.etcdDataPrefix) + klog.Infof("--ttl-keys-directory unset - defaulting to %s", opts.ttlKeysDirectory) + } + + if opts.etcdServerArgs == "" { + opts.etcdServerArgs = fallbackToEnvWithDefault("etcd-server-extra-args", etcdServerArgsEnv, "") + } + + if opts.supportedVersions, err = ParseSupportedVersions(opts.bundledVersions); err != nil { + return fmt.Errorf("failed to parse --bundled-versions: %v", err) + } + + if err := validateBundledVersions(opts.supportedVersions, opts.binDir); err != nil { + return fmt.Errorf("failed to validate that 'etcd-' and 'etcdctl-' binaries exist in --bin-dir '%s' for all --bundled-versions '%s': %v", + opts.binDir, strings.Join(opts.bundledVersions, ","), err) + } + return nil +} + +// validateBundledVersions checks that 'etcd-' and 'etcdctl-' binaries exist in the binDir +// for each version in the bundledVersions list. +func validateBundledVersions(bundledVersions SupportedVersions, binDir string) error { + for _, v := range bundledVersions { + for _, binaryName := range []string{"etcd", "etcdctl"} { + fn := filepath.Join(binDir, fmt.Sprintf("%s-%s", binaryName, v)) + if _, err := os.Stat(fn); err != nil { + return fmt.Errorf("failed to validate '%s' binary exists for bundled-version '%s': %v", fn, v, err) + } + + } + } + return nil +} diff --git a/cluster/images/etcd/migrate/options_test.go b/cluster/images/etcd/migrate/options_test.go new file mode 100644 index 00000000000..3347abd6052 --- /dev/null +++ b/cluster/images/etcd/migrate/options_test.go @@ -0,0 +1,146 @@ +/* +Copyright 2018 The Kubernetes 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 main + +import ( + "os" + "testing" +) + +func setEnvVar(t *testing.T, env, val string, exists bool) { + if exists { + if err := os.Setenv(env, val); err != nil { + t.Errorf("could't set env %s: %v", env, err) + } + return + } + if err := os.Unsetenv(env); err != nil { + t.Errorf("couldn't unset env %s: %v", env, err) + } +} + +func TestFallbackToEnv(t *testing.T) { + testCases := []struct { + desc string + env string + value string + valueSet bool + expectedValue string + expectedError bool + }{ + { + desc: "value unset", + env: "FOO", + valueSet: false, + expectedValue: "", + expectedError: true, + }, + { + desc: "value set empty", + env: "FOO", + value: "", + valueSet: true, + expectedValue: "", + expectedError: true, + }, + { + desc: "value set", + env: "FOO", + value: "foo", + valueSet: true, + expectedValue: "foo", + expectedError: false, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + prevVal, prevOk := os.LookupEnv(test.env) + defer func() { + // preserve the original behavior + setEnvVar(t, test.env, prevVal, prevOk) + }() + + setEnvVar(t, test.env, test.value, test.valueSet) + value, err := fallbackToEnv("some-flag", test.env) + if test.expectedError { + if err == nil { + t.Errorf("expected error, got: %v", err) + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if value != test.expectedValue { + t.Errorf("unexpected result: %s, expected: %s", value, test.expectedValue) + } + } + }) + } +} + +func TestFallbackToEnvWithDefault(t *testing.T) { + testCases := []struct { + desc string + env string + value string + valueSet bool + defaultValue string + expectedValue string + expectedError bool + }{ + { + desc: "value unset", + env: "FOO", + valueSet: false, + defaultValue: "default", + expectedValue: "default", + }, + { + desc: "value set empty", + env: "FOO", + value: "", + valueSet: true, + defaultValue: "default", + expectedValue: "default", + }, + { + desc: "value set", + env: "FOO", + value: "foo", + valueSet: true, + defaultValue: "default", + expectedValue: "foo", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + prevVal, prevOk := os.LookupEnv(test.env) + defer func() { + // preserve the original behavior + setEnvVar(t, test.env, prevVal, prevOk) + }() + + setEnvVar(t, test.env, test.value, test.valueSet) + value := fallbackToEnvWithDefault("some-flag", test.env, test.defaultValue) + if value != test.expectedValue { + t.Errorf("unexpected result: %s, expected: %s", value, test.expectedValue) + } + }) + } +} diff --git a/cluster/images/etcd/migrate/versions.go b/cluster/images/etcd/migrate/versions.go index 3b429c92d53..bdb7cd048a8 100644 --- a/cluster/images/etcd/migrate/versions.go +++ b/cluster/images/etcd/migrate/versions.go @@ -174,10 +174,9 @@ func (sv SupportedVersions) NextVersionPair(current *EtcdVersionPair) *EtcdVersi return &EtcdVersionPair{version: nextVersion, storageVersion: storageVersion} } -// ParseSupportedVersions parses a comma separated list of etcd versions. -func ParseSupportedVersions(s string) (SupportedVersions, error) { +// ParseSupportedVersions parses a list of etcd versions. +func ParseSupportedVersions(list []string) (SupportedVersions, error) { var err error - list := strings.Split(s, ",") versions := make(SupportedVersions, len(list)) for i, v := range list { versions[i], err = ParseEtcdVersion(strings.TrimSpace(v)) @@ -189,8 +188,8 @@ func ParseSupportedVersions(s string) (SupportedVersions, error) { } // MustParseSupportedVersions parses a comma separated list of etcd versions or panics if the parse fails. -func MustParseSupportedVersions(s string) SupportedVersions { - versions, err := ParseSupportedVersions(s) +func MustParseSupportedVersions(list []string) SupportedVersions { + versions, err := ParseSupportedVersions(list) if err != nil { panic(err) }