etcd: Remove unused legacy image build code

This commit is contained in:
Ciprian Hacman 2025-12-18 14:35:19 +02:00
parent a51c7d6f31
commit c1b4b41c1f
25 changed files with 0 additions and 2658 deletions

View file

@ -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

View file

@ -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/

View file

@ -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;"

View file

@ -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

View file

@ -1,10 +0,0 @@
# See the OWNERS docs at https://go.k8s.io/owners
reviewers:
- jpbetz
- serathius
approvers:
- jpbetz
- serathius
labels:
- sig/etcd

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}
})
}
}

View file

@ -1 +0,0 @@
3.1.12/etcd3

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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)
}
}
}