Skip to content

Commit

Permalink
feat(autok3s): support airgap install k3s
Browse files Browse the repository at this point in the history
When creating or upgrading cluster, you can specify the packageName or
packagePath to use airgap install.
  • Loading branch information
orangedeng authored and Jason-ZW committed Aug 18, 2022
1 parent b921cce commit c4685d2
Show file tree
Hide file tree
Showing 17 changed files with 793 additions and 356 deletions.
6 changes: 5 additions & 1 deletion cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ var (
channel = ""
version = ""
installScript = ""
uPackageName = ""
uPackagePath = ""
)

func init() {
Expand All @@ -25,6 +27,8 @@ func init() {
upgradeCmd.Flags().StringVarP(&channel, "k3s-channel", "", channel, "Channel to use for fetching K3s download URL. Defaults to “stable”. Options include: stable, latest, testing")
upgradeCmd.Flags().StringVarP(&version, "k3s-version", "", version, "Used to specify the version of k3s cluster, overrides k3s-channel")
upgradeCmd.Flags().StringVarP(&installScript, "k3s-install-script", "", installScript, "Change the default upstream k3s install script address, see: https://rancher.com/docs/k3s/latest/en/installation/install-options/#options-for-installation-with-script")
upgradeCmd.Flags().StringVarP(&uPackageName, "package-name", "", uPackageName, "Airgap package name which you want to upgrade k3s with")
upgradeCmd.Flags().StringVarP(&uPackagePath, "package-path", "", uPackagePath, "Airgap package path which you want to upgrade k3s with")
}

// UpgradeCommand help upgrade a K3s cluster to specified version
Expand Down Expand Up @@ -52,7 +56,7 @@ func upgradeCluster() {
if err != nil {
logrus.Fatalf("failed to get provider %v: %v", uProvider, err)
}
err = up.UpgradeK3sCluster(clusterName, installScript, channel, version)
err = up.UpgradeK3sCluster(clusterName, installScript, channel, version, uPackageName, uPackagePath)
if err != nil {
logrus.Fatalf("[%s] failed to upgrade cluster %s, got error: %v", uProvider, clusterName, err)
}
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ require (
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.1
github.com/tencentcloud/tencentcloud-sdk-go v1.0.34
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/net v0.0.0-20211209124913-491a49abca63
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
google.golang.org/api v0.62.0
gopkg.in/yaml.v3 v3.0.0
gorm.io/gorm v1.23.4
Expand Down Expand Up @@ -167,6 +167,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kubernetes-csi/external-snapshotter/v2 v2.1.3 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/lithammer/dedent v1.1.0 // indirect
Expand Down Expand Up @@ -197,6 +198,7 @@ require (
github.com/openshift/custom-resource-status v0.0.0-20200602122900-c002fd1547ca // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/sftp v1.13.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
Expand Down Expand Up @@ -1590,6 +1591,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.3.0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
Expand Down Expand Up @@ -2228,6 +2231,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down Expand Up @@ -2370,6 +2375,7 @@ golang.org/x/sys v0.0.0-20211112143042-c6105e7cf70d/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
15 changes: 0 additions & 15 deletions pkg/airgap/download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,6 @@ func TestGetExt(t *testing.T) {
name, ext := getExt(toTest)
assert.Equal(t, targetName, name)
assert.Equal(t, targetExt, ext)

// targetAMD64Map := map[string][]string{
// "k3s": {""},
// "k3s-airgap-images": {"-amd64.tar.gz", "-amd64.tar"},
// checksumBaseName: {"-amd64.txt"},
// }
// targetARM64Map := map[string][]string{
// "k3s": {"-arm64"},
// "k3s-airgap-images": {"-arm64.tar.gz", "-arm64.tar"},
// checksumBaseName: {"-arm64.txt"},
// }
// amd64Map := getResourceMapWithArch("amd64")
// arm64Map := getResourceMapWithArch("arm64")
// assert.Equal(t, targetAMD64Map, amd64Map)
// assert.Equal(t, targetARM64Map, arm64Map)
}

func TestSuffixWithArch(t *testing.T) {
Expand Down
285 changes: 285 additions & 0 deletions pkg/airgap/file_scp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
package airgap

import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sync"

"github.com/cnrancher/autok3s/pkg/common"
"github.com/cnrancher/autok3s/pkg/hosts"
"github.com/cnrancher/autok3s/pkg/settings"
"github.com/cnrancher/autok3s/pkg/types"

"github.com/pkg/errors"
"github.com/pkg/sftp"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)

type fileMap struct {
mode os.FileMode
targetPath string
}

const (
installScriptName = "install.sh"
)

var (
errArchNotSupport = errors.New("arch not support")
unameCommand = "uname -m"
remoteTmpDir = "/tmp/autok3s"
// mapping file kind to real filename in remote host
remoteFileMap = map[string]fileMap{
"k3s": {
mode: 0755,
targetPath: "/usr/local/bin",
},
"k3s-airgap-images": {
mode: 0644,
targetPath: "/var/lib/rancher/k3s/agent/images",
},
installScriptName: {
mode: 0755,
targetPath: "/usr/local/bin",
},
}
)

func ScpFiles(clusterName string, pkg *common.Package, dialer *hosts.SSHDialer) error {
conn := dialer.GetClient()
fieldLogger := logrus.WithFields(logrus.Fields{
"cluster": clusterName,
"component": "airgap",
})
installScript := settings.InstallScript.Get()
if installScript == "" {
return errors.New("install script must be configured")
}

arch, err := getRemoteArch(conn)
if err != nil {
return err
}
if ok, _ := ValidatedArch[arch]; !ok {
return errors.Wrapf(errArchNotSupport, "remote server arch: %s", arch)
}
if !pkg.Archs.Contains(arch) {
return fmt.Errorf("%s resource doesn't exist in package %s", arch, packagePath)
}

fieldLogger.Infof("Get remote server arch %s", arch)
files, err := getScpFileMap(arch, pkg)
if err != nil {
return err
}

scpClient, err := sftp.NewClient(conn)
if err != nil {
return err
}
defer scpClient.Close()

fieldLogger.Infof("connected to remote server %s with sftp", conn.RemoteAddr())

//scp files to tmp dir
tmpDir := getRemoteTmpDir(clusterName)
if err := scpClient.MkdirAll(tmpDir); err != nil {
return err
}
defer scpClient.RemoveDirectory(tmpDir)

for local, remote := range files {
filename := filepath.Base(local)
remoteFileName := filepath.Join(tmpDir, filename)
var source io.Reader
if local == installScriptName {
source = bytes.NewBufferString(installScript)
} else {
fp, err := os.Open(local)
if err != nil {
return err
}
defer fp.Close()
source = fp
}
rfp, err := scpClient.Create(remoteFileName)
if err != nil {
return err
}
defer rfp.Close()
fieldLogger.Infof("local file %s", local)
fieldLogger.Infof("copy to remote %s", remoteFileName)
if _, err := io.Copy(rfp, source); err != nil {
return err
}
fieldLogger.Infof("setting file %s mode, %s", filename, remote.mode)
if err := scpClient.Chmod(remoteFileName, remote.mode); err != nil {
return err
}

targetFilename := filepath.Join(remote.targetPath, filename)
var stdout, stderr bytes.Buffer
dialer = dialer.SetStdio(&stdout, &stderr, nil)
moveCMD := fmt.Sprintf("sudo mkdir -p %s;sudo mv %s %s", remote.targetPath, remoteFileName, targetFilename)
fieldLogger.Infof("executing cmd in remote server %s", moveCMD)
if err := dialer.Cmd(moveCMD).Run(); err != nil {
fieldLogger.Errorf("failed to execute cmd %s, stdout: %s, stderr: %s, %v", moveCMD, stdout.String(), stderr.String(), err)
return err
}
fieldLogger.Infof("file moved to %s", targetFilename)
fieldLogger.Infof("remote file %s transferred", filename)
}

fieldLogger.Info("all files transferred")
return nil
}

func PreparePackage(cluster *types.Cluster) (*common.Package, error) {
clusterName := cluster.Name
packageName := cluster.PackageName
packagePath := cluster.PackagePath

if packageName == "" && packagePath == "" {
return nil, nil
}

if packagePath == "" && packageName != "" {
pkgs, err := common.DefaultDB.ListPackages(&packageName)
if err != nil {
return nil, err
}
return &pkgs[0], nil
}

fieldLogger := logrus.WithFields(logrus.Fields{
"cluster": clusterName,
"component": "airgap",
})
info, err := os.Lstat(packagePath)
if err != nil {
return nil, err
}
var tmpPath, currentPath string
if !info.IsDir() {
tmpPath, err = SaveToTmp(packagePath, "cluster-"+clusterName)
if err != nil {
_ = os.RemoveAll(tmpPath)
return nil, err
}
fieldLogger.Infof("created tmp directory %s for package %s, will be removed after", tmpPath, packagePath)
currentPath = tmpPath
} else {
currentPath = packagePath
}

rtn, err := VerifyFiles(currentPath)
if err != nil {
if tmpPath != "" {
_ = os.RemoveAll(currentPath)
}
return nil, err
}
rtn.FilePath = currentPath
fieldLogger.Infof("airgap package %s validated", packagePath)
return rtn, nil
}

func getRemoteArch(conn *ssh.Client) (string, error) {
session, err := conn.NewSession()
if err != nil {
return "", err
}
defer session.Close()

stdoutPipe, err := session.StdoutPipe()
if err != nil {
return "", err
}
stderrPipe, err := session.StderrPipe()
if err != nil {
return "", err
}

outWriter := bytes.NewBuffer([]byte{})
errWriter := bytes.NewBuffer([]byte{})

wg := sync.WaitGroup{}

wg.Add(1)
go func() {
_, _ = io.Copy(outWriter, stdoutPipe)
wg.Done()
}()

wg.Add(1)
go func() {
_, _ = io.Copy(errWriter, stderrPipe)
wg.Done()
}()

err = session.Run(unameCommand)

wg.Wait()
if err != nil {
return "", err
}
remoteErr := errWriter.String()
if remoteErr != "" {
return "", fmt.Errorf("got errors from remote server, %s", remoteErr)
}
bufferReader := bufio.NewReader(outWriter)
line, _, err := bufferReader.ReadLine()
if err != nil && err != io.EOF {
return "", err
} else if err == io.EOF {
line = []byte{}
}
return parseUnameArch(string(line)), nil
}

func parseUnameArch(output string) string {
switch output {
case "x86_64":
return "amd64"
case "aarh64":
return "arm64"
case "armv7l":
return "arm"
default:
return output
}
}

// getScpFileMap will return the local file path for specific arch to target file map
func getScpFileMap(arch string, pkg *common.Package) (map[string]fileMap, error) {
var rtn = make(map[string]fileMap, len(remoteFileMap))
archBasePath := filepath.Join(pkg.FilePath, arch)
for key, file := range remoteFileMap {
if key == installScriptName {
rtn[installScriptName] = file
continue
}
hasFile := false
for _, suffix := range resourceSuffixes[key] {
filename := filepath.Join(archBasePath, key+suffix)
if _, err := os.Lstat(filename); err != nil {
continue
}
hasFile = true
rtn[filename] = file
}
if !hasFile {
return nil, fmt.Errorf("resource file %s is missing in package %s", key, pkg.FilePath)
}
}
return rtn, nil
}

func getRemoteTmpDir(clustername string) string {
return filepath.Join(remoteTmpDir, clustername)
}

0 comments on commit c4685d2

Please sign in to comment.