github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/cmd/jujud/main.go (about)

     1  // Copyright 2012-2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"crypto/tls"
     8  	"crypto/x509"
     9  	"fmt"
    10  	"io"
    11  	"math/rand"
    12  	"os"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/juju/clock"
    19  	"github.com/juju/cmd/v3"
    20  	"github.com/juju/errors"
    21  	"github.com/juju/featureflag"
    22  	"github.com/juju/loggo"
    23  	"github.com/juju/names/v5"
    24  	proxyutils "github.com/juju/proxy"
    25  	"github.com/juju/utils/v3/exec"
    26  	"github.com/juju/version/v2"
    27  
    28  	"github.com/juju/juju/agent/addons"
    29  	k8sexec "github.com/juju/juju/caas/kubernetes/provider/exec"
    30  	jujucmd "github.com/juju/juju/cmd"
    31  	agentcmd "github.com/juju/juju/cmd/jujud/agent"
    32  	"github.com/juju/juju/cmd/jujud/agent/agentconf"
    33  	"github.com/juju/juju/cmd/jujud/agent/caasoperator"
    34  	"github.com/juju/juju/cmd/jujud/agent/config"
    35  	"github.com/juju/juju/cmd/jujud/dumplogs"
    36  	"github.com/juju/juju/cmd/jujud/introspect"
    37  	"github.com/juju/juju/cmd/jujud/run"
    38  	"github.com/juju/juju/core/arch"
    39  	"github.com/juju/juju/core/machinelock"
    40  	coreos "github.com/juju/juju/core/os"
    41  	jujunames "github.com/juju/juju/juju/names"
    42  	"github.com/juju/juju/juju/osenv"
    43  	"github.com/juju/juju/juju/sockets"
    44  	_ "github.com/juju/juju/provider/all"         // Import the providers.
    45  	_ "github.com/juju/juju/secrets/provider/all" // Import the secret providers.
    46  	"github.com/juju/juju/upgrades"
    47  	"github.com/juju/juju/utils/proxy"
    48  	jujuversion "github.com/juju/juju/version"
    49  	"github.com/juju/juju/worker/logsender"
    50  	"github.com/juju/juju/worker/uniter/runner/jujuc"
    51  )
    52  
    53  var logger = loggo.GetLogger("juju.cmd.jujud")
    54  
    55  func init() {
    56  	rand.Seed(time.Now().UTC().UnixNano())
    57  	featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey)
    58  }
    59  
    60  var jujudDoc = `
    61  juju provides easy, intelligent service orchestration on top of models
    62  such as OpenStack, Amazon AWS, or bare metal. jujud is a component of juju.
    63  
    64  https://juju.is/
    65  
    66  The jujud command can also forward invocations over RPC for execution by the
    67  juju unit agent. When used in this way, it expects to be called via a symlink
    68  named for the desired remote command, and expects JUJU_AGENT_SOCKET_ADDRESS and
    69  JUJU_CONTEXT_ID be set in its model.
    70  `
    71  
    72  const (
    73  	// exit_err is the value that is returned when the user has run juju in an invalid way.
    74  	exit_err = 2
    75  	// exit_panic is the value that is returned when we exit due to an unhandled panic.
    76  	exit_panic = 3
    77  )
    78  
    79  func getenv(name string) (string, error) {
    80  	value := os.Getenv(name)
    81  	if value == "" {
    82  		return "", errors.Errorf("%s not set", name)
    83  	}
    84  	return value, nil
    85  }
    86  
    87  func getwd() (string, error) {
    88  	dir, err := os.Getwd()
    89  	if err != nil {
    90  		return "", err
    91  	}
    92  	abs, err := filepath.Abs(dir)
    93  	if err != nil {
    94  		return "", err
    95  	}
    96  	return abs, nil
    97  }
    98  
    99  func getSocket() (sockets.Socket, error) {
   100  	var err error
   101  	socket := sockets.Socket{}
   102  	socket.Address, err = getenv("JUJU_AGENT_SOCKET_ADDRESS")
   103  	if err != nil {
   104  		return sockets.Socket{}, err
   105  	}
   106  	socket.Network, err = getenv("JUJU_AGENT_SOCKET_NETWORK")
   107  	if err != nil {
   108  		return sockets.Socket{}, err
   109  	}
   110  
   111  	// If we are not connecting over tcp, no need for TLS.
   112  	if socket.Network != "tcp" {
   113  		return socket, nil
   114  	}
   115  
   116  	caCertFile, err := getenv("JUJU_AGENT_CA_CERT")
   117  	if err != nil {
   118  		return sockets.Socket{}, err
   119  	}
   120  	caCert, err := os.ReadFile(caCertFile)
   121  	if err != nil {
   122  		return sockets.Socket{}, errors.Annotatef(err, "reading %s", caCertFile)
   123  	}
   124  	rootCAs := x509.NewCertPool()
   125  	if ok := rootCAs.AppendCertsFromPEM(caCert); ok == false {
   126  		return sockets.Socket{}, errors.Errorf("invalid ca certificate")
   127  	}
   128  
   129  	unitName, err := getenv("JUJU_UNIT_NAME")
   130  	if err != nil {
   131  		return sockets.Socket{}, err
   132  	}
   133  	application, err := names.UnitApplication(unitName)
   134  	if err != nil {
   135  		return sockets.Socket{}, errors.Trace(err)
   136  	}
   137  	socket.TLSConfig = &tls.Config{
   138  		RootCAs:    rootCAs,
   139  		ServerName: application,
   140  	}
   141  	return socket, nil
   142  }
   143  
   144  // hookToolMain uses JUJU_CONTEXT_ID and JUJU_AGENT_SOCKET_ADDRESS to ask a running unit agent
   145  // to execute a Command on our behalf. Individual commands should be exposed
   146  // by symlinking the command name to this executable.
   147  func hookToolMain(commandName string, ctx *cmd.Context, args []string) (code int, err error) {
   148  	code = 1
   149  	contextID, err := getenv("JUJU_CONTEXT_ID")
   150  	if err != nil {
   151  		return
   152  	}
   153  	dir, err := getwd()
   154  	if err != nil {
   155  		return
   156  	}
   157  	req := jujuc.Request{
   158  		ContextId:   contextID,
   159  		Dir:         dir,
   160  		CommandName: commandName,
   161  		Args:        args[1:],
   162  		Token:       os.Getenv("JUJU_AGENT_TOKEN"),
   163  	}
   164  	socket, err := getSocket()
   165  	if err != nil {
   166  		return
   167  	}
   168  	client, err := sockets.Dial(socket)
   169  	if err != nil {
   170  		return code, err
   171  	}
   172  	defer client.Close()
   173  	var resp exec.ExecResponse
   174  	err = client.Call("Jujuc.Main", req, &resp)
   175  	if err != nil && err.Error() == jujuc.ErrNoStdin.Error() {
   176  		req.Stdin, err = io.ReadAll(os.Stdin)
   177  		if err != nil {
   178  			err = errors.Annotate(err, "cannot read stdin")
   179  			return
   180  		}
   181  		req.StdinSet = true
   182  		err = client.Call("Jujuc.Main", req, &resp)
   183  	}
   184  	if err != nil {
   185  		return
   186  	}
   187  	os.Stdout.Write(resp.Stdout)
   188  	os.Stderr.Write(resp.Stderr)
   189  	return resp.Code, nil
   190  }
   191  
   192  // versionDetail is populated with version information from juju/juju/cmd
   193  // and passed into each SuperCommand. It can be printed using `juju version --all`.
   194  type versionDetail struct {
   195  	// Version of the current binary.
   196  	Version string `json:"version" yaml:"version"`
   197  	// GitCommit of tree used to build the binary.
   198  	GitCommit string `json:"git-commit,omitempty" yaml:"git-commit,omitempty"`
   199  	// GitTreeState is "clean" if the working copy used to build the binary had no
   200  	// uncommitted changes or untracked files, otherwise "dirty".
   201  	GitTreeState string `json:"git-tree-state,omitempty" yaml:"git-tree-state,omitempty"`
   202  	// Compiler reported by runtime.Compiler
   203  	Compiler string `json:"compiler" yaml:"compiler"`
   204  	// OfficialBuild is a monotonic integer set by Jenkins.
   205  	OfficialBuild int `json:"official-build,omitempty" yaml:"official-build,omitempty"`
   206  	// GoBuildTags is the build tags used to build the binary.
   207  	GoBuildTags string `json:"go-build-tags,omitempty" yaml:"go-build-tags,omitempty"`
   208  }
   209  
   210  // Main registers subcommands for the jujud executable, and hands over control
   211  // to the cmd package.
   212  func jujuDMain(args []string, ctx *cmd.Context) (code int, err error) {
   213  	// Assuming an average of 200 bytes per log message, use up to
   214  	// 200MB for the log buffer.
   215  	defer logger.Debugf("jujud complete, code %d, err %v", code, err)
   216  	bufferedLogger, err := logsender.InstallBufferedLogWriter(loggo.DefaultContext(), 1048576)
   217  	if err != nil {
   218  		return 1, errors.Trace(err)
   219  	}
   220  
   221  	// Set the default transport to use the in-process proxy
   222  	// configuration.
   223  	if err := proxy.DefaultConfig.Set(proxyutils.DetectProxies()); err != nil {
   224  		return 1, errors.Trace(err)
   225  	}
   226  	if err := proxy.DefaultConfig.InstallInDefaultTransport(); err != nil {
   227  		return 1, errors.Trace(err)
   228  	}
   229  
   230  	current := version.Binary{
   231  		Number:  jujuversion.Current,
   232  		Arch:    arch.HostArch(),
   233  		Release: coreos.HostOSTypeName(),
   234  	}
   235  	detail := versionDetail{
   236  		Version:      current.String(),
   237  		GitCommit:    jujuversion.GitCommit,
   238  		GitTreeState: jujuversion.GitTreeState,
   239  		Compiler:     jujuversion.Compiler,
   240  		GoBuildTags:  jujuversion.GoBuildTags,
   241  	}
   242  
   243  	jujud := jujucmd.NewSuperCommand(cmd.SuperCommandParams{
   244  		Name: "jujud",
   245  		Doc:  jujudDoc,
   246  		Log:  jujucmd.DefaultLog,
   247  		// p.Version should be a version.Binary, but juju/cmd does not
   248  		// import juju/juju/version so this cannot happen. We have
   249  		// tests to assert that this string value is correct.
   250  		Version:       detail.Version,
   251  		VersionDetail: detail,
   252  	})
   253  
   254  	jujud.Log.NewWriter = func(target io.Writer) loggo.Writer {
   255  		return &jujudWriter{target: target}
   256  	}
   257  
   258  	jujud.Register(agentcmd.NewCAASUnitInitCommand())
   259  	jujud.Register(agentcmd.NewModelCommand(bufferedLogger))
   260  
   261  	// TODO(katco-): AgentConf type is doing too much. The
   262  	// MachineAgent type has called out the separate concerns; the
   263  	// AgentConf should be split up to follow suit.
   264  	agentConf := agentconf.NewAgentConf("")
   265  	machineAgentFactory := agentcmd.MachineAgentFactoryFn(
   266  		agentConf,
   267  		bufferedLogger,
   268  		addons.DefaultIntrospectionSocketName,
   269  		upgrades.PreUpgradeSteps,
   270  		"",
   271  	)
   272  	jujud.Register(agentcmd.NewMachineAgentCmd(ctx, machineAgentFactory, agentConf, agentConf))
   273  
   274  	caasOperatorAgent, err := agentcmd.NewCaasOperatorAgent(ctx, bufferedLogger, func(mc *caasoperator.ManifoldsConfig) error {
   275  		mc.NewExecClient = k8sexec.NewInCluster
   276  		return nil
   277  	})
   278  	if err != nil {
   279  		return -1, errors.Trace(err)
   280  	}
   281  	jujud.Register(caasOperatorAgent)
   282  
   283  	jujud.Register(agentcmd.NewCheckConnectionCommand(agentConf, agentcmd.ConnectAsAgent))
   284  
   285  	code = cmd.Main(jujud, ctx, args[1:])
   286  	return code, nil
   287  }
   288  
   289  // MainWrapper exists to preserve test functionality.
   290  func MainWrapper(args []string) {
   291  	os.Exit(Main(args))
   292  }
   293  
   294  func main() {
   295  	MainWrapper(os.Args)
   296  }
   297  
   298  // Main is not redundant with main(), because it provides an entry point
   299  // for testing with arbitrary command line arguments.
   300  func Main(args []string) int {
   301  	defer func() {
   302  		if r := recover(); r != nil {
   303  			buf := make([]byte, 4096)
   304  			buf = buf[:runtime.Stack(buf, false)]
   305  			logger.Criticalf("Unhandled panic: \n%v\n%s", r, buf)
   306  			os.Exit(exit_panic)
   307  		}
   308  	}()
   309  
   310  	ctx, err := cmd.DefaultContext()
   311  	if err != nil {
   312  		cmd.WriteError(os.Stderr, err)
   313  		os.Exit(exit_err)
   314  	}
   315  
   316  	var code int
   317  	commandName := filepath.Base(args[0])
   318  	switch commandName {
   319  	case jujunames.Jujud:
   320  		code, err = jujuDMain(args, ctx)
   321  	case jujunames.JujuExec:
   322  		lock, err := machinelock.New(machinelock.Config{
   323  			AgentName:   "juju-exec",
   324  			Clock:       clock.WallClock,
   325  			Logger:      loggo.GetLogger("juju.machinelock"),
   326  			LogFilename: filepath.Join(config.LogDir, "juju", machinelock.Filename),
   327  		})
   328  		if err != nil {
   329  			code = exit_err
   330  		} else {
   331  			run := &run.RunCommand{MachineLock: lock}
   332  			code = cmd.Main(run, ctx, args[1:])
   333  		}
   334  	case jujunames.JujuDumpLogs:
   335  		code = cmd.Main(dumplogs.NewCommand(), ctx, args[1:])
   336  	case jujunames.JujuIntrospect:
   337  		code = cmd.Main(&introspect.IntrospectCommand{}, ctx, args[1:])
   338  	default:
   339  		code, err = hookToolMain(commandName, ctx, args)
   340  	}
   341  	if err != nil {
   342  		cmd.WriteError(ctx.Stderr, err)
   343  	}
   344  	return code
   345  }
   346  
   347  type jujudWriter struct {
   348  	target io.Writer
   349  }
   350  
   351  func (w *jujudWriter) Write(entry loggo.Entry) {
   352  	if strings.HasPrefix(entry.Module, "unit.") {
   353  		fmt.Fprintln(w.target, w.unitFormat(entry))
   354  	} else {
   355  		fmt.Fprintln(w.target, loggo.DefaultFormatter(entry))
   356  	}
   357  }
   358  
   359  func (w *jujudWriter) unitFormat(entry loggo.Entry) string {
   360  	ts := entry.Timestamp.In(time.UTC).Format("2006-01-02 15:04:05")
   361  	// Just show the last element of the module.
   362  	lastDot := strings.LastIndex(entry.Module, ".")
   363  	module := entry.Module[lastDot+1:]
   364  	return fmt.Sprintf("%s %s %s %s", ts, entry.Level, module, entry.Message)
   365  }