Skip to content

Commit

Permalink
sshiofs, a io/fs.FS implementaion for ssh with sudo
Browse files Browse the repository at this point in the history
Signed-off-by: Artiom Diomin <[email protected]>
  • Loading branch information
kron4eg committed Apr 22, 2021
1 parent d386916 commit a7f17c2
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 10 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module k8c.io/kubeone

go 1.14
go 1.16

require (
github.com/BurntSushi/toml v0.3.1
Expand Down
3 changes: 0 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,6 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
Expand Down Expand Up @@ -758,7 +757,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
Expand Down Expand Up @@ -1036,7 +1034,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.1.0 h1:Phva6wqu+xR//Njw6iorylFFgn/z547tw5Ne3HZPQ+k=
gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU=
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
Expand Down
8 changes: 7 additions & 1 deletion pkg/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package runner

import (
"io/fs"
"os"

"github.com/koron-go/prefixw"
Expand All @@ -25,6 +26,7 @@ import (
kubeoneapi "k8c.io/kubeone/pkg/apis/kubeone"
"k8c.io/kubeone/pkg/scripts"
"k8c.io/kubeone/pkg/ssh"
"k8c.io/kubeone/pkg/ssh/sshiofs"
)

// Runner bundles a connection to a host with the verbosity and
Expand All @@ -39,6 +41,10 @@ type Runner struct {
// TemplateVariables is a render context for templates
type TemplateVariables map[string]interface{}

func (r *Runner) NewFS() fs.FS {
return sshiofs.New(r.Conn)
}

func (r *Runner) RunRaw(cmd string) (string, string, error) {
if r.Conn == nil {
return "", "", errors.New("runner is not tied to an opened SSH connection")
Expand All @@ -60,7 +66,7 @@ func (r *Runner) RunRaw(cmd string) (string, string, error) {
defer stderr.Close()

// run the command
_, err := r.Conn.Stream(cmd, stdout, stderr)
_, err := r.Conn.POpen(cmd, nil, stdout, stderr)

return stdout.String(), stderr.String(), err
}
Expand Down
6 changes: 1 addition & 5 deletions pkg/ssh/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ var (
type Connection interface {
Exec(cmd string) (stdout string, stderr string, exitCode int, err error)
File(filename string, flags int) (io.ReadWriteCloser, error)
Stream(cmd string, stdout io.Writer, stderr io.Writer) (exitCode int, err error)
POpen(cmd string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (exitCode int, err error)
io.Closer
}

Expand Down Expand Up @@ -295,10 +295,6 @@ func (c *connection) POpen(cmd string, stdin io.Reader, stdout io.Writer, stderr
return exitCode, err
}

func (c *connection) Stream(cmd string, stdout io.Writer, stderr io.Writer) (int, error) {
return c.POpen(cmd, nil, stdout, stderr)
}

func (c *connection) Exec(cmd string) (string, string, int, error) {
var stdoutBuf, stderrBuf strings.Builder

Expand Down
105 changes: 105 additions & 0 deletions pkg/ssh/sshiofs/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
Copyright 2021 The KubeOne Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package sshiofs

import (
"bytes"
"fmt"
"io"
"io/fs"
"strings"

"k8c.io/kubeone/pkg/ssh"
)

type sshfile struct {
conn ssh.Connection
name string
cursor int64
fi fs.FileInfo
}

func (sf *sshfile) Stat() (fs.FileInfo, error) {
if sf.fi != nil {
return sf.fi, nil
}

fi, err := newSSHFileInfo(sf.name, sf.conn)
sf.fi = fi
return fi, err
}

func (sf *sshfile) Read(p []byte) (int, error) {
const cmdTpl = `sudo dd status=none iflag=count_bytes,skip_bytes skip=%d count=%d if=%q`
var (
stdout bytes.Buffer
stderr bytes.Buffer
)

cmd := fmt.Sprintf(cmdTpl, sf.cursor, len(p), sf.name)
_, err := sf.conn.POpen(cmd, nil, &stdout, &stderr)
if err != nil {
return 0, &fs.PathError{
Op: "read",
Path: sf.name,
Err: fmt.Errorf("%s %w", stderr.String(), err),
}
}

n, err := stdout.Read(p)
sf.cursor += int64(n)
return n, err
}

func (sf *sshfile) Write(p []byte) (int, error) {
const cmdTpl = `sudo dd status=none oflag=seek_bytes conv=notrunc seek=%d of=%q`
cmd := fmt.Sprintf(cmdTpl, sf.cursor, sf.name)

var (
stdin = bytes.NewBuffer(p)
stdout strings.Builder
stderr strings.Builder
)

_, err := sf.conn.POpen(cmd, stdin, &stdout, &stderr)
if err != nil {
return 0, fmt.Errorf("%w: %v %v", err, stderr.String(), stdout.String())
}

n, err := stdout.Write(p)
sf.cursor += int64(n)
return n, err
}

func (sf *sshfile) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
sf.cursor = offset
case io.SeekCurrent:
sf.cursor += offset
case io.SeekEnd:
return sf.cursor, fmt.Errorf("io.SeekEnd unimplemented")
}

return sf.cursor, nil
}

func (sf *sshfile) Close() error {
sf.cursor = 0
sf.fi = nil
return nil
}
136 changes: 136 additions & 0 deletions pkg/ssh/sshiofs/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
Copyright 2021 The KubeOne Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package sshiofs

import (
"bytes"
"fmt"
"io/fs"
"strings"
"time"

"k8c.io/kubeone/pkg/ssh"
)

func New(conn ssh.Connection) fs.FS {
return &sshfs{conn: conn}
}

type sshfs struct {
conn ssh.Connection
}

func (sfs *sshfs) Open(name string) (fs.File, error) {
var hadSlashPrefix bool
if strings.HasPrefix(name, "/") {
name = strings.TrimPrefix(name, "/")
hadSlashPrefix = true
}
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fs.ErrInvalid,
}
}
if hadSlashPrefix {
name = "/" + name
}

return &sshfile{
conn: sfs.conn,
name: name,
}, nil
}

func (sfs *sshfs) ReadFile(name string) ([]byte, error) {
var buf bytes.Buffer
_, err := sfs.conn.POpen(fmt.Sprintf("sudo cat %q", name), nil, &buf, nil)
if err != nil {
return nil, err
}

return buf.Bytes(), nil
}

// TODO: implement
// func (sfs *sshfs) ReadDir(name string) ([]fs.DirEntry, error) {
// return nil, nil
// }

func newSSHFileInfo(name string, conn ssh.Connection) (fs.FileInfo, error) {
const statCmd = "sudo stat --printf='%%s %%f %%Y' %q"

var (
stdout strings.Builder
stderr strings.Builder
)

cmd := fmt.Sprintf(statCmd, name)
exitCode, err := conn.POpen(cmd, nil, &stdout, &stderr)
if exitCode != 0 || err != nil {
return nil, &fs.PathError{
Op: "open",
Path: name,
Err: fmt.Errorf("%s %s %w", stderr.String(), err.Error(), fs.ErrNotExist),
}
}

var (
size int64
mode fs.FileMode
modTime int64
)

fia := strings.Split(stdout.String(), " ")
if len(fia) != 3 {
return nil, fs.ErrInvalid
}

if _, err = fmt.Sscanf(fia[0], "%d", &size); err != nil {
return nil, err
}

if _, err = fmt.Sscanf(fia[1], "%x", &mode); err != nil {
return nil, err
}

if _, err = fmt.Sscanf(fia[2], "%d", &modTime); err != nil {
return nil, err
}

return &fileInfo{
name: name,
size: size,
mode: mode,
time: time.Unix(modTime, 0),
}, nil
}

type fileInfo struct {
name string
size int64
mode fs.FileMode
time time.Time
}

func (fi *fileInfo) Size() int64 { return fi.size }
func (fi *fileInfo) Mode() fs.FileMode { return fi.mode }
func (fi *fileInfo) ModTime() time.Time { return fi.time }
func (fi *fileInfo) Name() string { return fi.name }
func (fi *fileInfo) IsDir() bool { return fi.mode.IsDir() }
func (*fileInfo) Sys() interface{} { return nil }

0 comments on commit a7f17c2

Please sign in to comment.