Repository: vmware-tanzu/octant
Pull request #592 (2020-01-28 20:38:02)

Reload kubeconfig file when it changes on the fs
* Reload kubeconfig file when it changes on the fs
* move go generated fake checking to github actions

Re: #591

Signed-off-by: Bryan Liles <bryanliles@gmail.com>


Patch:
diff --git a/.drone.yml b/.drone.yml
index 97a4c72a..d848f136 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -11,13 +11,6 @@ steps:
     image: docker:git
     commands:
       - git fetch --tags
-  - name: mocks
-    image: golang:1.13
-    environment:
-      GOFLAGS: -mod=vendor
-      GO111MODULE: on
-    commands:
-      - ci/check-generated-mocks.sh
   - name: frontend
     image: sfoo/node-chrome:0.0.2
     commands:
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
new file mode 100644
index 00000000..994879f4
--- /dev/null
+++ b/.github/workflows/main.yaml
@@ -0,0 +1,30 @@
+name: Integrate
+on:
+  - push
+  - pull_request
+jobs:
+  test:
+    strategy:
+      matrix:
+        go-version:
+          - 1.13.x
+        platform: [ubuntu-latest, macos-latest]
+    runs-on: ${{ matrix.platform }}
+    name: Test ${{ matrix.platform }}
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v1
+        with:
+          go-version: ${{ matrix.go-version }}
+      - name: Checkout code
+        uses: actions/checkout@v2
+      - name: Set GOPATH
+        # temporary fix
+        # see https://github.com/actions/setup-go/issues/14
+        run: |
+          echo "##[set-env name=GOPATH;]$(dirname $GITHUB_WORKSPACE)"
+          echo "##[add-path]$(dirname $GITHUB_WORKSPACE)/bin"
+        shell: bash
+      - name: Verify generated code
+        run: ci/check-generated-mocks.sh
+
diff --git a/changelogs/unreleased/591-bryanl b/changelogs/unreleased/591-bryanl
new file mode 100644
index 00000000..08b258ce
--- /dev/null
+++ b/changelogs/unreleased/591-bryanl
@@ -0,0 +1,1 @@
+Reload cluster client when kubeconfig changes
diff --git a/ci/check-generated-mocks.sh b/ci/check-generated-mocks.sh
index dfef27cb..f8777f2a 100755
--- a/ci/check-generated-mocks.sh
+++ b/ci/check-generated-mocks.sh
@@ -3,11 +3,16 @@
 # This script makes sure that the checked-in mock files ("fake" packages) are
 # up-to-date.
 
+set -ex
+
 go run build.go go-install
 find pkg -type d -name fake -prune -exec rm -rf {} \;
 find internal -type d -name fake -prune -exec rm -rf {} \;
 go run build.go generate
+
+set +e
 diff=$(git status --porcelain | grep fake)
+set -e
 
 if [ ! -z "$diff" ]; then
     echo "The generated mock files are not up-to-date" >&2
