Files
tools/internal/gok/new.go
Michael Stapelberg 2c805ed001 refactor cobra command initialization to avoid stale state
Before this commit, we held onto *cobra.Command objects,
but that is not actually supported: after the first Execute(),
commands like updateCmd are stuck on the first-ever provided ctx.

Instead, turn command initialization into functions.

I only noticed this when trying to do two 'gok update'
from within the same test, where the fake build timestamp
is injected via the context (the timestamp was always the same).
2025-11-29 10:47:48 +01:00

189 lines
4.9 KiB
Go

package gok
import (
"context"
"crypto/rand"
"fmt"
"io"
"log"
"os"
"path/filepath"
"github.com/gokrazy/internal/config"
"github.com/gokrazy/internal/instanceflag"
"github.com/gokrazy/tools/internal/pwgen"
"github.com/spf13/cobra"
)
// newCmd is gok new.
func newCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "edit",
Use: "new",
Short: "Create a new gokrazy instance",
Long: `Create a new gokrazy instance.
If you are unfamiliar with gokrazy, please follow:
https://gokrazy.org/quickstart/
`,
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NArg() > 0 {
fmt.Fprint(os.Stderr, `positional arguments are not supported
`)
return cmd.Usage()
}
return newImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
instanceflag.RegisterPflags(cmd.Flags())
cmd.Flags().BoolVarP(&newImpl.empty, "empty", "", false, "create an empty gokrazy instance, without the default packages")
return cmd
}
type newImplConfig struct {
empty bool
}
var newImpl newImplConfig
func (r *newImplConfig) createBreakglassAuthorizedKeys(authorizedPath string, matches []string) error {
f, err := os.OpenFile(authorizedPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
if os.IsExist(err) {
log.Printf("%s already exists, not replacing it", authorizedPath)
return nil
}
return err
}
defer f.Close()
hostname, err := os.Hostname()
if err != nil {
log.Print(err)
}
authorized := "# This authorized_keys(5) file allows access from keys on " + hostname + "\n\n"
for _, match := range matches {
b, err := os.ReadFile(match)
if err != nil {
authorized += "# " + match + ": " + err.Error() + "\n\n"
continue
}
authorized += "# " + match + "\n" + string(b) + "\n"
}
if _, err := f.WriteString(authorized); err != nil {
return err
}
return f.Close()
}
func (r *newImplConfig) addBreakglassAuthorizedKeys(authorizedPath string, matches []string, packageConfig map[string]config.PackageConfig) error {
if err := r.createBreakglassAuthorizedKeys(authorizedPath, matches); err != nil {
return err
}
packageConfig["github.com/gokrazy/breakglass"] = config.PackageConfig{
CommandLineFlags: []string{
"-authorized_keys=/etc/breakglass.authorized_keys",
},
ExtraFilePaths: map[string]string{
"/etc/breakglass.authorized_keys": filepath.Base(authorizedPath),
},
}
return nil
}
func (r *newImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
parentDir := instanceflag.ParentDir()
instance := instanceflag.Instance()
if err := os.MkdirAll(filepath.Join(parentDir, instance), 0755); err != nil {
return err
}
configJSON := filepath.Join(parentDir, instance, "config.json")
f, err := os.OpenFile(configJSON, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
if os.IsExist(err) {
return fmt.Errorf("gokrazy instance already exists! If you want to re-create it, rm '%s' and retry", configJSON)
}
}
defer f.Close()
packageConfig := make(map[string]config.PackageConfig)
var packages []string
if !r.empty {
packages = append(packages,
"github.com/gokrazy/fbstatus",
"github.com/gokrazy/hello",
"github.com/gokrazy/serial-busybox")
idPattern := os.Getenv("HOME") + "/.ssh/id_*.pub"
matches, err := filepath.Glob(idPattern)
if err != nil {
return err
}
if len(matches) == 0 {
log.Printf("No SSH keys found in %s, not adding breakglass", idPattern)
}
if len(matches) > 0 {
packages = append(packages, "github.com/gokrazy/breakglass")
authorizedPath := filepath.Join(parentDir, instance, "breakglass.authorized_keys")
if err := r.addBreakglassAuthorizedKeys(authorizedPath, matches, packageConfig); err != nil {
return err
}
}
}
// Create a machine-id(5) file to uniquely identify a gokrazy instance
machineId, err := randomMachineId(rand.Reader)
if err != nil {
return fmt.Errorf("generating random machine id: %v", err)
}
packageConfig["github.com/gokrazy/gokrazy/cmd/randomd"] = config.PackageConfig{
ExtraFileContents: map[string]string{
"/etc/machine-id": machineId.String() + "\n",
},
}
pw, err := pwgen.RandomPassword(20)
if err != nil {
return err
}
cfg := &config.Struct{
Hostname: instance,
Packages: packages,
Update: &config.UpdateStruct{
HTTPPassword: pw,
},
Environment: []string{
"GOOS=linux",
"GOARCH=arm64",
},
PackageConfig: packageConfig,
SerialConsole: "disabled",
}
b, err := cfg.FormatForFile()
if err != nil {
return err
}
f.Write(b)
if err := f.Close(); err != nil {
return err
}
fmt.Printf("gokrazy instance configuration created in %s\n", configJSON)
fmt.Printf("(Use 'gok -i %s edit' to edit the configuration interactively.)\n", instance)
fmt.Println()
fmt.Printf("Use 'gok -i %s add' to add packages to this instance\n", instance)
fmt.Println()
fmt.Printf("To deploy this gokrazy instance, see 'gok help overwrite'\n")
return nil
}