Skip to content

Commit

Permalink
feat(airgap): Basic airgap cli function
Browse files Browse the repository at this point in the history
As design, following actions are supported:
- create
- update
- remove
- ls
- import
- export
- update-install-script

The downloaded resource will be stored in package dir under CfgPath.
The design can be found in
cnrancher#480.
  • Loading branch information
orangedeng authored and Jason-ZW committed Aug 18, 2022
1 parent e6ca11a commit b921cce
Show file tree
Hide file tree
Showing 27 changed files with 1,541 additions and 10 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ static/
upx*.tar.xz
upx*/
.vscode/
.dapper
.dapper

# Others
pkg/settings/install.sh
26 changes: 26 additions & 0 deletions cmd/airgap/airgap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package airgap

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

var (
airgap = &cobra.Command{
Use: "airgap",
Short: "The airgap packages management.",
Long: "The airgap command manages the airgap package for k3s.",
}
)

func Command() *cobra.Command {
airgap.AddCommand(
listCmd,
createCmd,
removeCmd,
updateCmd,
importCmd,
exportCmd,
updateScriptCmd,
)
return airgap
}
44 changes: 44 additions & 0 deletions cmd/airgap/args.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package airgap

import (
"fmt"

pkgairgap "github.com/cnrancher/autok3s/pkg/airgap"
"github.com/cnrancher/autok3s/pkg/common"

"github.com/AlecAivazis/survey/v2"
"k8s.io/apimachinery/pkg/util/validation"
)

var (
airgapFlags = flags{}
)

type flags struct {
isForce bool
isJSON bool
K3sVersion string
Archs []string
}

func getArchSelect(def []string) *survey.MultiSelect {
return &survey.MultiSelect{
Default: def,
Message: "What arch do you prefer?",
Options: pkgairgap.GetValidatedArchs(),
}
}

func validateName(name string) error {
if name == "" {
return errNameRequire
}
pkgs, _ := common.DefaultDB.ListPackages(&name)
if len(pkgs) > 0 {
return fmt.Errorf("package name %s already exists", name)
}
if errs := validation.IsDNS1123Subdomain(name); len(errs) > 0 {
return fmt.Errorf("name is not validated %s, %v", name, errs)
}
return nil
}
95 changes: 95 additions & 0 deletions cmd/airgap/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package airgap

import (
"sort"
"strings"

pkgairgap "github.com/cnrancher/autok3s/pkg/airgap"
"github.com/cnrancher/autok3s/pkg/common"
"github.com/cnrancher/autok3s/pkg/utils"

"github.com/AlecAivazis/survey/v2"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

var createCmd = &cobra.Command{
Use: "create <name>",
Short: "Create a new airgap package and will download related resources from internet.",
Args: cobra.ExactArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
sort.Strings(airgapFlags.Archs)
},
RunE: create,
}

func init() {
createCmd.Flags().StringVarP(&airgapFlags.K3sVersion, "k3s-version", "v", airgapFlags.K3sVersion, "The version of k3s to store airgap resources.")
createCmd.Flags().StringArrayVar(&airgapFlags.Archs, "arch", airgapFlags.Archs, "The archs of the k3s version. Following archs are support: "+strings.Join(pkgairgap.GetValidatedArchs(), ",")+".")
}

func create(cmd *cobra.Command, args []string) error {
name := args[0]
if err := validateName(name); err != nil {
return err
}

var qs []*survey.Question

if airgapFlags.K3sVersion == "" {
if !utils.IsTerm() {
return errors.New("k3s-version flags is required")
}
qs = append(qs, &survey.Question{
Name: "k3sVersion",
Prompt: &survey.Input{Message: "K3s Version?"},
Validate: survey.Required,
})
}

if len(airgapFlags.Archs) == 0 {
if !utils.IsTerm() {
return errors.New("at least one arch should be specified")
}
qs = append(qs, &survey.Question{
Name: "archs",
Prompt: getArchSelect([]string{}),
Validate: survey.Required,
})
}

if err := survey.Ask(qs, &airgapFlags); err != nil {
return err
}

if err := pkgairgap.ValidateArchs(airgapFlags.Archs); err != nil {
return err
}

pkg := common.Package{
Name: name,
K3sVersion: airgapFlags.K3sVersion,
Archs: airgapFlags.Archs,
State: common.PackageOutOfSync,
}

if err := common.DefaultDB.SavePackage(pkg); err != nil {
return err
}
cmd.Printf("airgap package %s record created, prepare to download\n", pkg.Name)
downloader, err := pkgairgap.NewDownloader(pkg)
if err != nil {
return errors.Wrapf(err, "failed to start download process for package %s", pkg.Name)
}
path, err := downloader.DownloadPackage()
if err != nil {
return errors.Wrapf(err, "failed to download package %s", pkg.Name)
}
pkg.FilePath = path
pkg.State = common.PackageActive
if err := common.DefaultDB.SavePackage(pkg); err != nil {
return err
}
cmd.Printf("airgap package %s created and stored in path %s\n", name, path)
return nil
}
72 changes: 72 additions & 0 deletions cmd/airgap/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package airgap

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

