Add generated file

This PR adds generated files under pkg/client and vendor folder.
This commit is contained in:
xing-yang
2018-07-12 10:55:15 -07:00
parent 36b1de0341
commit e213d1890d
17729 changed files with 5090889 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
# 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.
FROM busybox
COPY etcdctl etcd-empty-dir-cleanup.sh /
RUN chmod a+rx /etcdctl /etcd-empty-dir-cleanup.sh
ENV ETCDCTL /etcdctl
ENV SLEEP_SECOND 3600
USER nobody:nogroup
ENTRYPOINT ["/etcd-empty-dir-cleanup.sh"]

View File

@@ -0,0 +1,37 @@
# 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.
.PHONY: build push
ETCD_VERSION = 3.2.18
# Image should be pulled from k8s.gcr.io, which will auto-detect
# region (us, eu, asia, ...) and pull from the closest.
REGISTRY = k8s.gcr.io
# Images should be pushed to staging-k8s.gcr.io.
PUSH_REGISTRY = staging-k8s.gcr.io
TAG = 3.2.18.0
clean:
rm -rf etcdctl etcd-v$(ETCD_VERSION)-linux-amd64 etcd-v$(ETCD_VERSION)-linux-amd64.tar.gz
build: clean
curl -L -O https://github.com/coreos/etcd/releases/download/v$(ETCD_VERSION)/etcd-v$(ETCD_VERSION)-linux-amd64.tar.gz
tar xzvf etcd-v$(ETCD_VERSION)-linux-amd64.tar.gz
cp etcd-v$(ETCD_VERSION)-linux-amd64/etcdctl .
docker build --pull -t $(REGISTRY)/etcd-empty-dir-cleanup:$(TAG) .
rm -rf etcdctl etcd-v$(ETCD_VERSION)-linux-amd64 etcd-v$(ETCD_VERSION)-linux-amd64.tar.gz
push: build
docker tag $(REGISTRY)/etcd-empty-dir-cleanup:$(TAG) $(PUSH_REGISTRY)/etcd-empty-dir-cleanup:$(TAG)
docker push $(PUSH_REGISTRY)/etcd-empty-dir-cleanup:$(TAG)

View File

@@ -0,0 +1,37 @@
#!/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.
echo "Removing empty directories from etcd..."
cleanup_empty_dirs () {
if [ "$(${ETCDCTL} ls $1)" ]; then
for SUBDIR in $(${ETCDCTL} ls -p $1 | grep "/$")
do
cleanup_empty_dirs ${SUBDIR}
done
else
echo "Removing empty key $1 ..."
${ETCDCTL} rmdir $1
fi
}
while true
do
echo "Starting cleanup..."
cleanup_empty_dirs "/registry"
echo "Done with cleanup."
sleep ${SLEEP_SECOND}
done

View File

