mirror of https://github.com/k3s-io/k3s
Browse Source
* Add cert rotation command Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com> * add function to check for dynamic listener file Signed-off-by: Brian Downs <brian.downs@gmail.com> * Add dynamiclistener cert rotation support Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com> * fixes to the cert rotation Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com> * fix ci tests Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com> * fixes to certificate rotation command Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com> * more fixes Signed-off-by: galal-hussein <hussein.galal.ahmed.11@gmail.com> Co-authored-by: Brian Downs <brian.downs@gmail.com>pull/4636/head
Hussein Galal
3 years ago
committed by
GitHub
30 changed files with 943 additions and 5 deletions
@ -0,0 +1,27 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"os" |
||||
|
||||
"github.com/rancher/k3s/pkg/cli/cert" |
||||
"github.com/rancher/k3s/pkg/cli/cmds" |
||||
"github.com/rancher/k3s/pkg/configfilearg" |
||||
"github.com/sirupsen/logrus" |
||||
"github.com/urfave/cli" |
||||
) |
||||
|
||||
func main() { |
||||
app := cmds.NewApp() |
||||
app.Commands = []cli.Command{ |
||||
cmds.NewCertCommand( |
||||
cmds.NewCertSubcommands( |
||||
cert.Run), |
||||
), |
||||
} |
||||
|
||||
if err := app.Run(configfilearg.MustParse(os.Args)); err != nil && !errors.Is(err, context.Canceled) { |
||||
logrus.Fatal(err) |
||||
} |
||||
} |
@ -0,0 +1,221 @@
|
||||
package cert |
||||
|
||||
import ( |
||||
"errors" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"strconv" |
||||
"time" |
||||
|
||||
"github.com/erikdubbelboer/gspt" |
||||
"github.com/otiai10/copy" |
||||
"github.com/rancher/k3s/pkg/cli/cmds" |
||||
"github.com/rancher/k3s/pkg/daemons/config" |
||||
"github.com/rancher/k3s/pkg/daemons/control/deps" |
||||
"github.com/rancher/k3s/pkg/datadir" |
||||
"github.com/rancher/k3s/pkg/server" |
||||
"github.com/rancher/k3s/pkg/version" |
||||
"github.com/sirupsen/logrus" |
||||
"github.com/urfave/cli" |
||||
) |
||||
|
||||
const ( |
||||
adminService = "admin" |
||||
apiServerService = "api-server" |
||||
controllerManagerService = "controller-manager" |
||||
schedulerService = "scheduler" |
||||
etcdService = "etcd" |
||||
programControllerService = "-controller" |
||||
authProxyService = "auth-proxy" |
||||
cloudControllerService = "cloud-controller" |
||||
kubeletService = "kubelet" |
||||
kubeProxyService = "kube-proxy" |
||||
k3sServerService = "-server" |
||||
) |
||||
|
||||
func commandSetup(app *cli.Context, cfg *cmds.Server, sc *server.Config) (string, string, error) { |
||||
gspt.SetProcTitle(os.Args[0]) |
||||
|
||||
sc.ControlConfig.DataDir = cfg.DataDir |
||||
sc.ControlConfig.Runtime = &config.ControlRuntime{} |
||||
dataDir, err := datadir.Resolve(cfg.DataDir) |
||||
if err != nil { |
||||
return "", "", err |
||||
} |
||||
return filepath.Join(dataDir, "server"), filepath.Join(dataDir, "agent"), err |
||||
} |
||||
|
||||
func Run(app *cli.Context) error { |
||||
if err := cmds.InitLogging(); err != nil { |
||||
return err |
||||
} |
||||
return rotate(app, &cmds.ServerConfig) |
||||
} |
||||
|
||||
func rotate(app *cli.Context, cfg *cmds.Server) error { |
||||
var serverConfig server.Config |
||||
|
||||
serverDataDir, agentDataDir, err := commandSetup(app, cfg, &serverConfig) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
serverConfig.ControlConfig.DataDir = serverDataDir |
||||
serverConfig.ControlConfig.Runtime = &config.ControlRuntime{} |
||||
deps.CreateRuntimeCertFiles(&serverConfig.ControlConfig, serverConfig.ControlConfig.Runtime) |
||||
|
||||
tlsBackupDir, err := backupCertificates(serverDataDir, agentDataDir) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if len(cmds.ServicesList) == 0 { |
||||
// detecting if the service is an agent or server
|
||||
_, err := os.Stat(serverDataDir) |
||||
if err != nil { |
||||
if !os.IsNotExist(err) { |
||||
return err |
||||
} |
||||
logrus.Infof("Agent detected, rotating agent certificates") |
||||
cmds.ServicesList = []string{ |
||||
kubeletService, |
||||
kubeProxyService, |
||||
version.Program + programControllerService, |
||||
} |
||||
} else { |
||||
logrus.Infof("Server detected, rotating server certificates") |
||||
cmds.ServicesList = []string{ |
||||
adminService, |
||||
etcdService, |
||||
apiServerService, |
||||
controllerManagerService, |
||||
cloudControllerService, |
||||
schedulerService, |
||||
version.Program + k3sServerService, |
||||
version.Program + programControllerService, |
||||
authProxyService, |
||||
kubeletService, |
||||
kubeProxyService, |
||||
} |
||||
} |
||||
} |
||||
fileList := []string{} |
||||
for _, service := range cmds.ServicesList { |
||||
logrus.Infof("Rotating certificates for %s service", service) |
||||
switch service { |
||||
case adminService: |
||||
fileList = append(fileList, |
||||
serverConfig.ControlConfig.Runtime.ClientAdminCert, |
||||
serverConfig.ControlConfig.Runtime.ClientAdminKey) |
||||
case apiServerService: |
||||
fileList = append(fileList, |
||||
serverConfig.ControlConfig.Runtime.ClientKubeAPICert, |
||||
serverConfig.ControlConfig.Runtime.ClientKubeAPIKey, |
||||
serverConfig.ControlConfig.Runtime.ServingKubeAPICert, |
||||
serverConfig.ControlConfig.Runtime.ServingKubeAPIKey) |
||||
case controllerManagerService: |
||||
fileList = append(fileList, |
||||
serverConfig.ControlConfig.Runtime.ClientControllerCert, |
||||
serverConfig.ControlConfig.Runtime.ClientControllerKey) |
||||
case schedulerService: |
||||
fileList = append(fileList, |
||||
serverConfig.ControlConfig.Runtime.ClientSchedulerCert, |
||||
serverConfig.ControlConfig.Runtime.ClientSchedulerKey) |
||||
case etcdService: |
||||
fileList = append(fileList, |
||||
serverConfig.ControlConfig.Runtime.ClientETCDCert, |
||||
serverConfig.ControlConfig.Runtime.ClientETCDKey, |
||||
serverConfig.ControlConfig.Runtime.ServerETCDCert, |
||||
serverConfig.ControlConfig.Runtime.ServerETCDKey, |
||||
serverConfig.ControlConfig.Runtime.PeerServerClientETCDCert, |
||||
serverConfig.ControlConfig.Runtime.PeerServerClientETCDKey) |
||||
case cloudControllerService: |
||||
fileList = append(fileList, |
||||
serverConfig.ControlConfig.Runtime.ClientCloudControllerCert, |
||||
serverConfig.ControlConfig.Runtime.ClientCloudControllerKey) |
||||
case version.Program + k3sServerService: |
||||
dynamicListenerRegenFilePath := filepath.Join(serverDataDir, "tls", "dynamic-cert-regenerate") |
||||
if err := ioutil.WriteFile(dynamicListenerRegenFilePath, []byte{}, 0600); err != nil { |
||||
return err |
||||
} |
||||
logrus.Infof("Rotating dynamic listener certificate") |
||||
case version.Program + programControllerService: |
||||
fileList = append(fileList, |
||||
serverConfig.ControlConfig.Runtime.ClientK3sControllerCert, |
||||
serverConfig.ControlConfig.Runtime.ClientK3sControllerKey, |
||||
filepath.Join(agentDataDir, "client-"+version.Program+"-controller.crt"), |
||||
filepath.Join(agentDataDir, "client-"+version.Program+"-controller.key")) |
||||
case authProxyService: |
||||
fileList = append(fileList, |
||||
serverConfig.ControlConfig.Runtime.ClientAuthProxyCert, |
||||
serverConfig.ControlConfig.Runtime.ClientAuthProxyKey) |
||||
case kubeletService: |
||||
fileList = append(fileList, |
||||
serverConfig.ControlConfig.Runtime.ClientKubeletKey, |
||||
serverConfig.ControlConfig.Runtime.ServingKubeletKey, |
||||
filepath.Join(agentDataDir, "client-kubelet.crt"), |
||||
filepath.Join(agentDataDir, "client-kubelet.key"), |
||||
filepath.Join(agentDataDir, "serving-kubelet.crt"), |
||||
filepath.Join(agentDataDir, "serving-kubelet.key")) |
||||
case kubeProxyService: |
||||
fileList = append(fileList, |
||||
serverConfig.ControlConfig.Runtime.ClientKubeProxyCert, |
||||
serverConfig.ControlConfig.Runtime.ClientKubeProxyKey, |
||||
filepath.Join(agentDataDir, "client-kube-proxy.crt"), |
||||
filepath.Join(agentDataDir, "client-kube-proxy.key")) |
||||
default: |
||||
logrus.Fatalf("%s is not a recognized service", service) |
||||
} |
||||
} |
||||
|
||||
for _, file := range fileList { |
||||
if err := os.Remove(file); err == nil { |
||||
logrus.Debugf("file %s is deleted", file) |
||||
} |
||||
} |
||||
logrus.Infof("Successfully backed up certificates for all services to path %s, please restart %s server or agent to rotate certificates", tlsBackupDir, version.Program) |
||||
return nil |
||||
} |
||||
|
||||
func copyFile(src, destDir string) error { |
||||
_, err := os.Stat(src) |
||||
if err == nil { |
||||
input, err := ioutil.ReadFile(src) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return ioutil.WriteFile(filepath.Join(destDir, filepath.Base(src)), input, 0644) |
||||
} else if errors.Is(err, os.ErrNotExist) { |
||||
return nil |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func backupCertificates(serverDataDir, agentDataDir string) (string, error) { |
||||
serverTLSDir := filepath.Join(serverDataDir, "tls") |
||||
tlsBackupDir := filepath.Join(serverDataDir, "tls-"+strconv.Itoa(int(time.Now().Unix()))) |
||||
|
||||
if _, err := os.Stat(serverTLSDir); err != nil { |
||||
return "", err |
||||
} |
||||
if err := copy.Copy(serverTLSDir, tlsBackupDir); err != nil { |
||||
return "", err |
||||
} |
||||
agentCerts := []string{ |
||||
filepath.Join(agentDataDir, "client-"+version.Program+"-controller.crt"), |
||||
filepath.Join(agentDataDir, "client-"+version.Program+"-controller.key"), |
||||
filepath.Join(agentDataDir, "client-kubelet.crt"), |
||||
filepath.Join(agentDataDir, "client-kubelet.key"), |
||||
filepath.Join(agentDataDir, "serving-kubelet.crt"), |
||||
filepath.Join(agentDataDir, "serving-kubelet.key"), |
||||
filepath.Join(agentDataDir, "client-kube-proxy.crt"), |
||||
filepath.Join(agentDataDir, "client-kube-proxy.key"), |
||||
} |
||||
for _, cert := range agentCerts { |
||||
if err := copyFile(cert, tlsBackupDir); err != nil { |
||||
return "", err |
||||
} |
||||
} |
||||
return tlsBackupDir, nil |
||||
} |
@ -0,0 +1,52 @@
|
||||
package cmds |
||||
|
||||
import ( |
||||
"github.com/rancher/k3s/pkg/version" |
||||
"github.com/urfave/cli" |
||||
) |
||||
|
||||
const CertCommand = "certificate" |
||||
|
||||
var ( |
||||
ServicesList cli.StringSlice |
||||
CertCommandFlags = []cli.Flag{ |
||||
DebugFlag, |
||||
ConfigFlag, |
||||
LogFile, |
||||
AlsoLogToStderr, |
||||
cli.StringFlag{ |
||||
Name: "data-dir,d", |
||||
Usage: "(data) Folder to hold state default /var/lib/rancher/" + version.Program + " or ${HOME}/.rancher/" + version.Program + " if not root", |
||||
Destination: &ServerConfig.DataDir, |
||||
}, |
||||
cli.StringSliceFlag{ |
||||
Name: "service,s", |
||||
Usage: "List of services to rotate certificates for. Options include (admin, api-server, controller-manager, scheduler, " + version.Program + "-controller, " + version.Program + "-server, cloud-controller, etcd, auth-proxy, kubelet, kube-proxy)", |
||||
Value: &ServicesList, |
||||
}, |
||||
} |
||||
) |
||||
|
||||
func NewCertCommand(subcommands []cli.Command) cli.Command { |
||||
return cli.Command{ |
||||
Name: CertCommand, |
||||
Usage: "Certificates management", |
||||
SkipFlagParsing: false, |
||||
SkipArgReorder: true, |
||||
Subcommands: subcommands, |
||||
Flags: CertCommandFlags, |
||||
} |
||||
} |
||||
|
||||
func NewCertSubcommands(rotate func(ctx *cli.Context) error) []cli.Command { |
||||
return []cli.Command{ |
||||
{ |
||||
Name: "rotate", |
||||
Usage: "Certificate rotation", |
||||
SkipFlagParsing: false, |
||||
SkipArgReorder: true, |
||||
Action: rotate, |
||||
Flags: CertCommandFlags, |
||||
}, |
||||
} |
||||
} |
@ -0,0 +1,5 @@
|
||||
test/data.copy |
||||
coverage.txt |
||||
vendor |
||||
.vagrant |
||||
.idea/ |
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT) |
||||
|
||||
Copyright (c) 2018 otiai10 |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in |
||||
all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
THE SOFTWARE. |
@ -0,0 +1,62 @@
|
||||
# copy |
||||
|
||||
[![Go Reference](https://pkg.go.dev/badge/github.com/otiai10/copy.svg)](https://pkg.go.dev/github.com/otiai10/copy) |
||||
[![Actions Status](https://github.com/otiai10/copy/workflows/Go/badge.svg)](https://github.com/otiai10/copy/actions) |
||||
[![codecov](https://codecov.io/gh/otiai10/copy/branch/main/graph/badge.svg)](https://codecov.io/gh/otiai10/copy) |
||||
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/otiai10/copy/blob/main/LICENSE) |
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/otiai10/copy)](https://goreportcard.com/report/github.com/otiai10/copy) |
||||
[![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/otiai10/copy?sort=semver)](https://pkg.go.dev/github.com/otiai10/copy) |
||||
|
||||
`copy` copies directories recursively. |
||||
|
||||
# Example Usage |
||||
|
||||
```go |
||||
err := Copy("your/directory", "your/directory.copy") |
||||
``` |
||||
|
||||
# Advanced Usage |
||||
|
||||
```go |
||||
// Options specifies optional actions on copying. |
||||
type Options struct { |
||||
|
||||
// OnSymlink can specify what to do on symlink |
||||
OnSymlink func(src string) SymlinkAction |
||||
|
||||
// OnDirExists can specify what to do when there is a directory already existing in destination. |
||||
OnDirExists func(src, dest string) DirExistsAction |
||||
|
||||
// Skip can specify which files should be skipped |
||||
Skip func(src string) (bool, error) |
||||
|
||||
// AddPermission to every entities, |
||||
// NO MORE THAN 0777 |
||||
AddPermission os.FileMode |
||||
|
||||
// Sync file after copy. |
||||
// Useful in case when file must be on the disk |
||||
// (in case crash happens, for example), |
||||
// at the expense of some performance penalty |
||||
Sync bool |
||||
|
||||
// Preserve the atime and the mtime of the entries |
||||
// On linux we can preserve only up to 1 millisecond accuracy |
||||
PreserveTimes bool |
||||
|
||||
} |
||||
``` |
||||
|
||||
```go |
||||
// For example... |
||||
opt := Options{ |
||||
Skip: func(src string) (bool, error) { |
||||
return strings.HasSuffix(src, ".git"), nil |
||||
}, |
||||
} |
||||
err := Copy("your/directory", "your/directory.copy", opt) |
||||
``` |
||||
|
||||
# Issues |
||||
|
||||
- https://github.com/otiai10/copy/issues |
@ -0,0 +1,228 @@
|
||||
package copy |
||||
|
||||
import ( |
||||
"io" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"time" |
||||
) |
||||
|
||||
const ( |
||||
// tmpPermissionForDirectory makes the destination directory writable,
|
||||
// so that stuff can be copied recursively even if any original directory is NOT writable.
|
||||
// See https://github.com/otiai10/copy/pull/9 for more information.
|
||||
tmpPermissionForDirectory = os.FileMode(0755) |
||||
) |
||||
|
||||
type timespec struct { |
||||
Mtime time.Time |
||||
Atime time.Time |
||||
Ctime time.Time |
||||
} |
||||
|
||||
// Copy copies src to dest, doesn't matter if src is a directory or a file.
|
||||
func Copy(src, dest string, opt ...Options) error { |
||||
info, err := os.Lstat(src) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return switchboard(src, dest, info, assure(src, dest, opt...)) |
||||
} |
||||
|
||||
// switchboard switches proper copy functions regarding file type, etc...
|
||||
// If there would be anything else here, add a case to this switchboard.
|
||||
func switchboard(src, dest string, info os.FileInfo, opt Options) (err error) { |
||||
switch { |
||||
case info.Mode()&os.ModeSymlink != 0: |
||||
err = onsymlink(src, dest, info, opt) |
||||
case info.IsDir(): |
||||
err = dcopy(src, dest, info, opt) |
||||
case info.Mode()&os.ModeNamedPipe != 0: |
||||
err = pcopy(dest, info) |
||||
default: |
||||
err = fcopy(src, dest, info, opt) |
||||
} |
||||
|
||||
return err |
||||
} |
||||
|
||||
// copyNextOrSkip decide if this src should be copied or not.
|
||||
// Because this "copy" could be called recursively,
|
||||
// "info" MUST be given here, NOT nil.
|
||||
func copyNextOrSkip(src, dest string, info os.FileInfo, opt Options) error { |
||||
skip, err := opt.Skip(src) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if skip { |
||||
return nil |
||||
} |
||||
return switchboard(src, dest, info, opt) |
||||
} |
||||
|
||||
// fcopy is for just a file,
|
||||
// with considering existence of parent directory
|
||||
// and file permission.
|
||||
func fcopy(src, dest string, info os.FileInfo, opt Options) (err error) { |
||||
|
||||
if err = os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { |
||||
return |
||||
} |
||||
|
||||
f, err := os.Create(dest) |
||||
if err != nil { |
||||
return |
||||
} |
||||
defer fclose(f, &err) |
||||
|
||||
if err = os.Chmod(f.Name(), info.Mode()|opt.AddPermission); err != nil { |
||||
return |
||||
} |
||||
|
||||
s, err := os.Open(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
defer fclose(s, &err) |
||||
|
||||
var buf []byte = nil |
||||
var w io.Writer = f |
||||
// var r io.Reader = s
|
||||
if opt.CopyBufferSize != 0 { |
||||
buf = make([]byte, opt.CopyBufferSize) |
||||
// Disable using `ReadFrom` by io.CopyBuffer.
|
||||
// See https://github.com/otiai10/copy/pull/60#discussion_r627320811 for more details.
|
||||
w = struct{ io.Writer }{f} |
||||
// r = struct{ io.Reader }{s}
|
||||
} |
||||
if _, err = io.CopyBuffer(w, s, buf); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if opt.Sync { |
||||
err = f.Sync() |
||||
} |
||||
|
||||
if opt.PreserveTimes { |
||||
return preserveTimes(info, dest) |
||||
} |
||||
|
||||
return |
||||
} |
||||
|
||||
// dcopy is for a directory,
|
||||
// with scanning contents inside the directory
|
||||
// and pass everything to "copy" recursively.
|
||||
func dcopy(srcdir, destdir string, info os.FileInfo, opt Options) (err error) { |
||||
|
||||
_, err = os.Stat(destdir) |
||||
if err == nil && opt.OnDirExists != nil && destdir != opt.intent.dest { |
||||
switch opt.OnDirExists(srcdir, destdir) { |
||||
case Replace: |
||||
if err := os.RemoveAll(destdir); err != nil { |
||||
return err |
||||
} |
||||
case Untouchable: |
||||
return nil |
||||
} // case "Merge" is default behaviour. Go through.
|
||||
} else if err != nil && !os.IsNotExist(err) { |
||||
return err // Unwelcome error type...!
|
||||
} |
||||
|
||||
originalMode := info.Mode() |
||||
|
||||
// Make dest dir with 0755 so that everything writable.
|
||||
if err = os.MkdirAll(destdir, tmpPermissionForDirectory); err != nil { |
||||
return |
||||
} |
||||
// Recover dir mode with original one.
|
||||
defer chmod(destdir, originalMode|opt.AddPermission, &err) |
||||
|
||||
contents, err := ioutil.ReadDir(srcdir) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
for _, content := range contents { |
||||
cs, cd := filepath.Join(srcdir, content.Name()), filepath.Join(destdir, content.Name()) |
||||
|
||||
if err = copyNextOrSkip(cs, cd, content, opt); err != nil { |
||||
// If any error, exit immediately
|
||||
return |
||||
} |
||||
} |
||||
|
||||
if opt.PreserveTimes { |
||||
return preserveTimes(info, destdir) |
||||
} |
||||
|
||||
return |
||||
} |
||||
|
||||
func onsymlink(src, dest string, info os.FileInfo, opt Options) error { |
||||
switch opt.OnSymlink(src) { |
||||
case Shallow: |
||||
return lcopy(src, dest) |
||||
case Deep: |
||||
orig, err := os.Readlink(src) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
info, err = os.Lstat(orig) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return copyNextOrSkip(orig, dest, info, opt) |
||||
case Skip: |
||||
fallthrough |
||||
default: |
||||
return nil // do nothing
|
||||
} |
||||
} |
||||
|
||||
// lcopy is for a symlink,
|
||||
// with just creating a new symlink by replicating src symlink.
|
||||
func lcopy(src, dest string) error { |
||||
src, err := os.Readlink(src) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return os.Symlink(src, dest) |
||||
} |
||||
|
||||
// fclose ANYHOW closes file,
|
||||
// with asiging error raised during Close,
|
||||
// BUT respecting the error already reported.
|
||||
func fclose(f *os.File, reported *error) { |
||||
if err := f.Close(); *reported == nil { |
||||
*reported = err |
||||
} |
||||
} |
||||
|
||||
// chmod ANYHOW changes file mode,
|
||||
// with asiging error raised during Chmod,
|
||||
// BUT respecting the error already reported.
|
||||
func chmod(dir string, mode os.FileMode, reported *error) { |
||||
if err := os.Chmod(dir, mode); *reported == nil { |
||||
*reported = err |
||||
} |
||||
} |
||||
|
||||
// assure Options struct, should be called only once.
|
||||
// All optional values MUST NOT BE nil/zero after assured.
|
||||
func assure(src, dest string, opts ...Options) Options { |
||||
defopt := getDefaultOptions(src, dest) |
||||
if len(opts) == 0 { |
||||
return defopt |
||||
} |
||||
if opts[0].OnSymlink == nil { |
||||
opts[0].OnSymlink = defopt.OnSymlink |
||||
} |
||||
if opts[0].Skip == nil { |
||||
opts[0].Skip = defopt.Skip |
||||
} |
||||
opts[0].intent.src = defopt.intent.src |
||||
opts[0].intent.dest = defopt.intent.dest |
||||
return opts[0] |
||||
} |
@ -0,0 +1,17 @@
|
||||
// +build !windows
|
||||
|
||||
package copy |
||||
|
||||
import ( |
||||
"os" |
||||
"path/filepath" |
||||
"syscall" |
||||
) |
||||
|
||||
// pcopy is for just named pipes
|
||||
func pcopy(dest string, info os.FileInfo) error { |
||||
if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { |
||||
return err |
||||
} |
||||
return syscall.Mkfifo(dest, uint32(info.Mode())) |
||||
} |
@ -0,0 +1,12 @@
|
||||
// +build windows
|
||||
|
||||
package copy |
||||
|
||||
import ( |
||||
"os" |
||||
) |
||||
|
||||
// pcopy is for just named pipes. Windows doesn't support them
|
||||
func pcopy(dest string, info os.FileInfo) error { |
||||
return nil |
||||
} |
@ -0,0 +1,5 @@
|
||||
module github.com/otiai10/copy |
||||
|
||||
go 1.14 |
||||
|
||||
require github.com/otiai10/mint v1.3.2 |
@ -0,0 +1,6 @@
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= |
||||
github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI= |
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= |
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= |
||||
github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E= |
||||
github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= |
@ -0,0 +1,86 @@
|
||||
package copy |
||||
|
||||
import "os" |
||||
|
||||
// Options specifies optional actions on copying.
|
||||
type Options struct { |
||||
|
||||
// OnSymlink can specify what to do on symlink
|
||||
OnSymlink func(src string) SymlinkAction |
||||
|
||||
// OnDirExists can specify what to do when there is a directory already existing in destination.
|
||||
OnDirExists func(src, dest string) DirExistsAction |
||||
|
||||
// Skip can specify which files should be skipped
|
||||
Skip func(src string) (bool, error) |
||||
|
||||
// AddPermission to every entities,
|
||||
// NO MORE THAN 0777
|
||||
AddPermission os.FileMode |
||||
|
||||
// Sync file after copy.
|
||||
// Useful in case when file must be on the disk
|
||||
// (in case crash happens, for example),
|
||||
// at the expense of some performance penalty
|
||||
Sync bool |
||||
|
||||
// Preserve the atime and the mtime of the entries.
|
||||
// On linux we can preserve only up to 1 millisecond accuracy.
|
||||
PreserveTimes bool |
||||
|
||||
// The byte size of the buffer to use for copying files.
|
||||
// If zero, the internal default buffer of 32KB is used.
|
||||
// See https://golang.org/pkg/io/#CopyBuffer for more information.
|
||||
CopyBufferSize uint |
||||
|
||||
intent struct { |
||||
src string |
||||
dest string |
||||
} |
||||
} |
||||
|
||||
// SymlinkAction represents what to do on symlink.
|
||||
type SymlinkAction int |
||||
|
||||
const ( |
||||
// Deep creates hard-copy of contents.
|
||||
Deep SymlinkAction = iota |
||||
// Shallow creates new symlink to the dest of symlink.
|
||||
Shallow |
||||
// Skip does nothing with symlink.
|
||||
Skip |
||||
) |
||||
|
||||
// DirExistsAction represents what to do on dest dir.
|
||||
type DirExistsAction int |
||||
|
||||
const ( |
||||
// Merge preserves or overwrites existing files under the dir (default behavior).
|
||||
Merge DirExistsAction = iota |
||||
// Replace deletes all contents under the dir and copy src files.
|
||||
Replace |
||||
// Untouchable does nothing for the dir, and leaves it as it is.
|
||||
Untouchable |
||||
) |
||||
|
||||
// getDefaultOptions provides default options,
|
||||
// which would be modified by usage-side.
|
||||
func getDefaultOptions(src, dest string) Options { |
||||
return Options{ |
||||
OnSymlink: func(string) SymlinkAction { |
||||
return Shallow // Do shallow copy
|
||||
}, |
||||
OnDirExists: nil, // Default behavior is "Merge".
|
||||
Skip: func(string) (bool, error) { |
||||
return false, nil // Don't skip
|
||||
}, |
||||
AddPermission: 0, // Add nothing
|
||||
Sync: false, // Do not sync
|
||||
PreserveTimes: false, // Do not preserve the modification time
|
||||
CopyBufferSize: 0, // Do not specify, use default bufsize (32*1024)
|
||||
intent: struct { |
||||
src string |
||||
dest string |
||||
}{src, dest}, |
||||
} |
||||
} |
@ -0,0 +1,11 @@
|
||||
package copy |
||||
|
||||
import "os" |
||||
|
||||
func preserveTimes(srcinfo os.FileInfo, dest string) error { |
||||
spec := getTimeSpec(srcinfo) |
||||
if err := os.Chtimes(dest, spec.Atime, spec.Mtime); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,21 @@
|
||||
// +build !windows,!darwin,!freebsd
|
||||
|
||||
// TODO: add more runtimes
|
||||
|
||||
package copy |
||||
|
||||
import ( |
||||
"os" |
||||
"syscall" |
||||
"time" |
||||
) |
||||
|
||||
func getTimeSpec(info os.FileInfo) timespec { |
||||
stat := info.Sys().(*syscall.Stat_t) |
||||
times := timespec{ |
||||
Mtime: info.ModTime(), |
||||
Atime: time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)), |
||||
Ctime: time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)), |
||||
} |
||||
return times |
||||
} |
@ -0,0 +1,19 @@
|
||||
// +build darwin
|
||||
|
||||
package copy |
||||
|
||||
import ( |
||||
"os" |
||||
"syscall" |
||||
"time" |
||||
) |
||||
|
||||
func getTimeSpec(info os.FileInfo) timespec { |
||||
stat := info.Sys().(*syscall.Stat_t) |
||||
times := timespec{ |
||||
Mtime: info.ModTime(), |
||||
Atime: time.Unix(stat.Atimespec.Sec, stat.Atimespec.Nsec), |
||||
Ctime: time.Unix(stat.Ctimespec.Sec, stat.Ctimespec.Nsec), |
||||
} |
||||
return times |
||||
} |
@ -0,0 +1,19 @@
|
||||
// +build freebsd
|
||||
|
||||
package copy |
||||
|
||||
import ( |
||||
"os" |
||||
"syscall" |
||||
"time" |
||||
) |
||||
|
||||
func getTimeSpec(info os.FileInfo) timespec { |
||||
stat := info.Sys().(*syscall.Stat_t) |
||||
times := timespec{ |
||||
Mtime: info.ModTime(), |
||||
Atime: time.Unix(int64(stat.Atimespec.Sec), int64(stat.Atimespec.Nsec)), |
||||
Ctime: time.Unix(int64(stat.Ctimespec.Sec), int64(stat.Ctimespec.Nsec)), |
||||
} |
||||
return times |
||||
} |
@ -0,0 +1,18 @@
|
||||
// +build windows
|
||||
|
||||
package copy |
||||
|
||||
import ( |
||||
"os" |
||||
"syscall" |
||||
"time" |
||||
) |
||||
|
||||
func getTimeSpec(info os.FileInfo) timespec { |
||||
stat := info.Sys().(*syscall.Win32FileAttributeData) |
||||
return timespec{ |
||||
Mtime: time.Unix(0, stat.LastWriteTime.Nanoseconds()), |
||||
Atime: time.Unix(0, stat.LastAccessTime.Nanoseconds()), |
||||
Ctime: time.Unix(0, stat.CreationTime.Nanoseconds()), |
||||
} |
||||
} |
@ -0,0 +1,17 @@
|
||||
// +build !windows
|
||||
|
||||
package copy |
||||
|
||||
import ( |
||||
"os" |
||||
"syscall" |
||||
"testing" |
||||
) |
||||
|
||||
func setup(m *testing.M) { |
||||
os.MkdirAll("test/data.copy", os.ModePerm) |
||||
os.Symlink("test/data/case01", "test/data/case03/case01") |
||||
os.Chmod("test/data/case07/dir_0555", 0555) |
||||
os.Chmod("test/data/case07/file_0444", 0444) |
||||
syscall.Mkfifo("test/data/case11/foo/bar", 0555) |
||||
} |
@ -0,0 +1,15 @@
|
||||
// +build windows
|
||||
|
||||
package copy |
||||
|
||||
import ( |
||||
"os" |
||||
"testing" |
||||
) |
||||
|
||||
func setup(m *testing.M) { |
||||
os.MkdirAll("test/data.copy", os.ModePerm) |
||||
os.Symlink("test/data/case01", "test/data/case03/case01") |
||||
os.Chmod("test/data/case07/dir_0555", 0555) |
||||
os.Chmod("test/data/case07/file_0444", 0444) |
||||
} |
Loading…
Reference in new issue