Skip to content

Commit e096dd0

Browse files
committed
add plugins support, refactor fetcher names
1 parent 6a7c12e commit e096dd0

3 files changed

Lines changed: 118 additions & 12 deletions

File tree

main.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package main
22

33
import (
44
"context"
5+
"errors"
56
"flag"
67
"fmt"
78
"log"
89
"math/rand"
910
"net/url"
1011
"os"
12+
"os/exec"
1113
"os/signal"
1214
"syscall"
1315
"time"
@@ -21,6 +23,14 @@ type opts struct {
2123
checkFrequency string
2224
destinationDir string
2325
runOnce bool
26+
plugin string
27+
pluginArgs []string
28+
}
29+
30+
type plugin struct {
31+
binPath string
32+
name string
33+
args []string
2434
}
2535

2636
const (
@@ -33,15 +43,22 @@ func main() {
3343

3444
log.Print("gokrazy's selfupdate service starting up..")
3545

46+
gokrazy.WaitForClock()
47+
3648
var o opts
3749

3850
flag.StringVar(&o.gusServer, "gus_server", "", "the HTTP/S endpoint of the GUS (gokrazy Update System) server (required)")
3951
flag.StringVar(&o.checkFrequency, "check_frequency", "1h", "the time frequency for checks to the update service. The very first check is done on startup. default: 1h")
4052
flag.StringVar(&o.destinationDir, "destination_dir", "/tmp/selfupdate", "the destination directory for the fetched update file. default: /tmp/selfupdate")
4153
flag.BoolVar(&o.runOnce, "run_once", false, "exits right after the initial update attempt. default: false")
54+
flag.StringVar(&o.plugin, "plugin", "", "name of the desired plugin to be loaded (this will be used when needed). default: ''")
4255

4356
flag.Parse()
4457

58+
// Gather args after flag parsing termination "--".
59+
// They will be directly passed to the plugin binary.
60+
o.pluginArgs = flag.Args()
61+
4562
if err := logic(ctx, o); err != nil {
4663
log.Fatal(err)
4764
}
@@ -79,11 +96,16 @@ func logic(ctx context.Context, o opts) error {
7996
return fmt.Errorf("error joining gus server url: %w", err)
8097
}
8198

99+
plugins := make(map[string]plugin)
100+
if err := loadPlugin(plugins, o.plugin, o.pluginArgs); err != nil {
101+
return fmt.Errorf("error loading plugin %s: %w", o.plugin, err)
102+
}
103+
82104
gusCfg := gusapi.NewConfiguration()
83105
gusCfg.BasePath = gusBasePath
84106
gusCli := gusapi.NewAPIClient(gusCfg)
85107

86-
if err := updateProcess(ctx, gusCli, machineID, o.gusServer, sbomHash, o.destinationDir, httpPassword, httpPort); err != nil {
108+
if err := updateProcess(ctx, gusCli, plugins, machineID, o.gusServer, sbomHash, o.destinationDir, httpPassword, httpPort); err != nil {
87109
// If the updateProcess fails we exit with an error
88110
// so that gokrazy supervisor will restart the process.
89111
return fmt.Errorf("error performing updateProcess: %w", err)
@@ -107,15 +129,15 @@ func logic(ctx context.Context, o opts) error {
107129
case <-ticker.C:
108130
jitter := time.Duration(rand.Int63n(250)) * time.Second
109131
time.Sleep(jitter)
110-
if err := updateProcess(ctx, gusCli, machineID, o.gusServer, sbomHash, o.destinationDir, httpPassword, httpPort); err != nil {
132+
if err := updateProcess(ctx, gusCli, plugins, machineID, o.gusServer, sbomHash, o.destinationDir, httpPassword, httpPort); err != nil {
111133
log.Printf("error performing updateProcess: %v", err)
112134
continue
113135
}
114136
}
115137
}
116138
}
117139

118-
func updateProcess(ctx context.Context, gusCli *gusapi.APIClient, machineID, gusServer, sbomHash, destinationDir, httpPassword, httpPort string) error {
140+
func updateProcess(ctx context.Context, gusCli *gusapi.APIClient, plugins map[string]plugin, machineID, gusServer, sbomHash, destinationDir, httpPassword, httpPort string) error {
119141
response, err := checkForUpdates(ctx, gusCli, machineID)
120142
if err != nil {
121143
return fmt.Errorf("unable to check for updates: %w", err)
@@ -128,7 +150,7 @@ func updateProcess(ctx context.Context, gusCli *gusapi.APIClient, machineID, gus
128150
}
129151

130152
// The SBOMHash differs, start the selfupdate procedure.
131-
if err := selfupdate(ctx, gusCli, gusServer, machineID, destinationDir, response, httpPassword, httpPort); err != nil {
153+
if err := selfupdate(ctx, gusCli, plugins, gusServer, machineID, destinationDir, response, httpPassword, httpPort); err != nil {
132154
return fmt.Errorf("unable to perform the selfupdate procedure: %w", err)
133155
}
134156

@@ -140,3 +162,24 @@ func updateProcess(ctx context.Context, gusCli *gusapi.APIClient, machineID, gus
140162

141163
return nil
142164
}
165+
166+
func loadPlugin(plugins map[string]plugin, pluginName string, pluginArgs []string) error {
167+
var binPath string
168+
169+
// Try to find the plugin binary in PATH.
170+
fullPluginName := fmt.Sprintf("gokplugin-%s", pluginName)
171+
if p, err := exec.LookPath(fullPluginName); err == nil {
172+
binPath = p
173+
} else {
174+
// The binary can't be found in PATH.
175+
// Fall back to checking in the well known gokrazy's /user/ path.
176+
fallbackPath := fmt.Sprintf("/user/%s", fullPluginName)
177+
if _, err := os.Stat(fallbackPath); errors.Is(err, os.ErrNotExist) {
178+
return fmt.Errorf("unable to find %s", fullPluginName)
179+
}
180+
binPath = fallbackPath
181+
}
182+
plugins[pluginName] = plugin{binPath: binPath, name: pluginName, args: pluginArgs}
183+
184+
return nil
185+
}

selfupdate.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"log"
78
"net/http"
@@ -35,7 +36,7 @@ func shouldUpdate(response gusapi.UpdateResponse, sbomHash string) bool {
3536
return true
3637
}
3738

38-
func selfupdate(ctx context.Context, gusCli *gusapi.APIClient, gusServer, machineID, destinationDir string, response gusapi.UpdateResponse, httpPassword, httpPort string) error {
39+
func selfupdate(ctx context.Context, gusCli *gusapi.APIClient, plugins map[string]plugin, gusServer, machineID, destinationDir string, response gusapi.UpdateResponse, httpPassword, httpPort string) error {
3940
log.Print("starting self-update procedure")
4041

4142
if _, _, err := gusCli.UpdateApi.Attempt(ctx, &gusapi.UpdateApiAttemptOpts{
@@ -52,12 +53,19 @@ func selfupdate(ctx context.Context, gusCli *gusapi.APIClient, gusServer, machin
5253

5354
switch response.RegistryType {
5455
case "http", "localdisk":
55-
readClosers, err = httpFetcher(response, gusServer, destinationDir)
56+
readClosers, err = httpUpdateFetch(response, gusServer, destinationDir)
5657
if err != nil {
5758
return fmt.Errorf("error fetching %q update from link %q: %w", response.RegistryType, response.DownloadLink, err)
5859
}
5960
default:
60-
return fmt.Errorf("unrecognized registry type %q", response.RegistryType)
61+
if _, ok := plugins[response.RegistryType]; !ok {
62+
return fmt.Errorf("error %q is not a loaded plugin", response.RegistryType)
63+
}
64+
65+
readClosers, err = pluginFetchUpdate(ctx, plugins[response.RegistryType], destinationDir, response.DownloadLink)
66+
if err != nil {
67+
return fmt.Errorf("error fetching %q update from link %q: %w", response.RegistryType, response.DownloadLink, err)
68+
}
6169
}
6270

6371
uri := fmt.Sprintf("http://gokrazy:%s@localhost:%s/", httpPassword, httpPort)
@@ -99,7 +107,7 @@ func selfupdate(ctx context.Context, gusCli *gusapi.APIClient, gusServer, machin
99107
}
100108

101109
log.Print("reboot")
102-
if err := target.Reboot(ctx); err != nil {
110+
if err := target.Reboot(ctx); err != nil && !errors.Is(err, context.Canceled) {
103111
return fmt.Errorf("reboot: %v", err)
104112
}
105113

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package main
22

33
import (
44
"archive/zip"
5+
"context"
56
"fmt"
67
"io"
78
"log"
89
"net/http"
910
"net/url"
1011
"os"
12+
"os/exec"
1113
"path/filepath"
1214
"strconv"
1315
"syscall"
@@ -28,8 +30,8 @@ type rcs struct {
2830
root io.ReadCloser
2931
}
3032

31-
// httpFetcher handles a http update link.
32-
func httpFetcher(response gusapi.UpdateResponse, gusServer, destinationDir string) (rcs, error) {
33+
// httpUpdateFetch fetches the update payload via HTTP.
34+
func httpUpdateFetch(response gusapi.UpdateResponse, gusServer, destinationDir string) (rcs, error) {
3335
// The link may be a relative url if the server's backend registry is its local disk.
3436
// Ensure we have an absolute url by adding the base (gusServer) url
3537
// when necessary.
@@ -43,7 +45,7 @@ func httpFetcher(response gusapi.UpdateResponse, gusServer, destinationDir strin
4345
return rcs{}, fmt.Errorf("error ensuring destination directory exists: %w", err)
4446
}
4547

46-
log.Printf("downloading update file from registry %q with url: %s", response.RegistryType, link)
48+
log.Printf("downloading update from %q registry with url: %s", response.RegistryType, link)
4749

4850
filePath := filepath.Join(destinationDir, "disk.gaf")
4951
if err := httpDownloadFile(destinationDir, filePath, link); err != nil {
@@ -129,6 +131,7 @@ func httpDownloadFile(destinationDir, filePath string, url string) error {
129131
return nil
130132
}
131133

134+
// ensureAbsoluteHTTPLink ensures an HTTP link is absolute.
132135
func ensureAbsoluteHTTPLink(baseURL string, link string) (string, error) {
133136
base, err := url.Parse(baseURL)
134137
if err != nil {
@@ -143,7 +146,7 @@ func ensureAbsoluteHTTPLink(baseURL string, link string) (string, error) {
143146
return u.String(), nil
144147
}
145148

146-
// Function to get available disk space for path.
149+
// diskSpaceAvailable gets available disk space for path.
147150
func diskSpaceAvailable(path string) (uint64, error) {
148151
fs := syscall.Statfs_t{}
149152
err := syscall.Statfs(path, &fs)
@@ -152,3 +155,55 @@ func diskSpaceAvailable(path string) (uint64, error) {
152155
}
153156
return fs.Bfree * uint64(fs.Bsize), nil
154157
}
158+
159+
// pluginFetchUpdate fetches the update payload via plugin.
160+
func pluginFetchUpdate(ctx context.Context, p plugin, destinationDir string, link string) (rcs, error) {
161+
if err := os.MkdirAll(destinationDir, 0755); err != nil {
162+
return rcs{}, fmt.Errorf("error ensuring destination directory exists: %w", err)
163+
}
164+
165+
log.Printf("downloading update from %q registry with url: %q", p.name, link)
166+
167+
filePath := filepath.Join(destinationDir, "disk.gaf")
168+
169+
// TODO: add update size gather + check against available disk space.
170+
171+
args := append(p.args, []string{"--url", link, "--output", destinationDir}...)
172+
173+
cmd := exec.CommandContext(ctx, p.binPath, args...)
174+
175+
if err := cmd.Run(); err != nil {
176+
return rcs{}, fmt.Errorf("error running plugin command %q: %w", p.binPath, err)
177+
}
178+
179+
log.Print("finished downloading update file")
180+
log.Print("loading disk partitions from update file")
181+
182+
r, err := zip.OpenReader(filePath)
183+
if err != nil {
184+
return rcs{}, fmt.Errorf("error opening downloaded file %q: %w", filePath, err)
185+
}
186+
187+
var mbrReader, bootReader, rootReader io.ReadCloser
188+
for _, f := range r.File {
189+
switch f.Name {
190+
case mbrPartitionName:
191+
mbrReader, err = f.Open()
192+
if err != nil {
193+
return rcs{}, fmt.Errorf("error reading %s within update file: %w", mbrPartitionName, err)
194+
}
195+
case bootPartitionName:
196+
bootReader, err = f.Open()
197+
if err != nil {
198+
return rcs{}, fmt.Errorf("error reading %s within update file: %w", bootPartitionName, err)
199+
}
200+
case rootPartitionName:
201+
rootReader, err = f.Open()
202+
if err != nil {
203+
return rcs{}, fmt.Errorf("error reading %s within update file: %w", rootPartitionName, err)
204+
}
205+
}
206+
}
207+
208+
return rcs{r, mbrReader, bootReader, rootReader}, nil
209+
}

0 commit comments

Comments
 (0)