github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/proxyupdater/proxyupdater.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package proxyupdater 5 6 import ( 7 "io" 8 stdos "os" 9 stdexec "os/exec" 10 "strings" 11 12 "github.com/juju/errors" 13 "github.com/juju/os/v2/series" 14 "github.com/juju/packaging/v2/commands" 15 "github.com/juju/packaging/v2/config" 16 "github.com/juju/proxy" 17 "github.com/juju/worker/v3" 18 19 "github.com/juju/juju/api/agent/proxyupdater" 20 "github.com/juju/juju/core/os" 21 "github.com/juju/juju/core/snap" 22 "github.com/juju/juju/core/watcher" 23 ) 24 25 // Overridden by tests 26 var getHostOS = os.HostOS 27 28 type Config struct { 29 SupportLegacyValues bool 30 EnvFiles []string 31 SystemdFiles []string 32 API API 33 ExternalUpdate func(proxy.Settings) error 34 InProcessUpdate func(proxy.Settings) error 35 RunFunc func(string, string, ...string) (string, error) 36 Logger Logger 37 } 38 39 // Validate ensures that all the required fields have values. 40 func (c *Config) Validate() error { 41 if c.API == nil { 42 return errors.NotValidf("missing API") 43 } 44 if c.InProcessUpdate == nil { 45 return errors.NotValidf("missing InProcessUpdate") 46 } 47 if c.Logger == nil { 48 return errors.NotValidf("missing Logger") 49 } 50 return nil 51 } 52 53 // API is an interface that is provided to New 54 // which can be used to fetch the API host ports 55 type API interface { 56 ProxyConfig() (proxyupdater.ProxyConfiguration, error) 57 WatchForProxyConfigAndAPIHostPortChanges() (watcher.NotifyWatcher, error) 58 } 59 60 // proxyWorker is responsible for monitoring the juju environment 61 // configuration and making changes on the physical (or virtual) machine as 62 // necessary to match the environment changes. Examples of these types of 63 // changes are apt proxy configuration and the juju proxies stored in the juju 64 // proxy file. 65 type proxyWorker struct { 66 aptProxy proxy.Settings 67 aptMirror string 68 proxy proxy.Settings 69 70 snapProxy proxy.Settings 71 snapStoreProxy string 72 snapStoreAssertions string 73 snapStoreProxyURL string 74 75 // The whole point of the first value is to make sure that the files 76 // are written out the first time through, even if they are the same as 77 // "last" time, as the initial value for last time is the zeroed struct. 78 // There is the possibility that the files exist on disk with old 79 // settings, and the environment has been updated to now not have them. We 80 // need to make sure that the disk reflects the environment, so the first 81 // time through, even if the proxies are empty, we write the files to 82 // disk. 83 first bool 84 config Config 85 } 86 87 // NewWorker returns a worker.Worker that updates proxy environment variables for the 88 // process and for the whole machine. 89 var NewWorker = func(config Config) (worker.Worker, error) { 90 if err := config.Validate(); err != nil { 91 return nil, err 92 } 93 envWorker := &proxyWorker{ 94 first: true, 95 config: config, 96 } 97 w, err := watcher.NewNotifyWorker(watcher.NotifyConfig{ 98 Handler: envWorker, 99 }) 100 if err != nil { 101 return nil, errors.Trace(err) 102 } 103 return w, nil 104 } 105 106 func (w *proxyWorker) saveProxySettings() error { 107 // The proxy settings are (usually) stored in three files: 108 // - /etc/juju-proxy.conf - in 'env' format 109 // - /etc/systemd/system.conf.d/juju-proxy.conf 110 // - /etc/systemd/user.conf.d/juju-proxy.conf - both in 'systemd' format 111 for _, file := range w.config.EnvFiles { 112 err := stdos.WriteFile(file, []byte(w.proxy.AsScriptEnvironment()), 0644) 113 if err != nil { 114 w.config.Logger.Errorf("Error updating environment file %s - %v", file, err) 115 } 116 } 117 for _, file := range w.config.SystemdFiles { 118 err := stdos.WriteFile(file, []byte(w.proxy.AsSystemdDefaultEnv()), 0644) 119 if err != nil { 120 w.config.Logger.Errorf("Error updating systemd file - %v", err) 121 } 122 } 123 return nil 124 } 125 126 func (w *proxyWorker) handleProxyValues(legacyProxySettings, jujuProxySettings proxy.Settings) { 127 // Legacy proxy settings update the environment, and also call the 128 // InProcessUpdate, which installs the proxy into the default HTTP 129 // transport. The same occurs for jujuProxySettings. 130 settings := jujuProxySettings 131 if jujuProxySettings.HasProxySet() { 132 w.config.Logger.Debugf("applying in-process juju proxy settings %#v", jujuProxySettings) 133 } else { 134 settings = legacyProxySettings 135 w.config.Logger.Debugf("applying in-process legacy proxy settings %#v", legacyProxySettings) 136 } 137 138 settings.SetEnvironmentValues() 139 if err := w.config.InProcessUpdate(settings); err != nil { 140 w.config.Logger.Errorf("error updating in-process proxy settings: %v", err) 141 } 142 143 // If the external update function is passed in, it is to update the LXD 144 // proxies. We want to set this to the proxy specified regardless of whether 145 // it was set with the legacy fields or the new juju fields. 146 if externalFunc := w.config.ExternalUpdate; externalFunc != nil { 147 if err := externalFunc(settings); err != nil { 148 // It isn't really fatal, but we should record it. 149 w.config.Logger.Errorf("%v", err) 150 } 151 } 152 153 // Here we write files to disk. This is done only for legacyProxySettings. 154 if w.config.SupportLegacyValues && (legacyProxySettings != w.proxy || w.first) { 155 w.config.Logger.Debugf("saving new legacy proxy settings %#v", legacyProxySettings) 156 w.proxy = legacyProxySettings 157 if err := w.saveProxySettings(); err != nil { 158 // It isn't really fatal, but we should record it. 159 w.config.Logger.Errorf("error saving proxy settings: %v", err) 160 } 161 } 162 } 163 164 // getPackageCommander is a helper function which returns the 165 // package commands implementation for the current system. 166 func getPackageCommander() (commands.PackageCommander, error) { 167 hostSeries, err := series.HostSeries() 168 if err != nil { 169 return nil, errors.Trace(err) 170 } 171 return commands.NewPackageCommander(hostSeries) 172 } 173 174 func (w *proxyWorker) handleSnapProxyValues(proxy proxy.Settings, storeID, storeAssertions, storeProxyURL string) { 175 if hostOS := getHostOS(); hostOS == os.CentOS { 176 w.config.Logger.Tracef("no snap proxies on %s", hostOS) 177 return 178 } 179 if w.config.RunFunc == nil { 180 w.config.Logger.Tracef("snap proxies not updated") 181 return 182 } 183 184 var snapSettings []string 185 maybeAddSettings := func(setting, value, saved string) { 186 if value != saved || w.first { 187 snapSettings = append(snapSettings, setting+"="+value) 188 } 189 } 190 maybeAddSettings("proxy.http", proxy.Http, w.snapProxy.Http) 191 maybeAddSettings("proxy.https", proxy.Https, w.snapProxy.Https) 192 193 // Proxy URL changed; either a new proxy has been provided or the proxy 194 // has been removed. Proxy URL changes have a higher precedence than 195 // manually specifying the assertions and store ID. 196 if storeProxyURL != w.snapStoreProxyURL { 197 if storeProxyURL != "" { 198 var err error 199 if storeAssertions, storeID, err = snap.LookupAssertions(storeProxyURL); err != nil { 200 w.config.Logger.Errorf("unable to lookup snap store assertions: %v", err) 201 return 202 } else { 203 w.config.Logger.Infof("auto-detected snap store assertions from proxy") 204 w.config.Logger.Infof("auto-detected snap store ID as %q", storeID) 205 } 206 } else if storeAssertions != "" && storeID != "" { 207 // The proxy URL has been removed. However, if the user 208 // has manually provided assertion/store ID config 209 // options we should restore them. To do this, we reset 210 // the last seen values so we can force-apply the 211 // previously specified manual values. Otherwise, the 212 // provided storeAssertions/storeID values are empty 213 // and we simply fall through to allow the code to 214 // reset the proxy.store setting to an empty value. 215 w.snapStoreAssertions, w.snapStoreProxy = "", "" 216 } 217 w.snapStoreProxyURL = storeProxyURL 218 } else if storeProxyURL != "" { 219 // Re-use the storeID and assertions obtained by querying the 220 // proxy during the last update. 221 storeAssertions, storeID = w.snapStoreAssertions, w.snapStoreProxy 222 } 223 224 maybeAddSettings("proxy.store", storeID, w.snapStoreProxy) 225 226 // If an assertion file was provided we need to "snap ack" it before 227 // configuring snap to use the store ID. 228 if storeAssertions != w.snapStoreAssertions && storeAssertions != "" { 229 output, err := w.config.RunFunc(storeAssertions, "snap", "ack", "/dev/stdin") 230 if err != nil { 231 w.config.Logger.Warningf("unable to acknowledge assertions: %v, output: %q", err, output) 232 return 233 } 234 w.snapStoreAssertions = storeAssertions 235 } 236 237 if len(snapSettings) > 0 { 238 args := append([]string{"set", "system"}, snapSettings...) 239 output, err := w.config.RunFunc(noStdIn, "snap", args...) 240 if err != nil { 241 w.config.Logger.Warningf("unable to set snap core settings %v: %v, output: %q", snapSettings, err, output) 242 } else { 243 w.config.Logger.Debugf("snap core settings %v updated, output: %q", snapSettings, output) 244 w.snapProxy = proxy 245 w.snapStoreProxy = storeID 246 } 247 } 248 } 249 250 func (w *proxyWorker) handleAptProxyValues(aptSettings proxy.Settings, aptMirror string) { 251 if hostOS := getHostOS(); hostOS == os.CentOS { 252 w.config.Logger.Tracef("no apt proxies on %s", hostOS) 253 return 254 } 255 256 mirrorUpdateNeeded := aptMirror != "" && aptMirror != w.aptMirror 257 updateNeeded := w.first || aptSettings != w.aptProxy || mirrorUpdateNeeded 258 var ( 259 paccmder commands.PackageCommander 260 err error 261 ) 262 if updateNeeded { 263 paccmder, err = getPackageCommander() 264 if err != nil { 265 w.config.Logger.Errorf("unable to process apt proxy changes: %v", err) 266 return 267 } 268 } 269 270 if aptSettings != w.aptProxy || w.first { 271 w.config.Logger.Debugf("new apt proxy settings %#v", aptSettings) 272 w.aptProxy = aptSettings 273 274 // Always finish with a new line. 275 content := paccmder.ProxyConfigContents(w.aptProxy) + "\n" 276 err = stdos.WriteFile(config.AptProxyConfigFile, []byte(content), 0644) 277 if err != nil { 278 // It isn't really fatal, but we should record it. 279 w.config.Logger.Errorf("error writing apt proxy config file: %v", err) 280 } 281 } 282 if mirrorUpdateNeeded { 283 if w.config.RunFunc == nil { 284 w.config.Logger.Tracef("apt mirrors not updated") 285 return 286 } 287 w.config.Logger.Debugf("new apt mirror value %v", aptMirror) 288 w.aptMirror = aptMirror 289 290 cmds := paccmder.SetMirrorCommands(aptMirror, aptMirror) 291 script := []string{"#!/bin/bash", "set -e"} 292 script = append(script, "(") 293 script = append(script, cmds...) 294 script = append(script, ")") 295 w.config.Logger.Tracef(strings.Join(script, "\n")) 296 if output, err := w.config.RunFunc(noStdIn, "/bin/bash", "-c", strings.Join(script, "\n")); err != nil { 297 w.config.Logger.Warningf("unable to update apt mirrors: %v, output: %q", err, output) 298 } 299 } 300 return 301 } 302 303 func (w *proxyWorker) onChange() error { 304 config, err := w.config.API.ProxyConfig() 305 if err != nil { 306 return err 307 } 308 309 w.handleProxyValues(config.LegacyProxy, config.JujuProxy) 310 w.handleSnapProxyValues(config.SnapProxy, config.SnapStoreProxyId, config.SnapStoreProxyAssertions, config.SnapStoreProxyURL) 311 w.handleAptProxyValues(config.APTProxy, config.AptMirror) 312 return nil 313 } 314 315 // SetUp is defined on the worker.NotifyWatchHandler interface. 316 func (w *proxyWorker) SetUp() (watcher.NotifyWatcher, error) { 317 // We need to set this up initially as the NotifyWorker sucks up the first 318 // event. 319 err := w.onChange() 320 if err != nil { 321 return nil, err 322 } 323 w.first = false 324 return w.config.API.WatchForProxyConfigAndAPIHostPortChanges() 325 } 326 327 // Handle is defined on the worker.NotifyWatchHandler interface. 328 func (w *proxyWorker) Handle(_ <-chan struct{}) error { 329 return w.onChange() 330 } 331 332 // TearDown is defined on the worker.NotifyWatchHandler interface. 333 func (w *proxyWorker) TearDown() error { 334 // Nothing to cleanup, only state is the watcher 335 return nil 336 } 337 338 const noStdIn = "" 339 340 // RunWithStdIn executes the command specified with the args with optional stdin. 341 func RunWithStdIn(input string, command string, args ...string) (string, error) { 342 cmd := stdexec.Command(command, args...) 343 344 if input != "" { 345 stdin, err := cmd.StdinPipe() 346 if err != nil { 347 return "", errors.Annotate(err, "getting stdin pipe") 348 } 349 350 go func() { 351 defer stdin.Close() 352 _, _ = io.WriteString(stdin, input) 353 }() 354 } 355 356 out, err := cmd.CombinedOutput() 357 output := string(out) 358 return output, err 359 }