Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/o7k.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ func pluginCommand(args []string) error {
return nil
}

if len(args) == 1 && args[0] == "update" {
return updatePlugins()
}

if len(args) != 2 {
printPluginUsage()
return fmt.Errorf("invalid plugin command")
Expand All @@ -225,10 +229,12 @@ func pluginCommand(args []string) error {
if err := plugins.Install(context.Background(), args[1]); err != nil {
return fmt.Errorf("error installing plugin: %w", err)
}

case "remove":
if err := plugins.Remove(args[1]); err != nil {
return fmt.Errorf("error removing plugin: %w", err)
}

default:
printPluginUsage()
return fmt.Errorf("unknown plugin command %q", args[0])
Expand All @@ -237,21 +243,71 @@ func pluginCommand(args []string) error {
return nil
}

func updatePlugins() error {
openstackContext := openstack.Context{}
host := plugins.NewHost(&openstackContext)

paths, err := plugins.Discover()
if err != nil {
return fmt.Errorf("discovering plugins: %w", err)
}

for _, path := range paths {
client, err := plugins.NewClient(path, host)
if err != nil {
fmt.Fprintf(os.Stderr, "error loading plugin %s: %v\n", path, err)
continue
}

metadata, err := client.Plugin().Metadata()
client.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "error getting plugin metadata %s: %v\n", path, err)
continue
}

plugin := plugins.Info{
Path: path,
Name: metadata.Name,
Version: metadata.Version,
URL: metadata.URL,
}

result, err := plugins.Update(context.Background(), plugin)
if err != nil {
fmt.Fprintf(os.Stderr, "error updating plugin %q: %v\n", plugin.Name, err)
continue
}

if !result.Updated {
fmt.Printf("plugin %q is already up to date (%s)\n", plugin.Name, result.FromVersion)
continue
}

fmt.Printf("updated plugin %q from %s to %s\n", plugin.Name, result.FromVersion, result.ToVersion)
}

return nil
}

func printUsage() {
fmt.Fprintln(os.Stderr, "Usage:")
fmt.Fprintln(os.Stderr, " o7k")
fmt.Fprintln(os.Stderr, " o7k --version")
fmt.Fprintln(os.Stderr, " o7k plugin install <source>")
fmt.Fprintln(os.Stderr, " o7k plugin update")
fmt.Fprintln(os.Stderr, " o7k plugin remove <name>")
}

func printPluginUsage() {
fmt.Fprintln(os.Stderr, "Usage:")
fmt.Fprintln(os.Stderr, " o7k plugin install <source>")
fmt.Fprintln(os.Stderr, " o7k plugin update")
fmt.Fprintln(os.Stderr, " o7k plugin remove <name>")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Commands:")
fmt.Fprintln(os.Stderr, " install Install a plugin from a local path or HTTP(S) URL")
fmt.Fprintln(os.Stderr, " update Update all installed plugins")
fmt.Fprintln(os.Stderr, " remove Remove an installed plugin")
}

Expand Down
4 changes: 1 addition & 3 deletions internal/plugins/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ import (
)

func Discover() ([]string, error) {
configDir, err := os.UserConfigDir()
dir, err := useUserPluginsDir()
if err != nil {
return nil, err
}

dir := filepath.Join(configDir, "o7k", "plugins")

entries, err := os.ReadDir(dir)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
Expand Down
116 changes: 79 additions & 37 deletions internal/plugins/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,67 +41,55 @@ func installLocal(source string) error {
}

func installRemote(ctx context.Context, source *url.URL) error {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, source.String(), nil)
body, err := download(ctx, source.String())
if err != nil {
return fmt.Errorf("creating request: %w", err)
}

response, err := http.DefaultClient.Do(request)
if err != nil {
return fmt.Errorf("downloading plugin: %w", err)
}
defer response.Body.Close()

if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("downloading plugin: %s", response.Status)
return err
}
defer body.Close()

name := filepath.Base(source.Path)
if name == "." || name == "/" || name == "" {
return fmt.Errorf("plugin URL does not contain a filename")
}

return install(name, response.Body)
return install(name, body)
}

