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  }