Skip to content

Commit

Permalink
Adds AWS CLI to system apps
Browse files Browse the repository at this point in the history
Signed-off-by: liam.baker <[email protected]>
  • Loading branch information
liam.baker committed Jan 4, 2024
1 parent 9d729e1 commit 35fd063
Show file tree
Hide file tree
Showing 16 changed files with 2,261 additions and 55 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Or get a copy of his eBook on Go so you can learn how to build tools like k3sup,
> [Ivan Velichko](https://twitter.com/iximiuz/status/1422605221226860548?s=20), SRE @ Booking.com
> Before arkade whenever I used to spin up an instance, I used to go to multiple sites and download the binary. Arkade is one of my favourite tools.
>
>
> [Kumar Anurag](https://kubesimplify.com/arkade) - Cloud Native Enthusiast
> It's hard to use K8s without Arkade these days.
Expand All @@ -88,7 +88,7 @@ Or get a copy of his eBook on Go so you can learn how to build tools like k3sup,
> [@Yankexe](https://twitter.com/yankexe/status/1305427718050250754?s=20)
> arkade is really a great tool to install CLI tools, and system packages, check this blog on how to get started with arkade it's a time saver.
>
>
> [Kiran Satya Raj](https://twitter.com/jksrtwt/status/1556592117627047936?s=20&t=g0gnSP98jg3ZwU7sQqUrLw)
> This is real magic get #kubernetes up and going in a second; then launch #openfaas a free better than lambda solution that uses docker images.
Expand All @@ -103,7 +103,7 @@ Or get a copy of his eBook on Go so you can learn how to build tools like k3sup,
> I finally got around to installing Arkade, super simple!
> quicker to install this than the argocli standalone commands, but there are lots of handy little tools in there.
> also, the neat little part about arkade, not only does it make it easy to install a ton of different apps and CLIs you can also get the info on them as well pretty quickly.
>
>
> [Michael Cade @ Kasten](https://twitter.com/MichaelCade1/status/1390403831167700995?s=20)
> You've to install latest and greatest tools for your daily @kubernetesio tasks? No problem, check out #arkade the open source #kubernetes marketplace 👍
Expand Down Expand Up @@ -241,6 +241,7 @@ Run the following to see what's available `arkade system install`:

```
actions-runner Install GitHub Actions Runner
aws-cli Install AWS CLI
buildkitd Install Buildkitd
cni Install CNI plugins
containerd Install containerd
Expand Down Expand Up @@ -287,12 +288,12 @@ If you just need system applications, you could also try "setup-arkade":
There are two commands built into arkade designed for software vendors and open source maintainers.
* `arkade helm chart upgrade` - run this command to scan for container images and update them automatically by querying a remote registry.
* `arkade helm chart upgrade` - run this command to scan for container images and update them automatically by querying a remote registry.
* `arkade helm chart verify` - after changing the contents of a values.yaml or docker-compose.yaml file, this command will check each image exists on a remote registry

Whilst end-users may use a GitOps-style tool to deploy charts and update their versions, maintainers need to make conscious decisions about when and which images to change within a Helm chart or compose file.

These two features are used by OpenFaaS Ltd on projects and products like OpenFaaS CE/Pro (Serverless platform) and faasd (docker-compose file).
These two features are used by OpenFaaS Ltd on projects and products like OpenFaaS CE/Pro (Serverless platform) and faasd (docker-compose file).

### Upgrade images within a Helm chart

Expand Down
329 changes: 329 additions & 0 deletions cmd/system/aws_cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
package system

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
"time"

"golang.org/x/exp/slices"

"github.com/Masterminds/semver"
"github.com/alexellis/arkade/pkg/archive"
"github.com/alexellis/arkade/pkg/env"
"github.com/alexellis/arkade/pkg/get"
execute "github.com/alexellis/go-execute/v2"
"github.com/spf13/cobra"
)

type ReferenceObject struct {
Type string `json:"tag,omitempty"`
}

type Reference struct {
Ref string `json:"ref,omitempty"`
Url string `json:"url,omitempty"`
Object ReferenceObject `json:"object,omitempty"`
}

func MakeInstallAWSCLI() *cobra.Command {
command := &cobra.Command{
Use: "aws-cli",
Short: "Install AWS CLI",
Long: `Install AWS CLI for interacting with Amazon Web Services APIs.`,
Example: ` arkade system install aws-cli
arkade system install aws-cli --version <version>`,
SilenceUsage: true,
}

command.Flags().StringP("version", "v", githubLatest, "The version or leave blank to determine the latest available version")
command.Flags().String("path", "/usr/local/bin", "Installation path, where a aws cli subfolder will be created")
command.Flags().Bool("progress", true, "Show download progress")
command.Flags().StringP("work-dir", "w", "", "Working directory that installer files should be copied to (current directory if not supplied)")
command.Flags().Bool("run-installer", true, "Whether or not arkade should run the downloaded installer")

command.PreRunE = func(cmd *cobra.Command, args []string) error {
return nil
}

command.RunE = func(cmd *cobra.Command, args []string) error {
installPath, _ := cmd.Flags().GetString("path")
version, _ := cmd.Flags().GetString("version")
progress, _ := cmd.Flags().GetBool("progress")
workDir, _ := cmd.Flags().GetString("work-dir")
runInstaller, _ := cmd.Flags().GetBool("run-installer")
fmt.Printf("Installing AWS CLI to %s\n", installPath)

installPath = strings.ReplaceAll(installPath, "$HOME", os.Getenv("HOME"))

if err := os.MkdirAll(installPath, 0755); err != nil && !os.IsExist(err) {
fmt.Printf("Error creating directory %s, error: %s\n", installPath, err.Error())
}

arch, osVer := env.GetClientArch()

if cmd.Flags().Changed("arch") {
arch, _ = cmd.Flags().GetString("arch")
}

if version == githubLatest {
v, err := getAWSCLIVersion("aws", "aws-cli")
if err != nil {
return err
}
version = v
}

fmt.Printf("Installing version: %s for: %s\n", version, arch)

awsCliTool := get.Tool{
Owner: "amazonaws",
Repo: "awscli",
Name: "awscli",
Version: version,
URLTemplate: `
{{$version := .Version}}
{{$base := printf "https://%s.%s.com" .Repo .Owner}}
{{$ext := "zip"}}
{{$uri := printf "%s-exe-linux-%s" .Repo .Arch}}
{{ if HasPrefix .OS "Ming" }}
{{$uri = "AWSCLIV2"}}
{{$ext = "msi"}}
{{ else if eq .OS "Darwin" -}}
{{$uri = "AWSCLIV2"}}
{{$ext = "pkg"}}
{{ end -}}
{{$base}}/{{$uri}}-{{$version}}.{{$ext}}
`,
}

dUrl, err := awsCliTool.GetURL(osVer, arch, version, !progress)
if err != nil {
return err
}
fmt.Printf("Downloading from: %s\n", dUrl)

outPath, err := get.DownloadFileP(dUrl, progress)
if err != nil {
return err
}
fmt.Printf("Downloaded to: %s\n", outPath)

f, err := os.OpenFile(outPath, os.O_RDONLY, 0644)
if err != nil {
return err
}
defer f.Close()

if workDir == "" {
cwd, err := os.Getwd()
if err != nil {
return err
}
workDir = cwd
}

fmt.Printf("Copying file to: %s\n", workDir)

filename := filepath.Base(outPath)
if _, err = get.CopyFileP(
outPath,
fmt.Sprintf("%s/%s", workDir, filename), readWriteExecuteEveryone,
); err != nil {
return err
}

isArchive, err := awsCliTool.IsArchive(true)
if err != nil {
return err
}

if isArchive {
unpackPath := fmt.Sprintf("%s/awscli", workDir)
fmt.Printf("Unpacking AWS CLI to: %s\n", unpackPath)

fInfo, err := f.Stat()
if err != nil {
return err
}
if err := archive.Unzip(f, fInfo.Size(), unpackPath, true); err != nil {
return err
}

workDir = unpackPath
}

if runInstaller {
if err := runBundledInstaller(osVer, workDir, filename, installPath); err != nil {
return err
}
} else {
tpl, err := installationInstructions(osVer, workDir, filename, installPath)
if err != nil {
return err
}
fmt.Printf("\n%s", tpl)
}

return nil
}

return command
}

func getAWSCLIVersion(owner, repo string) (string, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/refs/tags", owner, repo)

client := http.Client{Timeout: time.Second * 10}
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}

req, err := http.NewRequest(
http.MethodGet,
url,
nil,
)
if err != nil {
return "", err
}

var references []Reference
response, err := client.Do(req)
if err != nil {
return "", err
}

defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}

if err := json.Unmarshal(body, &references); err != nil {
return "", err
}

tags := make([]*semver.Version, 0)
for _, reference := range references {
if reference.Object.Type == "tag" {
trimmed := strings.TrimPrefix(reference.Ref, "refs/tags/")
v, err := semver.NewVersion(trimmed)
if err != nil && errors.Is(err, semver.ErrInvalidSemVer) {
continue
}

tags = append(tags, v)
}
}

var comparer = func(a, b *semver.Version) int {
return a.Compare(b)
}

slices.SortFunc(tags, comparer)
latest := tags[len(tags)-1]

return latest.String(), nil
}

func runBundledInstaller(osVer string, workDir string, filename string, installPath string) error {
fmt.Printf("Running bundled installer from download URL\n")

cmd := ""
args := make([]string, 0)

switch osVer {
case "Darwin":
cmd = "installer"
pkgDir := fmt.Sprintf(" -pkg %s/%s", workDir, filename)
args = append(args, pkgDir, "-target /")
case "Linux":
cmd = fmt.Sprintf(".%s/install", workDir)
args = append(args, fmt.Sprintf("--bin-dir %s", installPath))
default:
if strings.HasPrefix(osVer, "Ming") {
cmd = "msiexec"
msiDir := fmt.Sprintf("%s/%s", workDir, filename)
args = append(args, msiDir, fmt.Sprintf("INSTALLDIR=%s", installPath))
}
}

installTask := execute.ExecTask{
Command: cmd,
Args: args,
StreamStdio: false,
}

result, err := installTask.Execute(context.Background())
if err != nil {
return err
}

if result.ExitCode != 0 {
return fmt.Errorf("error running bundled installer for platform, stderr: %s", result.Stderr)
}

return nil
}

func installationInstructions(osVer string, workDir string, filename string, installPath string) ([]byte, error) {
t := template.New("Installation Instructions")

switch osVer {
case "Darwin":
t.Parse(`# Run the downloaded .pkg installer, you will be prompted for authorisation
sudo installer -pkg {{.WorkDir}}/{{.Filename}} -target /
# Test the binary:
aws --version
`)
case "Linux":
t.Parse(`# Run the downloaded script installer, you will be prompted for authorisation
sudo ./{{.WorkDir}}/awscli/install --bin-dir {{.InstallPath}}
# Test the binary:
aws --version
`)
default:
if strings.HasPrefix(osVer, "Ming") {
t.Parse(`# Run the downloaded .msi installer
msiexec {{.WorkDir}}/{{.Filename}} INSTALLDIR={{.InstallPath}}
# Test the binary:
aws --version
`)
}
}

var tpl bytes.Buffer
var data = struct {
Filename string
WorkDir string
InstallPath string
}{
Filename: filename,
WorkDir: workDir,
InstallPath: installPath,
}

if err := t.Execute(&tpl, data); err != nil {
return nil, err
}

return tpl.Bytes(), nil
}
1 change: 1 addition & 0 deletions cmd/system/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func MakeInstall() *cobra.Command {
command.AddCommand(MakeInstallRegistry())
command.AddCommand(MakeInstallGitLabRunner())
command.AddCommand((MakeInstallBuildkitd()))
command.AddCommand(MakeInstallAWSCLI())

return command
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
Expand Down
Loading

0 comments on commit 35fd063

Please sign in to comment.