mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-02-18 18:28:18 -05:00
etcd: Remove unused legacy image build code
This commit is contained in:
parent
a51c7d6f31
commit
c1b4b41c1f
25 changed files with 0 additions and 2658 deletions
|
|
@ -79,13 +79,6 @@ dependencies:
|
|||
- path: test/utils/image/manifest.go
|
||||
match: configs\[Etcd\] = Config{list\.GcEtcdRegistry, "etcd", "\d+\.\d+.\d+(-(alpha|beta|rc).\d+)?(-\d+)?"}
|
||||
|
||||
- name: "etcd-image"
|
||||
version: 3.6.4
|
||||
refPaths:
|
||||
- path: cluster/images/etcd/Makefile
|
||||
match: BUNDLED_ETCD_VERSIONS\?|LATEST_ETCD_VERSION\?
|
||||
- path: cluster/images/etcd/migrate/options.go
|
||||
|
||||
- name: "node-problem-detector"
|
||||
version: 1.34.0
|
||||
refPaths:
|
||||
|
|
@ -107,13 +100,6 @@ dependencies:
|
|||
#- path: cluster/gce/windows/k8s-node-setup.psm1
|
||||
# match: DEFAULT_NPD_VERSION
|
||||
|
||||
# From https://github.com/etcd-io/etcd/blob/main/Makefile
|
||||
- name: "golang: etcd release version"
|
||||
version: 1.23.11 # https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.6.md
|
||||
refPaths:
|
||||
- path: cluster/images/etcd/Makefile
|
||||
match: 'GOLANG_VERSION := \d+.\d+(alpha|beta|rc)?\.?(\d+)?'
|
||||
|
||||
# Golang
|
||||
# TODO: this should really be eliminated and controlled by .go-version
|
||||
- name: "golang: upstream version"
|
||||
|
|
@ -151,16 +137,6 @@ dependencies:
|
|||
- name: "registry.k8s.io/debian-base: dependents"
|
||||
version: bookworm-v1.0.6
|
||||
refPaths:
|
||||
- path: cluster/images/etcd/Makefile
|
||||
match: BASEIMAGE\?\=registry\.k8s\.io\/build-image\/debian-base:[a-zA-Z]+\-v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)
|
||||
- path: cluster/images/etcd/Makefile
|
||||
match: BASEIMAGE\?\=registry\.k8s\.io\/build-image\/debian-base-arm:[a-zA-Z]+\-v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)
|
||||
- path: cluster/images/etcd/Makefile
|
||||
match: BASEIMAGE\?\=registry\.k8s\.io\/build-image\/debian-base-arm64:[a-zA-Z]+\-v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)
|
||||
- path: cluster/images/etcd/Makefile
|
||||
match: BASEIMAGE\?\=registry\.k8s\.io\/build-image\/debian-base-ppc64le:[a-zA-Z]+\-v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)
|
||||
- path: cluster/images/etcd/Makefile
|
||||
match: BASEIMAGE\?\=registry\.k8s\.io\/build-image\/debian-base-s390x:[a-zA-Z]+\-v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)
|
||||
- path: test/conformance/image/Makefile
|
||||
match: BASE_IMAGE_VERSION\?=
|
||||
- path: test/images/pets/peer-finder/BASEIMAGE
|
||||
|
|
@ -267,7 +243,5 @@ dependencies:
|
|||
refPaths:
|
||||
- path: build/pause/cloudbuild.yaml
|
||||
match: gcr.io/k8s-staging-test-infra/gcb-docker-gcloud
|
||||
- path: cluster/images/etcd/cloudbuild.yaml
|
||||
match: gcr.io/k8s-staging-test-infra/gcb-docker-gcloud
|
||||
- path: test/images/cloudbuild.yaml
|
||||
match: gcr.io/k8s-staging-test-infra/gcb-docker-gcloud
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
# Copyright 2016 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.
|
||||
|
||||
ARG BASEIMAGE
|
||||
ARG RUNNERIMAGE
|
||||
|
||||
FROM ${BASEIMAGE} as builder
|
||||
|
||||
# This image needs bash for running "migrate-if-needed.sh". Instead of a full debian image
|
||||
# we use just the bash-static and we wrap bash-static into a distroless image instead of
|
||||
# a full debian image
|
||||
RUN apt-get update -y \
|
||||
&& apt-get -yy -q install --no-install-recommends --no-install-suggests --fix-missing \
|
||||
bash-static
|
||||
|
||||
RUN cp /bin/bash-static /sh
|
||||
|
||||
FROM ${RUNNERIMAGE}
|
||||
WORKDIR /
|
||||
|
||||
COPY --from=builder /sh /bin/
|
||||
|
||||
EXPOSE 2379 2380 4001 7001
|
||||
# etcdctl is used by etcd.manifest for livenessProbe.
|
||||
COPY etcd* etcdctl* /usr/local/bin/
|
||||
COPY migrate-if-needed.sh migrate /usr/local/bin/
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# Copyright 2021 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.
|
||||
|
||||
ARG RUNNERIMAGE
|
||||
FROM ${RUNNERIMAGE}
|
||||
|
||||
EXPOSE 2379 2380 4001 7001
|
||||
|
||||
WORKDIR C:/usr/local/bin
|
||||
COPY etcd* etcdctl* /usr/local/bin/
|
||||
COPY migrate-if-needed.bat /usr/local/bin/
|
||||
COPY migrate /usr/local/bin/migrate.exe
|
||||
|
||||
# NOTE(claudiub): docker buildx sets the PATH env variable to a Linux-like PATH,
|
||||
# # which is not desirable. See: https://github.com/moby/buildkit/issues/1560
|
||||
# # TODO(claudiub): remove this once the issue has been resolved.
|
||||
ENV PATH="C:\usr\local\bin;C:\Windows\system32;C:\Windows;"
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
# Copyright 2016 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.
|
||||
|
||||
# Build the etcd image
|
||||
#
|
||||
# Usage:
|
||||
# [BUNDLED_ETCD_VERSIONS=3.4.18 3.5.21 3.6.4] [REGISTRY=registry.k8s.io] [ARCH=amd64] [BASEIMAGE=busybox] make (build|push)
|
||||
#
|
||||
# The image contains different etcd versions to simplify
|
||||
# upgrades. Thus be careful when removing any versions from here.
|
||||
#
|
||||
# NOTE: The etcd upgrade rules are that you can upgrade only 1 minor
|
||||
# version at a time, and patch release don't matter.
|
||||
#
|
||||
# Except from etcd-$(version) and etcdctl-$(version) binaries, we also
|
||||
# need etcd and etcdctl binaries for backward compatibility reasons.
|
||||
# That binary will be set to the last version from $(BUNDLED_ETCD_VERSIONS).
|
||||
BUNDLED_ETCD_VERSIONS?=3.4.18 3.5.21 3.6.4
|
||||
|
||||
# LATEST_ETCD_VERSION identifies the most recent etcd version available.
|
||||
LATEST_ETCD_VERSION?=3.6.4
|
||||
|
||||
# REVISION provides a version number for this image and all it's bundled
|
||||
# artifacts. It should start at zero for each LATEST_ETCD_VERSION and increment
|
||||
# for each revision of this image at that etcd version.
|
||||
REVISION?=0
|
||||
|
||||
# IMAGE_TAG Uniquely identifies registry.k8s.io/etcd docker image with a tag of the form "<etcd-version>-<revision>".
|
||||
IMAGE_TAG=$(LATEST_ETCD_VERSION)-$(REVISION)
|
||||
|
||||
ARCH?=amd64
|
||||
|
||||
# Operating systems supported: linux, windows
|
||||
OS ?= linux
|
||||
# OS Version for the Windows images: 1809, ltsc2022
|
||||
OSVERSION ?= 1809
|
||||
|
||||
# The output type could either be docker (local), or registry.
|
||||
# If it is registry, it will also allow us to push the Windows images.
|
||||
OUTPUT_TYPE ?= docker
|
||||
|
||||
ALL_OS = linux windows
|
||||
ALL_ARCH.linux = amd64 arm arm64 ppc64le s390x
|
||||
ALL_OS_ARCH.linux = $(foreach arch, ${ALL_ARCH.linux}, linux-$(arch))
|
||||
ALL_ARCH.windows = amd64
|
||||
ALL_OSVERSIONS.windows := 1809 ltsc2022
|
||||
ALL_OS_ARCH.windows = $(foreach arch, $(ALL_ARCH.windows), $(foreach osversion, ${ALL_OSVERSIONS.windows}, windows-$(arch)-${osversion}))
|
||||
ALL_OS_ARCH = $(foreach os, $(ALL_OS), ${ALL_OS_ARCH.${os}})
|
||||
|
||||
IMAGE_SUFFIX.linux = $(OS)-$(ARCH)
|
||||
IMAGE_SUFFIX.windows = $(OS)-$(ARCH)-$(OSVERSION)
|
||||
IMAGE_SUFFIX := ${IMAGE_SUFFIX.${OS}}
|
||||
|
||||
# Image should be pulled from registry.k8s.io, which will auto-detect
|
||||
# region (us, eu, asia, ...) and pull from the closest.
|
||||
REGISTRY?=registry.k8s.io
|
||||
# Images should be pushed to staging-k8s.gcr.io.
|
||||
PUSH_REGISTRY?=staging-k8s.gcr.io
|
||||
|
||||
MANIFEST_IMAGE := $(PUSH_REGISTRY)/etcd
|
||||
|
||||
# Install binaries matching base distro permissions
|
||||
BIN_INSTALL := install -m 0555
|
||||
|
||||
# Hosts running SELinux need :z added to volume mounts
|
||||
SELINUX_ENABLED := $(shell cat /sys/fs/selinux/enforce 2> /dev/null || echo 0)
|
||||
|
||||
ifeq ($(SELINUX_ENABLED),1)
|
||||
DOCKER_VOL_OPTS?=:z
|
||||
endif
|
||||
|
||||
# This option is for running docker manifest command
|
||||
export DOCKER_CLI_EXPERIMENTAL := enabled
|
||||
# golang version should match the golang version of the official build from https://github.com/etcd-io/etcd/releases.
|
||||
GOLANG_VERSION := 1.23.11 # https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.6.md
|
||||
GOARM?=7
|
||||
TEMP_DIR:=$(shell mktemp -d)
|
||||
|
||||
DOCKERFILE.linux = Dockerfile
|
||||
DOCKERFILE.windows = Dockerfile.windows
|
||||
DOCKERFILE := ${DOCKERFILE.${OS}}
|
||||
|
||||
ifeq ($(ARCH),amd64)
|
||||
BASEIMAGE?=registry.k8s.io/build-image/debian-base:bookworm-v1.0.6
|
||||
endif
|
||||
ifeq ($(ARCH),arm)
|
||||
BASEIMAGE?=registry.k8s.io/build-image/debian-base-arm:bookworm-v1.0.6
|
||||
endif
|
||||
ifeq ($(ARCH),arm64)
|
||||
BASEIMAGE?=registry.k8s.io/build-image/debian-base-arm64:bookworm-v1.0.6
|
||||
endif
|
||||
ifeq ($(ARCH),ppc64le)
|
||||
BASEIMAGE?=registry.k8s.io/build-image/debian-base-ppc64le:bookworm-v1.0.6
|
||||
endif
|
||||
ifeq ($(ARCH),s390x)
|
||||
BASEIMAGE?=registry.k8s.io/build-image/debian-base-s390x:bookworm-v1.0.6
|
||||
endif
|
||||
|
||||
BASE.windows = mcr.microsoft.com/windows/nanoserver
|
||||
|
||||
RUNNERIMAGE.windows?=$(BASE.windows):$(OSVERSION)
|
||||
RUNNERIMAGE.linux?=gcr.io/distroless/static:latest
|
||||
RUNNERIMAGE := ${RUNNERIMAGE.${OS}}
|
||||
|
||||
QEMUVERSION?=5.2.0-2
|
||||
|
||||
build:
|
||||
# Explicitly copy files to the temp directory
|
||||
$(BIN_INSTALL) migrate-if-needed.sh $(TEMP_DIR)
|
||||
$(BIN_INSTALL) migrate-if-needed.bat $(TEMP_DIR)
|
||||
install $(DOCKERFILE) $(TEMP_DIR)
|
||||
|
||||
# Compile migrate
|
||||
migrate_tmp_dir=$(shell mktemp -d); \
|
||||
docker run --rm --interactive -v $(shell pwd)/../../../:/go/src/k8s.io/kubernetes$(DOCKER_VOL_OPTS) -v $${migrate_tmp_dir}:/build$(DOCKER_VOL_OPTS) -e GOOS=$(OS) -e GOARCH=$(ARCH) golang:$(GOLANG_VERSION) \
|
||||
/bin/bash -c "CGO_ENABLED=0 GO111MODULE=off go build -o /build/migrate k8s.io/kubernetes/cluster/images/etcd/migrate"; \
|
||||
$(BIN_INSTALL) $${migrate_tmp_dir}/migrate $(TEMP_DIR);
|
||||
|
||||
ifeq ($(ARCH),amd64)
|
||||
|
||||
# Do not compile if we should make an image for amd64, use the official etcd binaries instead
|
||||
# For each release create a tmp dir 'etcd_release_tmp_dir' and unpack the release tar there.
|
||||
ifeq ($(OS),windows)
|
||||
for version in $(BUNDLED_ETCD_VERSIONS); do \
|
||||
etcd_release_tmp_dir=$(shell mktemp -d); \
|
||||
curl -sSL --retry 5 https://github.com/etcd-io/etcd/releases/download/v$$version/etcd-v$$version-windows-amd64.zip -o etcd-v$$version-windows-amd64.zip; \
|
||||
unzip -q -d $$etcd_release_tmp_dir etcd-v$$version-windows-amd64.zip; \
|
||||
rm etcd-v$$version-windows-amd64.zip; \
|
||||
$(BIN_INSTALL) $$etcd_release_tmp_dir/etcd-v$$version-windows-amd64/etcd.exe $$etcd_release_tmp_dir/etcd-v$$version-windows-amd64/etcdctl.exe $(TEMP_DIR)/; \
|
||||
$(BIN_INSTALL) $(TEMP_DIR)/etcd.exe $(TEMP_DIR)/etcd-$$version.exe; \
|
||||
$(BIN_INSTALL) $(TEMP_DIR)/etcdctl.exe $(TEMP_DIR)/etcdctl-$$version.exe; \
|
||||
done
|
||||
else
|
||||
for version in $(BUNDLED_ETCD_VERSIONS); do \
|
||||
etcd_release_tmp_dir=$(shell mktemp -d); \
|
||||
curl -sSL --retry 5 https://github.com/etcd-io/etcd/releases/download/v$$version/etcd-v$$version-linux-amd64.tar.gz | tar -xz -C $$etcd_release_tmp_dir --strip-components=1; \
|
||||
$(BIN_INSTALL) $$etcd_release_tmp_dir/etcd $$etcd_release_tmp_dir/etcdctl $(TEMP_DIR)/; \
|
||||
$(BIN_INSTALL) $(TEMP_DIR)/etcd $(TEMP_DIR)/etcd-$$version; \
|
||||
$(BIN_INSTALL) $(TEMP_DIR)/etcdctl $(TEMP_DIR)/etcdctl-$$version; \
|
||||
done
|
||||
endif
|
||||
|
||||
else
|
||||
|
||||
# Download etcd in a golang container and cross-compile it statically
|
||||
# For each release create a tmp dir 'etcd_release_tmp_dir' and unpack the release tar there.
|
||||
arch_prefix=""
|
||||
ifeq ($(ARCH),arm)
|
||||
arch_prefix="GOARM=$(GOARM)"
|
||||
endif
|
||||
|
||||
# use '/go/src/go.etcd.io/etcd' to build etcd 3.4 and later.
|
||||
for version in $(BUNDLED_ETCD_VERSIONS); do \
|
||||
etcd_release_tmp_dir=$(shell mktemp -d); \
|
||||
etcd_build_dir="/go/src/github.com/coreos/etcd"; \
|
||||
etcd_build_script="./build.sh"; \
|
||||
if [ $$(echo $$version | cut -d. -f2) -gt 3 ]; then \
|
||||
etcd_build_dir="/go/src/go.etcd.io/etcd"; \
|
||||
fi; \
|
||||
if [ $$(echo $$version | cut -d. -f2) -gt 5 ]; then \
|
||||
etcd_build_script="./scripts/build.sh"; \
|
||||
fi; \
|
||||
docker run --rm --interactive -v $${etcd_release_tmp_dir}:/etcdbin golang:$(GOLANG_VERSION)$(DOCKER_VOL_OPTS) /bin/bash -c \
|
||||
"git clone https://github.com/etcd-io/etcd $$etcd_build_dir \
|
||||
&& cd $$etcd_build_dir \
|
||||
&& git checkout v$${version} \
|
||||
&& $(arch_prefix) GOARCH=$(ARCH) $$etcd_build_script \
|
||||
&& cp -f bin/$(ARCH)/etcd* bin/etcd* /etcdbin; echo 'done'"; \
|
||||
$(BIN_INSTALL) $$etcd_release_tmp_dir/etcd $$etcd_release_tmp_dir/etcdctl $(TEMP_DIR)/; \
|
||||
$(BIN_INSTALL) $(TEMP_DIR)/etcd $(TEMP_DIR)/etcd-$$version; \
|
||||
$(BIN_INSTALL) $(TEMP_DIR)/etcdctl $(TEMP_DIR)/etcdctl-$$version; \
|
||||
done
|
||||
|
||||
# Add this ENV variable in order to workaround an unsupported arch blocker
|
||||
# On arm (which is 32-bit), it can't handle >1GB data in-memory
|
||||
ifeq ($(ARCH),arm)
|
||||
cd $(TEMP_DIR) && echo "ENV ETCD_UNSUPPORTED_ARCH=$(ARCH)" >> $(DOCKERFILE)
|
||||
endif
|
||||
endif
|
||||
|
||||
docker run --rm --privileged multiarch/qemu-user-static:$(QEMUVERSION) --reset -p yes
|
||||
docker buildx version
|
||||
BUILDER=$(shell docker buildx create --use)
|
||||
|
||||
# And build the image
|
||||
docker buildx build \
|
||||
--pull \
|
||||
--provenance=false \
|
||||
--sbom=false \
|
||||
--output=type=$(OUTPUT_TYPE) \
|
||||
--platform "$(OS)/$(ARCH)" \
|
||||
-t $(REGISTRY)/etcd:$(IMAGE_TAG)-$(IMAGE_SUFFIX) \
|
||||
--build-arg BASEIMAGE=$(BASEIMAGE) \
|
||||
--build-arg RUNNERIMAGE=$(RUNNERIMAGE) \
|
||||
-f $(TEMP_DIR)/$(DOCKERFILE) \
|
||||
$(TEMP_DIR)
|
||||
docker buildx rm $$BUILDER
|
||||
|
||||
push: build
|
||||
|
||||
# split words on hyphen, access by 1-index
|
||||
word-hyphen = $(word $2,$(subst -, ,$1))
|
||||
|
||||
sub-build-%:
|
||||
$(MAKE) OUTPUT_TYPE=docker OS=$(call word-hyphen,$*,1) ARCH=$(call word-hyphen,$*,2) build
|
||||
|
||||
all-build: $(addprefix sub-build-,$(ALL_OS_ARCH))
|
||||
|
||||
sub-push-image-%:
|
||||
$(MAKE) OUTPUT_TYPE=registry OS=$(call word-hyphen,$*,1) ARCH=$(call word-hyphen,$*,2) OSVERSION=$(call word-hyphen,$*,3) REGISTRY=$(PUSH_REGISTRY) push
|
||||
|
||||
all-push-images: $(addprefix sub-push-image-,$(ALL_OS_ARCH))
|
||||
|
||||
# NOTE(claudiub): A non-default builder instance is needed in order to build Windows images.
|
||||
all-push: all-push-images push-manifest
|
||||
|
||||
push-manifest:
|
||||
docker manifest create --amend $(MANIFEST_IMAGE):$(IMAGE_TAG) $(shell echo $(ALL_OS_ARCH) | sed -e "s~[^ ]*~$(MANIFEST_IMAGE):$(IMAGE_TAG)\-&~g")
|
||||
set -x; for arch in $(ALL_ARCH.linux); do docker manifest annotate --os linux --arch $${arch} ${MANIFEST_IMAGE}:${IMAGE_TAG} ${MANIFEST_IMAGE}:${IMAGE_TAG}-linux-$${arch}; done
|
||||
# For Windows images, we also need to include the "os.version" in the manifest list, so the Windows node can pull the proper image it needs.
|
||||
# we use awk to also trim the quotes around the OS version string.
|
||||
set -x; \
|
||||
for arch in $(ALL_ARCH.windows); do \
|
||||
for osversion in ${ALL_OSVERSIONS.windows}; do \
|
||||
full_version=`docker manifest inspect ${BASE.windows}:$${osversion} | grep "os.version" | head -n 1 | awk -F\" '{print $$4}'` || true; \
|
||||
docker manifest annotate --os windows --arch $${arch} --os-version $${full_version} ${MANIFEST_IMAGE}:${IMAGE_TAG} ${MANIFEST_IMAGE}:${IMAGE_TAG}-windows-$${arch}-$${osversion}; \
|
||||
done; \
|
||||
done
|
||||
docker manifest push --purge ${MANIFEST_IMAGE}:${IMAGE_TAG}
|
||||
|
||||
unit-test:
|
||||
docker run --rm --interactive -v $(shell pwd)/../../../:/go/src/k8s.io/kubernetes$(DOCKER_VOL_OPTS) -e GOARCH=$(ARCH) golang:$(GOLANG_VERSION) \
|
||||
/bin/bash -c "CGO_ENABLED=0 go test -v k8s.io/kubernetes/cluster/images/etcd/migrate"
|
||||
|
||||
# Integration tests require both a golang build environment and all the etcd binaries from a `registry.k8s.io/etcd` image (`/usr/local/bin/etcd-<version>`, ...).
|
||||
# Since the `registry.k8s.io/etcd` image is for runtime only and does not have a build golang environment, we create a new docker image to run integration tests
|
||||
# with.
|
||||
build-integration-test-image: build
|
||||
cp -r $(TEMP_DIR) $(TEMP_DIR)_integration_test
|
||||
cp Dockerfile $(TEMP_DIR)_integration_test/Dockerfile
|
||||
docker build \
|
||||
--pull \
|
||||
-t etcd-integration-test \
|
||||
--build-arg BASEIMAGE=golang:$(GOLANG_VERSION) \
|
||||
--build-arg RUNNERIMAGE=$(RUNNERIMAGE) \
|
||||
$(TEMP_DIR)_integration_test
|
||||
|
||||
integration-test:
|
||||
docker run --rm --interactive -v $(shell pwd)/../../../:/go/src/k8s.io/kubernetes$(DOCKER_VOL_OPTS) -e GOARCH=$(ARCH) etcd-integration-test \
|
||||
/bin/bash -c "CGO_ENABLED=0 go test -tags=integration k8s.io/kubernetes/cluster/images/etcd/migrate -args -v 10 -logtostderr true"
|
||||
|
||||
integration-build-test: build-integration-test-image integration-test
|
||||
test: unit-test integration-build-test
|
||||
all: all-build test
|
||||
.PHONY: build push push-manifest all-push all-push-images all-build unit-test build-integration-test-image integration-test integration-build-test test
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
reviewers:
|
||||
- jpbetz
|
||||
- serathius
|
||||
approvers:
|
||||
- jpbetz
|
||||
- serathius
|
||||
labels:
|
||||
- sig/etcd
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
### registry.k8s.io/etcd docker image
|
||||
|
||||
Provides docker images containing etcd and etcdctl binaries for multiple etcd
|
||||
version as well as a migration operator utility for upgrading and downgrading
|
||||
etcd--it's data directory in particular--to a target version.
|
||||
|
||||
#### Versioning
|
||||
|
||||
Each `registry.k8s.io/etcd` docker image is tagged with an version string of the form
|
||||
`<etcd-version>-<image-revision>`, e.g. `3.0.17-0`. The etcd version is the
|
||||
SemVer of latest etcd version available in the image. The image revision
|
||||
distinguishes between docker images with the same lastest etcd version but
|
||||
changes (bug fixes and backward compatible improvements) to the migration
|
||||
utility bundled with the image.
|
||||
|
||||
In addition to the latest etcd version, each `registry.k8s.io/etcd` image contains
|
||||
etcd and etcdctl binaries for older versions of etcd. These are used by the
|
||||
migration operator utility when performing downgrades and multi-step upgrades,
|
||||
but can also be used as the etcd target version.
|
||||
|
||||
#### Usage
|
||||
|
||||
Always run `/usr/local/bin/migrate` (or the
|
||||
`/usr/local/bin/migrate-if-needed.sh` wrapper script) before starting the etcd
|
||||
server. On Windows, run `C:\bin\migrate.exe` (or the `C:\bin\migrate-if-needed.bat
|
||||
wrapper script`).
|
||||
|
||||
`migrate` writes a `version.txt` file to track the "current" version
|
||||
of etcd that was used to persist data to disk. A "target" version may also be provided
|
||||
by the `TARGET_STORAGE` (e.g. "etcd3") and `TARGET_VERSION` (e.g. "3.4.13" )
|
||||
environment variables. If the persisted version differs from the target version,
|
||||
`migrate-if-needed.sh` will migrate the data from the current to the target
|
||||
version.
|
||||
|
||||
Upgrades to any target version are supported. The data will be automatically upgraded
|
||||
in steps to each minor version until the target version is reached.
|
||||
|
||||
Downgrades to the previous minor version of the 3.x series is supported.
|
||||
|
||||
#### Permissions
|
||||
|
||||
By default, `migrate` will write data directory files with default permissions
|
||||
according to the umask it is run with. When run in the published
|
||||
`registry.k8s.io/etcd` images the default umask is 0022 which will result in 0755
|
||||
directory permissions and 0644 file permissions.
|
||||
|
||||
#### Cross building
|
||||
|
||||
For `amd64`, official `etcd` and `etcdctl` binaries are downloaded from Github
|
||||
to maintain official support. For other architectures, `etcd` is cross-compiled
|
||||
from source. Arch-specific `debian` images serve as base images.
|
||||
|
||||
Windows images can be built on Linux nodes due to `docker buildx`, but they will
|
||||
only be created and pushed when using the `all-push` make target.
|
||||
|
||||
#### How to release
|
||||
|
||||
First, update `ETCD_VERSION` and `REVSION` in the `Makefile`.
|
||||
|
||||
Next, build and test the image:
|
||||
|
||||
```console
|
||||
$ make build test
|
||||
```
|
||||
|
||||
Last, build and push the docker images for all supported architectures.
|
||||
|
||||
```console
|
||||
# Build images for all the architecture and push the manifest image as well
|
||||
$ make all-push
|
||||
|
||||
# Build images for all the architecture
|
||||
$ make all-build
|
||||
|
||||
# Build image for target architecture(default=amd64)
|
||||
$ make build ARCH=ppc64le
|
||||
```
|
||||
|
||||
If you don't want to push the images, run `make` or `make build` instead
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# See https://cloud.google.com/cloud-build/docs/build-config
|
||||
timeout: 1200s
|
||||
options:
|
||||
substitution_option: ALLOW_LOOSE
|
||||
machineType: 'N1_HIGHCPU_8'
|
||||
steps:
|
||||
- name: 'gcr.io/k8s-staging-test-infra/gcb-docker-gcloud:v20240523-a15ad90fc9@sha256:bb04162508c2c61637eae700a0d8e8c8be8f2d4c831d2b75e59db2d4dd6cf75d'
|
||||
entrypoint: 'bash'
|
||||
dir: ./cluster/images/etcd
|
||||
env:
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- REGISTRY=gcr.io/$PROJECT_ID
|
||||
- PUSH_REGISTRY=gcr.io/$PROJECT_ID
|
||||
- IMAGE=gcr.io/$PROJECT_ID/etcd
|
||||
- BUILD_IMAGE=debian-build
|
||||
- TMPDIR=/workspace
|
||||
- HOME=/root # for docker buildx
|
||||
args:
|
||||
- '-c'
|
||||
- |
|
||||
gcloud auth configure-docker \
|
||||
&& docker buildx create --name img-builder --use \
|
||||
&& docker buildx inspect --bootstrap \
|
||||
&& docker run --rm --privileged linuxkit/binfmt:4ea3b9b0938cbd19834c096aa31ff475cc75d281 \
|
||||
&& make all-push
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
@echo off
|
||||
REM Copyright 2021 The Kubernetes Authors.
|
||||
REM
|
||||
REM Licensed under the Apache License, Version 2.0 (the "License");
|
||||
REM you may not use this file except in compliance with the License.
|
||||
REM You may obtain a copy of the License at
|
||||
REM
|
||||
REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
REM
|
||||
REM Unless required by applicable law or agreed to in writing, software
|
||||
REM distributed under the License is distributed on an "AS IS" BASIS,
|
||||
REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
REM See the License for the specific language governing permissions and
|
||||
REM limitations under the License.
|
||||
|
||||
REM DEPRECATED:
|
||||
REM The functionality has been moved to migrate binary and this script
|
||||
REM if left for backward compatibility with previous manifests. It will be
|
||||
REM removed in the future.
|
||||
|
||||
C:\bin\migrate.exe
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Copyright 2016 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.
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
/usr/local/bin/migrate
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
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
|
||||
}
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// DataDirectory provides utilities for initializing and backing up an
|
||||
// etcd "data-dir" as well as managing a version.txt file to track the
|
||||
// etcd server version and storage version of the etcd data in the
|
||||
// directory.
|
||||
type DataDirectory struct {
|
||||
path string
|
||||
versionFile *VersionFile
|
||||
}
|
||||
|
||||
// OpenOrCreateDataDirectory opens a data directory, creating the directory
|
||||
// if it doesn't not already exist.
|
||||
func OpenOrCreateDataDirectory(path string) (*DataDirectory, error) {
|
||||
exists, err := exists(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
klog.Infof("data directory '%s' does not exist, creating it", path)
|
||||
err := os.MkdirAll(path, 0777)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create data directory %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
versionFile := &VersionFile{
|
||||
path: filepath.Join(path, versionFilename),
|
||||
}
|
||||
return &DataDirectory{path, versionFile}, nil
|
||||
}
|
||||
|
||||
// Initialize set the version.txt to the target version if the data
|
||||
// directory is empty. If the data directory is non-empty, no
|
||||
// version.txt file will be written since the actual version of etcd
|
||||
// used to create the data is unknown.
|
||||
func (d *DataDirectory) Initialize(target *EtcdVersionPair) error {
|
||||
isEmpty, err := d.IsEmpty()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isEmpty {
|
||||
klog.Infof("data directory '%s' is empty, writing target version '%s' to version.txt", d.path, target)
|
||||
err = d.versionFile.Write(target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write version.txt to '%s': %v", d.path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Backup creates a backup copy of data directory.
|
||||
func (d *DataDirectory) Backup() error {
|
||||
backupDir := fmt.Sprintf("%s.bak", d.path)
|
||||
err := os.RemoveAll(backupDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll(backupDir, 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = copyDirectory(d.path, backupDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the data directory is entirely empty.
|
||||
func (d *DataDirectory) IsEmpty() (bool, error) {
|
||||
dir, err := os.Open(d.path)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to open data directory %s: %v", d.path, err)
|
||||
}
|
||||
defer dir.Close()
|
||||
_, err = dir.Readdirnames(1)
|
||||
if err == io.EOF {
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// String returns the data directory path.
|
||||
func (d *DataDirectory) String() string {
|
||||
return d.path
|
||||
}
|
||||
|
||||
// VersionFile provides utilities for reading and writing version.txt files
|
||||
// to etcd "data-dir" for tracking the etcd server and storage versions
|
||||
// of the data in the directory.
|
||||
type VersionFile struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (v *VersionFile) nextPath() string {
|
||||
return fmt.Sprintf("%s-next", v.path)
|
||||
}
|
||||
|
||||
// Exists returns true if a version.txt file exists on the file system.
|
||||
func (v *VersionFile) Exists() (bool, error) {
|
||||
return exists(v.path)
|
||||
}
|
||||
|
||||
// Read parses the version.txt file and returns it's contents.
|
||||
func (v *VersionFile) Read() (*EtcdVersionPair, error) {
|
||||
data, err := os.ReadFile(v.path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read version file %s: %v", v.path, err)
|
||||
}
|
||||
txt := strings.TrimSpace(string(data))
|
||||
vp, err := ParseEtcdVersionPair(txt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse etcd '<version>/<storage-version>' string from version.txt file contents '%s': %v", txt, err)
|
||||
}
|
||||
return vp, nil
|
||||
}
|
||||
|
||||
// equals returns true iff VersionFile exists and contains given EtcdVersionPair.
|
||||
func (v *VersionFile) equals(vp *EtcdVersionPair) (bool, error) {
|
||||
exists, err := v.Exists()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
cvp, err := v.Read()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return vp.Equals(cvp), nil
|
||||
}
|
||||
|
||||
// Write creates or overwrites the contents of the version.txt file with the given EtcdVersionPair.
|
||||
func (v *VersionFile) Write(vp *EtcdVersionPair) error {
|
||||
// We do write only if file content differs from given EtcdVersionPair.
|
||||
isUpToDate, err := v.equals(vp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to to check if version file %s should be changed: %v", v.path, err)
|
||||
}
|
||||
if isUpToDate {
|
||||
return nil
|
||||
}
|
||||
// We do write + rename instead of just write to protect from version.txt
|
||||
// corruption under full disk condition.
|
||||
// See https://github.com/kubernetes/kubernetes/issues/98989.
|
||||
err = os.WriteFile(v.nextPath(), []byte(vp.String()), 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write new version file %s: %v", v.nextPath(), err)
|
||||
}
|
||||
return os.Rename(v.nextPath(), v.path)
|
||||
}
|
||||
|
||||
func exists(path string) (bool, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
latestVersion = semver.MustParse("3.1.12")
|
||||
)
|
||||
|
||||
func TestExistingDataDirWithVersionFile(t *testing.T) {
|
||||
d, err := OpenOrCreateDataDirectory("testdata/datadir_with_version")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open data dir: %v", err)
|
||||
}
|
||||
isEmpty, err := d.IsEmpty()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if data dir is empty: %v", err)
|
||||
}
|
||||
if isEmpty {
|
||||
t.Errorf("Expected non-empty data directory to exist")
|
||||
}
|
||||
exists, err := d.versionFile.Exists()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("Expected version file %s to exist", d.versionFile.path)
|
||||
}
|
||||
vp, err := d.versionFile.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read version file %s: %v", d.versionFile.path, err)
|
||||
}
|
||||
expectedVersion := &EtcdVersionPair{&EtcdVersion{latestVersion}, storageEtcd3}
|
||||
if !vp.Equals(expectedVersion) {
|
||||
t.Errorf("Expected version file to contain %s, but got %s", expectedVersion, vp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistingDataDirWithoutVersionFile(t *testing.T) {
|
||||
targetVersion := &EtcdVersionPair{&EtcdVersion{latestVersion}, storageEtcd3}
|
||||
|
||||
d, err := OpenOrCreateDataDirectory("testdata/datadir_without_version")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open data dir: %v", err)
|
||||
}
|
||||
exists, err := d.versionFile.Exists()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exists {
|
||||
t.Errorf("Expected version file %s not to exist", d.versionFile.path)
|
||||
}
|
||||
err = d.Initialize(targetVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed initialize data directory %s: %v", d.path, err)
|
||||
}
|
||||
exists, err = d.versionFile.Exists()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatalf("Expected version file %s not to exist after initializing non-empty data-dir", d.versionFile.path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonexistingDataDir(t *testing.T) {
|
||||
targetVersion := &EtcdVersionPair{&EtcdVersion{latestVersion}, storageEtcd3}
|
||||
path := newTestPath(t)
|
||||
defer os.RemoveAll(path)
|
||||
d, err := OpenOrCreateDataDirectory(filepath.Join(path, "data-dir"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open data dir: %v", err)
|
||||
}
|
||||
isEmpty, err := d.IsEmpty()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if data dir is empty: %v", err)
|
||||
}
|
||||
if !isEmpty {
|
||||
t.Errorf("Expected empty data directory to exist")
|
||||
}
|
||||
err = d.Initialize(targetVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed initialize data directory %s: %v", d.path, err)
|
||||
}
|
||||
exists, err := d.versionFile.Exists()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("Expected version file %s to exist", d.versionFile.path)
|
||||
}
|
||||
isEmpty, err = d.IsEmpty()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to check if data dir is empty: %v", err)
|
||||
}
|
||||
if isEmpty {
|
||||
t.Errorf("Expected non-empty data directory to exist after Initialize()")
|
||||
}
|
||||
vp, err := d.versionFile.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read version file %s: %v", d.versionFile.path, err)
|
||||
}
|
||||
if !vp.Equals(targetVersion) {
|
||||
t.Errorf("Expected version file to contain %s, but got %s", targetVersion, vp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackup(t *testing.T) {
|
||||
path := newTestPath(t)
|
||||
defer os.RemoveAll(path)
|
||||
d, err := OpenOrCreateDataDirectory(filepath.Join(path, "data-dir"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open data dir: %v", err)
|
||||
}
|
||||
_, err = os.Create(filepath.Join(path, "data-dir", "empty.txt"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = d.Backup()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to backup data directory %s: %v", d.path, err)
|
||||
}
|
||||
bak, err := OpenOrCreateDataDirectory(filepath.Join(path, "data-dir.bak"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open backup data dir: %v", err)
|
||||
}
|
||||
isEmpty, err := bak.IsEmpty()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if isEmpty {
|
||||
t.Errorf("Expected non-empty backup directory to exist after Backup()")
|
||||
}
|
||||
}
|
||||
|
||||
func newTestPath(t *testing.T) string {
|
||||
path, err := os.MkdirTemp("", "etcd-migrate-test-")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create tmp dir for test: %v", err)
|
||||
}
|
||||
err = os.Chmod(path, 0777)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to granting permission to tmp dir for test: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
|
@ -1,384 +0,0 @@
|
|||
//go:build integration
|
||||
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"k8s.io/klog/v2"
|
||||
netutils "k8s.io/utils/net"
|
||||
)
|
||||
|
||||
var (
|
||||
testSupportedVersions = mustParseSupportedVersions([]string{"3.0.17", "3.1.12"})
|
||||
testVersionPrevious = &EtcdVersion{semver.MustParse("3.0.17")}
|
||||
testVersionLatest = &EtcdVersion{semver.MustParse("3.1.12")}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Enable klog which is used in dependencies
|
||||
klog.InitFlags(nil)
|
||||
flag.Set("logtostderr", "true")
|
||||
flag.Set("v", "9")
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
migrations := []struct {
|
||||
title string
|
||||
memberCount int
|
||||
startVersion string
|
||||
endVersion string
|
||||
protocol string
|
||||
clientListenUrls string
|
||||
}{
|
||||
// upgrades
|
||||
{"v3-v3-up", 1, "3.0.17/etcd3", "3.1.12/etcd3", "https", ""},
|
||||
{"oldest-newest-up", 1, "3.0.17/etcd3", "3.1.12/etcd3", "https", ""},
|
||||
{"v3-v3-up-with-additional-client-url", 1, "3.0.17/etcd3", "3.1.12/etcd3", "https", "http://127.0.0.1:2379,http://10.128.0.1:2379"},
|
||||
|
||||
// warning: v2->v3 ha upgrades not currently supported.
|
||||
{"ha-v3-v3-up", 3, "3.0.17/etcd3", "3.1.12/etcd3", "https", ""},
|
||||
|
||||
// downgrades
|
||||
{"v3-v3-down", 1, "3.1.12/etcd3", "3.0.17/etcd3", "https", ""},
|
||||
|
||||
// warning: ha downgrades not yet supported.
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
t.Run(m.title, func(t *testing.T) {
|
||||
start := mustParseEtcdVersionPair(m.startVersion)
|
||||
end := mustParseEtcdVersionPair(m.endVersion)
|
||||
|
||||
testCfgs := clusterConfig(t, m.title, m.memberCount, m.protocol, m.clientListenUrls)
|
||||
|
||||
servers := []*EtcdMigrateServer{}
|
||||
for _, cfg := range testCfgs {
|
||||
client, err := NewEtcdMigrateClient(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
server := NewEtcdMigrateServer(cfg, client)
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
// Start the servers.
|
||||
parallel(servers, func(server *EtcdMigrateServer) {
|
||||
dataDir, err := OpenOrCreateDataDirectory(server.cfg.dataDirectory)
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening or creating data directory %s: %v", server.cfg.dataDirectory, err)
|
||||
}
|
||||
migrator := &Migrator{server.cfg, dataDir, server.client}
|
||||
err = migrator.MigrateIfNeeded(start)
|
||||
if err != nil {
|
||||
t.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
err = server.Start(start.version)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Write a value to each server, read it back.
|
||||
parallel(servers, func(server *EtcdMigrateServer) {
|
||||
key := fmt.Sprintf("/registry/%s", server.cfg.name)
|
||||
value := fmt.Sprintf("value-%s", server.cfg.name)
|
||||
err := server.client.Put(start.version, key, value)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write text value: %v", err)
|
||||
}
|
||||
|
||||
checkVal, err := server.client.Get(start.version, key)
|
||||
if err != nil {
|
||||
t.Errorf("Error getting %s for validation: %v", key, err)
|
||||
}
|
||||
if checkVal != value {
|
||||
t.Errorf("Expected %s from %s but got %s", value, key, checkVal)
|
||||
}
|
||||
})
|
||||
|
||||
// Migrate the servers in series.
|
||||
serial(servers, func(server *EtcdMigrateServer) {
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("Stop server failed: %v", err)
|
||||
}
|
||||
dataDir, err := OpenOrCreateDataDirectory(server.cfg.dataDirectory)
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening or creating data directory %s: %v", server.cfg.dataDirectory, err)
|
||||
}
|
||||
migrator := &Migrator{server.cfg, dataDir, server.client}
|
||||
err = migrator.MigrateIfNeeded(end)
|
||||
if err != nil {
|
||||
t.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
err = server.Start(end.version)
|
||||
if err != nil {
|
||||
t.Fatalf("Start server failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Check that all test values can be read back from all the servers.
|
||||
parallel(servers, func(server *EtcdMigrateServer) {
|
||||
for _, s := range servers {
|
||||
key := fmt.Sprintf("/registry/%s", s.cfg.name)
|
||||
value := fmt.Sprintf("value-%s", s.cfg.name)
|
||||
checkVal, err := server.client.Get(end.version, key)
|
||||
if err != nil {
|
||||
t.Errorf("Error getting %s from etcd 2.x after rollback from 3.x: %v", key, err)
|
||||
}
|
||||
if checkVal != value {
|
||||
t.Errorf("Expected %s from %s but got %s when reading after rollback from %s to %s", value, key, checkVal, start, end)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Stop the servers.
|
||||
parallel(servers, func(server *EtcdMigrateServer) {
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stop server: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Check that version.txt contains the correct end version.
|
||||
parallel(servers, func(server *EtcdMigrateServer) {
|
||||
dataDir, err := OpenOrCreateDataDirectory(server.cfg.dataDirectory)
|
||||
v, err := dataDir.versionFile.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read version.txt file: %v", err)
|
||||
}
|
||||
if !v.Equals(end) {
|
||||
t.Errorf("Expected version.txt to contain %s but got %s", end, v)
|
||||
}
|
||||
// Integration tests are run in a docker container with umask of 0022.
|
||||
checkPermissions(t, server.cfg.dataDirectory, 0755|os.ModeDir)
|
||||
checkPermissions(t, dataDir.versionFile.path, 0644)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func parallel(servers []*EtcdMigrateServer, fn func(server *EtcdMigrateServer)) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(servers))
|
||||
for _, server := range servers {
|
||||
go func(s *EtcdMigrateServer) {
|
||||
defer wg.Done()
|
||||
fn(s)
|
||||
}(server)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func serial(servers []*EtcdMigrateServer, fn func(server *EtcdMigrateServer)) {
|
||||
for _, server := range servers {
|
||||
fn(server)
|
||||
}
|
||||
}
|
||||
|
||||
func checkPermissions(t *testing.T, path string, expected os.FileMode) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat file %s: %v", path, err)
|
||||
}
|
||||
if info.Mode() != expected {
|
||||
t.Errorf("Expected permissions for file %s of %s, but got %s", path, expected, info.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
func clusterConfig(t *testing.T, name string, memberCount int, protocol string, clientListenUrls string) []*EtcdMigrateCfg {
|
||||
peers := []string{}
|
||||
for i := 0; i < memberCount; i++ {
|
||||
memberName := fmt.Sprintf("%s-%d", name, i)
|
||||
peerPort := uint64(2380 + i*10000)
|
||||
peer := fmt.Sprintf("%s=%s://127.0.0.1:%d", memberName, protocol, peerPort)
|
||||
peers = append(peers, peer)
|
||||
}
|
||||
initialCluster := strings.Join(peers, ",")
|
||||
|
||||
extraArgs := ""
|
||||
if protocol == "https" {
|
||||
extraArgs = getOrCreateTLSPeerCertArgs(t)
|
||||
}
|
||||
|
||||
cfgs := []*EtcdMigrateCfg{}
|
||||
for i := 0; i < memberCount; i++ {
|
||||
memberName := fmt.Sprintf("%s-%d", name, i)
|
||||
peerURL := fmt.Sprintf("%s://127.0.0.1:%d", protocol, uint64(2380+i*10000))
|
||||
cfg := &EtcdMigrateCfg{
|
||||
binPath: "/usr/local/bin",
|
||||
name: memberName,
|
||||
initialCluster: initialCluster,
|
||||
port: uint64(2379 + i*10000),
|
||||
peerListenUrls: peerURL,
|
||||
peerAdvertiseUrls: peerURL,
|
||||
clientListenUrls: clientListenUrls,
|
||||
etcdDataPrefix: "/registry",
|
||||
ttlKeysDirectory: "/registry/events",
|
||||
supportedVersions: testSupportedVersions,
|
||||
dataDirectory: fmt.Sprintf("/tmp/etcd-data-dir-%s", memberName),
|
||||
etcdServerArgs: extraArgs,
|
||||
}
|
||||
cfgs = append(cfgs, cfg)
|
||||
}
|
||||
return cfgs
|
||||
}
|
||||
|
||||
func getOrCreateTLSPeerCertArgs(t *testing.T) string {
|
||||
spec := TestCertSpec{
|
||||
host: "localhost",
|
||||
ips: []string{"127.0.0.1"},
|
||||
}
|
||||
certDir := "/tmp/certs"
|
||||
certFile := filepath.Join(certDir, "test.crt")
|
||||
keyFile := filepath.Join(certDir, "test.key")
|
||||
err := getOrCreateTestCertFiles(certFile, keyFile, spec)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create server cert: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("--peer-client-cert-auth --peer-trusted-ca-file=%s --peer-cert-file=%s --peer-key-file=%s", certFile, certFile, keyFile)
|
||||
}
|
||||
|
||||
type TestCertSpec struct {
|
||||
host string
|
||||
names, ips []string // in certificate
|
||||
}
|
||||
|
||||
func getOrCreateTestCertFiles(certFileName, keyFileName string, spec TestCertSpec) (err error) {
|
||||
if _, err := os.Stat(certFileName); err == nil {
|
||||
if _, err := os.Stat(keyFileName); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
certPem, keyPem, err := generateSelfSignedCertKey(spec.host, parseIPList(spec.ips), spec.names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Dir(certFileName), os.FileMode(0777))
|
||||
err = os.WriteFile(certFileName, certPem, os.FileMode(0777))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Dir(keyFileName), os.FileMode(0777))
|
||||
err = os.WriteFile(keyFileName, keyPem, os.FileMode(0777))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseIPList(ips []string) []net.IP {
|
||||
var netIPs []net.IP
|
||||
for _, ip := range ips {
|
||||
netIPs = append(netIPs, netutils.ParseIPSloppy(ip))
|
||||
}
|
||||
return netIPs
|
||||
}
|
||||
|
||||
// generateSelfSignedCertKey creates a self-signed certificate and key for the given host.
|
||||
// Host may be an IP or a DNS name
|
||||
// You may also specify additional subject alt names (either ip or dns names) for the certificate
|
||||
func generateSelfSignedCertKey(host string, alternateIPs []net.IP, alternateDNS []string) ([]byte, []byte, error) {
|
||||
priv, err := rsa.GenerateKey(cryptorand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()),
|
||||
},
|
||||
NotBefore: time.Unix(0, 0),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 100),
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
if ip := netutils.ParseIPSloppy(host); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, host)
|
||||
}
|
||||
|
||||
template.IPAddresses = append(template.IPAddresses, alternateIPs...)
|
||||
template.DNSNames = append(template.DNSNames, alternateDNS...)
|
||||
|
||||
derBytes, err := x509.CreateCertificate(cryptorand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Generate cert
|
||||
certBuffer := bytes.Buffer{}
|
||||
if err := pem.Encode(&certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Generate key
|
||||
keyBuffer := bytes.Buffer{}
|
||||
if err := pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return certBuffer.Bytes(), keyBuffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// mustParseEtcdVersionPair parses a "<version>/<storage-version>" string to an EtcdVersionPair
|
||||
// or panics if the parse fails.
|
||||
func mustParseEtcdVersionPair(s string) *EtcdVersionPair {
|
||||
pair, err := ParseEtcdVersionPair(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pair
|
||||
}
|
||||
|
||||
// mustParseSupportedVersions parses a comma separated list of etcd versions or panics if the parse fails.
|
||||
func mustParseSupportedVersions(list []string) SupportedVersions {
|
||||
versions, err := ParseSupportedVersions(list)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return versions
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
versionFilename = "version.txt"
|
||||
)
|
||||
|
||||
var (
|
||||
migrateCmd = &cobra.Command{
|
||||
Short: "Upgrade/downgrade etcd data across multiple versions",
|
||||
Long: `Upgrade or downgrade etcd data across multiple versions to the target version
|
||||
|
||||
Given a 'bin-dir' directory of etcd and etcdctl binaries, an etcd 'data-dir' with a 'version.txt' file and
|
||||
a target etcd version, this tool will upgrade or downgrade the etcd data from the version specified in
|
||||
'version.txt' to the target version.
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runMigrate()
|
||||
},
|
||||
}
|
||||
opts = migrateOpts{}
|
||||
)
|
||||
|
||||
func main() {
|
||||
registerFlags(migrateCmd.Flags(), &opts)
|
||||
err := migrateCmd.Execute()
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to execute migratecmd: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// runMigrate starts the migration.
|
||||
func runMigrate() {
|
||||
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.clientListenUrls,
|
||||
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.
|
||||
func migrate(name string, port uint64, peerListenUrls string, peerAdvertiseUrls string, clientListenUrls string,
|
||||
binPath string, dataDirPath string, etcdDataPrefix string, ttlKeysDirectory string,
|
||||
initialCluster string, target *EtcdVersionPair, bundledVersions SupportedVersions, etcdServerArgs string) {
|
||||
|
||||
dataDir, err := OpenOrCreateDataDirectory(dataDirPath)
|
||||
if err != nil {
|
||||
klog.Fatalf("Error opening or creating data directory %s: %v", dataDirPath, err)
|
||||
}
|
||||
|
||||
cfg := &EtcdMigrateCfg{
|
||||
binPath: binPath,
|
||||
name: name,
|
||||
port: port,
|
||||
peerListenUrls: peerListenUrls,
|
||||
peerAdvertiseUrls: peerAdvertiseUrls,
|
||||
clientListenUrls: clientListenUrls,
|
||||
etcdDataPrefix: etcdDataPrefix,
|
||||
ttlKeysDirectory: ttlKeysDirectory,
|
||||
initialCluster: initialCluster,
|
||||
supportedVersions: bundledVersions,
|
||||
dataDirectory: dataDirPath,
|
||||
etcdServerArgs: etcdServerArgs,
|
||||
}
|
||||
client, err := NewEtcdMigrateClient(cfg)
|
||||
if err != nil {
|
||||
klog.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
migrator := &Migrator{cfg, dataDir, client}
|
||||
|
||||
err = migrator.MigrateIfNeeded(target)
|
||||
if err != nil {
|
||||
klog.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"google.golang.org/grpc"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// CombinedEtcdClient provides an implementation of EtcdMigrateClient using a combination of the etcd v2 client, v3 client
|
||||
// and etcdctl commands called via the shell.
|
||||
type CombinedEtcdClient struct {
|
||||
cfg *EtcdMigrateCfg
|
||||
}
|
||||
|
||||
// NewEtcdMigrateClient creates a new EtcdMigrateClient from the given EtcdMigrateCfg.
|
||||
func NewEtcdMigrateClient(cfg *EtcdMigrateCfg) (EtcdMigrateClient, error) {
|
||||
return &CombinedEtcdClient{cfg}, nil
|
||||
}
|
||||
|
||||
// Close closes the client and releases any resources it holds.
|
||||
func (e *CombinedEtcdClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetEtcdVersionKeyValue writes the given version to the etcd 'etcd_version' key.
|
||||
// If no error is returned, the write was successful, indicating the etcd server is available
|
||||
// and able to perform consensus writes.
|
||||
func (e *CombinedEtcdClient) SetEtcdVersionKeyValue(version *EtcdVersion) error {
|
||||
return e.Put(version, "etcd_version", version.String())
|
||||
}
|
||||
|
||||
// Put write a single key value pair to etcd.
|
||||
func (e *CombinedEtcdClient) Put(version *EtcdVersion, key, value string) error {
|
||||
v3client, err := e.clientV3()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer v3client.Close()
|
||||
_, err = v3client.KV.Put(context.Background(), key, value)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get reads a single value for a given key.
|
||||
func (e *CombinedEtcdClient) Get(version *EtcdVersion, key string) (string, error) {
|
||||
v3client, err := e.clientV3()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer v3client.Close()
|
||||
resp, err := v3client.KV.Get(context.Background(), key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
kvs := resp.Kvs
|
||||
if len(kvs) != 1 {
|
||||
return "", fmt.Errorf("expected exactly one value for key %s but got %d", key, len(kvs))
|
||||
}
|
||||
|
||||
return string(kvs[0].Value), nil
|
||||
}
|
||||
|
||||
func (e *CombinedEtcdClient) clientV3() (*clientv3.Client, error) {
|
||||
return clientv3.New(clientv3.Config{
|
||||
Endpoints: []string{e.endpoint()},
|
||||
DialTimeout: 20 * time.Second,
|
||||
DialOptions: []grpc.DialOption{
|
||||
grpc.WithBlock(), // block until the underlying connection is up
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Backup creates a backup of an etcd2 data directory at the given backupDir.
|
||||
func (e *CombinedEtcdClient) Backup(version *EtcdVersion, backupDir string) error {
|
||||
// We cannot use etcd/client (v2) to make this call. It is implemented in the etcdctl client code.
|
||||
if version.Major != 2 {
|
||||
return fmt.Errorf("etcd 2.x required but got version '%s'", version)
|
||||
}
|
||||
return e.runEtcdctlCommand(version,
|
||||
"--debug",
|
||||
"backup",
|
||||
"--data-dir", e.cfg.dataDirectory,
|
||||
"--backup-dir", backupDir,
|
||||
)
|
||||
}
|
||||
|
||||
// Snapshot captures a snapshot from a running etcd3 server and saves it to the given snapshotFile.
|
||||
// We cannot use etcd/clientv3 to make this call. It is implemented in the etcdctl client code.
|
||||
func (e *CombinedEtcdClient) Snapshot(version *EtcdVersion, snapshotFile string) error {
|
||||
if version.Major != 3 {
|
||||
return fmt.Errorf("etcd 3.x required but got version '%s'", version)
|
||||
}
|
||||
return e.runEtcdctlCommand(version,
|
||||
"--endpoints", e.endpoint(),
|
||||
"snapshot", "save", snapshotFile,
|
||||
)
|
||||
}
|
||||
|
||||
// Restore restores a given snapshotFile into the data directory specified this clients config.
|
||||
func (e *CombinedEtcdClient) Restore(version *EtcdVersion, snapshotFile string) error {
|
||||
// We cannot use etcd/clientv3 to make this call. It is implemented in the etcdctl client code.
|
||||
if version.Major != 3 {
|
||||
return fmt.Errorf("etcd 3.x required but got version '%s'", version)
|
||||
}
|
||||
return e.runEtcdctlCommand(version,
|
||||
"snapshot", "restore", snapshotFile,
|
||||
"--data-dir", e.cfg.dataDirectory,
|
||||
"--name", e.cfg.name,
|
||||
"--initial-advertise-peer-urls", e.cfg.peerAdvertiseUrls,
|
||||
"--initial-cluster", e.cfg.initialCluster,
|
||||
)
|
||||
}
|
||||
|
||||
// Migrate upgrades a 'etcd2' storage version data directory to a 'etcd3' storage version
|
||||
// data directory.
|
||||
func (e *CombinedEtcdClient) Migrate(version *EtcdVersion) error {
|
||||
// We cannot use etcd/clientv3 to make this call as it is implemented in etcd/etcdctl.
|
||||
if version.Major != 3 {
|
||||
return fmt.Errorf("etcd 3.x required but got version '%s'", version)
|
||||
}
|
||||
return e.runEtcdctlCommand(version,
|
||||
"migrate",
|
||||
"--data-dir", e.cfg.dataDirectory,
|
||||
)
|
||||
}
|
||||
|
||||
func (e *CombinedEtcdClient) runEtcdctlCommand(version *EtcdVersion, args ...string) error {
|
||||
etcdctlCmd := exec.Command(filepath.Join(e.cfg.binPath, fmt.Sprintf("etcdctl-%s", version)), args...)
|
||||
etcdctlCmd.Env = []string{fmt.Sprintf("ETCDCTL_API=%d", version.Major)}
|
||||
etcdctlCmd.Stdout = os.Stdout
|
||||
etcdctlCmd.Stderr = os.Stderr
|
||||
return etcdctlCmd.Run()
|
||||
}
|
||||
|
||||
// AttachLease attaches leases of the given leaseDuration to all the etcd objects under
|
||||
// ttlKeysDirectory specified in this client's config.
|
||||
func (e *CombinedEtcdClient) AttachLease(leaseDuration time.Duration) error {
|
||||
ttlKeysPrefix := e.cfg.ttlKeysDirectory
|
||||
// Make sure that ttlKeysPrefix is ended with "/" so that we only get children "directories".
|
||||
if !strings.HasSuffix(ttlKeysPrefix, "/") {
|
||||
ttlKeysPrefix += "/"
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
v3client, err := e.clientV3()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer v3client.Close()
|
||||
objectsResp, err := v3client.KV.Get(ctx, ttlKeysPrefix, clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while getting objects to attach to the lease")
|
||||
}
|
||||
|
||||
lease, err := v3client.Lease.Grant(ctx, int64(leaseDuration/time.Second))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while creating lease: %v", err)
|
||||
}
|
||||
klog.Infof("Lease with TTL: %v created", lease.TTL)
|
||||
|
||||
klog.Infof("Attaching lease to %d entries", len(objectsResp.Kvs))
|
||||
for _, kv := range objectsResp.Kvs {
|
||||
putResp, err := v3client.KV.Put(ctx, string(kv.Key), string(kv.Value), clientv3.WithLease(lease.ID), clientv3.WithPrevKV())
|
||||
if err != nil {
|
||||
klog.Errorf("Error while attaching lease to: %s", string(kv.Key))
|
||||
}
|
||||
if !bytes.Equal(putResp.PrevKv.Value, kv.Value) {
|
||||
return fmt.Errorf("concurrent access to key detected when setting lease on %s, expected previous value of %s but got %s",
|
||||
kv.Key, kv.Value, putResp.PrevKv.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *CombinedEtcdClient) endpoint() string {
|
||||
return fmt.Sprintf("http://127.0.0.1:%d", e.cfg.port)
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// EtcdMigrateServer manages starting and stopping a versioned etcd server binary.
|
||||
type EtcdMigrateServer struct {
|
||||
cfg *EtcdMigrateCfg
|
||||
client EtcdMigrateClient
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
// NewEtcdMigrateServer creates a EtcdMigrateServer for starting and stopping a etcd server at the given version.
|
||||
func NewEtcdMigrateServer(cfg *EtcdMigrateCfg, client EtcdMigrateClient) *EtcdMigrateServer {
|
||||
return &EtcdMigrateServer{cfg: cfg, client: client}
|
||||
}
|
||||
|
||||
// Start starts an etcd server as a separate process, waits until it has started, and returns a exec.Cmd.
|
||||
// TODO: Add support for listening to client via TLS.
|
||||
func (r *EtcdMigrateServer) Start(version *EtcdVersion) error {
|
||||
etcdCmd := exec.Command(
|
||||
fmt.Sprintf("%s/etcd-%s", r.cfg.binPath, version),
|
||||
"--name", r.cfg.name,
|
||||
"--initial-cluster", r.cfg.initialCluster,
|
||||
"--debug",
|
||||
"--data-dir", r.cfg.dataDirectory,
|
||||
"--listen-client-urls", r.cfg.clientListenUrls,
|
||||
"--advertise-client-urls", fmt.Sprintf("http://127.0.0.1:%d", r.cfg.port),
|
||||
"--listen-peer-urls", r.cfg.peerListenUrls,
|
||||
"--initial-advertise-peer-urls", r.cfg.peerAdvertiseUrls,
|
||||
)
|
||||
if r.cfg.etcdServerArgs != "" {
|
||||
extraArgs := strings.Fields(r.cfg.etcdServerArgs)
|
||||
etcdCmd.Args = append(etcdCmd.Args, extraArgs...)
|
||||
}
|
||||
fmt.Printf("Starting server %s: %+v\n", r.cfg.name, etcdCmd.Args)
|
||||
|
||||
etcdCmd.Stdout = os.Stdout
|
||||
etcdCmd.Stderr = os.Stderr
|
||||
err := etcdCmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
interval := time.NewTicker(time.Millisecond * 500)
|
||||
defer interval.Stop()
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
time.Sleep(time.Minute * 2)
|
||||
close(done)
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-interval.C:
|
||||
err := r.client.SetEtcdVersionKeyValue(version)
|
||||
if err != nil {
|
||||
klog.Infof("Still waiting for etcd to start, current error: %v", err)
|
||||
// keep waiting
|
||||
} else {
|
||||
klog.Infof("Etcd on port %d is up.", r.cfg.port)
|
||||
r.cmd = etcdCmd
|
||||
return nil
|
||||
}
|
||||
case <-done:
|
||||
err = etcdCmd.Process.Kill()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error killing etcd: %v", err)
|
||||
}
|
||||
return fmt.Errorf("timed out waiting for etcd on port %d", r.cfg.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop terminates the etcd server process. If the etcd server process has not been started
|
||||
// or is not still running, this returns an error.
|
||||
func (r *EtcdMigrateServer) Stop() error {
|
||||
if r.cmd == nil {
|
||||
return fmt.Errorf("cannot stop EtcdMigrateServer that has not been started")
|
||||
}
|
||||
err := r.cmd.Process.Signal(os.Interrupt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending SIGINT to etcd for graceful shutdown: %v", err)
|
||||
}
|
||||
gracefulWait := time.Minute * 2
|
||||
stopped := make(chan bool)
|
||||
timedout := make(chan bool)
|
||||
go func() {
|
||||
time.Sleep(gracefulWait)
|
||||
close(timedout)
|
||||
}()
|
||||
go func() {
|
||||
select {
|
||||
case <-stopped:
|
||||
return
|
||||
case <-timedout:
|
||||
klog.Infof("etcd server has not terminated gracefully after %s, killing it.", gracefulWait)
|
||||
r.cmd.Process.Kill()
|
||||
return
|
||||
}
|
||||
}()
|
||||
err = r.cmd.Wait()
|
||||
close(stopped)
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
klog.Infof("etcd server stopped (signal: %s)", exiterr.Error())
|
||||
// stopped
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error waiting for etcd to stop: %v", err)
|
||||
}
|
||||
klog.Infof("Stopped etcd server %s", r.cfg.name)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// EtcdMigrateCfg provides all configuration required to perform etcd data upgrade/downgrade migrations.
|
||||
type EtcdMigrateCfg struct {
|
||||
binPath string
|
||||
name string
|
||||
initialCluster string
|
||||
port uint64
|
||||
peerListenUrls string
|
||||
peerAdvertiseUrls string
|
||||
clientListenUrls string
|
||||
etcdDataPrefix string
|
||||
ttlKeysDirectory string
|
||||
supportedVersions SupportedVersions
|
||||
dataDirectory string
|
||||
etcdServerArgs string
|
||||
}
|
||||
|
||||
// EtcdMigrateClient defines the etcd client operations required to perform migrations.
|
||||
type EtcdMigrateClient interface {
|
||||
SetEtcdVersionKeyValue(version *EtcdVersion) error
|
||||
Get(version *EtcdVersion, key string) (string, error)
|
||||
Put(version *EtcdVersion, key, value string) error
|
||||
Backup(version *EtcdVersion, backupDir string) error
|
||||
Snapshot(version *EtcdVersion, snapshotFile string) error
|
||||
Restore(version *EtcdVersion, snapshotFile string) error
|
||||
Migrate(version *EtcdVersion) error
|
||||
AttachLease(leaseDuration time.Duration) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Migrator manages etcd data migrations.
|
||||
type Migrator struct {
|
||||
cfg *EtcdMigrateCfg // TODO: don't wire this directly in
|
||||
dataDirectory *DataDirectory
|
||||
client EtcdMigrateClient
|
||||
}
|
||||
|
||||
// MigrateIfNeeded upgrades or downgrades the etcd data directory to the given target version.
|
||||
func (m *Migrator) MigrateIfNeeded(target *EtcdVersionPair) error {
|
||||
klog.Infof("Starting migration to %s", target)
|
||||
err := m.dataDirectory.Initialize(target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize data directory %s: %v", m.dataDirectory.path, err)
|
||||
}
|
||||
|
||||
var current *EtcdVersionPair
|
||||
vfExists, err := m.dataDirectory.versionFile.Exists()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if vfExists {
|
||||
current, err = m.dataDirectory.versionFile.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("existing data directory '%s' is missing version.txt file, unable to migrate", m.dataDirectory.path)
|
||||
}
|
||||
|
||||
for {
|
||||
klog.Infof("Converging current version '%s' to target version '%s'", current, target)
|
||||
currentNextMinorVersion := &EtcdVersion{Version: semver.Version{Major: current.version.Major, Minor: current.version.Minor + 1}}
|
||||
switch {
|
||||
case current.version.MajorMinorEquals(target.version) || currentNextMinorVersion.MajorMinorEquals(target.version):
|
||||
klog.Infof("current version '%s' equals or is one minor version previous of target version '%s' - migration complete", current, target)
|
||||
err = m.dataDirectory.versionFile.Write(target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write version.txt to '%s': %v", m.dataDirectory.path, err)
|
||||
}
|
||||
return nil
|
||||
case current.storageVersion == storageEtcd2 && target.storageVersion == storageEtcd3:
|
||||
return fmt.Errorf("upgrading from etcd2 storage to etcd3 storage is not supported")
|
||||
case current.version.Major == 3 && target.version.Major == 2:
|
||||
return fmt.Errorf("downgrading from etcd 3.x to 2.x is not supported")
|
||||
case current.version.Major == target.version.Major && current.version.Minor < target.version.Minor:
|
||||
stepVersion := m.cfg.supportedVersions.NextVersionPair(current)
|
||||
klog.Infof("upgrading etcd from %s to %s", current, stepVersion)
|
||||
current, err = m.minorVersionUpgrade(current, stepVersion)
|
||||
case current.version.Major == 3 && target.version.Major == 3 && current.version.Minor > target.version.Minor:
|
||||
klog.Infof("rolling etcd back from %s to %s", current, target)
|
||||
current, err = m.rollbackEtcd3MinorVersion(current, target)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrator) rollbackEtcd3MinorVersion(current *EtcdVersionPair, target *EtcdVersionPair) (*EtcdVersionPair, error) {
|
||||
if target.version.Minor != current.version.Minor-1 {
|
||||
return nil, fmt.Errorf("rollback from %s to %s not supported, only rollbacks to the previous minor version are supported", current.version, target.version)
|
||||
}
|
||||
|
||||
klog.Infof("Performing etcd %s -> %s rollback", current.version, target.version)
|
||||
err := m.dataDirectory.Backup()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snapshotFilename := fmt.Sprintf("%s.snapshot.db", m.dataDirectory.path)
|
||||
err = os.Remove(snapshotFilename)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("failed to clean snapshot file before rollback: %v", err)
|
||||
}
|
||||
|
||||
// Start current version of etcd.
|
||||
runner := m.newServer()
|
||||
klog.Infof("Starting etcd version %s to capture rollback snapshot.", current.version)
|
||||
err = runner.Start(current.version)
|
||||
if err != nil {
|
||||
klog.Fatalf("Unable to automatically downgrade etcd: starting etcd version %s to capture rollback snapshot failed: %v", current.version, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
klog.Infof("Snapshotting etcd %s to %s", current.version, snapshotFilename)
|
||||
err = m.client.Snapshot(current.version, snapshotFilename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = runner.Stop()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
klog.Info("Backing up data before rolling back")
|
||||
backupDir := fmt.Sprintf("%s.bak", m.dataDirectory)
|
||||
err = os.RemoveAll(backupDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
origInfo, err := os.Stat(m.dataDirectory.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = os.Rename(m.dataDirectory.path, backupDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
klog.Infof("Restoring etcd %s from %s", target.version, snapshotFilename)
|
||||
err = m.client.Restore(target.version, snapshotFilename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = os.Chmod(m.dataDirectory.path, origInfo.Mode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (m *Migrator) minorVersionUpgrade(current *EtcdVersionPair, target *EtcdVersionPair) (*EtcdVersionPair, error) {
|
||||
runner := m.newServer()
|
||||
|
||||
// Do the migration step, by just starting etcd in the target version.
|
||||
err := runner.Start(target.version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = runner.Stop()
|
||||
return target, err
|
||||
}
|
||||
|
||||
func (m *Migrator) newServer() *EtcdMigrateServer {
|
||||
return NewEtcdMigrateServer(m.cfg, m.client)
|
||||
}
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
/*
|
||||
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.4.18", "3.5.21", "3.6.4"}
|
||||
)
|
||||
|
||||
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"
|
||||
clientListenURLsEnv = "LISTEN_CLIENT_URLS"
|
||||
clientListenURLFmt = "http://127.0.0.1:%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
|
||||
clientListenUrls 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-<ETCD_HOSTNAME> env, if unset defaults to etcd-<HOSTNAME> 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 depending on <data-dir>.")
|
||||
flags.Uint64Var(&opts.peerPort, "peer-port", 0,
|
||||
"etcd peer port to use during migration operations. If unset defaults to 2380 or 2381 depending on <data-dir>.")
|
||||
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:<peer-port>.")
|
||||
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:<peer-port>.")
|
||||
flags.StringVar(&opts.clientListenUrls, "listen-client-urls", "",
|
||||
"etcd --listen-client-urls flag. If unset, fallbacks to LISTEN_CLIENT_URLS env, and if unset defaults to http://127.0.0.1:<port>.")
|
||||
flags.StringVar(&opts.binDir, "bin-dir", "/usr/local/bin",
|
||||
"directory of etcd and etcdctl binaries, must contain etcd-<version> and etcdctl-<version> for each version listed in <bundled-versions>.")
|
||||
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 <etcd-data-prefix>/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 <etcd-name>=https://localhost:<peer-port>.")
|
||||
flags.StringVar(&opts.targetVersion, "target-version", "",
|
||||
"version of etcd to migrate to. Format must be <major>.<minor>.<patch>. 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 for %s flag unset - defaulting to %s", env, flag, 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.clientListenUrls == "" {
|
||||
def := fmt.Sprintf(clientListenURLFmt, opts.port)
|
||||
opts.clientListenUrls = fallbackToEnvWithDefault("listen-client-urls", clientListenURLsEnv, 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-<version>' and 'etcdctl-<version>' 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-<version>' and 'etcdctl-<version>' 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
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
/*
|
||||
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 {
|
||||
t.Setenv(env, val)
|
||||
} else if prev, ok := os.LookupEnv(env); ok {
|
||||
t.Cleanup(func() { os.Setenv(env, prev) })
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
3.1.12/etcd3
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
/*
|
||||
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 (
|
||||
"github.com/mrunalp/fileutils"
|
||||
)
|
||||
|
||||
func copyDirectory(source string, dest string) error {
|
||||
return fileutils.CopyDirectory(source, dest)
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
//go:build windows
|
||||
|
||||
/*
|
||||
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"
|
||||
|
||||
func copyDirectory(source string, dest string) error {
|
||||
return fmt.Errorf("no support for windows")
|
||||
}
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
)
|
||||
|
||||
// EtcdVersion specifies an etcd server binaries SemVer.
|
||||
type EtcdVersion struct {
|
||||
semver.Version
|
||||
}
|
||||
|
||||
// ParseEtcdVersion parses a SemVer string to an EtcdVersion.
|
||||
func ParseEtcdVersion(s string) (*EtcdVersion, error) {
|
||||
v, err := semver.Make(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EtcdVersion{v}, nil
|
||||
}
|
||||
|
||||
// MustParseEtcdVersion parses a SemVer string to an EtcdVersion and panics if the parse fails.
|
||||
func MustParseEtcdVersion(s string) *EtcdVersion {
|
||||
return &EtcdVersion{semver.MustParse(s)}
|
||||
}
|
||||
|
||||
// String returns the version in SemVer string format.
|
||||
func (v *EtcdVersion) String() string {
|
||||
return v.Version.String()
|
||||
}
|
||||
|
||||
// Equals returns true if the versions are exactly equal.
|
||||
func (v *EtcdVersion) Equals(o *EtcdVersion) bool {
|
||||
return v.Version.Equals(o.Version)
|
||||
}
|
||||
|
||||
// MajorMinorEquals returns true if the major and minor parts of the versions are equal;
|
||||
// if only patch versions differ, this returns true.
|
||||
func (v *EtcdVersion) MajorMinorEquals(o *EtcdVersion) bool {
|
||||
return v.Major == o.Major && v.Minor == o.Minor
|
||||
}
|
||||
|
||||
// EtcdStorageVersion identifies the storage version of an etcd data directory.
|
||||
type EtcdStorageVersion int
|
||||
|
||||
const (
|
||||
storageUnknown EtcdStorageVersion = iota
|
||||
storageEtcd2
|
||||
storageEtcd3
|
||||
)
|
||||
|
||||
// ParseEtcdStorageVersion parses an etcd storage version string to an EtcdStorageVersion.
|
||||
func ParseEtcdStorageVersion(s string) (EtcdStorageVersion, error) {
|
||||
switch s {
|
||||
case "etcd2":
|
||||
return storageEtcd2, nil
|
||||
case "etcd3":
|
||||
return storageEtcd3, nil
|
||||
default:
|
||||
return storageUnknown, fmt.Errorf("unrecognized storage version: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
// MustParseEtcdStorageVersion parses an etcd storage version string to an EtcdStorageVersion and
|
||||
// panics if the parse fails.
|
||||
func MustParseEtcdStorageVersion(s string) EtcdStorageVersion {
|
||||
version, err := ParseEtcdStorageVersion(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// String returns the text representation of the EtcdStorageVersion, 'etcd2' or 'etcd3'.
|
||||
func (v EtcdStorageVersion) String() string {
|
||||
switch v {
|
||||
case storageEtcd2:
|
||||
return "etcd2"
|
||||
case storageEtcd3:
|
||||
return "etcd3"
|
||||
default:
|
||||
panic(fmt.Sprintf("enum value %d missing from EtcdStorageVersion String() function", v))
|
||||
}
|
||||
}
|
||||
|
||||
// EtcdVersionPair is composed of an etcd version and storage version.
|
||||
type EtcdVersionPair struct {
|
||||
version *EtcdVersion
|
||||
storageVersion EtcdStorageVersion
|
||||
}
|
||||
|
||||
// ParseEtcdVersionPair parses a "<version>/<storage-version>" string to an EtcdVersionPair.
|
||||
func ParseEtcdVersionPair(s string) (*EtcdVersionPair, error) {
|
||||
parts := strings.Split(s, "/")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("malformed version file, expected <major>.<minor>.<patch>/<storage> but got %s", s)
|
||||
}
|
||||
version, err := ParseEtcdVersion(parts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storageVersion, err := ParseEtcdStorageVersion(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EtcdVersionPair{version, storageVersion}, nil
|
||||
}
|
||||
|
||||
// String returns "<version>/<storage-version>" string of the EtcdVersionPair.
|
||||
func (vp *EtcdVersionPair) String() string {
|
||||
return fmt.Sprintf("%s/%s", vp.version, vp.storageVersion)
|
||||
}
|
||||
|
||||
// Equals returns true if both the versions and storage versions are exactly equal.
|
||||
func (vp *EtcdVersionPair) Equals(o *EtcdVersionPair) bool {
|
||||
return vp.version.Equals(o.version) && vp.storageVersion == o.storageVersion
|
||||
}
|
||||
|
||||
// SupportedVersions provides a list of etcd versions that are "supported" for some purpose.
|
||||
// The list must be sorted from lowest semantic version to high.
|
||||
type SupportedVersions []*EtcdVersion
|
||||
|
||||
// NextVersion returns the next supported version after the given current version, or nil if no
|
||||
// next version exists.
|
||||
func (sv SupportedVersions) NextVersion(current *EtcdVersion) *EtcdVersion {
|
||||
var nextVersion *EtcdVersion
|
||||
for i, supportedVersion := range sv {
|
||||
if current.MajorMinorEquals(supportedVersion) && len(sv) > i+1 {
|
||||
nextVersion = sv[i+1]
|
||||
}
|
||||
}
|
||||
return nextVersion
|
||||
}
|
||||
|
||||
// NextVersionPair returns the next supported version after the given current version and infers
|
||||
// the storage version from the major version part of the next version.
|
||||
func (sv SupportedVersions) NextVersionPair(current *EtcdVersionPair) *EtcdVersionPair {
|
||||
nextVersion := sv.NextVersion(current.version)
|
||||
if nextVersion == nil {
|
||||
return nil
|
||||
}
|
||||
storageVersion := storageEtcd3
|
||||
if nextVersion.Major == 2 {
|
||||
storageVersion = storageEtcd2
|
||||
}
|
||||
return &EtcdVersionPair{version: nextVersion, storageVersion: storageVersion}
|
||||
}
|
||||
|
||||
// ParseSupportedVersions parses a list of etcd versions.
|
||||
func ParseSupportedVersions(list []string) (SupportedVersions, error) {
|
||||
var err error
|
||||
versions := make(SupportedVersions, len(list))
|
||||
for i, v := range list {
|
||||
versions[i], err = ParseEtcdVersion(strings.TrimSpace(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return versions, nil
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
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 (
|
||||
"testing"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
)
|
||||
|
||||
func TestSerializeEtcdVersionPair(t *testing.T) {
|
||||
cases := []struct {
|
||||
versionTxt string
|
||||
version *EtcdVersionPair
|
||||
match bool
|
||||
}{
|
||||
{"3.1.2/etcd3", &EtcdVersionPair{&EtcdVersion{semver.MustParse("3.1.2")}, storageEtcd3}, true},
|
||||
{"1.1.1-rc.0/etcd3", &EtcdVersionPair{&EtcdVersion{semver.MustParse("1.1.1-rc.0")}, storageEtcd3}, true},
|
||||
{"10.100.1000/etcd3", &EtcdVersionPair{&EtcdVersion{semver.MustParse("10.100.1000")}, storageEtcd3}, true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
vp, err := ParseEtcdVersionPair(c.versionTxt)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to parse '%s': %v", c.versionTxt, err)
|
||||
}
|
||||
if vp.Equals(c.version) != c.match {
|
||||
t.Errorf("Expected '%s' to be parsed as '%+v', got '%+v'", c.versionTxt, c.version, vp)
|
||||
}
|
||||
if vp.String() != c.versionTxt {
|
||||
t.Errorf("Expected round trip serialization back to '%s', got '%s'", c.versionTxt, vp.String())
|
||||
}
|
||||
}
|
||||
|
||||
unparsables := []string{
|
||||
"1.1/etcd3",
|
||||
"1.1.1.1/etcd3",
|
||||
"1.1.1/etcd4",
|
||||
}
|
||||
for _, unparsable := range unparsables {
|
||||
vp, err := ParseEtcdVersionPair(unparsable)
|
||||
if err == nil {
|
||||
t.Errorf("Should have failed to parse '%s' but got '%s'", unparsable, vp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMajorMinorEquals(t *testing.T) {
|
||||
cases := []struct {
|
||||
first *EtcdVersion
|
||||
second *EtcdVersion
|
||||
match bool
|
||||
}{
|
||||
{&EtcdVersion{semver.Version{Major: 3, Minor: 1, Patch: 2}}, &EtcdVersion{semver.Version{Major: 3, Minor: 1, Patch: 0}}, true},
|
||||
{&EtcdVersion{semver.Version{Major: 3, Minor: 1, Patch: 2}}, &EtcdVersion{semver.Version{Major: 3, Minor: 1, Patch: 2}}, true},
|
||||
|
||||
{&EtcdVersion{semver.Version{Major: 3, Minor: 0, Patch: 0}}, &EtcdVersion{semver.Version{Major: 3, Minor: 1, Patch: 0}}, false},
|
||||
{&EtcdVersion{semver.Version{Major: 2, Minor: 0, Patch: 0}}, &EtcdVersion{semver.Version{Major: 3, Minor: 0, Patch: 0}}, false},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
if c.first.MajorMinorEquals(c.second) != c.match {
|
||||
t.Errorf("Expected (%+v == %+v) == %t, got %t", c.first, c.second, c.match, !c.match)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue