Skip to content

Commit

Permalink
Add oci extract command
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Ellis (OpenFaaS Ltd) <[email protected]>
  • Loading branch information
alexellis committed Feb 15, 2024
1 parent 2f4355b commit d6c8509
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 16 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ With over 120 CLIs and 55 Kubernetes apps (charts, manifests, installers) availa
- [Usage overview](#usage-overview)
- [Download CLI tools with arkade](#download-cli-tools-with-arkade)
- [Install System Packages](#install-system-packages)
- [Install packages from OCI images](#install-packages-from-oci-images)
- [Install CLIs during CI with GitHub Actions](#install-clis-during-ci-with-github-actions)
- [Verify and upgrade images in Helm charts](#verify-and-upgrade-images-in-helm-charts)
- [Upgrade images within a Helm chart](#upgrade-images-within-a-helm-chart)
Expand Down Expand Up @@ -255,6 +256,21 @@ Run the following to see what's available `arkade system install`:

The initial set of system apps is now complete, learn more in the original proposal: [Feature: system packages for Linux servers, CI and workstations #654](https://github.com/alexellis/arkade/issues/654)

## Install Packages from OCI images

For packages distributed in Open Container Initiative (OCI) images, you can use `arkade oci install` to extract them to a given folder on your system.

vmmeter is one example of a package that is only published as a container image, which is not released on a GitHub releases page.

```bash
arkade oci install ghcr.io/openfaasltd/vmmeter \
--path /usr/local/bin
```

* `--path` - the folder to extract the package to
* `--version` - the version of the package to extract, if not specified the `:latest` tag is used
* `--arch` - the architecture to extract, if not specified the host's architecture is used

## Install CLIs during CI with GitHub Actions

* [alexellis/arkade-get@master](https://github.com/alexellis/arkade-get)
Expand Down
132 changes: 132 additions & 0 deletions cmd/oci/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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 oci

import (
"fmt"
"os"
"strings"
"time"

"github.com/alexellis/arkade/pkg/archive"
"github.com/alexellis/arkade/pkg/env"
"github.com/google/go-containerregistry/pkg/crane"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/spf13/cobra"
)

func MakeOciInstall() *cobra.Command {
command := &cobra.Command{
Use: "install",
Short: "Install the contents of an OCI image to a given path",
Long: `Use this command to install binaries or packages distributed within an
OCI image.`,
Example: ` # Install vmmeter to /usr/local/bin
arkade oci extract ghcr.io/openfaasltd/vmmeter
# Install a specific version of vmmeter to /tmp/
arkade oci extract ghcr.io/openfaasltd/vmmeter --path /tmp --version 0.1.0
# Install vmmeter for arm64 as an architecture override, instead of using uname
arkade oci extract ghcr.io/openfaasltd/vmmeter --arch arm64
`,
SilenceUsage: true,
}

command.Flags().StringP("version", "v", "latest", "The version or leave blank to determine the latest available version")
command.Flags().String("path", "/usr/local/bin", "Installation path, where a buildkitd subfolder will be created")
command.Flags().Bool("progress", true, "Show download progress")
command.Flags().String("arch", "", "CPU architecture i.e. amd64")
command.Flags().BoolP("gzipped", "g", false, "Is this a gzipped tarball?")
command.Flags().Bool("quiet", false, "Suppress progress output")

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")
gzipped, _ := cmd.Flags().GetBool("gzipped")
quiet, _ := cmd.Flags().GetBool("quiet")

if len(args) < 1 {
return fmt.Errorf("please provide an image name")
}

imageName := args[0]

if !strings.Contains(imageName, ":") {
imageName = imageName + ":" + version
}

st := time.Now()

fmt.Printf("Installing %s to %s\n", imageName, installPath)

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

clientArch, clientOS := env.GetClientArch()

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

tempFile, err := os.CreateTemp(os.TempDir(), "arkade-oci-")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(tempFile.Name())

f, err := os.Create(tempFile.Name())
if err != nil {
return fmt.Errorf("failed to open %s: %w", tempFile.Name(), err)
}
defer f.Close()

var img v1.Image

downloadArch, downloadOS := getDownloadArch(clientArch, clientOS)

img, err = crane.Pull(imageName, crane.WithPlatform(&v1.Platform{Architecture: downloadArch, OS: downloadOS}))
if err != nil {
return fmt.Errorf("pulling %s: %w", imageName, err)
}

if err := crane.Export(img, f); err != nil {
return fmt.Errorf("exporting %s: %w", imageName, err)
}

tarFile, err := os.Open(tempFile.Name())
if err != nil {
return fmt.Errorf("failed to open %s: %w", tempFile.Name(), err)
}
defer tarFile.Close()

if err := archive.UntarNested(tarFile, installPath, gzipped, quiet); err != nil {
return fmt.Errorf("failed to untar %s: %w", tempFile.Name(), err)
}

fmt.Printf("Took %s\n", time.Since(st).Round(time.Millisecond))

return nil
}

return command
}

func getDownloadArch(clientArch, clientOS string) (arch string, os string) {
downloadArch := strings.ToLower(clientArch)
downloadOS := strings.ToLower(clientOS)

if downloadArch == "x86_64" {
downloadArch = "amd64"
} else if downloadArch == "aarch64" {
downloadArch = "arm64"
}

return downloadArch, downloadOS
}
30 changes: 30 additions & 0 deletions cmd/oci/oci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.

// system contains packages for Linux servers and workstations
package oci

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

func MakeOci() *cobra.Command {

command := &cobra.Command{
Use: "oci",
Short: "oci apps",
Long: `Apps from OCI images.`,
Aliases: []string{"o"},
Example: ` arkade oci install [container image]
arkade oci i [container image]`,
SilenceUsage: true,
}

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

command.AddCommand(MakeOciInstall())

return command
}
2 changes: 1 addition & 1 deletion cmd/system/actions_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func MakeInstallActionsRunner() *cobra.Command {

fmt.Printf("Unpacking Actions Runner to: %s\n", path.Join(installPath, "actions-runner"))

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

Expand Down
6 changes: 3 additions & 3 deletions cmd/system/cni.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ func MakeInstallCNI() *cobra.Command {
command := &cobra.Command{
Use: "cni",
Short: "Install CNI plugins",
Long: `Install CNI plugins for use with faasd, CNI, Kubernetes, etc.`,
Long: `Install CNI plugins for use with faasd, actuated, Kubernetes, etc.`,
Example: ` arkade system install cni
arkade system install cni --version v1.0.0`,
arkade system install cni --version v1.4.0`,
SilenceUsage: true,
}

command.Flags().StringP("version", "v", "v0.8.5", "The version for CNI to install")
command.Flags().StringP("version", "v", "v1.4.0", "The version for CNI to install")
command.Flags().StringP("path", "p", "/opt/cni/bin/", "Installation path, where a go subfolder will be created")
command.Flags().Bool("progress", true, "Show download progress")

Expand Down
2 changes: 1 addition & 1 deletion cmd/system/containerd.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func MakeInstallContainerd() *cobra.Command {

tempDirName := os.TempDir() + "/containerd"

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

Expand Down
2 changes: 1 addition & 1 deletion cmd/system/go.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func MakeInstallGo() *cobra.Command {

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

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

Expand Down
2 changes: 1 addition & 1 deletion cmd/system/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func MakeInstallNode() *cobra.Command {
}
defer os.RemoveAll(tempUnpackPath)
fmt.Printf("Unpacking binaries to: %s\n", tempUnpackPath)
if err = archive.UntarNested(f, tempUnpackPath); err != nil {
if err = archive.UntarNested(f, tempUnpackPath, true, false); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/system/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func MakeInstallRegistry() *cobra.Command {

tempDirName := fmt.Sprintf("%s/%s", os.TempDir(), toolName)
defer os.RemoveAll(tempDirName)
if err := archive.UntarNested(f, tempDirName); err != nil {
if err := archive.UntarNested(f, tempDirName, true, false); err != nil {
return err
}

Expand Down
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/chart"
"github.com/alexellis/arkade/cmd/oci"
"github.com/alexellis/arkade/cmd/system"
"github.com/spf13/cobra"
)
Expand All @@ -33,6 +34,7 @@ func main() {

rootCmd.AddCommand(chart.MakeChart())
rootCmd.AddCommand(system.MakeSystem())
rootCmd.AddCommand(oci.MakeOci())

if err := rootCmd.Execute(); err != nil {
os.Exit(1)
Expand Down
28 changes: 20 additions & 8 deletions pkg/archive/untar_nested.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,36 @@ import (
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
func UntarNested(r io.Reader, dir string) error {
return untarNested(r, dir)
func UntarNested(r io.Reader, dir string, gzipped, quiet bool) error {
return untarNested(r, dir, gzipped, quiet)
}

func untarNested(r io.Reader, dir string) (err error) {
func untarNested(r io.Reader, dir string, gzipped, quiet bool) (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)
if !quiet {
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)

reader := r

if gzipped {
zr, err := gzip.NewReader(r)
if err != nil {
return fmt.Errorf("requires gzip-compressed body: %v", err)
}
reader = zr
}
tr := tar.NewReader(zr)

tr := tar.NewReader(reader)
loggedChtimesError := false
for {
f, err := tr.Next()
Expand All @@ -54,6 +63,9 @@ func untarNested(r io.Reader, dir string) (err error) {

fi := f.FileInfo()
mode := fi.Mode()
if !quiet {
fmt.Printf("Extracting: %s\n", abs)
}
switch {
case mode.IsRegular():
// Make the directory. This is redundant because it should
Expand Down

0 comments on commit d6c8509

Please sign in to comment.