diff --git a/go.mod b/go.mod
index f681d96a..809f0a97 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ require (
 	github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
 	github.com/elazarl/goproxy v0.0.0-20190703090003-6125c262ffb0 // indirect
 	github.com/elazarl/goproxy/ext v0.0.0-20190703090003-6125c262ffb0 // indirect
+	github.com/fsnotify/fsnotify v1.4.7
 	github.com/gobwas/glob v0.2.3
 	github.com/golang/mock v1.3.1
 	github.com/golang/protobuf v1.3.1
@@ -36,7 +37,6 @@ require (
 	github.com/stretchr/testify v1.3.0
 	go.opencensus.io v0.22.1
 	go.uber.org/zap v1.10.0
-	golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8
 	golang.org/x/sync v0.0.0-20190423024810-112230192c58
 	google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 // indirect
 	google.golang.org/grpc v1.23.0
diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go
index bf021654..a30f042b 100644
--- a/internal/cluster/cluster.go
+++ b/internal/cluster/cluster.go
@@ -231,9 +231,9 @@ func (c *Cluster) Version() (string, error) {
 	return fmt.Sprint(serverVersion), nil
 }
 
-// FromKubeConfig creates a Cluster from a kubeConfig.
-func FromKubeConfig(ctx context.Context, kubeConfig, contextName string, initialNamespace string, options RESTConfigOptions) (*Cluster, error) {
-	chain := strings.Deduplicate(filepath.SplitList(kubeConfig))
+// FromKubeConfig creates a Cluster from a kubeConfig chain.
+func FromKubeConfig(ctx context.Context, kubeConfigList, contextName string, initialNamespace string, options RESTConfigOptions) (*Cluster, error) {
+	chain := strings.Deduplicate(filepath.SplitList(kubeConfigList))
 
 	rules := &clientcmd.ClientConfigLoadingRules{
 		Precedence: chain,
diff --git a/pkg/dash/dash.go b/pkg/dash/dash.go
index e8cc4ee7..1c3b96cf 100644
--- a/pkg/dash/dash.go
+++ b/pkg/dash/dash.go
@@ -161,6 +161,10 @@ func Run(ctx context.Context, logger log.Logger, shutdownCh chan bool, options O
 		options.Context,
 		restConfigOptions)
 
+	if err := watchConfigs(ctx, dashConfig, options.KubeConfig); err != nil {
+		return fmt.Errorf("set up config watcher: %w", err)
+	}
+
 	moduleList, err := initModules(ctx, dashConfig, options.Namespace, options)
 	if err != nil {
 		return fmt.Errorf("initializing modules: %w", err)
diff --git a/pkg/dash/fake/mock_file_watcher.go b/pkg/dash/fake/mock_file_watcher.go
new file mode 100644
index 00000000..6e0934a4
--- /dev/null
+++ b/pkg/dash/fake/mock_file_watcher.go
@@ -0,0 +1,76 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/vmware-tanzu/octant/pkg/dash (interfaces: FileWatcher)
+
+// Package fake is a generated GoMock package.
+package fake
+
+import (
+	fsnotify "github.com/fsnotify/fsnotify"
+	gomock "github.com/golang/mock/gomock"
+	reflect "reflect"
+)
+
+// MockFileWatcher is a mock of FileWatcher interface
+type MockFileWatcher struct {
+	ctrl     *gomock.Controller
+	recorder *MockFileWatcherMockRecorder
+}
+
+// MockFileWatcherMockRecorder is the mock recorder for MockFileWatcher
+type MockFileWatcherMockRecorder struct {
+	mock *MockFileWatcher
+}
+
+// NewMockFileWatcher creates a new mock instance
+func NewMockFileWatcher(ctrl *gomock.Controller) *MockFileWatcher {
+	mock := &MockFileWatcher{ctrl: ctrl}
+	mock.recorder = &MockFileWatcherMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockFileWatcher) EXPECT() *MockFileWatcherMockRecorder {
+	return m.recorder
+}
+
+// Add mocks base method
+func (m *MockFileWatcher) Add(arg0 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Add", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Add indicates an expected call of Add
+func (mr *MockFileWatcherMockRecorder) Add(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockFileWatcher)(nil).Add), arg0)
+}
+
+// Errors mocks base method
+func (m *MockFileWatcher) Errors() chan error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Errors")
+	ret0, _ := ret[0].(chan error)
+	return ret0
+}
+
+// Errors indicates an expected call of Errors
+func (mr *MockFileWatcherMockRecorder) Errors() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errors", reflect.TypeOf((*MockFileWatcher)(nil).Errors))
+}
+
+// Events mocks base method
+func (m *MockFileWatcher) Events() chan fsnotify.Event {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Events")
+	ret0, _ := ret[0].(chan fsnotify.Event)
+	return ret0
+}
+
+// Events indicates an expected call of Events
+func (mr *MockFileWatcherMockRecorder) Events() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Events", reflect.TypeOf((*MockFileWatcher)(nil).Events))
+}
diff --git a/pkg/dash/fake/mock_watcher_config.go b/pkg/dash/fake/mock_watcher_config.go
new file mode 100644
index 00000000..067b1e3d
--- /dev/null
+++ b/pkg/dash/fake/mock_watcher_config.go
@@ -0,0 +1,62 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/vmware-tanzu/octant/pkg/dash (interfaces: WatcherConfig)
+
+// Package fake is a generated GoMock package.
+package fake
+
+import (
+	context "context"
+	gomock "github.com/golang/mock/gomock"
+	reflect "reflect"
+)
+
+// MockWatcherConfig is a mock of WatcherConfig interface
+type MockWatcherConfig struct {
+	ctrl     *gomock.Controller
+	recorder *MockWatcherConfigMockRecorder
+}
+
+// MockWatcherConfigMockRecorder is the mock recorder for MockWatcherConfig
+type MockWatcherConfigMockRecorder struct {
+	mock *MockWatcherConfig
+}
+
+// NewMockWatcherConfig creates a new mock instance
+func NewMockWatcherConfig(ctrl *gomock.Controller) *MockWatcherConfig {
+	mock := &MockWatcherConfig{ctrl: ctrl}
+	mock.recorder = &MockWatcherConfigMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockWatcherConfig) EXPECT() *MockWatcherConfigMockRecorder {
+	return m.recorder
+}
+
+// ContextName mocks base method
+func (m *MockWatcherConfig) ContextName() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ContextName")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// ContextName indicates an expected call of ContextName
+func (mr *MockWatcherConfigMockRecorder) ContextName() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContextName", reflect.TypeOf((*MockWatcherConfig)(nil).ContextName))
+}
+
+// UseContext mocks base method
+func (m *MockWatcherConfig) UseContext(arg0 context.Context, arg1 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UseContext", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// UseContext indicates an expected call of UseContext
+func (mr *MockWatcherConfigMockRecorder) UseContext(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseContext", reflect.TypeOf((*MockWatcherConfig)(nil).UseContext), arg0, arg1)
+}
diff --git a/pkg/dash/watcher.go b/pkg/dash/watcher.go
new file mode 100644
index 00000000..00e032b4
--- /dev/null
+++ b/pkg/dash/watcher.go
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ */
+
+package dash
+
+import (
+	"context"
+	"fmt"
+	"path/filepath"
+
+	"github.com/fsnotify/fsnotify"
+
+	"github.com/vmware-tanzu/octant/internal/log"
+	internalStrings "github.com/vmware-tanzu/octant/internal/util/strings"
+)
+
+//go:generate mockgen -destination=./fake/mock_file_watcher.go -package=fake github.com/vmware-tanzu/octant/pkg/dash FileWatcher
+//go:generate mockgen -destination=./fake/mock_watcher_config.go -package=fake github.com/vmware-tanzu/octant/pkg/dash WatcherConfig
+
+// FileWatcher watches files.
+type FileWatcher interface {
+	// Add adds a file to watch.
+	Add(name string) error
+	// Events returns a channel with fsnotify events.
+	Events() chan fsnotify.Event
+	// Errors returns errors.
+	Errors() chan error
+}
+
+// DefaultFileWatcher wraps fsnotify.Watcher so it can adhere to the
+// FileWatcher API.
+type DefaultFileWatcher struct {
+	watcher *fsnotify.Watcher
+}
+
+var _ FileWatcher = (*DefaultFileWatcher)(nil)
+
+// NewDefaultFileWatcher creates an instance of DefaultFileWatcher.
+func NewDefaultFileWatcher() (*DefaultFileWatcher, error) {
+	watcher, err := fsnotify.NewWatcher()
+	if err != nil {
+		return nil, fmt.Errorf("create fsnotify watcher: %w", err)
+	}
+
+	return &DefaultFileWatcher{
+		watcher: watcher,
+	}, nil
+}
+
+// Add adds a file name to be watched.
+func (d *DefaultFileWatcher) Add(name string) error {
+	return d.watcher.Add(name)
+}
+
+// Events returns a channel of fsnotify events.
+func (d *DefaultFileWatcher) Events() chan fsnotify.Event {
+	return d.watcher.Events
+}
+
+// Errors returns a channel of errors.
+func (d DefaultFileWatcher) Errors() chan error {
+	return d.watcher.Errors
+}
+
+// ConfigWatcherOption is an option for configuration ConfigWatcher.
+type ConfigWatcherOption func(cw *ConfigWatcher)
+
+// ConfigWatcherFileWatcher sets the file watcher for ConfigWatcher.
+func ConfigWatcherFileWatcher(fw FileWatcher) ConfigWatcherOption {
+	return func(cw *ConfigWatcher) {
+		cw.FileWatcher = fw
+	}
+}
+
+// WatcherConfig is an interface with configuration for ConfigWatcher.
+type WatcherConfig interface {
+	ContextName() string
+	UseContext(ctx context.Context, name string) error
+}
+
+// ConfigWatcher watches kubernetes configurations.
+type ConfigWatcher struct {
+	FileWatcher   FileWatcher
+	watcherConfig WatcherConfig
+}
+
+// NewConfigWatcher creates an instance of ConfigWatcher.
+func NewConfigWatcher(wc WatcherConfig, options ...ConfigWatcherOption) (*ConfigWatcher, error) {
+	cw := &ConfigWatcher{
+		watcherConfig: wc,
+	}
+
+	for _, option := range options {
+		option(cw)
+	}
+
+	if cw.FileWatcher == nil {
+		fw, err := NewDefaultFileWatcher()
+		if err != nil {
+			return nil, fmt.Errorf("create file watcher: %w", err)
+		}
+		cw.FileWatcher = fw
+	}
+
+	return cw, nil
+}
+
+// Add adds file names to be watched.
+func (cw *ConfigWatcher) Add(ctx context.Context, names ...string) error {
+	logger := log.From(ctx).With("component", "config-watcher")
+
+	for _, name := range names {
+		logger.With("config", name).Infof("watching config file")
+		if err := cw.FileWatcher.Add(name); err != nil {
+			return fmt.Errorf("unable to watch %s: %w", name, err)
+		}
+	}
+
+	return nil
+}
+
+// Watch runs the config watcher loop
+func (cw *ConfigWatcher) Watch(ctx context.Context) {
+	logger := log.From(ctx).With("component", "config-watcher")
+	done := false
+	for !done {
+		select {
+		case <-ctx.Done():
+			done = true
+			logger.Infof("shutting down config watcher")
+		case _ = <-cw.FileWatcher.Events():
+			if err := cw.watcherConfig.UseContext(ctx, cw.watcherConfig.ContextName()); err != nil {
+				logger.WithErr(err).Errorf("reload config")
+			}
+		case err := <-cw.FileWatcher.Errors():
+			logger.WithErr(err).Errorf("event error")
+		}
+	}
+}
+
+// watchConfigs watches kubernetes config files. If any file changes, reload the cluster client.
+func watchConfigs(ctx context.Context, wc WatcherConfig, configChainStr string) error {
+	cw, err := NewConfigWatcher(wc)
+	if err != nil {
+		return fmt.Errorf("create config watcher: %w", err)
+	}
+
+	chain := internalStrings.Deduplicate(filepath.SplitList(configChainStr))
+	for _, f := range chain {
+		if err := cw.Add(ctx, f); err != nil {
+			return fmt.Errorf("unable to watch %s: %w", f, err)
+		}
+	}
+
+	go cw.Watch(ctx)
+
+	return nil
+}
diff --git a/vendor/github.com/hashicorp/go-plugin/go.mod b/vendor/github.com/hashicorp/go-plugin/go.mod
index f3ddf44e..df921016 100644
--- a/vendor/github.com/hashicorp/go-plugin/go.mod
+++ b/vendor/github.com/hashicorp/go-plugin/go.mod
@@ -15,3 +15,5 @@ require (
 	google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 // indirect
 	google.golang.org/grpc v1.14.0
 )
+
+go 1.13
diff --git a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/doc.go b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/doc.go
index deb88283..10f5ab9f 100644
--- a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/doc.go
+++ b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/doc.go
@@ -21,4 +21,4 @@ limitations under the License.
 // +groupName=metrics.k8s.io
 
 // Package v1beta1 is the v1beta1 version of the metrics API.
-package metricsv1beta1 // import "k8s.io/metrics/pkg/apis/metrics/v1beta1"
+package v1beta1 // import "k8s.io/metrics/pkg/apis/metrics/v1beta1"
diff --git a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/generated.pb.go b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/generated.pb.go
index b4aee6a6..185ee262 100644
--- a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/generated.pb.go
+++ b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/generated.pb.go
@@ -17,7 +17,7 @@ limitations under the License.
 // Code generated by protoc-gen-gogo. DO NOT EDIT.
 // source: k8s.io/kubernetes/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/generated.proto
 
-package metricsv1beta1
+package v1beta1
 
 import (
 	fmt "fmt"
diff --git a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/register.go b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/register.go
index 0b9e8615..205d253c 100644
--- a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/register.go
+++ b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/register.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package metricsv1beta1
+package v1beta1
 
 import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/types.go b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/types.go
index 8bfa52fd..c6ea24a3 100644
--- a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/types.go
+++ b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/types.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package metricsv1beta1
+package v1beta1
 
 import (
 	"k8s.io/api/core/v1"
diff --git a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/zz_generated.conversion.go b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/zz_generated.conversion.go
index ac77072a..99621ac2 100644
--- a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/zz_generated.conversion.go
+++ b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/zz_generated.conversion.go
@@ -18,7 +18,7 @@ limitations under the License.
 
 // Code generated by conversion-gen. DO NOT EDIT.
 
-package metricsv1beta1
+package v1beta1
 
 import (
 	unsafe "unsafe"
diff --git a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/zz_generated.deepcopy.go b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/zz_generated.deepcopy.go
index b8523c1e..f418d2a4 100644
--- a/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/zz_generated.deepcopy.go
+++ b/vendor/k8s.io/metrics/pkg/apis/metrics/v1beta1/zz_generated.deepcopy.go
@@ -18,7 +18,7 @@ limitations under the License.
 
 // Code generated by deepcopy-gen. DO NOT EDIT.
 
-package metricsv1beta1
+package v1beta1
 
 import (
 	v1 "k8s.io/api/core/v1"


Test patch:
diff --git a/pkg/dash/watcher_test.go b/pkg/dash/watcher_test.go
new file mode 100644
index 00000000..e9c99591
--- /dev/null
+++ b/pkg/dash/watcher_test.go
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ */
+
+package dash_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/fsnotify/fsnotify"
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/require"
+
+	"github.com/vmware-tanzu/octant/pkg/dash"
+	"github.com/vmware-tanzu/octant/pkg/dash/fake"
+)
+
+func TestConfigWatcher_Add(t *testing.T) {
+	tests := []struct {
+		name         string
+		filenames    []string
+		filenamesErr error
+	}{
+		{
+			name:      "in general",
+			filenames: []string{"kubeconfig"},
+		},
+		{
+			name:         "file add error",
+			filenames:    []string{"kubeconfig"},
+			filenamesErr: fmt.Errorf("boom"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			controller := gomock.NewController(t)
+			defer controller.Finish()
+
+			watcherConfig := fake.NewMockWatcherConfig(controller)
+			fileWatcher := fake.NewMockFileWatcher(controller)
+
+			for _, f := range tt.filenames {
+				fileWatcher.EXPECT().Add(f).Return(tt.filenamesErr)
+			}
+
+			fileWatcherOption := dash.ConfigWatcherFileWatcher(fileWatcher)
+
+			ctx := context.Background()
+			cw, err := dash.NewConfigWatcher(watcherConfig, fileWatcherOption)
+			require.NoError(t, err)
+
+			err = cw.Add(ctx, tt.filenames...)
+			if tt.filenamesErr != nil {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+func TestConfigWatcher_Watch(t *testing.T) {
+	controller := gomock.NewController(t)
+	defer controller.Finish()
+
+	watcherConfig := fake.NewMockWatcherConfig(controller)
+
+	watcherConfig.EXPECT().ContextName().Return("name")
+	watcherConfig.EXPECT().UseContext(gomock.Any(), "name").Return(nil)
+
+	fileWatcher := fake.NewMockFileWatcher(controller)
+
+	eventCh := make(chan fsnotify.Event)
+	fileWatcher.EXPECT().Events().Return(eventCh).AnyTimes()
+
+	errCh := make(chan error)
+	fileWatcher.EXPECT().Errors().Return(errCh).AnyTimes()
+
+	fileWatcherOption := dash.ConfigWatcherFileWatcher(fileWatcher)
+
+	ctx, cancel := context.WithCancel(context.Background())
+	cw, err := dash.NewConfigWatcher(watcherConfig, fileWatcherOption)
+	require.NoError(t, err)
+
+	go cw.Watch(ctx)
+
+	event := fsnotify.Event{
+		Name: "event",
+		Op:   0,
+	}
+
+	eventCh <- event
+	cancel()
+}