From f8a25ee028fb58ac97b5912e7e155f2ec57a5fd7 Mon Sep 17 00:00:00 2001 From: ShotaKitazawa Date: Sun, 19 Jun 2022 23:20:56 +0900 Subject: [PATCH 1/5] add mock --- pkg/shell/mock/mock.go | 101 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 pkg/shell/mock/mock.go diff --git a/pkg/shell/mock/mock.go b/pkg/shell/mock/mock.go new file mode 100644 index 0000000..4f1b200 --- /dev/null +++ b/pkg/shell/mock/mock.go @@ -0,0 +1,101 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./common.go + +// Package mock is a generated GoMock package. +package mock + +import ( + bytes "bytes" + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockIface is a mock of Iface interface. +type MockIface struct { + ctrl *gomock.Controller + recorder *MockIfaceMockRecorder +} + +// MockIfaceMockRecorder is the mock recorder for MockIface. +type MockIfaceMockRecorder struct { + mock *MockIface +} + +// NewMockIface creates a new mock instance. +func NewMockIface(ctrl *gomock.Controller) *MockIface { + mock := &MockIface{ctrl: ctrl} + mock.recorder = &MockIfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIface) EXPECT() *MockIfaceMockRecorder { + return m.recorder +} + +// Deploy mocks base method. +func (m *MockIface) Deploy(ctx context.Context, src, dst string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Deploy", ctx, src, dst) + ret0, _ := ret[0].(error) + return ret0 +} + +// Deploy indicates an expected call of Deploy. +func (mr *MockIfaceMockRecorder) Deploy(ctx, src, dst interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deploy", reflect.TypeOf((*MockIface)(nil).Deploy), ctx, src, dst) +} + +// Exec mocks base method. +func (m *MockIface) Exec(ctx context.Context, basedir, command string) (bytes.Buffer, bytes.Buffer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Exec", ctx, basedir, command) + ret0, _ := ret[0].(bytes.Buffer) + ret1, _ := ret[1].(bytes.Buffer) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Exec indicates an expected call of Exec. +func (mr *MockIfaceMockRecorder) Exec(ctx, basedir, command interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockIface)(nil).Exec), ctx, basedir, command) +} + +// Execf mocks base method. +func (m *MockIface) Execf(ctx context.Context, basedir, command string, a ...interface{}) (bytes.Buffer, bytes.Buffer, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, basedir, command} + for _, a_2 := range a { + varargs = append(varargs, a_2) + } + ret := m.ctrl.Call(m, "Execf", varargs...) + ret0, _ := ret[0].(bytes.Buffer) + ret1, _ := ret[1].(bytes.Buffer) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Execf indicates an expected call of Execf. +func (mr *MockIfaceMockRecorder) Execf(ctx, basedir, command interface{}, a ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, basedir, command}, a...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execf", reflect.TypeOf((*MockIface)(nil).Execf), varargs...) +} + +// Host mocks base method. +func (m *MockIface) Host() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Host") + ret0, _ := ret[0].(string) + return ret0 +} + +// Host indicates an expected call of Host. +func (mr *MockIfaceMockRecorder) Host() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Host", reflect.TypeOf((*MockIface)(nil).Host)) +} From 10cbb092c44c16fa2e8da816540e6a964ca3fc04 Mon Sep 17 00:00:00 2001 From: ShotaKitazawa Date: Sun, 19 Jun 2022 23:21:18 +0900 Subject: [PATCH 2/5] make deploy-command allow to seek symbolic-link both file and directory --- go.mod | 4 +- go.sum | 3 + pkg/usecases/deploy/deploy.go | 35 +++- pkg/usecases/deploy/deploy_test.go | 172 ++++++++++++++++++ .../deploy/testdata/error/unresolved_link | 1 + pkg/usecases/deploy/testdata/nginx/nginx.conf | 1 + .../testdata/nginx/sites-available/default | 1 + .../nginx/sites-available/isucondition.conf | 1 + .../nginx/sites-enabled/isucondition.conf | 1 + pkg/usecases/deploy/testdata/nginx_symlink | 1 + 10 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 pkg/usecases/deploy/deploy_test.go create mode 120000 pkg/usecases/deploy/testdata/error/unresolved_link create mode 100644 pkg/usecases/deploy/testdata/nginx/nginx.conf create mode 100644 pkg/usecases/deploy/testdata/nginx/sites-available/default create mode 100644 pkg/usecases/deploy/testdata/nginx/sites-available/isucondition.conf create mode 120000 pkg/usecases/deploy/testdata/nginx/sites-enabled/isucondition.conf create mode 120000 pkg/usecases/deploy/testdata/nginx_symlink diff --git a/go.mod b/go.mod index 5f5edb8..549b133 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,11 @@ go 1.17 require ( github.com/cheggaaa/pb v1.0.29 + github.com/golang/mock v1.6.0 github.com/pkg/sftp v1.13.4 github.com/slack-go/slack v0.10.3 github.com/spf13/cobra v1.4.0 + github.com/spf13/pflag v1.0.5 go.uber.org/zap v1.21.0 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b golang.org/x/sync v0.0.0-20210220032951-036812b2e83c @@ -15,12 +17,12 @@ require ( ) require ( + github.com/benbjohnson/clock v1.1.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/kr/fs v0.1.0 // indirect github.com/mattn/go-runewidth v0.0.4 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect diff --git a/go.sum b/go.sum index fc58e55..1579c1c 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= @@ -91,6 +93,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/usecases/deploy/deploy.go b/pkg/usecases/deploy/deploy.go index 83f543e..f408096 100644 --- a/pkg/usecases/deploy/deploy.go +++ b/pkg/usecases/deploy/deploy.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io/fs" + "os" "path/filepath" "reflect" "strings" @@ -52,27 +53,51 @@ func new(logger *zap.Logger, s shell.Iface, templator *template.Templator, local } func (d Deployer) Deploy(ctx context.Context, targets []config.DeployTarget) error { + host := d.shell.Host() for _, target := range targets { - src := filepath.Join(d.localRepoPath, d.shell.Host(), target.Src) + src := filepath.Join(d.localRepoPath, host, target.Src) if err := filepath.WalkDir(src, func(path string, info fs.DirEntry, err error) error { if info != nil && !reflect.ValueOf(info).IsNil() && !info.IsDir() { dst := filepath.Join(target.Target, strings.TrimPrefix(path, src)) dirname := filepath.Dir(dst) if _, _, err := d.shell.Execf(ctx, "", `test -d "%s"`, dirname); err != nil { - d.log.Debug(fmt.Sprintf("%s does not exist, mkdir", dirname), zap.String("host", d.shell.Host())) + d.log.Debug(fmt.Sprintf("%s does not exist, mkdir", dirname), zap.String("host", host)) if _, _, err := d.shell.Execf(ctx, "", `mkdir -p "%s"`, dirname); err != nil { return err } } - d.log.Debug(fmt.Sprintf("deploy %s to %s", path, dst), zap.String("host", d.shell.Host())) - return d.shell.Deploy(ctx, path, dst) + finfo, err := info.Info() + if err != nil { + return err + } + if finfo.Mode()&os.ModeSymlink == os.ModeSymlink { // copy source is symlink + origin, err := filepath.EvalSymlinks(path) + if err != nil { + return err + } + originAbs, err := filepath.Abs(origin) + if err != nil { + return err + } + newSrc := strings.TrimPrefix(originAbs, filepath.Join(d.localRepoPath, host)+"/") + if newSrc == originAbs { + return fmt.Errorf("%s: cannot seek symlink", originAbs) + } + d.log.Debug(fmt.Sprintf("%s is symlink, seek to %s", path, originAbs), zap.String("host", host)) + if err := d.Deploy(ctx, []config.DeployTarget{{Src: newSrc, Target: dst}}); err != nil { + return err + } + } else { // copy source is file + d.log.Debug(fmt.Sprintf("deploy %s to %s", path, dst), zap.String("host", host)) + return d.shell.Deploy(ctx, path, dst) + } } return nil }); err != nil { return err } if target.Compile != "" { - d.log.Debug(fmt.Sprintf(`exec compile: "%s"`, target.Compile), zap.String("host", d.shell.Host())) + d.log.Debug(fmt.Sprintf(`exec compile: "%s"`, target.Compile), zap.String("host", host)) if _, stderr, err := d.shell.Exec(ctx, target.Target, target.Compile); err != nil { return myerrros.NewErrorCommandExecutionFailed(stderr) } diff --git a/pkg/usecases/deploy/deploy_test.go b/pkg/usecases/deploy/deploy_test.go new file mode 100644 index 0000000..44a0878 --- /dev/null +++ b/pkg/usecases/deploy/deploy_test.go @@ -0,0 +1,172 @@ +package deploy + +import ( + "bytes" + "context" + "fmt" + "path/filepath" + "runtime" + "testing" + + "github.com/golang/mock/gomock" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" + + "github.com/ShotaKitazawa/isucontinuous/pkg/config" + "github.com/ShotaKitazawa/isucontinuous/pkg/shell" + mock_shell "github.com/ShotaKitazawa/isucontinuous/pkg/shell/mock" + "github.com/ShotaKitazawa/isucontinuous/pkg/template" +) + +func TestDeployer_Deploy(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + _, testFilename, _, _ := runtime.Caller(0) + testDir := filepath.Dir(testFilename) + + type fields struct { + log *zap.Logger + shell shell.Iface + template *template.Templator + localRepoPath string + } + type args struct { + targets []config.DeployTarget + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "normal", + fields: fields{ + log: zaptest.NewLogger(t), + shell: func() shell.Iface { + m := mock_shell.NewMockIface(mockCtrl) + m.EXPECT().Host().Return("testdata") + // /etc/nginx/nginx.conf (/etc/nginx is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/nginx.conf"), "/etc/nginx/nginx.conf"). + Return(nil) + // /etc/nginx/sites-available/default (/etc/nginxsites-available isn't existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-available"). + Return(bytes.Buffer{}, bytes.Buffer{}, fmt.Errorf("")) + m.EXPECT().Execf(ctx, "", `mkdir -p "%s"`, "/etc/nginx/sites-available"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/default"), "/etc/nginx/sites-available/default"). + Return(nil) + // /etc/nginx/sites-available/default (/etc/nginxsites-available is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-available"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-available/isucondition.conf"). + Return(nil) + // /etc/nginx/sites-enabled/isucondition.conf (/etc/nginxsites-available is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-enabled"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + { // recursive due to resolve symlink + m.EXPECT().Host().Return("testdata") + // /etc/nginx/sites-available/default (/etc/nginxsites-available is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-enabled"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-enabled/isucondition.conf"). + Return(nil) + } + return m + }(), + }, + args: args{targets: []config.DeployTarget{ + { + Src: "nginx", + Target: "/etc/nginx", + }, + }}, + }, + { + name: "normal_topLevelFileIsSymlink", + fields: fields{ + log: zaptest.NewLogger(t), + shell: func() shell.Iface { + m := mock_shell.NewMockIface(mockCtrl) + m.EXPECT().Host().Return("testdata") + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + { // recursive due to resolve symlink + m.EXPECT().Host().Return("testdata") + // /etc/nginx/nginx.conf (/etc/nginx is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/nginx.conf"), "/etc/nginx/nginx.conf"). + Return(nil) + // /etc/nginx/sites-available/default (/etc/nginxsites-available isn't existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-available"). + Return(bytes.Buffer{}, bytes.Buffer{}, fmt.Errorf("")) + m.EXPECT().Execf(ctx, "", `mkdir -p "%s"`, "/etc/nginx/sites-available"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/default"), "/etc/nginx/sites-available/default"). + Return(nil) + // /etc/nginx/sites-available/default (/etc/nginxsites-available is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-available"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-available/isucondition.conf"). + Return(nil) + // /etc/nginx/sites-enabled/isucondition.conf (/etc/nginxsites-available is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-enabled"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + { // recursive due to resolve symlink + m.EXPECT().Host().Return("testdata") + // /etc/nginx/sites-available/default (/etc/nginxsites-available is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-enabled"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-enabled/isucondition.conf"). + Return(nil) + } + } + return m + }(), + }, + args: args{targets: []config.DeployTarget{ + { + Src: "nginx_symlink", + Target: "/etc/nginx", + }, + }}, + }, + { + name: "abnormal_symlinkCannotResolve", + fields: fields{ + log: zaptest.NewLogger(t), + shell: func() shell.Iface { + m := mock_shell.NewMockIface(mockCtrl) + m.EXPECT().Host().Return("testdata") + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/error"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + return m + }(), + }, + args: args{targets: []config.DeployTarget{ + { + Src: "error", + Target: "/etc/error", + }, + }}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := Deployer{ + log: tt.fields.log, + shell: tt.fields.shell, + template: tt.fields.template, + localRepoPath: testDir, + } + if err := d.Deploy(ctx, tt.args.targets); (err != nil) != tt.wantErr { + t.Errorf("Deployer.Deploy() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/usecases/deploy/testdata/error/unresolved_link b/pkg/usecases/deploy/testdata/error/unresolved_link new file mode 120000 index 0000000..3b46a48 --- /dev/null +++ b/pkg/usecases/deploy/testdata/error/unresolved_link @@ -0,0 +1 @@ +/this_is_dummy_src \ No newline at end of file diff --git a/pkg/usecases/deploy/testdata/nginx/nginx.conf b/pkg/usecases/deploy/testdata/nginx/nginx.conf new file mode 100644 index 0000000..249c014 --- /dev/null +++ b/pkg/usecases/deploy/testdata/nginx/nginx.conf @@ -0,0 +1 @@ +# For unittest (nginx.conf) diff --git a/pkg/usecases/deploy/testdata/nginx/sites-available/default b/pkg/usecases/deploy/testdata/nginx/sites-available/default new file mode 100644 index 0000000..0549981 --- /dev/null +++ b/pkg/usecases/deploy/testdata/nginx/sites-available/default @@ -0,0 +1 @@ +# For unittest (default) diff --git a/pkg/usecases/deploy/testdata/nginx/sites-available/isucondition.conf b/pkg/usecases/deploy/testdata/nginx/sites-available/isucondition.conf new file mode 100644 index 0000000..69d7c24 --- /dev/null +++ b/pkg/usecases/deploy/testdata/nginx/sites-available/isucondition.conf @@ -0,0 +1 @@ +# For unittest (isucondition.conf) diff --git a/pkg/usecases/deploy/testdata/nginx/sites-enabled/isucondition.conf b/pkg/usecases/deploy/testdata/nginx/sites-enabled/isucondition.conf new file mode 120000 index 0000000..0a267d7 --- /dev/null +++ b/pkg/usecases/deploy/testdata/nginx/sites-enabled/isucondition.conf @@ -0,0 +1 @@ +../sites-available/isucondition.conf \ No newline at end of file diff --git a/pkg/usecases/deploy/testdata/nginx_symlink b/pkg/usecases/deploy/testdata/nginx_symlink new file mode 120000 index 0000000..da70e20 --- /dev/null +++ b/pkg/usecases/deploy/testdata/nginx_symlink @@ -0,0 +1 @@ +nginx \ No newline at end of file From 0e52271e9d03acaf42a8df15470d879da6021f00 Mon Sep 17 00:00:00 2001 From: ShotaKitazawa Date: Sun, 19 Jun 2022 23:23:15 +0900 Subject: [PATCH 3/5] add action --- .github/workflows/test.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9fca7a8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: test +on: push + +jobs: + unit-test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + with: + fetch-depth: 1 + - name: Setup Go + uses: actions/setup-go@v1 + with: + go-version: 1.17 + - name: Go Test + run: go test -v ./... + From 0778b1e5bca94a5acf61cd6bcc85d11d59fb030b Mon Sep 17 00:00:00 2001 From: ShotaKitazawa Date: Sun, 19 Jun 2022 23:36:08 +0900 Subject: [PATCH 4/5] make import command ignore to import symbolic link --- pkg/cmd/import.go | 1 + pkg/usecases/imports/imports.go | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/pkg/cmd/import.go b/pkg/cmd/import.go index 71f41ef..8796f4f 100644 --- a/pkg/cmd/import.go +++ b/pkg/cmd/import.go @@ -75,6 +75,7 @@ func runImport( if err != nil { return err } + files = importer.ExcludeSymlinkFiles(ctx, files) for _, file := range files { fileAbsPath := filepath.Join(target.Target, file) content, mode, err := importer.GetFileContent(ctx, fileAbsPath) diff --git a/pkg/usecases/imports/imports.go b/pkg/usecases/imports/imports.go index 88c08bc..f5892f7 100644 --- a/pkg/usecases/imports/imports.go +++ b/pkg/usecases/imports/imports.go @@ -105,3 +105,13 @@ func (l *Importer) ListUntrackedFiles(ctx context.Context, path string) ([]strin } return strings.Split(stdout.String(), "\n"), nil } + +func (l *Importer) ExcludeSymlinkFiles(ctx context.Context, files []string) []string { + result := []string{} + for _, f := range files { + if _, _, err := l.shell.Execf(ctx, "", `test -L %s`, f); err != nil { + result = append(result, f) + } + } + return result +} From acf8e91f283b9c714bf94531cb9bded634708615 Mon Sep 17 00:00:00 2001 From: ShotaKitazawa Date: Fri, 22 Jul 2022 15:23:12 +0900 Subject: [PATCH 5/5] bug fix --- pkg/cmd/deploy.go | 2 +- pkg/usecases/deploy/deploy.go | 26 +-- pkg/usecases/deploy/deploy_test.go | 166 +++++++++++++----- .../{ => host01}/error/unresolved_link | 0 .../testdata/{ => host01}/nginx/nginx.conf | 0 .../nginx/sites-available/default | 0 .../nginx/sites-available/isucondition.conf | 0 .../nginx/sites-enabled/isucondition.conf | 0 .../testdata/{ => host01}/nginx_symlink | 0 .../deploy/testdata/host02/hosts_symlink | 1 + .../deploy/testdata/host02/nginx_symlink | 1 + 11 files changed, 143 insertions(+), 53 deletions(-) rename pkg/usecases/deploy/testdata/{ => host01}/error/unresolved_link (100%) rename pkg/usecases/deploy/testdata/{ => host01}/nginx/nginx.conf (100%) rename pkg/usecases/deploy/testdata/{ => host01}/nginx/sites-available/default (100%) rename pkg/usecases/deploy/testdata/{ => host01}/nginx/sites-available/isucondition.conf (100%) rename pkg/usecases/deploy/testdata/{ => host01}/nginx/sites-enabled/isucondition.conf (100%) rename pkg/usecases/deploy/testdata/{ => host01}/nginx_symlink (100%) create mode 120000 pkg/usecases/deploy/testdata/host02/hosts_symlink create mode 120000 pkg/usecases/deploy/testdata/host02/nginx_symlink diff --git a/pkg/cmd/deploy.go b/pkg/cmd/deploy.go index 6169ed8..d68d940 100644 --- a/pkg/cmd/deploy.go +++ b/pkg/cmd/deploy.go @@ -98,7 +98,7 @@ func runDeploy( return err } // Deploy - if err = deployer.Deploy(ctx, host.Deploy.Targets); err != nil { + if err = deployer.Deploy(ctx, host.Host, host.Deploy.Targets); err != nil { return err } // Execute postCommand diff --git a/pkg/usecases/deploy/deploy.go b/pkg/usecases/deploy/deploy.go index f408096..4ac60b1 100644 --- a/pkg/usecases/deploy/deploy.go +++ b/pkg/usecases/deploy/deploy.go @@ -52,8 +52,8 @@ func new(logger *zap.Logger, s shell.Iface, templator *template.Templator, local return &Deployer{logger, s, templator, localRepoPath} } -func (d Deployer) Deploy(ctx context.Context, targets []config.DeployTarget) error { - host := d.shell.Host() +func (d Deployer) Deploy(ctx context.Context, host string, targets []config.DeployTarget) error { + realHost := d.shell.Host() for _, target := range targets { src := filepath.Join(d.localRepoPath, host, target.Src) if err := filepath.WalkDir(src, func(path string, info fs.DirEntry, err error) error { @@ -61,7 +61,7 @@ func (d Deployer) Deploy(ctx context.Context, targets []config.DeployTarget) err dst := filepath.Join(target.Target, strings.TrimPrefix(path, src)) dirname := filepath.Dir(dst) if _, _, err := d.shell.Execf(ctx, "", `test -d "%s"`, dirname); err != nil { - d.log.Debug(fmt.Sprintf("%s does not exist, mkdir", dirname), zap.String("host", host)) + d.log.Debug(fmt.Sprintf("%s does not exist, mkdir", dirname), zap.String("host", realHost)) if _, _, err := d.shell.Execf(ctx, "", `mkdir -p "%s"`, dirname); err != nil { return err } @@ -75,20 +75,22 @@ func (d Deployer) Deploy(ctx context.Context, targets []config.DeployTarget) err if err != nil { return err } - originAbs, err := filepath.Abs(origin) - if err != nil { - return err + newHostAndSrc := strings.TrimPrefix(origin, d.localRepoPath+"/") + if newHostAndSrc == origin { + return fmt.Errorf("%s: cannot seek symlink bacause source of symlic isn't in localRepoPath", origin) } - newSrc := strings.TrimPrefix(originAbs, filepath.Join(d.localRepoPath, host)+"/") - if newSrc == originAbs { - return fmt.Errorf("%s: cannot seek symlink", originAbs) + newHostAndSrcSlice := strings.Split(newHostAndSrc, "/") + newHost := newHostAndSrcSlice[0] + newSrc := strings.Join(newHostAndSrcSlice[1:], "/") + if len(newHostAndSrcSlice) < 2 { + return fmt.Errorf("%s: cannot seek symlink bacause this directory is hostdir", origin) } - d.log.Debug(fmt.Sprintf("%s is symlink, seek to %s", path, originAbs), zap.String("host", host)) - if err := d.Deploy(ctx, []config.DeployTarget{{Src: newSrc, Target: dst}}); err != nil { + d.log.Debug(fmt.Sprintf("%s is symlink, seek to %s", path, origin), zap.String("host", realHost)) + if err := d.Deploy(ctx, newHost, []config.DeployTarget{{Src: newSrc, Target: dst}}); err != nil { return err } } else { // copy source is file - d.log.Debug(fmt.Sprintf("deploy %s to %s", path, dst), zap.String("host", host)) + d.log.Debug(fmt.Sprintf("deploy %s to %s", path, dst), zap.String("host", realHost)) return d.shell.Deploy(ctx, path, dst) } } diff --git a/pkg/usecases/deploy/deploy_test.go b/pkg/usecases/deploy/deploy_test.go index 44a0878..ec2d4d8 100644 --- a/pkg/usecases/deploy/deploy_test.go +++ b/pkg/usecases/deploy/deploy_test.go @@ -23,7 +23,7 @@ func TestDeployer_Deploy(t *testing.T) { defer mockCtrl.Finish() ctx := context.Background() _, testFilename, _, _ := runtime.Caller(0) - testDir := filepath.Dir(testFilename) + testDir := filepath.Join(filepath.Dir(testFilename), "testdata") type fields struct { log *zap.Logger @@ -32,6 +32,7 @@ func TestDeployer_Deploy(t *testing.T) { localRepoPath string } type args struct { + host string targets []config.DeployTarget } tests := []struct { @@ -46,94 +47,153 @@ func TestDeployer_Deploy(t *testing.T) { log: zaptest.NewLogger(t), shell: func() shell.Iface { m := mock_shell.NewMockIface(mockCtrl) - m.EXPECT().Host().Return("testdata") + m.EXPECT().Host().Return("host01") // /etc/nginx/nginx.conf (/etc/nginx is existed) m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) - m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/nginx.conf"), "/etc/nginx/nginx.conf"). + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/nginx.conf"), "/etc/nginx/nginx.conf"). Return(nil) - // /etc/nginx/sites-available/default (/etc/nginxsites-available isn't existed) + // /etc/nginx/sites-available/default (/etc/nginx/sites-available isn't existed) m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-available"). Return(bytes.Buffer{}, bytes.Buffer{}, fmt.Errorf("")) m.EXPECT().Execf(ctx, "", `mkdir -p "%s"`, "/etc/nginx/sites-available"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) - m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/default"), "/etc/nginx/sites-available/default"). + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/sites-available/default"), "/etc/nginx/sites-available/default"). Return(nil) - // /etc/nginx/sites-available/default (/etc/nginxsites-available is existed) + // /etc/nginx/sites-available/default (/etc/nginx/sites-available is existed) m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-available"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) - m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-available/isucondition.conf"). + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-available/isucondition.conf"). Return(nil) - // /etc/nginx/sites-enabled/isucondition.conf (/etc/nginxsites-available is existed) + // /etc/nginx/sites-enabled/isucondition.conf (/etc/nginx/sites-available is existed) m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-enabled"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) { // recursive due to resolve symlink - m.EXPECT().Host().Return("testdata") - // /etc/nginx/sites-available/default (/etc/nginxsites-available is existed) + m.EXPECT().Host().Return("host01") + // /etc/nginx/sites-available/default (/etc/nginx/sites-available is existed) m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-enabled"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) - m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-enabled/isucondition.conf"). + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-enabled/isucondition.conf"). Return(nil) } return m }(), }, - args: args{targets: []config.DeployTarget{ - { - Src: "nginx", - Target: "/etc/nginx", + args: args{ + host: "host01", + targets: []config.DeployTarget{ + { + Src: "nginx", + Target: "/etc/nginx", + }, }, - }}, + }, }, { - name: "normal_topLevelFileIsSymlink", + name: "normal_symlinkToSameHost", fields: fields{ log: zaptest.NewLogger(t), shell: func() shell.Iface { m := mock_shell.NewMockIface(mockCtrl) - m.EXPECT().Host().Return("testdata") + m.EXPECT().Host().Return("host01") m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) { // recursive due to resolve symlink - m.EXPECT().Host().Return("testdata") + m.EXPECT().Host().Return("host01") // /etc/nginx/nginx.conf (/etc/nginx is existed) m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) - m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/nginx.conf"), "/etc/nginx/nginx.conf"). + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/nginx.conf"), "/etc/nginx/nginx.conf"). Return(nil) - // /etc/nginx/sites-available/default (/etc/nginxsites-available isn't existed) + // /etc/nginx/sites-available/default (/etc/nginx/sites-available isn't existed) m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-available"). Return(bytes.Buffer{}, bytes.Buffer{}, fmt.Errorf("")) m.EXPECT().Execf(ctx, "", `mkdir -p "%s"`, "/etc/nginx/sites-available"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) - m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/default"), "/etc/nginx/sites-available/default"). + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/sites-available/default"), "/etc/nginx/sites-available/default"). Return(nil) - // /etc/nginx/sites-available/default (/etc/nginxsites-available is existed) + // /etc/nginx/sites-available/isucondition.conf (/etc/nginx/sites-available is existed) m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-available"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) - m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-available/isucondition.conf"). + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-available/isucondition.conf"). Return(nil) - // /etc/nginx/sites-enabled/isucondition.conf (/etc/nginxsites-available is existed) + // /etc/nginx/sites-enabled/isucondition.conf (/etc/nginx/sites-available is existed) m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-enabled"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) { // recursive due to resolve symlink - m.EXPECT().Host().Return("testdata") - // /etc/nginx/sites-available/default (/etc/nginxsites-available is existed) + m.EXPECT().Host().Return("host01") + // /etc/nginx/sites-available/default (/etc/nginx/sites-available is existed) m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-enabled"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) - m.EXPECT().Deploy(ctx, filepath.Join(testDir, "testdata", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-enabled/isucondition.conf"). + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-enabled/isucondition.conf"). Return(nil) } } return m }(), }, - args: args{targets: []config.DeployTarget{ - { - Src: "nginx_symlink", - Target: "/etc/nginx", + args: args{ + host: "host01", + targets: []config.DeployTarget{ + { + Src: "nginx_symlink", + Target: "/etc/nginx", + }, }, - }}, + }, + }, + { + name: "normal_symlinkToOtherHost", + fields: fields{ + log: zaptest.NewLogger(t), + shell: func() shell.Iface { + m := mock_shell.NewMockIface(mockCtrl) + m.EXPECT().Host().Return("host02") + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + { // recursive due to resolve symlink + m.EXPECT().Host().Return("host02") + // /etc/nginx/nginx.conf (/etc/nginx is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/nginx.conf"), "/etc/nginx/nginx.conf"). + Return(nil) + // /etc/nginx/sites-available/default (/etc/nginx/sites-available isn't existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-available"). + Return(bytes.Buffer{}, bytes.Buffer{}, fmt.Errorf("")) + m.EXPECT().Execf(ctx, "", `mkdir -p "%s"`, "/etc/nginx/sites-available"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/sites-available/default"), "/etc/nginx/sites-available/default"). + Return(nil) + // /etc/nginx/sites-available/isucondition.conf (/etc/nginx/sites-available is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-available"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-available/isucondition.conf"). + Return(nil) + // /etc/nginx/sites-enabled/isucondition.conf (/etc/nginx/sites-enabled is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-enabled"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + { // recursive due to resolve symlink + m.EXPECT().Host().Return("host02") + // /etc/nginx/sites-available/default (/etc/nginx/sites-available is existed) + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/nginx/sites-enabled"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + m.EXPECT().Deploy(ctx, filepath.Join(testDir, "host01", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-enabled/isucondition.conf"). + Return(nil) + } + } + return m + }(), + }, + args: args{ + host: "host02", + targets: []config.DeployTarget{ + { + Src: "nginx_symlink", + Target: "/etc/nginx", + }, + }, + }, }, { name: "abnormal_symlinkCannotResolve", @@ -141,18 +201,44 @@ func TestDeployer_Deploy(t *testing.T) { log: zaptest.NewLogger(t), shell: func() shell.Iface { m := mock_shell.NewMockIface(mockCtrl) - m.EXPECT().Host().Return("testdata") + m.EXPECT().Host().Return("host01") m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/error"). Return(bytes.Buffer{}, bytes.Buffer{}, nil) return m }(), }, - args: args{targets: []config.DeployTarget{ - { - Src: "error", - Target: "/etc/error", + args: args{ + host: "host01", + targets: []config.DeployTarget{ + { + Src: "error", + Target: "/etc/error", + }, }, - }}, + }, + wantErr: true, + }, + { + name: "abnormal_symlinkToOutsideOfLocalRepo", + fields: fields{ + log: zaptest.NewLogger(t), + shell: func() shell.Iface { + m := mock_shell.NewMockIface(mockCtrl) + m.EXPECT().Host().Return("host02") + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + return m + }(), + }, + args: args{ + host: "host02", + targets: []config.DeployTarget{ + { + Src: "hosts_symlink", + Target: "/etc/hosts", + }, + }, + }, wantErr: true, }, } @@ -164,7 +250,7 @@ func TestDeployer_Deploy(t *testing.T) { template: tt.fields.template, localRepoPath: testDir, } - if err := d.Deploy(ctx, tt.args.targets); (err != nil) != tt.wantErr { + if err := d.Deploy(ctx, tt.args.host, tt.args.targets); (err != nil) != tt.wantErr { t.Errorf("Deployer.Deploy() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/pkg/usecases/deploy/testdata/error/unresolved_link b/pkg/usecases/deploy/testdata/host01/error/unresolved_link similarity index 100% rename from pkg/usecases/deploy/testdata/error/unresolved_link rename to pkg/usecases/deploy/testdata/host01/error/unresolved_link diff --git a/pkg/usecases/deploy/testdata/nginx/nginx.conf b/pkg/usecases/deploy/testdata/host01/nginx/nginx.conf similarity index 100% rename from pkg/usecases/deploy/testdata/nginx/nginx.conf rename to pkg/usecases/deploy/testdata/host01/nginx/nginx.conf diff --git a/pkg/usecases/deploy/testdata/nginx/sites-available/default b/pkg/usecases/deploy/testdata/host01/nginx/sites-available/default similarity index 100% rename from pkg/usecases/deploy/testdata/nginx/sites-available/default rename to pkg/usecases/deploy/testdata/host01/nginx/sites-available/default diff --git a/pkg/usecases/deploy/testdata/nginx/sites-available/isucondition.conf b/pkg/usecases/deploy/testdata/host01/nginx/sites-available/isucondition.conf similarity index 100% rename from pkg/usecases/deploy/testdata/nginx/sites-available/isucondition.conf rename to pkg/usecases/deploy/testdata/host01/nginx/sites-available/isucondition.conf diff --git a/pkg/usecases/deploy/testdata/nginx/sites-enabled/isucondition.conf b/pkg/usecases/deploy/testdata/host01/nginx/sites-enabled/isucondition.conf similarity index 100% rename from pkg/usecases/deploy/testdata/nginx/sites-enabled/isucondition.conf rename to pkg/usecases/deploy/testdata/host01/nginx/sites-enabled/isucondition.conf diff --git a/pkg/usecases/deploy/testdata/nginx_symlink b/pkg/usecases/deploy/testdata/host01/nginx_symlink similarity index 100% rename from pkg/usecases/deploy/testdata/nginx_symlink rename to pkg/usecases/deploy/testdata/host01/nginx_symlink diff --git a/pkg/usecases/deploy/testdata/host02/hosts_symlink b/pkg/usecases/deploy/testdata/host02/hosts_symlink new file mode 120000 index 0000000..555dec9 --- /dev/null +++ b/pkg/usecases/deploy/testdata/host02/hosts_symlink @@ -0,0 +1 @@ +/etc/hosts \ No newline at end of file diff --git a/pkg/usecases/deploy/testdata/host02/nginx_symlink b/pkg/usecases/deploy/testdata/host02/nginx_symlink new file mode 120000 index 0000000..1cfd1c3 --- /dev/null +++ b/pkg/usecases/deploy/testdata/host02/nginx_symlink @@ -0,0 +1 @@ +../host01/nginx \ No newline at end of file