pkgairgap "github.com/cnrancher/autok3s/pkg/airgap"
"github.com/cnrancher/autok3s/pkg/common"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"gorm.io/gorm"
)

var (
exportCmd = &cobra.Command{
Use: "export <name> <path>",
Short: "export package to a tar.gz file, path can be a specific filename or a directory.",
Args: cobra.ExactArgs(2),
RunE: export,
}
errPathInvalid = errors.New("path should be an existing directory or a file with .tar.gz/tgz suffix")
)

func export(cmd *cobra.Command, args []string) error {
name := args[0]
pkgs, err := common.DefaultDB.ListPackages(&name)
if err == gorm.ErrRecordNotFound {
return fmt.Errorf("package %s not found", name)
}
if err != nil {
return err
}
targetPackage := pkgs[0]
if targetPackage.State != common.PackageActive {
return fmt.Errorf("package %s is not active, airgap resources maybe missing", name)
}

path := args[1]

info, err := os.Lstat(path)
if err != nil && !os.IsNotExist(err) {
return err
}

// check file name if path not exist.
if os.IsNotExist(err) {
base := filepath.Base(path)
if !strings.HasSuffix(base, ".tgz") &&
!strings.HasSuffix(base, ".tar.gz") {
return errPathInvalid
}
if _, err := os.Lstat(filepath.Dir(path)); err != nil {
return err
}
// here means path is a file with tar.gz/tgz suffix and parent dir exists
} else if !info.IsDir() {
// here means that input path is a regular file and should return error
return errPathInvalid
} else {
// here means that the input path is a dir and will use package name as the output name
path = filepath.Join(path, name+".tar.gz")
}

if err := pkgairgap.TarAndGzip(targetPackage.FilePath, path); err != nil {
return err
}
cmd.Printf("package %s export to %s succeed\n", name, path)
return nil
}
82 changes: 82 additions & 0 deletions cmd/airgap/import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package airgap

import (
"errors"
"os"

pkgairgap "github.com/cnrancher/autok3s/pkg/airgap"
"github.com/cnrancher/autok3s/pkg/common"

"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
)

var (
importCmd = &cobra.Command{
Use: "import <path> [name]",
Short: "Import an existing tar.gz file of airgap package. Please refer to export command",
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.MaximumNArgs(2)(cmd, args); err != nil {
return err
}
if err := cobra.MinimumNArgs(1)(cmd, args); err != nil {
return err
}
return nil
},
RunE: importFunc,
}
errNameRequire = errors.New("name is required for importing a airgap package")
)

func importFunc(cmd *cobra.Command, args []string) error {
path := args[0]
var name string
var err error
if len(args) < 2 {
name, err = askForName()
if err != nil {
return err
}
} else {
name = args[1]
}

if name == "" {
return errNameRequire
}
tmpPath, err := pkgairgap.SaveToTmp(path, name)
if err != nil {
return err
}
defer os.RemoveAll(tmpPath)

toSave, err := pkgairgap.VerifyFiles(tmpPath)
if err != nil {
return err
}

toSave.Name = name
toSave.FilePath = pkgairgap.PackagePath(name)
if err := os.Rename(tmpPath, toSave.FilePath); err != nil {
return err
}
toSave.State = common.PackageActive
if err := common.DefaultDB.SavePackage(*toSave); err != nil {
_ = os.RemoveAll(toSave.FilePath)
return err
}
cmd.Printf("package %s imported from %s\n", name, path)
return nil
}

func askForName() (string, error) {
rtn := ""
err := survey.AskOne(&survey.Input{
Message: "Please input the package name",
}, &rtn, survey.WithValidator(func(ans interface{}) error {
name, _ := ans.(string)
return validateName(name)
}))
return rtn, err
}
56 changes: 56 additions & 0 deletions cmd/airgap/ls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package airgap

import (
"encoding/json"
"os"
"strings"

"github.com/cnrancher/autok3s/pkg/common"

"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

var (
listCmd = &cobra.Command{
Use: "ls",
Short: "List all stored airgap packages.",
RunE: list,
}
)

func init() {
listCmd.Flags().BoolVarP(&airgapFlags.isJSON, "json", "j", airgapFlags.isJSON, "json output")
}

func list(cmd *cobra.Command, args []string) error {
pkgs, err := common.DefaultDB.ListPackages(nil)
if err != nil {
return errors.Wrap(err, "failed to list airgap packages.")
}
if airgapFlags.isJSON {
data, err := json.Marshal(pkgs)
if err != nil {
return err
}
cmd.Printf("%s\n", string(data))
return nil
}
table := tablewriter.NewWriter(os.Stdout)
table.SetBorder(false)
table.SetHeaderLine(false)
table.SetColumnSeparator("")
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetHeader([]string{"Name", "K3sVersion", "Archs", "State"})
for _, pkg := range pkgs {
table.Append([]string{
pkg.Name,
pkg.K3sVersion,
strings.Join(pkg.Archs, ","),
string(pkg.State),
})
}
table.Render()
return nil
}
Loading

0 comments on commit b921cce

Please sign in to comment.