1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package controlplane 18 19 import ( 20 "io" 21 "net" 22 "net/url" 23 "strconv" 24 "time" 25 26 "sigs.k8s.io/controller-runtime/pkg/internal/testing/addr" 27 "sigs.k8s.io/controller-runtime/pkg/internal/testing/process" 28 ) 29 30 // Etcd knows how to run an etcd server. 31 type Etcd struct { 32 // URL is the address the Etcd should listen on for client connections. 33 // 34 // If this is not specified, we default to a random free port on localhost. 35 URL *url.URL 36 37 // Path is the path to the etcd binary. 38 // 39 // If this is left as the empty string, we will attempt to locate a binary, 40 // by checking for the TEST_ASSET_ETCD environment variable, and the default 41 // test assets directory. See the "Binaries" section above (in doc.go) for 42 // details. 43 Path string 44 45 // Args is a list of arguments which will passed to the Etcd binary. Before 46 // they are passed on, the`y will be evaluated as go-template strings. This 47 // means you can use fields which are defined and exported on this Etcd 48 // struct (e.g. "--data-dir={{ .Dir }}"). 49 // Those templates will be evaluated after the defaulting of the Etcd's 50 // fields has already happened and just before the binary actually gets 51 // started. Thus you have access to calculated fields like `URL` and others. 52 // 53 // If not specified, the minimal set of arguments to run the Etcd will be 54 // used. 55 // 56 // They will be loaded into the same argument set as Configure. Each flag 57 // will be Append-ed to the configured arguments just before launch. 58 // 59 // Deprecated: use Configure instead. 60 Args []string 61 62 // DataDir is a path to a directory in which etcd can store its state. 63 // 64 // If left unspecified, then the Start() method will create a fresh temporary 65 // directory, and the Stop() method will clean it up. 66 DataDir string 67 68 // StartTimeout, StopTimeout specify the time the Etcd is allowed to 69 // take when starting and stopping before an error is emitted. 70 // 71 // If not specified, these default to 20 seconds. 72 StartTimeout time.Duration 73 StopTimeout time.Duration 74 75 // Out, Err specify where Etcd should write its StdOut, StdErr to. 76 // 77 // If not specified, the output will be discarded. 78 Out io.Writer 79 Err io.Writer 80 81 // processState contains the actual details about this running process 82 processState *process.State 83 84 // args contains the structured arguments to use for running etcd. 85 // Lazily initialized by .Configure(), Defaulted eventually with .defaultArgs() 86 args *process.Arguments 87 88 // listenPeerURL is the address the Etcd should listen on for peer connections. 89 // It's automatically generated and a random port is picked during execution. 90 listenPeerURL *url.URL 91 } 92 93 // Start starts the etcd, waits for it to come up, and returns an error, if one 94 // occurred. 95 func (e *Etcd) Start() error { 96 if err := e.setProcessState(); err != nil { 97 return err 98 } 99 return e.processState.Start(e.Out, e.Err) 100 } 101 102 func (e *Etcd) setProcessState() error { 103 e.processState = &process.State{ 104 Dir: e.DataDir, 105 Path: e.Path, 106 StartTimeout: e.StartTimeout, 107 StopTimeout: e.StopTimeout, 108 } 109 110 // unconditionally re-set this so we can successfully restart 111 // TODO(directxman12): we supported this in the past, but do we actually 112 // want to support re-using an API server object to restart? The loss 113 // of provisioned users is surprising to say the least. 114 if err := e.processState.Init("etcd"); err != nil { 115 return err 116 } 117 118 // Set the listen url. 119 if e.URL == nil { 120 port, host, err := addr.Suggest("") 121 if err != nil { 122 return err 123 } 124 e.URL = &url.URL{ 125 Scheme: "http", 126 Host: net.JoinHostPort(host, strconv.Itoa(port)), 127 } 128 } 129 130 // Set the listen peer URL. 131 { 132 port, host, err := addr.Suggest("") 133 if err != nil { 134 return err 135 } 136 e.listenPeerURL = &url.URL{ 137 Scheme: "http", 138 Host: net.JoinHostPort(host, strconv.Itoa(port)), 139 } 140 } 141 142 // can use /health as of etcd 3.3.0 143 e.processState.HealthCheck.URL = *e.URL 144 e.processState.HealthCheck.Path = "/health" 145 146 e.DataDir = e.processState.Dir 147 e.Path = e.processState.Path 148 e.StartTimeout = e.processState.StartTimeout 149 e.StopTimeout = e.processState.StopTimeout 150 151 var err error 152 e.processState.Args, e.Args, err = process.TemplateAndArguments(e.Args, e.Configure(), process.TemplateDefaults{ //nolint:staticcheck 153 Data: e, 154 Defaults: e.defaultArgs(), 155 }) 156 return err 157 } 158 159 // Stop stops this process gracefully, waits for its termination, and cleans up 160 // the DataDir if necessary. 161 func (e *Etcd) Stop() error { 162 if e.processState.DirNeedsCleaning { 163 e.DataDir = "" // reset the directory if it was randomly allocated, so that we can safely restart 164 } 165 return e.processState.Stop() 166 } 167 168 func (e *Etcd) defaultArgs() map[string][]string { 169 args := map[string][]string{ 170 "listen-peer-urls": {e.listenPeerURL.String()}, 171 "data-dir": {e.DataDir}, 172 } 173 if e.URL != nil { 174 args["advertise-client-urls"] = []string{e.URL.String()} 175 args["listen-client-urls"] = []string{e.URL.String()} 176 } 177 178 // Add unsafe no fsync, available from etcd 3.5 179 if ok, _ := e.processState.CheckFlag("unsafe-no-fsync"); ok { 180 args["unsafe-no-fsync"] = []string{"true"} 181 } 182 return args 183 } 184 185 // Configure returns Arguments that may be used to customize the 186 // flags used to launch etcd. A set of defaults will 187 // be applied underneath. 188 func (e *Etcd) Configure() *process.Arguments { 189 if e.args == nil { 190 e.args = process.EmptyArguments() 191 } 192 return e.args 193 } 194 195 // EtcdDefaultArgs exposes the default args for Etcd so that you 196 // can use those to append your own additional arguments. 197 var EtcdDefaultArgs = []string{ 198 "--listen-peer-urls=http://localhost:0", 199 "--advertise-client-urls={{ if .URL }}{{ .URL.String }}{{ end }}", 200 "--listen-client-urls={{ if .URL }}{{ .URL.String }}{{ end }}", 201 "--data-dir={{ .DataDir }}", 202 } 203