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 }