Skip to content

Commit

Permalink
Add system apps top-level command and Go package
Browse files Browse the repository at this point in the history
Part of #654, tested on an Intel x86_64 machine.

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <[email protected]>
  • Loading branch information
alexellis committed Jun 29, 2022
1 parent 98e0af3 commit 7c94f11
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 0 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ With over 52 helm charts and apps available for Kubernetes, gone are the days of
- [Getting arkade](#getting-arkade)
- [Usage overview](#usage-overview)
- [Download CLI tools with arkade](#download-cli-tools-with-arkade)
- [Install System packages](#install-system-packages)
- [Installing apps with arkade](#installing-apps-with-arkade)
- [Community & contributing](#community--contributing)
- [Sponsored apps](#sponsored-apps)
Expand Down Expand Up @@ -154,6 +155,26 @@ Adding a new tool for download is as simple as editing [tools.go](https://github

[Click here for the full catalog of CLIs](#catalog-of-apps)

## Install System packages

System packages, or "system apps" are tools designed for installation on a Linux workstation, server or CI runner.

These are a more limited group of applications designed for quick setup, scripting and CI, and generally do not fit into the `arkade get` pattern, due to additional installation steps or system configuration.

```bash
arkade system install --help

# Install latest version of Go to /usr/local/bin/go
arkade system install go

# Install Go 1.18 to /tmp/go
arkade system install go \
--version 1.18 \
--path /tmp/
```

System apps are in preview, see more details in the proposal: [Feature: system packages for Linux servers, CI and workstations #654](https://github.com/alexellis/arkade/issues/654)

## Installing apps with arkade

You'll need a Kubernetes cluster to arkade. Unlike cloud-based marketplaces, arkade doesn't have any special pre-requirements and can be used with any private or public cluster.
Expand Down
130 changes: 130 additions & 0 deletions cmd/system/go.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) arkade author(s) 2022. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

package system

import (
"fmt"
"io"
"net/http"
"os"
"path"
"strings"

"github.com/alexellis/arkade/pkg/archive"
"github.com/alexellis/arkade/pkg/env"
"github.com/alexellis/arkade/pkg/get"
"github.com/spf13/cobra"
)

func MakeInstallGo() *cobra.Command {

command := &cobra.Command{
Use: "go",
Short: "Install Go",
Long: `Install Go programming language and SDK.`,
Example: ` arkade system install go
arkade system install go --version v1.18.1`,
SilenceUsage: true,
}

command.Flags().StringP("version", "v", "", "The version for Go, or leave blank for pinned version")
command.Flags().String("path", "/usr/local/", "Installation path, where a go subfolder will be created")
command.Flags().Bool("progress", true, "Show download progress")

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")
fmt.Printf("Installing Go to %s\n", installPath)

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 strings.ToLower(osVer) != "linux" {
return fmt.Errorf("this app only supports Linux")
}

dlArch := arch
if arch == "x86_64" {
dlArch = "amd64"
} else if arch == "aarch64" {
dlArch = "arm64"
} else if arch == "armv7" || arch == "armv7l" {
dlArch = "armv6l"
}

if len(version) == 0 {
v, err := getGoVersion()
if err != nil {
return err
}

version = v
} else if !strings.HasPrefix(version, "go") {
version = "go" + version
}

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

dlURL := fmt.Sprintf("https://go.dev/dl/%s.%s-%s.tar.gz", version, strings.ToLower(osVer), dlArch)
fmt.Printf("Downloading from: %s\n", dlURL)

progress := true
outPath, err := get.DownloadFileP(dlURL, 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()

fmt.Printf("Unpacking Go to: %s\n", path.Join(installPath, "go"))

if err := archive.UntarNested(f, installPath); err != nil {
return err
}

fmt.Printf("\nexport PATH=$PATH:%s:$HOME/go/bin\n"+
"export GOPATH=$HOME/go/\n", path.Join(installPath, "go", "bin"))

return nil
}

return command
}

func getGoVersion() (string, error) {
req, err := http.NewRequest(http.MethodGet, "https://go.dev/VERSION?m=text", nil)
if err != nil {
return "", err
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}

if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
if res.Body == nil {
return "", fmt.Errorf("unexpected empty body")
}

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

return strings.TrimSpace(string(body)), nil
}
27 changes: 27 additions & 0 deletions cmd/system/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) arkade author(s) 2022. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

package system

import "github.com/spf13/cobra"

func MakeInstall() *cobra.Command {

command := &cobra.Command{
Use: "install",
Short: "Install system apps",
Long: `Install system apps for Linux hosts`,
Aliases: []string{"i"},
Example: ` arkade system install [APP]
arkade system install --help`,
SilenceUsage: true,
}

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

command.AddCommand(MakeInstallGo())

return command
}
30 changes: 30 additions & 0 deletions cmd/system/system.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) arkade author(s) 2020. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

// system contains a suite of Sponsored Apps for arkade
package system

import (
"github.com/spf13/cobra"
)

func MakeSystem() *cobra.Command {

command := &cobra.Command{
Use: "system",
Short: "System apps",
Long: `Apps for systems.`,
Aliases: []string{"s"},
Example: ` arkade system install [APP]
arkade s i [APP]`,
SilenceUsage: true,
}

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

command.AddCommand(MakeInstall())

return command
}
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/alexellis/arkade/cmd"
"github.com/alexellis/arkade/cmd/kasten"
"github.com/alexellis/arkade/cmd/system"
"github.com/alexellis/arkade/cmd/venafi"
"github.com/spf13/cobra"
)
Expand All @@ -33,6 +34,7 @@ func main() {

rootCmd.AddCommand(venafi.MakeVenafi())
rootCmd.AddCommand(kasten.MakeK10())
rootCmd.AddCommand(system.MakeSystem())

if err := rootCmd.Execute(); err != nil {
os.Exit(1)
Expand Down
129 changes: 129 additions & 0 deletions pkg/archive/untar_nested.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package archive

import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"log"
"os"
"path/filepath"
"time"
)

// Untar reads the gzip-compressed tar file from r and writes it into dir.
func UntarNested(r io.Reader, dir string) error {
return untarNested(r, dir)
}

func untarNested(r io.Reader, dir string) (err error) {
t0 := time.Now()
nFiles := 0
madeDir := map[string]bool{}
defer func() {
td := time.Since(t0)
if err == nil {
log.Printf("extracted tarball into %s: %d files, %d dirs (%v)", dir, nFiles, len(madeDir), td)
} else {
log.Printf("error extracting tarball into %s after %d files, %d dirs, %v: %v", dir, nFiles, len(madeDir), td, err)
}
}()
zr, err := gzip.NewReader(r)
if err != nil {
return fmt.Errorf("requires gzip-compressed body: %v", err)
}
tr := tar.NewReader(zr)
loggedChtimesError := false
for {
f, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
log.Printf("tar reading error: %v", err)
return fmt.Errorf("tar error: %v", err)
}
if !validRelPath(f.Name) {
return fmt.Errorf("tar contained invalid name error %q", f.Name)
}
rel := filepath.FromSlash(f.Name)
abs := filepath.Join(dir, rel)

fi := f.FileInfo()
mode := fi.Mode()
switch {
case mode.IsRegular():
// Make the directory. This is redundant because it should
// already be made by a directory entry in the tar
// beforehand. Thus, don't check for errors; the next
// write will fail with the same error.
dir := filepath.Dir(abs)
if !madeDir[dir] {
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
return err
}
madeDir[dir] = true
}
wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
if err != nil {
return err
}
n, err := io.Copy(wf, tr)
if closeErr := wf.Close(); closeErr != nil && err == nil {
err = closeErr
}
if err != nil {
return fmt.Errorf("error writing to %s: %v", abs, err)
}
if n != f.Size {
return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
}
modTime := f.ModTime
if modTime.After(t0) {
// Clamp modtimes at system time. See
// golang.org/issue/19062 when clock on
// buildlet was behind the gitmirror server
// doing the git-archive.
modTime = t0
}
if !modTime.IsZero() {
if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
// benign error. Gerrit doesn't even set the
// modtime in these, and we don't end up relying
// on it anywhere (the gomote push command relies
// on digests only), so this is a little pointless
// for now.
log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
loggedChtimesError = true // once is enough
}
}
nFiles++
case mode.IsDir():
if err := os.MkdirAll(abs, 0755); err != nil {
return err
}
madeDir[abs] = true
default:
return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode)
}
}
return nil
}

// func validRelativeDir(dir string) bool {
// if strings.Contains(dir, `\`) || path.IsAbs(dir) {
// return false
// }
// dir = path.Clean(dir)
// if strings.HasPrefix(dir, "../") || strings.HasSuffix(dir, "/..") || dir == ".." {
// return false
// }
// return true
// }

// func validRelPath(p string) bool {
// if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") {
// return false
// }
// return true
// }
4 changes: 4 additions & 0 deletions pkg/get/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ func Download(tool *Tool, arch, operatingSystem, version string, downloadMode in
return outFilePath, finalName, nil
}

func DownloadFileP(downloadURL string, displayProgress bool) (string, error) {
return downloadFile(downloadURL, displayProgress)
}

func downloadFile(downloadURL string, displayProgress bool) (string, error) {
res, err := http.DefaultClient.Get(downloadURL)
if err != nil {
Expand Down

0 comments on commit 7c94f11

Please sign in to comment.