@@ -0,0 +1,40 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
go_binary(
name = "etcd-version-monitor",
embed = [":go_default_library"],
)
go_library(
name = "go_default_library",
srcs = ["etcd-version-monitor.go"],
importpath = "k8s.io/kubernetes/cluster/images/etcd-version-monitor",
deps = [
"//vendor/github.com/gogo/protobuf/proto:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
"//vendor/github.com/prometheus/client_golang/prometheus/promhttp:go_default_library",
"//vendor/github.com/prometheus/client_model/go:go_default_library",
"//vendor/github.com/prometheus/common/expfmt:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@@ -0,0 +1,20 @@
# Copyright 2017 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.
FROM scratch
LABEL maintainer "Shyam JVS <shyamjvs@google.com>"
COPY etcd-version-monitor /etcd-version-monitor
EXPOSE 9101

View File

@@ -0,0 +1,47 @@
# Copyright 2017 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-version-monitor image
#
# Usage:
# [GOLANG_VERSION=1.8.3] [REGISTRY=staging-k8s.gcr.io] [TAG=test] make (build|push)
# TODO(shyamjvs): Support architectures other than amd64 if needed.
ARCH:=amd64
GOLANG_VERSION?=1.8.3
REGISTRY?=staging-k8s.gcr.io
TAG?=0.1.2
IMAGE:=$(REGISTRY)/etcd-version-monitor:$(TAG)
CURRENT_DIR:=$(pwd)
TEMP_DIR:=$(shell mktemp -d)
build:
# Copy the necessary files for building the image to TEMP_DIR.
cp etcd-version-monitor.go Dockerfile $(TEMP_DIR)
# Compile etcd-version-monitor.
docker run -it \
-v $(shell pwd)/../../../:/go/src/k8s.io/kubernetes \
-v $(TEMP_DIR):/build \
-e GOARCH=$(ARCH) \
golang:$(GOLANG_VERSION) \
/bin/bash -c "CGO_ENABLED=0 go build -o /build/etcd-version-monitor k8s.io/kubernetes/cluster/images/etcd-version-monitor"
docker build -t $(IMAGE) $(TEMP_DIR)
push: build
docker push $(IMAGE)
all: build
.PHONY: build push

View File

@@ -0,0 +1,33 @@
# etcd-version-monitor
This is a tool for exporting etcd metrics and supplementing them with etcd
server binary version and cluster version. These metrics are in
prometheus format and can be scraped by a prometheus server.
The metrics are exposed at the http://localhost:9101/metrics endpoint.
For etcd 3.1+, the
[go-grpc-prometheus](https://github.com/grpc-ecosystem/go-grpc-prometheus)
metrics format, which backward incompatibly replaces the 3.0 legacy grpc metric
format, is exposed in both the 3.1 format and in the 3.0. This preserves
backward compatibility.
For etcd 3.1+, the `--metrics=extensive` must be set on etcd for grpc request
latency metrics (`etcd_grpc_unary_requests_duration_seconds`) to be exposed.
**RUNNING THE TOOL**
To run this tool as a docker container:
- make build
- docker run --net=host -i -t k8s.gcr.io/etcd-version-monitor:test /etcd-version-monitor --logtostderr
To run this as a pod on the kubernetes cluster:
- Place the 'etcd-version-monitor.yaml' in the manifests directory of
kubelet on the master machine.
*Note*: This tool has to run on the same machine as etcd, as communication
with etcd is over localhost.
**VERIFYING THE TOOL**
- Goto [http://localhost:9101/metrics](http://localhost:9101/metrics) in order to view the exported metrics.
- The metrics prefixed with "etcd_" are the ones of interest to us.

View File

@@ -0,0 +1,405 @@
/*
Copyright 2017 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"
"encoding/json"
goflag "flag"
"fmt"
"net/http"
"time"
"github.com/gogo/protobuf/proto"
"github.com/golang/glog"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"github.com/spf13/pflag"
)
// Initialize the prometheus instrumentation and client related flags.
var (
listenAddress string
metricsPath string
etcdVersionScrapeURI string
etcdMetricsScrapeURI string
scrapeTimeout time.Duration
)
func registerFlags(fs *pflag.FlagSet) {
fs.StringVar(&listenAddress, "listen-address", "localhost:9101", "Address to listen on for serving prometheus metrics")
fs.StringVar(&metricsPath, "metrics-path", "/metrics", "Path under which prometheus metrics are to be served")
fs.StringVar(&etcdVersionScrapeURI, "etcd-version-scrape-uri", "http://localhost:2379/version", "URI to scrape etcd version info")
fs.StringVar(&etcdMetricsScrapeURI, "etcd-metrics-scrape-uri", "http://localhost:2379/metrics", "URI to scrape etcd metrics")
fs.DurationVar(&scrapeTimeout, "scrape-timeout", 15*time.Second, "Timeout for trying to get stats from etcd")
}
const (
namespace = "etcd" // For prefixing prometheus metrics
)
// Initialize prometheus metrics to be exported.
var (
// Register all custom metrics with a dedicated registry to keep them separate.
customMetricRegistry = prometheus.NewRegistry()
// Custom etcd version metric since etcd 3.2- does not export one.
// This will be replaced by https://github.com/coreos/etcd/pull/8960 in etcd 3.3.
etcdVersion = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: "version_info",
Help: "Etcd server's binary version",
},
[]string{"binary_version"})
gatherer = &monitorGatherer{
// Rewrite rules for etcd metrics that are exported by default.
exported: map[string]*exportedMetric{
// etcd 3.0 metric format for total grpc requests with renamed method and service labels.
"etcd_grpc_requests_total": {
rewriters: []rewriteFunc{
func(mf *dto.MetricFamily) (*dto.MetricFamily, error) {
mf = deepCopyMetricFamily(mf)
renameLabels(mf, map[string]string{
"grpc_method": "method",
"grpc_service": "service",
})
return mf, nil
},
},
},
// etcd 3.1+ metric format for total grpc requests.
"grpc_server_handled_total": {
rewriters: []rewriteFunc{
// Export the metric exactly as-is. For 3.1+ metrics, we will
// pass all metrics directly through.
identity,
// Write to the etcd 3.0 metric format for backward compatibility.
func(mf *dto.MetricFamily) (*dto.MetricFamily, error) {
mf = deepCopyMetricFamily(mf)
renameMetric(mf, "etcd_grpc_requests_total")
renameLabels(mf, map[string]string{
"grpc_method": "method",
"grpc_service": "service",
})
filterMetricsByLabels(mf, map[string]string{
"grpc_type": "unary",
})
groupCounterMetricsByLabels(mf, map[string]bool{
"grpc_type": true,
"grpc_code": true,
})
return mf, nil
},
},
},
// etcd 3.0 metric format for grpc request latencies,
// rewritten to the etcd 3.1+ format.
"etcd_grpc_unary_requests_duration_seconds": {
rewriters: []rewriteFunc{
func(mf *dto.MetricFamily) (*dto.MetricFamily, error) {
mf = deepCopyMetricFamily(mf)
renameMetric(mf, "grpc_server_handling_seconds")
tpeName := "grpc_type"
tpeVal := "unary"
for _, m := range mf.Metric {
m.Label = append(m.Label, &dto.LabelPair{Name: &tpeName, Value: &tpeVal})
}
return mf, nil
},
},
},
// etcd 3.1+ metric format for total grpc requests.
"grpc_server_handling_seconds": {},
},
}
)
// monitorGatherer is a custom metric gatherer for prometheus that exports custom metrics
// defined by this monitor as well as rewritten etcd metrics.
type monitorGatherer struct {
exported map[string]*exportedMetric
}
// exportedMetric identifies a metric that is exported and defines how it is rewritten before
// it is exported.
type exportedMetric struct {
rewriters []rewriteFunc
}
// rewriteFunc rewrites metrics before they are exported.
type rewriteFunc func(mf *dto.MetricFamily) (*dto.MetricFamily, error)
func (m *monitorGatherer) Gather() ([]*dto.MetricFamily, error) {
etcdMetrics, err := scrapeMetrics()
if err != nil {
return nil, err
}
exported, err := m.rewriteExportedMetrics(etcdMetrics)
if err != nil {
return nil, err
}
custom, err := customMetricRegistry.Gather()
if err != nil {
return nil, err
}
result := make([]*dto.MetricFamily, 0, len(exported)+len(custom))
result = append(result, exported...)
result = append(result, custom...)
return result, nil
}
func (m *monitorGatherer) rewriteExportedMetrics(metrics map[string]*dto.MetricFamily) ([]*dto.MetricFamily, error) {
results := make([]*dto.MetricFamily, 0, len(metrics))
for n, mf := range metrics {
if e, ok := m.exported[n]; ok {
// Apply rewrite rules for metrics that have them.
if e.rewriters == nil {
results = append(results, mf)
} else {
for _, rewriter := range e.rewriters {
new, err := rewriter(mf)
if err != nil {
return nil, err
}
results = append(results, new)
}
}
} else {
// Proxy all metrics without any rewrite rules directly.
results = append(results, mf)
}
}
return results, nil
}
// Struct for unmarshalling the json response from etcd's /version endpoint.
type EtcdVersion struct {
BinaryVersion string `json:"etcdserver"`
ClusterVersion string `json:"etcdcluster"`
}
// Function for fetching etcd version info and feeding it to the prometheus metric.
func getVersion(lastSeenBinaryVersion *string) error {
// Create the get request for the etcd version endpoint.
req, err := http.NewRequest("GET", etcdVersionScrapeURI, nil)
if err != nil {
return fmt.Errorf("Failed to create GET request for etcd version: %v", err)
}
// Send the get request and receive a response.
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("Failed to receive GET response for etcd version: %v", err)
}
defer resp.Body.Close()
// Obtain EtcdVersion from the JSON response.
var version EtcdVersion
if err := json.NewDecoder(resp.Body).Decode(&version); err != nil {
return fmt.Errorf("Failed to decode etcd version JSON: %v", err)
}
// Return without updating the version if it stayed the same since last time.
if *lastSeenBinaryVersion == version.BinaryVersion {
return nil
}
// Delete the metric for the previous version.
if *lastSeenBinaryVersion != "" {
deleted := etcdVersion.Delete(prometheus.Labels{"binary_version": *lastSeenBinaryVersion})
if !deleted {
return fmt.Errorf("Failed to delete previous version's metric")
}
}
// Record the new version in a metric.
etcdVersion.With(prometheus.Labels{
"binary_version": version.BinaryVersion,
}).Set(0)
*lastSeenBinaryVersion = version.BinaryVersion
return nil
}
// Periodically fetches etcd version info.
func getVersionPeriodically(stopCh <-chan struct{}) {
lastSeenBinaryVersion := ""
for {
if err := getVersion(&lastSeenBinaryVersion); err != nil {
glog.Errorf("Failed to fetch etcd version: %v", err)
}
select {
case <-stopCh:
break
case <-time.After(scrapeTimeout):
}
}
}
// scrapeMetrics scrapes the prometheus metrics from the etcd metrics URI.
func scrapeMetrics() (map[string]*dto.MetricFamily, error) {
req, err := http.NewRequest("GET", etcdMetricsScrapeURI, nil)
if err != nil {
return nil, fmt.Errorf("Failed to create GET request for etcd metrics: %v", err)
}
// Send the get request and receive a response.
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("Failed to receive GET response for etcd metrics: %v", err)
}
defer resp.Body.Close()
// Parse the metrics in text format to a MetricFamily struct.
var textParser expfmt.TextParser
return textParser.TextToMetricFamilies(resp.Body)
}
func renameMetric(mf *dto.MetricFamily, name string) {
mf.Name = &name
}
func renameLabels(mf *dto.MetricFamily, nameMapping map[string]string) {
for _, m := range mf.Metric {
for _, lbl := range m.Label {
if alias, ok := nameMapping[*lbl.Name]; ok {
lbl.Name = &alias
}
}
}
}
func filterMetricsByLabels(mf *dto.MetricFamily, labelValues map[string]string) {
buf := mf.Metric[:0]
for _, m := range mf.Metric {
shouldRemove := false
for _, lbl := range m.Label {
if val, ok := labelValues[*lbl.Name]; ok && val != *lbl.Value {
shouldRemove = true
break
}
}
if !shouldRemove {
buf = append(buf, m)
}
}
mf.Metric = buf
}
func groupCounterMetricsByLabels(mf *dto.MetricFamily, names map[string]bool) {
buf := mf.Metric[:0]
deleteLabels(mf, names)
byLabels := map[string]*dto.Metric{}
for _, m := range mf.Metric {
if metric, ok := byLabels[labelsKey(m.Label)]; ok {
metric.Counter.Value = proto.Float64(*metric.Counter.Value + *m.Counter.Value)
} else {
byLabels[labelsKey(m.Label)] = m
buf = append(buf, m)
}
}
mf.Metric = buf
}
func labelsKey(lbls []*dto.LabelPair) string {
var buf bytes.Buffer
for i, lbl := range lbls {
buf.WriteString(lbl.String())
if i < len(lbls)-1 {
buf.WriteString(",")
}
}
return buf.String()
}
func deleteLabels(mf *dto.MetricFamily, names map[string]bool) {
for _, m := range mf.Metric {
buf := m.Label[:0]
for _, lbl := range m.Label {
shouldRemove := names[*lbl.Name]
if !shouldRemove {
buf = append(buf, lbl)
}
}
m.Label = buf
}
}
func identity(mf *dto.MetricFamily) (*dto.MetricFamily, error) {
return mf, nil
}
func deepCopyMetricFamily(mf *dto.MetricFamily) *dto.MetricFamily {
r := &dto.MetricFamily{}
r.Name = mf.Name
r.Help = mf.Help
r.Type = mf.Type
r.Metric = make([]*dto.Metric, len(mf.Metric))
for i, m := range mf.Metric {
r.Metric[i] = deepCopyMetric(m)
}
return r
}
func deepCopyMetric(m *dto.Metric) *dto.Metric {
r := &dto.Metric{}
r.Label = make([]*dto.LabelPair, len(m.Label))
for i, lp := range m.Label {
r.Label[i] = deepCopyLabelPair(lp)
}
r.Gauge = m.Gauge
r.Counter = m.Counter
r.Summary = m.Summary
r.Untyped = m.Untyped
r.Histogram = m.Histogram
r.TimestampMs = m.TimestampMs
return r
}
func deepCopyLabelPair(lp *dto.LabelPair) *dto.LabelPair {
r := &dto.LabelPair{}
r.Name = lp.Name
r.Value = lp.Value
return r
}
func main() {
// Register the commandline flags passed to the tool.
registerFlags(pflag.CommandLine)
pflag.CommandLine.AddGoFlagSet(goflag.CommandLine)
pflag.Parse()
// Register the metrics we defined above with prometheus.
customMetricRegistry.MustRegister(etcdVersion)
customMetricRegistry.Unregister(prometheus.NewGoCollector())
// Spawn threads for periodically scraping etcd version metrics.
stopCh := make(chan struct{})
defer close(stopCh)
go getVersionPeriodically(stopCh)
// Serve our metrics on listenAddress/metricsPath.
glog.Infof("Listening on: %v", listenAddress)
http.Handle(metricsPath, promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{}))
glog.Errorf("Stopped listening/serving metrics: %v", http.ListenAndServe(listenAddress, nil))
}

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Pod
metadata:
name: etcd-version-monitor
namespace: kube-system
spec:
hostNetwork: true
containers:
- name: etcd-version-monitor
image: k8s.gcr.io/etcd-version-monitor:0.1.2
command:
- /etcd-version-monitor
- --logtostderr

View File

@@ -0,0 +1,19 @@
# 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.
FROM BASEIMAGE
EXPOSE 2379 2380 4001 7001
COPY etcd* etcdctl* /usr/local/bin/
COPY migrate-if-needed.sh migrate /usr/local/bin/

150
vendor/k8s.io/kubernetes/cluster/images/etcd/Makefile generated vendored Normal file
View File

@@ -0,0 +1,150 @@
# 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=2.2.1 2.3.7 3.0.17 3.1.12 3.2.18] [REGISTRY=k8s.gcr.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?=2.2.1 2.3.7 3.0.17 3.1.12 3.2.18
# LATEST_ETCD_VERSION identifies the most recent etcd version available.
LATEST_ETCD_VERSION?=3.2.18
# REVISION provides a version number fo 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 k8s.gcr.io/etcd docker image with a tag of the form "<etcd-version>-<revision>".
IMAGE_TAG=$(LATEST_ETCD_VERSION)-$(REVISION)
ARCH?=amd64
# Image should be pulled from k8s.gcr.io, which will auto-detect
# region (us, eu, asia, ...) and pull from the closest.
REGISTRY?=k8s.gcr.io
# Images should be pushed to staging-k8s.gcr.io.
PUSH_REGISTRY?=staging-k8s.gcr.io
# golang version should match the golang version from https://github.com/coreos/etcd/releases for the current ETCD_VERSION.
GOLANG_VERSION?=1.8.7
GOARM=7
TEMP_DIR:=$(shell mktemp -d)
ifeq ($(ARCH),amd64)
BASEIMAGE?=busybox
endif
ifeq ($(ARCH),arm)
BASEIMAGE?=arm32v7/busybox
endif
ifeq ($(ARCH),arm64)
BASEIMAGE?=arm64v8/busybox
endif
ifeq ($(ARCH),ppc64le)
BASEIMAGE?=ppc64le/busybox
endif
ifeq ($(ARCH),s390x)
BASEIMAGE?=s390x/busybox
endif
build:
# Copy the content in this dir to the temp dir,
# without copying the subdirectories.
find ./ -maxdepth 1 -type f | xargs -I {} cp {} $(TEMP_DIR)
# Compile migrate
docker run --interactive -v $(shell pwd)/../../../:/go/src/k8s.io/kubernetes -v $(TEMP_DIR):/build -e GOARCH=$(ARCH) golang:$(GOLANG_VERSION) \
/bin/bash -c "CGO_ENABLED=0 go build -o /build/migrate k8s.io/kubernetes/cluster/images/etcd/migrate"
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.
for version in $(BUNDLED_ETCD_VERSIONS); do \
etcd_release_tmp_dir=$(shell mktemp -d); \
curl -sSL --retry 5 https://github.com/coreos/etcd/releases/download/v$$version/etcd-v$$version-linux-amd64.tar.gz | tar -xz -C $$etcd_release_tmp_dir --strip-components=1; \
cp $$etcd_release_tmp_dir/etcd $$etcd_release_tmp_dir/etcdctl $(TEMP_DIR)/; \
cp $(TEMP_DIR)/etcd $(TEMP_DIR)/etcd-$$version; \
cp $(TEMP_DIR)/etcdctl $(TEMP_DIR)/etcdctl-$$version; \
done
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.
for version in $(BUNDLED_ETCD_VERSIONS); do \
etcd_release_tmp_dir=$(shell mktemp -d); \
docker run --interactive -v $${etcd_release_tmp_dir}:/etcdbin golang:$(GOLANG_VERSION) /bin/bash -c \
"git clone https://github.com/coreos/etcd /go/src/github.com/coreos/etcd \
&& cd /go/src/github.com/coreos/etcd \
&& git checkout v$${version} \
&& GOARM=$(GOARM) GOARCH=$(ARCH) ./build \
&& cp -f bin/$(ARCH)/etcd* bin/etcd* /etcdbin; echo 'done'"; \
cp $$etcd_release_tmp_dir/etcd $$etcd_release_tmp_dir/etcdctl $(TEMP_DIR)/; \
cp $(TEMP_DIR)/etcd $(TEMP_DIR)/etcd-$$version; \
cp $(TEMP_DIR)/etcdctl $(TEMP_DIR)/etcdctl-$$version; \
done
# Add this ENV variable in order to workaround an unsupported arch blocker
# The multiarch feature is in an limited and experimental state right now, and etcd should work fine on arm64
# On arm (which is 32-bit), it can't handle >1GB data in-memory, but it is very unlikely someone tinkering with their limited arm devices would reach such a high usage
# ppc64le is still quite untested, but compiles and is probably in the process of being validated by IBM.
cd $(TEMP_DIR) && echo "ENV ETCD_UNSUPPORTED_ARCH=$(ARCH)" >> Dockerfile
endif
# Replace BASEIMAGE with the real base image
cd $(TEMP_DIR) && sed -i.bak 's|BASEIMAGE|$(BASEIMAGE)|g' Dockerfile
# And build the image
docker build --pull -t $(REGISTRY)/etcd-$(ARCH):$(IMAGE_TAG) $(TEMP_DIR)
push: build
docker tag $(REGISTRY)/etcd-$(ARCH):$(IMAGE_TAG) $(PUSH_REGISTRY)/etcd-$(ARCH):$(IMAGE_TAG)
docker push $(PUSH_REGISTRY)/etcd-$(ARCH):$(IMAGE_TAG)
ifeq ($(ARCH),amd64)
# Backward compatibility. TODO: deprecate this image tag
docker tag $(REGISTRY)/etcd-$(ARCH):$(IMAGE_TAG) $(PUSH_REGISTRY)/etcd:$(IMAGE_TAG)
docker push $(PUSH_REGISTRY)/etcd:$(IMAGE_TAG)
endif
unit-test:
docker run --interactive -v $(shell pwd)/../../../:/go/src/k8s.io/kubernetes -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 `k8s.gcr.io/etcd` image (`/usr/local/bin/etcd-<version>`, ...).
# Since the `k8s.gcr.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
cd $(TEMP_DIR)_integration_test && sed -i.bak 's|BASEIMAGE|golang:$(GOLANG_VERSION)|g' Dockerfile
docker build --pull -t etcd-integration-test $(TEMP_DIR)_integration_test
integration-test:
docker run --interactive -v $(shell pwd)/../../../:/go/src/k8s.io/kubernetes -e GOARCH=$(ARCH) etcd-integration-test \
/bin/bash -c "CGO_ENABLED=0 go test -tags=integration -v 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: build test
.PHONY: build push unit-test build-integration-test-image integration-test integration-build-test test

86
vendor/k8s.io/kubernetes/cluster/images/etcd/README.md generated vendored Normal file
View File

@@ -0,0 +1,86 @@
### k8s.gcr.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 `k8s.gcr.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 `k8s.gcr.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.
`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.2.11" )
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 and from 3.0 to 2.3.7 are 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
`k8s.gcr.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 `busybox` images serve as base images.
#### 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 for linux/amd64 (default)
$ make push ARCH=amd64
# ---> staging-k8s.gcr.io/etcd-amd64:TAG
# ---> staging-k8s.gcr.io/etcd:TAG
$ make push ARCH=arm
# ---> staging-k8s.gcr.io/etcd-arm:TAG
$ make push ARCH=arm64
# ---> staging-k8s.gcr.io/etcd-arm64:TAG
$ make push ARCH=ppc64le
# ---> staging-k8s.gcr.io/etcd-ppc64le:TAG
$ make push ARCH=s390x
# ---> staging-k8s.gcr.io/etcd-s390x:TAG
```
If you don't want to push the images, run `make` or `make build` instead
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/cluster/images/etcd/README.md?pixel)]()

View File

@@ -0,0 +1,105 @@
#!/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.
# NOTES
# This script performs etcd upgrade based on the following environmental
# variables:
# TARGET_STORAGE - API of etcd to be used (supported: 'etcd2', 'etcd3')
# TARGET_VERSION - etcd release to be used (supported: '2.2.1', '2.3.7', '3.0.17', '3.1.12', '3.2.18')
# DATA_DIRECTORY - directory with etcd data
#
# The current etcd version and storage format is detected based on the
# contents of "${DATA_DIRECTORY}/version.txt" file (if the file doesn't
# exist, we default it to "2.2.1/etcd2".
#
# The update workflow support the following upgrade steps:
# - 2.2.1/etcd2 -> 2.3.7/etcd2
# - 2.3.7/etcd2 -> 3.0.17/etcd2
# - 3.0.17/etcd3 -> 3.1.12/etcd3
# - 3.1.12/etcd3 -> 3.2.18/etcd3
#
# NOTE: The releases supported in this script has to match release binaries
# present in the etcd image (to make this script work correctly).
#
# Based on the current etcd version and storage format we detect what
# upgrade step from this should be done to get reach target configuration
set -o errexit
set -o nounset
# NOTE: BUNDLED_VERSION has to match release binaries present in the
# etcd image (to make this script work correctly).
BUNDLED_VERSIONS="2.2.1, 2.3.7, 3.0.17, 3.1.12, 3.2.18"
ETCD_NAME="${ETCD_NAME:-etcd-$(hostname)}"
if [ -z "${DATA_DIRECTORY:-}" ]; then
echo "DATA_DIRECTORY variable unset - unexpected failure"
exit 1
fi
case "${DATA_DIRECTORY}" in
*event*)
ETCD_PEER_PORT=2381
ETCD_CLIENT_PORT=18631
;;
*)
ETCD_PEER_PORT=2380
ETCD_CLIENT_PORT=18629
;;
esac
if [ -z "${INITIAL_CLUSTER:-}" ]; then
echo "Warn: INITIAL_CLUSTER variable unset - defaulting to ${ETCD_NAME}=http://localhost:${ETCD_PEER_PORT}"
INITIAL_CLUSTER="${ETCD_NAME}=http://localhost:${ETCD_PEER_PORT}"
fi
if [ -z "${LISTEN_PEER_URLS:-}" ]; then
echo "Warn: LISTEN_PEER_URLS variable unset - defaulting to http://localhost:${ETCD_PEER_PORT}"
LISTEN_PEER_URLS="http://localhost:${ETCD_PEER_PORT}"
fi
if [ -z "${INITIAL_ADVERTISE_PEER_URLS:-}" ]; then
echo "Warn: INITIAL_ADVERTISE_PEER_URLS variable unset - defaulting to http://localhost:${ETCD_PEER_PORT}"
INITIAL_ADVERTISE_PEER_URLS="http://localhost:${ETCD_PEER_PORT}"
fi
if [ -z "${TARGET_VERSION:-}" ]; then
echo "TARGET_VERSION variable unset - unexpected failure"
exit 1
fi
if [ -z "${TARGET_STORAGE:-}" ]; then
echo "TARGET_STORAGE variable unset - unexpected failure"
exit 1
fi
ETCD_DATA_PREFIX="${ETCD_DATA_PREFIX:-/registry}"
ETCD_CREDS="${ETCD_CREDS:-}"
# Correctly support upgrade and rollback to non-default version.
if [ "${DO_NOT_MOVE_BINARIES:-}" != "true" ]; then
cp "/usr/local/bin/etcd-${TARGET_VERSION}" "/usr/local/bin/etcd"
cp "/usr/local/bin/etcdctl-${TARGET_VERSION}" "/usr/local/bin/etcdctl"
fi
/usr/local/bin/migrate \
--name "${ETCD_NAME}" \
--port "${ETCD_CLIENT_PORT}" \
--listen-peer-urls "${LISTEN_PEER_URLS}" \
--initial-advertise-peer-urls "${INITIAL_ADVERTISE_PEER_URLS}" \
--data-dir "${DATA_DIRECTORY}" \
--bundled-versions "${BUNDLED_VERSIONS}" \
--initial-cluster "${INITIAL_CLUSTER}" \
--target-version "${TARGET_VERSION}" \
--target-storage "${TARGET_STORAGE}" \
--etcd-data-prefix "${ETCD_DATA_PREFIX}" \
--ttl-keys-directory "${TTL_KEYS_DIRECTORY:-${ETCD_DATA_PREFIX}/events}" \
--etcd-server-extra-args "${ETCD_CREDS}"

View File

@@ -0,0 +1,72 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
)
go_binary(
name = "migrate",
embed = [":go_default_library"],
)
go_library(
name = "go_default_library",
srcs = [
"data_dir.go",
"migrate.go",
"migrate_client.go",
"migrate_server.go",
"migrator.go",
"rollback_v2.go",
"versions.go",
],
importpath = "k8s.io/kubernetes/cluster/images/etcd/migrate",
deps = [
"//third_party/forked/etcd221/wal:go_default_library",
"//vendor/github.com/blang/semver:go_default_library",
"//vendor/github.com/coreos/etcd/client:go_default_library",
"//vendor/github.com/coreos/etcd/clientv3:go_default_library",
"//vendor/github.com/coreos/etcd/etcdserver:go_default_library",
"//vendor/github.com/coreos/etcd/etcdserver/etcdserverpb:go_default_library",
"//vendor/github.com/coreos/etcd/etcdserver/membership:go_default_library",
"//vendor/github.com/coreos/etcd/mvcc/backend:go_default_library",
"//vendor/github.com/coreos/etcd/mvcc/mvccpb:go_default_library",
"//vendor/github.com/coreos/etcd/pkg/pbutil:go_default_library",
"//vendor/github.com/coreos/etcd/pkg/types:go_default_library",
"//vendor/github.com/coreos/etcd/raft/raftpb:go_default_library",
"//vendor/github.com/coreos/etcd/snap:go_default_library",
"//vendor/github.com/coreos/etcd/store:go_default_library",
"//vendor/github.com/coreos/etcd/wal:go_default_library",
"//vendor/github.com/coreos/etcd/wal/walpb:go_default_library",
"//vendor/github.com/coreos/go-semver/semver:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = [
"data_dir_test.go",
"versions_test.go",
],
data = glob(["testdata/**"]),
embed = [":go_default_library"],
deps = ["//vendor/github.com/blang/semver:go_default_library"],
)

View File

@@ -0,0 +1,157 @@
/*
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"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/golang/glog"
)
// 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 verion 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 {
glog.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 {
glog.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 = exec.Command("cp", "-r", d.path, backupDir).Run()
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 verions
// of the data in the directory.
type VersionFile struct {
path string
}
// Exists returns true if a version.txt file exists on the filesystem.
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 := ioutil.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
}
// Write creates or overwrites the contents of the version.txt file with the given EtcdVersionPair.
func (v *VersionFile) Write(vp *EtcdVersionPair) error {
data := []byte(fmt.Sprintf("%s/%s", vp.version, vp.storageVersion))
return ioutil.WriteFile(v.path, data, 0666)
}
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

@@ -0,0 +1,159 @@
/*
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 (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/blang/semver"
)
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("Data directory is non-empty")
}
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)
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("Data directory is empty")
}
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("Data directory is non-empty")
}
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)
d, err := OpenOrCreateDataDirectory(filepath.Join(path, "data-dir"))
if err != nil {
t.Fatalf("Failed to open data dir: %v", 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 afer Backup()")
}
}
func newTestPath(t *testing.T) string {
path, err := ioutil.TempDir("", "etcd-migrate-test-")
os.Chmod(path, 0777)
if err != nil {
t.Fatalf("Failed to create tmp dir for test: %v", err)
}
return path
}

View File

@@ -0,0 +1,356 @@
// +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"
"fmt"
"io/ioutil"
"math/big"
"net"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/blang/semver"
)
var (
testSupportedVersions = MustParseSupportedVersions("2.2.1, 2.3.7, 3.0.17, 3.1.12")
testVersionOldest = &EtcdVersion{semver.MustParse("2.2.1")}
testVersionPrevious = &EtcdVersion{semver.MustParse("3.0.17")}
testVersionLatest = &EtcdVersion{semver.MustParse("3.1.12")}
)
func TestMigrate(t *testing.T) {
migrations := []struct {
title string
memberCount int
startVersion string
endVersion string
protocol string
}{
// upgrades
{"v2-v3-up", 1, "2.2.1/etcd2", "3.0.17/etcd3", "https"},
{"v3-v3-up", 1, "3.0.17/etcd3", "3.1.12/etcd3", "https"},
{"oldest-newest-up", 1, "2.2.1/etcd2", "3.1.12/etcd3", "https"},
// warning: v2->v3 ha upgrades not currently supported.
{"ha-v3-v3-up", 3, "3.0.17/etcd3", "3.1.12/etcd3", "https"},
// downgrades
{"v3-v2-down", 1, "3.0.17/etcd3", "2.2.1/etcd2", "https"},
{"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)
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) []*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,
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 = ioutil.WriteFile(certFileName, certPem, os.FileMode(0777))
if err != nil {
return err
}
os.MkdirAll(filepath.Dir(keyFileName), os.FileMode(0777))
err = ioutil.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, net.ParseIP(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 := net.ParseIP(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
}

View File

@@ -0,0 +1,188 @@
/*
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"
"github.com/golang/glog"
"github.com/spf13/cobra"
)
const (
versionFilename = "version.txt"
defaultPort uint64 = 18629
)
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{}
)
type migrateOpts struct {
name string
port uint64
peerListenUrls string
peerAdvertiseUrls string
binDir string
dataDir string
bundledVersionString string
etcdDataPrefix string
ttlKeysDirectory string
initialCluster string
targetVersion string
targetStorage string
etcdServerArgs string
}
func main() {
flags := migrateCmd.Flags()
flags.StringVar(&opts.name, "name", "", "etcd cluster member name. Defaults to etcd-{hostname}")
flags.Uint64Var(&opts.port, "port", defaultPort, "etcd client port to use during migration operations. This should be a different port than typically used by etcd to avoid clients accidentally connecting during upgrade/downgrade operations.")
flags.StringVar(&opts.peerListenUrls, "listen-peer-urls", "", "etcd --listen-peer-urls flag, required for HA clusters")
flags.StringVar(&opts.peerAdvertiseUrls, "initial-advertise-peer-urls", "", "etcd --initial-advertise-peer-urls flag, required for HA clusters")
flags.StringVar(&opts.binDir, "bin-dir", "/usr/local/bin", "directory of etcd and etcdctl binaries, must contain etcd-<version> and etcdctl-<version> for each version listed in bindled-versions")
flags.StringVar(&opts.dataDir, "data-dir", "", "etcd data directory of etcd server to migrate")
flags.StringVar(&opts.bundledVersionString, "bundled-versions", "", "comma separated list of etcd binary versions present under the bin-dir")
flags.StringVar(&opts.etcdDataPrefix, "etcd-data-prefix", "/registry", "etcd key prefix under which all objects are kept")
flags.StringVar(&opts.ttlKeysDirectory, "ttl-keys-directory", "", "etcd key prefix under which all keys with TTLs are kept. Defaults to {etcd-data-prefix}/events")
flags.StringVar(&opts.initialCluster, "initial-cluster", "", "comma separated list of name=endpoint pairs. Defaults to etcd-{hostname}=http://localhost:2380")
flags.StringVar(&opts.targetVersion, "target-version", "", "version of etcd to migrate to. Format must be '<major>.<minor>.<patch>'")
flags.StringVar(&opts.targetStorage, "target-storage", "", "storage version of etcd to migrate to, one of: etcd2, etcd3")
flags.StringVar(&opts.etcdServerArgs, "etcd-server-extra-args", "", "additional etcd server args for starting etcd servers during migration steps, --peer-* TLS cert flags should be added for etcd clusters with more than 1 member that use mutual TLS for peer communication.")
migrateCmd.Execute()
}
// runMigrate validates the command line flags and starts the migration.
func runMigrate() {
if opts.name == "" {
hostname, err := os.Hostname()
if err != nil {
glog.Errorf("Error while getting hostname to supply default --name: %v", err)
os.Exit(1)
}
opts.name = fmt.Sprintf("etcd-%s", hostname)
}
if opts.ttlKeysDirectory == "" {
opts.ttlKeysDirectory = fmt.Sprintf("%s/events", opts.etcdDataPrefix)
}
if opts.initialCluster == "" {
opts.initialCluster = fmt.Sprintf("%s=http://localhost:2380", opts.name)
}
if opts.targetStorage == "" {
glog.Errorf("--target-storage is required")
os.Exit(1)
}
if opts.targetVersion == "" {
glog.Errorf("--target-version is required")
os.Exit(1)
}
if opts.dataDir == "" {
glog.Errorf("--data-dir is required")
os.Exit(1)
}
if opts.bundledVersionString == "" {
glog.Errorf("--bundled-versions is required")
os.Exit(1)
}
bundledVersions, err := ParseSupportedVersions(opts.bundledVersionString)
if err != nil {
glog.Errorf("Failed to parse --supported-versions: %v", err)
}
err = validateBundledVersions(bundledVersions, opts.binDir)
if err != nil {
glog.Errorf("Failed to validate that 'etcd-<version>' and 'etcdctl-<version>' binaries exist in --bin-dir '%s' for all --bundled-verions '%s': %v",
opts.binDir, opts.bundledVersionString, err)
os.Exit(1)
}
target := &EtcdVersionPair{
version: MustParseEtcdVersion(opts.targetVersion),
storageVersion: MustParseEtcdStorageVersion(opts.targetStorage),
}
migrate(opts.name, opts.port, opts.peerListenUrls, opts.peerAdvertiseUrls, opts.binDir, opts.dataDir, opts.etcdDataPrefix, opts.ttlKeysDirectory, opts.initialCluster, target, bundledVersions, opts.etcdServerArgs)
}
// migrate opens or initializes the etcd data directory, configures the migrator, and starts the migration.
func migrate(name string, port uint64, peerListenUrls string, peerAdvertiseUrls string, binPath string, dataDirPath string, etcdDataPrefix string, ttlKeysDirectory string,
initialCluster string, target *EtcdVersionPair, bundledVersions SupportedVersions, etcdServerArgs string) {
dataDir, err := OpenOrCreateDataDirectory(dataDirPath)
if err != nil {
glog.Errorf("Error opening or creating data directory %s: %v", dataDirPath, err)
os.Exit(1)
}
cfg := &EtcdMigrateCfg{
binPath: binPath,
name: name,
port: port,
peerListenUrls: peerListenUrls,
peerAdvertiseUrls: peerAdvertiseUrls,
etcdDataPrefix: etcdDataPrefix,
ttlKeysDirectory: ttlKeysDirectory,
initialCluster: initialCluster,
supportedVersions: bundledVersions,
dataDirectory: dataDirPath,
etcdServerArgs: etcdServerArgs,
}
client, err := NewEtcdMigrateClient(cfg)
if err != nil {
glog.Errorf("Migration failed: %v", err)
os.Exit(1)
}
defer client.Close()
migrator := &Migrator{cfg, dataDir, client}
err = migrator.MigrateIfNeeded(target)
if err != nil {
glog.Errorf("Migration failed: %v", err)
os.Exit(1)
}
}
// 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

@@ -0,0 +1,223 @@
/*
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"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"context"
clientv2 "github.com/coreos/etcd/client"
"github.com/coreos/etcd/clientv3"
"github.com/golang/glog"
)
// 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 {
if version.Major == 2 {
v2client, err := e.clientV2()
if err != nil {
return err
}
_, err = v2client.Set(context.Background(), key, value, nil)
return err
}
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) {
if version.Major == 2 {
v2client, err := e.clientV2()
if err != nil {
return "", err
}
resp, err := v2client.Get(context.Background(), key, nil)
if err != nil {
return "", err
}
return resp.Node.Value, nil
}
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) clientV2() (clientv2.KeysAPI, error) {
v2client, err := clientv2.New(clientv2.Config{Endpoints: []string{e.endpoint()}})
if err != nil {
return nil, err
}
return clientv2.NewKeysAPI(v2client), nil
}
func (e *CombinedEtcdClient) clientV3() (*clientv3.Client, error) {
return clientv3.New(clientv3.Config{Endpoints: []string{e.endpoint()}})
}
// 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)
}
glog.Infof("Lease with TTL: %v created", lease.TTL)
glog.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 {
glog.Errorf("Error while attaching lease to: %s", string(kv.Key))
}
if bytes.Compare(putResp.PrevKv.Value, kv.Value) != 0 {
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

@@ -0,0 +1,132 @@
/*
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"
"github.com/golang/glog"
)
// 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.
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", fmt.Sprintf("http://127.0.0.1:%d", r.cfg.port),
"--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)
done <- true
}()
for {
select {
case <-interval.C:
err := r.client.SetEtcdVersionKeyValue(version)
if err != nil {
glog.Infof("Still waiting for etcd to start, current error: %v", err)
// keep waiting
} else {
glog.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)
timedout <- true
}()
go func() {
select {
case <-stopped:
return
case <-timedout:
glog.Infof("etcd server has not terminated gracefully after %s, killing it.", gracefulWait)
r.cmd.Process.Kill()
return
}
}()
err = r.cmd.Wait()
stopped <- true
if exiterr, ok := err.(*exec.ExitError); ok {
glog.Infof("etcd server stopped (signal: %s)", exiterr.Error())
// stopped
} else if err != nil {
return fmt.Errorf("error waiting for etcd to stop: %v", err)
}
glog.Infof("Stopped etcd server %s", r.cfg.name)
return nil
}

View File

@@ -0,0 +1,258 @@
/*
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"
"time"
"github.com/blang/semver"
"github.com/golang/glog"
)
// 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
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 {
glog.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 {
glog.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):
glog.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:
glog.Infof("upgrading from etcd2 storage to etcd3 storage")
current, err = m.etcd2ToEtcd3Upgrade(current, target)
case current.version.Major == 3 && target.version.Major == 2:
glog.Infof("downgrading from etcd 3.x to 2.x")
current, err = m.rollbackToEtcd2(current, target)
case current.version.Major == target.version.Major && current.version.Minor < target.version.Minor:
stepVersion := m.cfg.supportedVersions.NextVersionPair(current)
glog.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:
glog.Infof("rolling etcd back from %s to %s", current, target)
current, err = m.rollbackEtcd3MinorVersion(current, target)
}
if err != nil {
return err
}
}
}
func (m *Migrator) backupEtcd2(current *EtcdVersion) error {
backupDir := fmt.Sprintf("%s/%s", m.dataDirectory, "migration-backup")
glog.Infof("Backup etcd before starting migration")
err := os.Mkdir(backupDir, 0666)
if err != nil {
return fmt.Errorf("failed to create backup directory before starting migration: %v", err)
}
m.client.Backup(current, backupDir)
glog.Infof("Backup done in %s", backupDir)
return nil
}
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)
}
glog.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()
glog.Infof("Starting etcd version %s to capture rollback snapshot.", current.version)
err = runner.Start(current.version)
if err != nil {
glog.Fatalf("Unable to automatically downgrade etcd: starting etcd version %s to capture rollback snapshot failed: %v", current.version, err)
return nil, err
}
glog.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
}
glog.Infof("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 = exec.Command("mv", m.dataDirectory.path, backupDir).Run()
if err != nil {
return nil, err
}
glog.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) rollbackToEtcd2(current *EtcdVersionPair, target *EtcdVersionPair) (*EtcdVersionPair, error) {
if !(current.version.Major == 3 && current.version.Minor == 0 && target.version.Major == 2 && target.version.Minor == 2) {
return nil, fmt.Errorf("etcd3 -> etcd2 downgrade is supported only between 3.0.x and 2.2.x, got current %s target %s", current, target)
}
glog.Infof("Backup and remove all existing v2 data")
err := m.dataDirectory.Backup()
if err != nil {
return nil, err
}
err = RollbackV3ToV2(m.dataDirectory.path, time.Hour)
if err != nil {
return nil, fmt.Errorf("rollback to etcd 2.x failed: %v", err)
}
return target, nil
}
func (m *Migrator) etcd2ToEtcd3Upgrade(current *EtcdVersionPair, target *EtcdVersionPair) (*EtcdVersionPair, error) {
if current.storageVersion != storageEtcd2 || target.version.Major != 3 || target.storageVersion != storageEtcd3 {
return nil, fmt.Errorf("etcd2 to etcd3 upgrade is supported only for x.x.x/etcd2 to 3.0.x/etcd3, got current %s target %s", current, target)
}
runner := m.newServer()
glog.Infof("Performing etcd2 -> etcd3 migration")
err := m.client.Migrate(target.version)
if err != nil {
return nil, err
}
glog.Infof("Attaching leases to TTL entries")
// Now attach lease to all keys.
// To do it, we temporarily start etcd on a random port (so that
// apiserver actually cannot access it).
err = runner.Start(target.version)
if err != nil {
return nil, err
}
defer func() {
err = runner.Stop()
}()
// Create a lease and attach all keys to it.
err = m.client.AttachLease(1 * time.Hour)
if err != nil {
return nil, err
}
return target, err
}
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

@@ -0,0 +1,328 @@
/*
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.
*/
package main
import (
"encoding/json"
"os"
"path"
"strconv"
"strings"
"time"
// Uncomment when you want to rollback to 2.2.1 version.
oldwal "k8s.io/kubernetes/third_party/forked/etcd221/wal"
// Uncomment when you want to rollback to 2.3.7 version.
// oldwal "k8s.io/kubernetes/third_party/forked/etcd237/wal"
"github.com/coreos/etcd/etcdserver"
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/etcdserver/membership"
"github.com/coreos/etcd/mvcc/backend"
"github.com/coreos/etcd/mvcc/mvccpb"
"github.com/coreos/etcd/pkg/pbutil"
"github.com/coreos/etcd/pkg/types"
"github.com/coreos/etcd/raft/raftpb"
"github.com/coreos/etcd/snap"
"github.com/coreos/etcd/store"
"github.com/coreos/etcd/wal"
"github.com/coreos/etcd/wal/walpb"
"github.com/coreos/go-semver/semver"
"github.com/golang/glog"
)
const rollbackVersion = "2.2.0"
// RollbackV3ToV2 rolls back an etcd 3.0.x data directory to the 2.x.x version specified by rollbackVersion.
func RollbackV3ToV2(migrateDatadir string, ttl time.Duration) error {
dbpath := path.Join(migrateDatadir, "member", "snap", "db")
glog.Infof("Rolling db file %s back to etcd 2.x", dbpath)
// etcd3 store backend. We will use it to parse v3 data files and extract information.
be := backend.NewDefaultBackend(dbpath)
tx := be.BatchTx()
// etcd2 store backend. We will use v3 data to update this and then save snapshot to disk.
st := store.New(etcdserver.StoreClusterPrefix, etcdserver.StoreKeysPrefix)
expireTime := time.Now().Add(ttl)
tx.Lock()
err := tx.UnsafeForEach([]byte("key"), func(k, v []byte) error {
kv := &mvccpb.KeyValue{}
kv.Unmarshal(v)
// This is compact key.
if !strings.HasPrefix(string(kv.Key), "/") {
return nil
}
ttlOpt := store.TTLOptionSet{}
if kv.Lease != 0 {
ttlOpt = store.TTLOptionSet{ExpireTime: expireTime}
}
if !isTombstone(k) {
sk := path.Join(strings.Trim(etcdserver.StoreKeysPrefix, "/"), string(kv.Key))
_, err := st.Set(sk, false, string(kv.Value), ttlOpt)
if err != nil {
return err
}
} else {
st.Delete(string(kv.Key), false, false)
}
return nil
})
if err != nil {
return err
}
tx.Unlock()
if err := traverseAndDeleteEmptyDir(st, "/"); err != nil {
return err
}
// rebuild cluster state.
metadata, hardstate, oldSt, err := rebuild(migrateDatadir)
if err != nil {
return err
}
// In the following, it's low level logic that saves metadata and data into v2 snapshot.
backupPath := migrateDatadir + ".rollback.backup"
if err := os.Rename(migrateDatadir, backupPath); err != nil {
return err
}
if err := os.MkdirAll(path.Join(migrateDatadir, "member", "snap"), 0777); err != nil {
return err
}
walDir := path.Join(migrateDatadir, "member", "wal")
w, err := oldwal.Create(walDir, metadata)
if err != nil {
return err
}
err = w.SaveSnapshot(walpb.Snapshot{Index: hardstate.Commit, Term: hardstate.Term})
w.Close()
if err != nil {
return err
}
event, err := oldSt.Get(etcdserver.StoreClusterPrefix, true, false)
if err != nil {
return err
}
// nodes (members info) for ConfState
nodes := []uint64{}
traverseMetadata(event.Node, func(n *store.NodeExtern) {
if n.Key != etcdserver.StoreClusterPrefix {
// update store metadata
v := ""
if !n.Dir {
v = *n.Value
}
if n.Key == path.Join(etcdserver.StoreClusterPrefix, "version") {
v = rollbackVersion
}
if _, err := st.Set(n.Key, n.Dir, v, store.TTLOptionSet{}); err != nil {
glog.Error(err)
}
// update nodes
fields := strings.Split(n.Key, "/")
if len(fields) == 4 && fields[2] == "members" {
nodeID, err := strconv.ParseUint(fields[3], 16, 64)
if err != nil {
glog.Fatalf("failed to parse member ID (%s): %v", fields[3], err)
}
nodes = append(nodes, nodeID)
}
}
})
data, err := st.Save()
if err != nil {
return err
}
raftSnap := raftpb.Snapshot{
Data: data,
Metadata: raftpb.SnapshotMetadata{
Index: hardstate.Commit,
Term: hardstate.Term,
ConfState: raftpb.ConfState{
Nodes: nodes,
},
},
}
snapshotter := snap.New(path.Join(migrateDatadir, "member", "snap"))
if err := snapshotter.SaveSnap(raftSnap); err != nil {
return err
}
glog.Infof("Finished successfully")
return nil
}
func traverseMetadata(head *store.NodeExtern, handleFunc func(*store.NodeExtern)) {
q := []*store.NodeExtern{head}
for len(q) > 0 {
n := q[0]
q = q[1:]
handleFunc(n)
for _, next := range n.Nodes {
q = append(q, next)
}
}
}
const (
revBytesLen = 8 + 1 + 8
markedRevBytesLen = revBytesLen + 1
markBytePosition = markedRevBytesLen - 1
markTombstone byte = 't'
)
func isTombstone(b []byte) bool {
return len(b) == markedRevBytesLen && b[markBytePosition] == markTombstone
}
func traverseAndDeleteEmptyDir(st store.Store, dir string) error {
e, err := st.Get(dir, true, false)
if err != nil {
return err
}
if len(e.Node.Nodes) == 0 {
st.Delete(dir, true, true)
return nil
}
for _, node := range e.Node.Nodes {
if !node.Dir {
glog.V(2).Infof("key: %s", node.Key[len(etcdserver.StoreKeysPrefix):])
} else {
err := traverseAndDeleteEmptyDir(st, node.Key)
if err != nil {
return err
}
}
}
return nil
}
func rebuild(datadir string) ([]byte, *raftpb.HardState, store.Store, error) {
waldir := path.Join(datadir, "member", "wal")
snapdir := path.Join(datadir, "member", "snap")
ss := snap.New(snapdir)
snapshot, err := ss.Load()
if err != nil && err != snap.ErrNoSnapshot {
return nil, nil, nil, err
}
var walsnap walpb.Snapshot
if snapshot != nil {
walsnap.Index, walsnap.Term = snapshot.Metadata.Index, snapshot.Metadata.Term
}
w, err := wal.OpenForRead(waldir, walsnap)
if err != nil {
return nil, nil, nil, err
}
defer w.Close()
meta, hardstate, ents, err := w.ReadAll()
if err != nil {
return nil, nil, nil, err
}
st := store.New(etcdserver.StoreClusterPrefix, etcdserver.StoreKeysPrefix)
if snapshot != nil {
err := st.Recovery(snapshot.Data)
if err != nil {
return nil, nil, nil, err
}
}
cluster := membership.NewCluster("")
cluster.SetStore(st)
cluster.Recover(func(*semver.Version) {})
applier := etcdserver.NewApplierV2(st, cluster)
for _, ent := range ents {
if ent.Type == raftpb.EntryConfChange {
var cc raftpb.ConfChange
pbutil.MustUnmarshal(&cc, ent.Data)
switch cc.Type {
case raftpb.ConfChangeAddNode:
m := new(membership.Member)
if err := json.Unmarshal(cc.Context, m); err != nil {
return nil, nil, nil, err
}
cluster.AddMember(m)
case raftpb.ConfChangeRemoveNode:
id := types.ID(cc.NodeID)
cluster.RemoveMember(id)
case raftpb.ConfChangeUpdateNode:
m := new(membership.Member)
if err := json.Unmarshal(cc.Context, m); err != nil {
return nil, nil, nil, err
}
cluster.UpdateRaftAttributes(m.ID, m.RaftAttributes)
}
continue
}
var raftReq pb.InternalRaftRequest
if !pbutil.MaybeUnmarshal(&raftReq, ent.Data) { // backward compatible
var r pb.Request
pbutil.MustUnmarshal(&r, ent.Data)
applyRequest(&r, applier)
} else {
if raftReq.V2 != nil {
req := raftReq.V2
applyRequest(req, applier)
}
}
}
return meta, &hardstate, st, nil
}
func toTTLOptions(r *pb.Request) store.TTLOptionSet {
refresh, _ := pbutil.GetBool(r.Refresh)
ttlOptions := store.TTLOptionSet{Refresh: refresh}
if r.Expiration != 0 {
ttlOptions.ExpireTime = time.Unix(0, r.Expiration)
}
return ttlOptions
}
func applyRequest(r *pb.Request, applyV2 etcdserver.ApplierV2) {
toTTLOptions(r)
switch r.Method {
case "PUT":
applyV2.Put(r)
case "DELETE":
applyV2.Delete(r)
case "POST", "QGET", "SYNC":
return
default:
glog.Fatal("unknown command")
}
}

View File

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

View File

@@ -0,0 +1,198 @@
/*
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"
)
// 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
}
// 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
}
// 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 comma separated list of etcd versions.
func ParseSupportedVersions(s string) (SupportedVersions, error) {
var err error
list := strings.Split(s, ",")
versions := make(SupportedVersions, len(list))
for i, v := range list {
versions[i], err = ParseEtcdVersion(strings.TrimSpace(v))
if err != nil {
return nil, err
}
}
return versions, nil
}
// MustParseSupportedVersions parses a comma separated list of etcd versions or panics if the parse fails.
func MustParseSupportedVersions(s string) SupportedVersions {
versions, err := ParseSupportedVersions(s)
if err != nil {
panic(err)
}
return versions
}

View File

@@ -0,0 +1,84 @@
/*
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"
)
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},
{"2.2.1/etcd2", &EtcdVersionPair{&EtcdVersion{semver.MustParse("2.2.1")}, storageEtcd2}, 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},
{"2.2.2/etcd2", &EtcdVersionPair{&EtcdVersion{semver.MustParse("2.2.1")}, storageEtcd2}, false},
{"2.2.1/etcd3", &EtcdVersionPair{&EtcdVersion{semver.MustParse("2.2.1")}, storageEtcd2}, false},
}
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)
}
}
}

View File

@@ -0,0 +1,46 @@
load("@io_bazel_rules_docker//docker:docker.bzl", "docker_build", "docker_bundle")
docker_build(
name = "hyperkube-internal",
base = "@debian-hyperkube-base-amd64//image",
files = [
"//cmd/hyperkube",
],
symlinks = {
"/%s" % path: "/hyperkube"
for path in [
"/apiserver",
"/controller-manager",
"/kubectl",
"/kubelet",
"/proxy",
"/scheduler",
"/usr/local/bin/kube-apiserver",
"/usr/local/bin/kube-controller-manager",
"/usr/local/bin/kubectl",
"/usr/local/bin/kubelet",
"/usr/local/bin/kube-proxy",
"/usr/local/bin/kube-scheduler",
]
},
)
docker_bundle(
name = "hyperkube",
images = {"k8s.gcr.io/hyperkube-amd64:{STABLE_DOCKER_TAG}": "hyperkube-internal"},
stamp = True,
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,38 @@
# 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.
FROM BASEIMAGE
# Create symlinks for each hyperkube server
# Also create symlinks to /usr/local/bin/ where the server image binaries live, so the hyperkube image may be
# used instead of k8s.gcr.io/kube-* without any modifications.
# TODO: replace manual symlink creation with --make-symlink command once
# cross-building with qemu supports go binaries. See #28702
# RUN /hyperkube --make-symlinks
RUN ln -s /hyperkube /apiserver \
&& ln -s /hyperkube /controller-manager \
&& ln -s /hyperkube /kubectl \
&& ln -s /hyperkube /kubelet \
&& ln -s /hyperkube /proxy \
&& ln -s /hyperkube /scheduler \
&& ln -s /hyperkube /aggregator \
&& ln -s /hyperkube /usr/local/bin/kube-apiserver \
&& ln -s /hyperkube /usr/local/bin/kube-controller-manager \
&& ln -s /hyperkube /usr/local/bin/kubectl \
&& ln -s /hyperkube /usr/local/bin/kubelet \
&& ln -s /hyperkube /usr/local/bin/kube-proxy \
&& ln -s /hyperkube /usr/local/bin/kube-scheduler
# Copy the hyperkube binary
COPY hyperkube /hyperkube

View File

@@ -0,0 +1,55 @@
# 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 hyperkube image.
#
# Usage:
# [ARCH=amd64] [REGISTRY="staging-k8s.gcr.io"] make (build|push) VERSION={some_released_version_of_kubernetes}
REGISTRY?=staging-k8s.gcr.io
ARCH?=amd64
OUT_DIR?=_output
HYPERKUBE_BIN?=$(shell pwd)/../../../$(OUT_DIR)/dockerized/bin/linux/$(ARCH)/hyperkube
BASEIMAGE=k8s.gcr.io/debian-hyperkube-base-$(ARCH):0.10
TEMP_DIR:=$(shell mktemp -d -t hyperkubeXXXXXX)
all: build
build:
ifndef VERSION
$(error VERSION is undefined)
endif
cp -r ./* ${TEMP_DIR}
cp ${HYPERKUBE_BIN} ${TEMP_DIR}
chmod a+rx ${TEMP_DIR}/hyperkube
cd ${TEMP_DIR} && sed -i.back "s|BASEIMAGE|${BASEIMAGE}|g" Dockerfile
# Register /usr/bin/qemu-ARCH-static as the handler for non-x86 binaries in the kernel
docker run --rm --privileged multiarch/qemu-user-static:register --reset
docker build --pull -t ${REGISTRY}/hyperkube-${ARCH}:${VERSION} ${TEMP_DIR}
rm -rf "${TEMP_DIR}"
push: build
docker push ${REGISTRY}/hyperkube-${ARCH}:${VERSION}
ifeq ($(ARCH),amd64)
docker rmi ${REGISTRY}/hyperkube:${VERSION} 2>/dev/null || true
docker tag ${REGISTRY}/hyperkube-${ARCH}:${VERSION} ${REGISTRY}/hyperkube:${VERSION}
docker push ${REGISTRY}/hyperkube:${VERSION}
endif
.PHONY: build push all

View File

@@ -0,0 +1,8 @@
reviewers:
- ixdy
- luxas
- mikedanese
approvers:
- ixdy
- luxas
- mikedanese

View File

@@ -0,0 +1,35 @@
### hyperkube
`hyperkube` is an all-in-one binary for the Kubernetes server components
`hyperkube` is built for multiple architectures and _the image is pushed automatically on every release._
#### How to release by hand
```console
# First, build the binaries
$ build/run.sh make cross
# Build for linux/amd64 (default)
# export REGISTRY=$HOST/$ORG to switch from staging-k8s.gcr.io
$ make push VERSION={target_version} ARCH=amd64
# ---> staging-k8s.gcr.io/hyperkube-amd64:VERSION
# ---> staging-k8s.gcr.io/hyperkube:VERSION (image with backwards-compatible naming)
$ make push VERSION={target_version} ARCH=arm
# ---> staging-k8s.gcr.io/hyperkube-arm:VERSION
$ make push VERSION={target_version} ARCH=arm64
# ---> staging-k8s.gcr.io/hyperkube-arm64:VERSION
$ make push VERSION={target_version} ARCH=ppc64le
# ---> staging-k8s.gcr.io/hyperkube-ppc64le:VERSION
$ make push VERSION={target_version} ARCH=s390x
# ---> staging-k8s.gcr.io/hyperkube-s390x:VERSION
```
If you don't want to push the images, run `make` or `make build` instead
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/cluster/images/hyperkube/README.md?pixel)]()

34
vendor/k8s.io/kubernetes/cluster/images/kubemark/BUILD generated vendored Normal file
View File

@@ -0,0 +1,34 @@
package(default_visibility = ["//visibility:public"])
load("@io_bazel_rules_docker//docker:docker.bzl", "docker_build", "docker_push")
docker_build(
name = "image",
base = "@official_busybox//image",
entrypoint = ["/kubemark"],
files = ["//cmd/kubemark"],
)
docker_push(
name = "push",
image = ":image",
registry = "$(REGISTRY)",
repository = "kubemark",
stamp = True,
tag = "$(IMAGE_TAG)",
tags = ["manual"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,17 @@
# 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.
FROM debian:jessie
COPY kubemark /kubemark

View File

@@ -0,0 +1,36 @@
# 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 Kubemark image from currently built binaries containing both 'real' master and Hollow Node.
# This makefile assumes that the kubemark binary is present in this directory.
# Allow the caller to override this. Beware make's precedence. This:
# REGISTRY=$VAR make
# .. is not the same as:
# make REGISTRY=$VAR
REGISTRY := $(if $(REGISTRY),$(REGISTRY),staging-k8s.gcr.io)
IMAGE_TAG := $(if $(IMAGE_TAG),$(IMAGE_TAG),latest)
all: gcloudpush
build:
docker build --pull -t $(REGISTRY)/kubemark:$(IMAGE_TAG) .
gcloudpush: build
docker push $(REGISTRY)/kubemark:$(IMAGE_TAG)
push: build
docker -- push $(REGISTRY)/kubemark:$(IMAGE_TAG)
.PHONY: all build gcloudpush push

View File

@@ -0,0 +1,8 @@
reviewers:
- gmarek
- shyamjvs
- wojtek-t
approvers:
- gmarek
- shyamjvs
- wojtek-t