func install(name string, source io.Reader) error {
configDir, err := os.UserConfigDir()
pluginDir, err := useUserPluginsDir()
if err != nil {
return fmt.Errorf("getting user config directory: %w", err)
return err
}

pluginDir := filepath.Join(configDir, "o7k", "plugins")

if err := os.MkdirAll(pluginDir, 0755); err != nil {
return fmt.Errorf("creating plugin directory: %w", err)
}

temp, err := os.CreateTemp(pluginDir, ".o7k-plugin-*")
tempPath, err := writePluginTemp(pluginDir, source)
if err != nil {
return fmt.Errorf("creating temporary plugin: %w", err)
return err
}

tempPath := temp.Name()
defer os.Remove(tempPath)

written, err := io.Copy(temp, io.LimitReader(source, maxPluginSize+1))
binaryName := pluginBinaryName(name)

entries, err := os.ReadDir(pluginDir)
if err != nil {
temp.Close()
return fmt.Errorf("writing plugin: %w", err)
return fmt.Errorf("reading plugin directory: %w", err)
}

if written > maxPluginSize {
temp.Close()
return fmt.Errorf("plugin exceeds maximum size of %d MiB", maxPluginSize>>20)
}
for _, entry := range entries {
if entry.IsDir() || entry.Name() == name {
continue
}

if err := temp.Chmod(0755); err != nil {
temp.Close()
return fmt.Errorf("making plugin executable: %w", err)
}
if pluginBinaryName(entry.Name()) != binaryName {
continue
}

if err := temp.Close(); err != nil {
return fmt.Errorf("closing plugin: %w", err)
if err := os.Remove(filepath.Join(pluginDir, entry.Name())); err != nil {
return fmt.Errorf("removing previous plugin version: %w", err)
}
}

destination := filepath.Join(pluginDir, name)
Expand All @@ -120,12 +108,12 @@ func Remove(name string) error {
return fmt.Errorf("invalid plugin name %q", name)
}

configDir, err := os.UserConfigDir()
pluginDir, err := useUserPluginsDir()
if err != nil {
return fmt.Errorf("getting user config directory: %w", err)
return err
}

path := filepath.Join(configDir, "o7k", "plugins", name)
path := filepath.Join(pluginDir, name)

if err := os.Remove(path); err != nil {
return fmt.Errorf("removing plugin %q: %w", name, err)
Expand All @@ -135,3 +123,57 @@ func Remove(name string) error {

return nil
}

func download(ctx context.Context, source string) (io.ReadCloser, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}

response, err := http.DefaultClient.Do(request)
if err != nil {
return nil, fmt.Errorf("downloading plugin: %w", err)
}

if response.StatusCode < 200 || response.StatusCode >= 300 {
response.Body.Close()
return nil, fmt.Errorf("downloading plugin: %s", response.Status)
}

return response.Body, nil
}

func writePluginTemp(pluginDir string, source io.Reader) (string, error) {
temp, err := os.CreateTemp(pluginDir, ".o7k-plugin-*")
if err != nil {
return "", fmt.Errorf("creating temporary plugin: %w", err)
}

tempPath := temp.Name()

written, err := io.Copy(temp, io.LimitReader(source, maxPluginSize+1))
if err != nil {
temp.Close()
os.Remove(tempPath)
return "", fmt.Errorf("writing plugin: %w", err)
}

if written > maxPluginSize {
temp.Close()
os.Remove(tempPath)
return "", fmt.Errorf("plugin exceeds maximum size of %d MiB", maxPluginSize>>20)
}

if err := temp.Chmod(0755); err != nil {
temp.Close()
os.Remove(tempPath)
return "", fmt.Errorf("making plugin executable: %w", err)
}

if err := temp.Close(); err != nil {
os.Remove(tempPath)
return "", fmt.Errorf("closing plugin: %w", err)
}

return tempPath, nil
}
3 changes: 3 additions & 0 deletions internal/plugins/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Info struct {
Path string
Name string
Version string
URL string
Status Status
Err error
}
Expand Down Expand Up @@ -67,6 +68,7 @@ func (m *Manager) Load(path string) error {
Name: metadata.Name,
Version: metadata.Version,
Status: StatusFailed,
URL: metadata.URL,
Err: err,
})

Expand All @@ -79,6 +81,7 @@ func (m *Manager) Load(path string) error {
Name: metadata.Name,
Version: metadata.Version,
Status: StatusLoaded,
URL: metadata.URL,
})

return nil
Expand Down
25 changes: 25 additions & 0 deletions internal/plugins/paths.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package plugins

import (
"fmt"
"os"
"path/filepath"
)

func useUserPluginsDir() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("getting user config directory: %w", err)
}

return filepath.Join(configDir, "o7k", "plugins"), nil
}

func useUserPluginsBackupDir() (string, error) {
dir, err := useUserPluginsDir()
if err != nil {
return "", err
}

return filepath.Join(dir, "backup"), nil
}
Loading
Loading