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 ./... + 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/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/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/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)) +} diff --git a/pkg/usecases/deploy/deploy.go b/pkg/usecases/deploy/deploy.go index 83f543e..4ac60b1 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" @@ -51,28 +52,54 @@ 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 { +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, 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", realHost)) 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 + } + 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) + } + 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, 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", realHost)) + 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..ec2d4d8 --- /dev/null +++ b/pkg/usecases/deploy/deploy_test.go @@ -0,0 +1,258 @@ +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.Join(filepath.Dir(testFilename), "testdata") + + type fields struct { + log *zap.Logger + shell shell.Iface + template *template.Templator + localRepoPath string + } + type args struct { + host string + 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("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, "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/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, "host01", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-available/isucondition.conf"). + Return(nil) + // /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("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, "host01", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-enabled/isucondition.conf"). + Return(nil) + } + return m + }(), + }, + args: args{ + host: "host01", + targets: []config.DeployTarget{ + { + Src: "nginx", + Target: "/etc/nginx", + }, + }, + }, + }, + { + name: "normal_symlinkToSameHost", + fields: fields{ + log: zaptest.NewLogger(t), + shell: func() shell.Iface { + m := mock_shell.NewMockIface(mockCtrl) + 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("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, "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-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("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, "host01", "nginx/sites-available/isucondition.conf"), "/etc/nginx/sites-enabled/isucondition.conf"). + Return(nil) + } + } + return m + }(), + }, + 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", + fields: fields{ + log: zaptest.NewLogger(t), + shell: func() shell.Iface { + m := mock_shell.NewMockIface(mockCtrl) + m.EXPECT().Host().Return("host01") + m.EXPECT().Execf(ctx, "", `test -d "%s"`, "/etc/error"). + Return(bytes.Buffer{}, bytes.Buffer{}, nil) + return m + }(), + }, + 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, + }, + } + 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.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/host01/error/unresolved_link b/pkg/usecases/deploy/testdata/host01/error/unresolved_link new file mode 120000 index 0000000..3b46a48 --- /dev/null +++ b/pkg/usecases/deploy/testdata/host01/error/unresolved_link @@ -0,0 +1 @@ +/this_is_dummy_src \ No newline at end of file diff --git a/pkg/usecases/deploy/testdata/host01/nginx/nginx.conf b/pkg/usecases/deploy/testdata/host01/nginx/nginx.conf new file mode 100644 index 0000000..249c014 --- /dev/null +++ b/pkg/usecases/deploy/testdata/host01/nginx/nginx.conf @@ -0,0 +1 @@ +# For unittest (nginx.conf) diff --git a/pkg/usecases/deploy/testdata/host01/nginx/sites-available/default b/pkg/usecases/deploy/testdata/host01/nginx/sites-available/default new file mode 100644 index 0000000..0549981 --- /dev/null +++ b/pkg/usecases/deploy/testdata/host01/nginx/sites-available/default @@ -0,0 +1 @@ +# For unittest (default) diff --git a/pkg/usecases/deploy/testdata/host01/nginx/sites-available/isucondition.conf b/pkg/usecases/deploy/testdata/host01/nginx/sites-available/isucondition.conf new file mode 100644 index 0000000..69d7c24 --- /dev/null +++ b/pkg/usecases/deploy/testdata/host01/nginx/sites-available/isucondition.conf @@ -0,0 +1 @@ +# For unittest (isucondition.conf) diff --git a/pkg/usecases/deploy/testdata/host01/nginx/sites-enabled/isucondition.conf b/pkg/usecases/deploy/testdata/host01/nginx/sites-enabled/isucondition.conf new file mode 120000 index 0000000..0a267d7 --- /dev/null +++ b/pkg/usecases/deploy/testdata/host01/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/host01/nginx_symlink b/pkg/usecases/deploy/testdata/host01/nginx_symlink new file mode 120000 index 0000000..da70e20 --- /dev/null +++ b/pkg/usecases/deploy/testdata/host01/nginx_symlink @@ -0,0 +1 @@ +nginx \ No newline at end of file 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 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 +}