Skip to content

Commit

Permalink
feat(extensions): add install extensions from registry (#223)
Browse files Browse the repository at this point in the history
* "feat(extensions): add install extensions from registry

This pr includes everything up to actually downloading the extension from the upstream registry

Such as pulling an example registry entry

```json
{
  "name": "SuperExtender",
  "description": "Enhances your experience with superpowers.",
  "version": "1.0.3",
  "downloadUrls": [
    {
      "architecture": "amd64",
      "os": "linux",
      "url": "https://example.com/linux/amd64/superextender.tar.gz",
      "checksum": "efgh5678"
    },
    {
      "architecture": "x86",
      "os": "windows",
      "url": "https://example.com/windows/x86/superextender.zip",
      "checksum": "ijkl91011"
    },
    {
      "architecture": "arm64",
      "os": "darwin",
      "url": "https://example.com/windows/x86/superextender.zip",
      "checksum": "ijkl91011"
    }
  ]
}
```

Which will match on the current os using $GOOS and $GOARCH set by golang itself. We may however have to depend on some other variables if go is not installed by I believe it should work anyway"

* feat: print error

Signed-off-by: Kasper J. Hermansen <[email protected]>

* fix: review comments, wording etc.

Signed-off-by: Kasper J. Hermansen <[email protected]>

* feat(extensions): add github release downloader (#224)

* feat(extensions): add github release downloader

This specifically adds a downloader for github releases it uses either the

SHUTTLE_EXTENSIONS_GITHUB_ACCESS_TOKEN or GITHUB_ACCESS_TOKEN for access to the releases

The downloader expects a full url from the registry as seen in a previous pr

Signed-off-by: Kasper J. Hermansen <[email protected]>

* chore: reword bearer

Signed-off-by: Kasper J. Hermansen <[email protected]>

* feat(extensions): add execute command (#225)

* feat(extensions): add execute command

this adds Extensions to the global cmd.go resulting in another section showing up when doing a shuttle --help

To execute an extension simply shuttle myExtension where myExtension is the name of the downloaded extension. All args are passed to the child as well

Signed-off-by: Kasper J. Hermansen <[email protected]>

* feat: remember to add error

Signed-off-by: Kasper J. Hermansen <[email protected]>

* feat: without empty line

Signed-off-by: Kasper J. Hermansen <[email protected]>

* feat: adds github remote registry index (#226)

* feat: with github registry

Signed-off-by: Kasper J. Hermansen <[email protected]>

* fix: shuttle extensions

Signed-off-by: Kasper J. Hermansen <[email protected]>

* feat: can download private files

Signed-off-by: Kasper J. Hermansen <[email protected]>

* feat: remove fluff

Signed-off-by: Kasper J. Hermansen <[email protected]>

* feat: fix review comments

Signed-off-by: Kasper J. Hermansen <[email protected]>

* feat: it needs to implement the functions

Signed-off-by: Kasper J. Hermansen <[email protected]>

---------

Signed-off-by: Kasper J. Hermansen <[email protected]>

---------

Signed-off-by: Kasper J. Hermansen <[email protected]>

---------

Signed-off-by: Kasper J. Hermansen <[email protected]>

---------

Signed-off-by: Kasper J. Hermansen <[email protected]>
  • Loading branch information
kjuulh authored Jun 13, 2024
1 parent 9c865a6 commit 1474236
Show file tree
Hide file tree
Showing 13 changed files with 812 additions and 5 deletions.
3 changes: 3 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ func initializedRootFromArgs(stdout, stderr io.Writer, args []string) (*cobra.Co
rootCmd.ParseFlags(args)

rootCmd.AddCommand(newExtCmd())
if err := addExtensions(rootCmd); err != nil {
uii.Verboseln("failed to register extensions: %s", err.Error())
}

if isInRepoContext() {
runCmd, err := newRun(uii, ctxProvider)
Expand Down
74 changes: 74 additions & 0 deletions cmd/ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package cmd
import (
"errors"
"os"
"os/exec"

stdcontext "context"

"github.com/lunarway/shuttle/internal/extensions"
"github.com/lunarway/shuttle/internal/global"
Expand All @@ -25,6 +28,52 @@ func (c *extGlobalConfig) getRegistry() (string, bool) {
return "", false
}

func addExtensions(rootCmd *cobra.Command) error {
extManager := extensions.NewExtensionsManager(global.NewGlobalStore())

extensions, err := extManager.GetAll(stdcontext.Background())
if err != nil {
return err
}
grp := &cobra.Group{
ID: "extensions",
Title: "Extensions",
}
rootCmd.AddGroup(grp)
for _, extension := range extensions {
extension := extension

rootCmd.AddCommand(
&cobra.Command{
Use: extension.Name(),
Short: extension.Description(),
Version: extension.Version(),
GroupID: "extensions",
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
extCmd := exec.CommandContext(cmd.Context(), extension.FullPath(), args...)

extCmd.Stdout = os.Stdout
extCmd.Stderr = os.Stderr
extCmd.Stdin = os.Stdin

if err := extCmd.Start(); err != nil {
return err
}

if err := extCmd.Wait(); err != nil {
return err
}

return nil
},
},
)
}

return nil
}

func newExtCmd() *cobra.Command {
globalConfig := &extGlobalConfig{}

Expand All @@ -37,6 +86,7 @@ func newExtCmd() *cobra.Command {
newExtInstallCmd(globalConfig),
newExtUpdateCmd(globalConfig),
newExtInitCmd(globalConfig),
newExtPublishCmd(globalConfig),
)

cmd.PersistentFlags().StringVar(&globalConfig.registry, "registry", "", "the given registry, if not set will default to SHUTTLE_EXTENSIONS_REGISTRY")
Expand Down Expand Up @@ -96,3 +146,27 @@ func newExtInitCmd(globalConfig *extGlobalConfig) *cobra.Command {

return cmd
}

func newExtPublishCmd(globalConfig *extGlobalConfig) *cobra.Command {
var version string

// Publish can either be called by a user to rollback an extension, or by CI to automatically publish an extension.
cmd := &cobra.Command{
Use: "publish",
Short: "Publishes the current extension to a registry",
RunE: func(cmd *cobra.Command, args []string) error {
extManager := extensions.NewExtensionsManager(global.NewGlobalStore())

if err := extManager.Publish(cmd.Context(), version); err != nil {
return err
}

return nil
},
}

cmd.Flags().StringVar(&version, "version", "", "the version to publish")
cmd.MarkFlagRequired("version")

return cmd
}
75 changes: 75 additions & 0 deletions internal/extensions/downloader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package extensions

import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
)

type Downloader interface {
Download(ctx context.Context, dest string) error
}

func NewDownloader(downloadLink *registryExtensionDownloadLink) (Downloader, error) {
switch downloadLink.Provider {
case "github-release":
return newGitHubReleaseDownloader(downloadLink), nil
default:
return nil, fmt.Errorf("invalid provider type: %s", downloadLink.Provider)
}
}

type gitHubReleaseDownloader struct {
link *registryExtensionDownloadLink
}

func newGitHubReleaseDownloader(downloadLink *registryExtensionDownloadLink) Downloader {
return &gitHubReleaseDownloader{
link: downloadLink,
}
}

func (d *gitHubReleaseDownloader) Download(ctx context.Context, dest string) error {
client := http.DefaultClient
client.Timeout = time.Second * 60

req, err := http.NewRequestWithContext(ctx, http.MethodGet, d.link.Url, nil)
if err != nil {
return err
}

bearer, err := getGithubToken()
if err != nil {
return err
}

req.Header.Add("Authorization", fmt.Sprintf("token %s", bearer))
req.Header.Add("Accept", "application/octet-stream")

resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if err := os.RemoveAll(dest); err != nil {
log.Printf("failed to remove extension before downloading new: %s, please try again", err.Error())
}

extensionBinary, err := os.Create(dest)
if err != nil {
return err
}
defer extensionBinary.Close()
extensionBinary.Chmod(0o755)

if _, err := io.Copy(extensionBinary, resp.Body); err != nil {
return err
}

return nil
}
86 changes: 85 additions & 1 deletion internal/extensions/extension.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,88 @@
package extensions

import (
"context"
"fmt"
"path"
"runtime"

"github.com/lunarway/shuttle/internal/global"
)

// Extension is the descriptor of a single extension, it is used to add description to the cli, as well as calling the specific extension in question
type Extension struct{}
type Extension struct {
os string
arch string
globalStore *global.GlobalStore
remote *registryExtension
}

func newExtensionFromRegistry(globalStore *global.GlobalStore, registryExtension *registryExtension) (*Extension, error) {
return &Extension{
os: runtime.GOOS,
arch: runtime.GOARCH,
globalStore: globalStore,
remote: registryExtension,
}, nil
}

func (e *Extension) Ensure(ctx context.Context) error {
extensionsCachePath := getExtensionsCachePath(e.globalStore)
binaryName := e.getExtensionBinaryName()
if err := ensureExists(extensionsCachePath); err != nil {
return fmt.Errorf("failed to create cache path: %w", err)
}

binaryPath := path.Join(extensionsCachePath, binaryName)
if exists(binaryPath) {
// TODO: do a checksum chck
//return nil
}

downloadLink := e.getRemoteBinaryDownloadLink()
if downloadLink == nil {
return fmt.Errorf("failed to find a valid extension matching your os and architecture")
}

downloader, err := NewDownloader(downloadLink)
if err != nil {
return err
}

if err := downloader.Download(ctx, binaryPath); err != nil {
return err
}

return nil
}

func (e *Extension) Name() string {
return e.remote.Name
}

func (e *Extension) Version() string {
return e.remote.Version
}

func (e *Extension) Description() string {
return e.remote.Description
}

func (e *Extension) getExtensionBinaryName() string {
return e.remote.Name
}

func (e *Extension) FullPath() string {
return path.Join(getExtensionsCachePath(e.globalStore), e.Name())
}

func (e *Extension) getRemoteBinaryDownloadLink() *registryExtensionDownloadLink {
for _, download := range e.remote.DownloadUrls {
if download.Os == e.os &&
download.Architecture == e.arch {
return &download
}
}

return nil
}
44 changes: 44 additions & 0 deletions internal/extensions/extension_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package extensions

import (
"context"
"fmt"
"os"

"gopkg.in/yaml.v2"
)

type shuttleExtensionsRegistry struct {
GitHub *string `json:"github" yaml:"github"`
}

type shuttleExtensionProviderGitHubRelease struct {
Owner string `json:"owner" yaml:"owner"`
Repo string `json:"repo" yaml:"repo"`
}

type shuttleExtensionsProvider struct {
GitHubRelease *shuttleExtensionProviderGitHubRelease `json:"github-release" yaml:"github-release"`
}

type shuttleExtensionsFile struct {
Name string `json:"name" yaml:"name"`
Description string `json:"description" yaml:"description"`

Provider shuttleExtensionsProvider `json:"provider" yaml:"provider"`
Registry shuttleExtensionsRegistry `json:"registry" yaml:"registry"`
}

func getExtensionsFile(_ context.Context) (*shuttleExtensionsFile, error) {
templateFileContent, err := os.ReadFile("shuttle.template.yaml")
if err != nil {
return nil, fmt.Errorf("failed to find shuttle.template.yaml: %w", err)
}

var templateFile shuttleExtensionsFile
if err := yaml.Unmarshal(templateFileContent, &templateFile); err != nil {
return nil, fmt.Errorf("failed to parse shuttle.template.yaml: %w", err)
}

return &templateFile, nil
}
Loading

0 comments on commit 1474236

Please sign in to comment.