diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/lint.yml similarity index 80% rename from .github/workflows/golangci-lint.yml rename to .github/workflows/lint.yml index d4cb9be..3a503b1 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/lint.yml @@ -24,5 +24,11 @@ jobs: - name: Install golangci-lint run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 + - name: Install golint + run: go get -u golang.org/x/lint/golint + - name: Run golangci-lint run: $(go env GOPATH)/bin/golangci-lint run ./... --timeout 5m + + - name: Run golint + run: $(go env GOPATH)/bin/golint ./... diff --git a/.golangci.yml b/.golangci.yml index 80b94c3..89d5f52 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,13 +1,10 @@ linters: - disable: - - goimports - - gochecknoglobals - - gosec - - prealloc enable-all: true # all available settings of specific linters linters-settings: + unparam: + check-exported: true funlen: lines: 120 statements: 80 diff --git a/cmd/hostctl/actions/add_domains.go b/cmd/hostctl/actions/add_domains.go index 6b551e2..dcd3f06 100644 --- a/cmd/hostctl/actions/add_domains.go +++ b/cmd/hostctl/actions/add_domains.go @@ -42,7 +42,7 @@ If the profile already exists it will be added to it.`, return err } if !quiet { - cligger.Success("Domains '%s' added.\n\n", strings.Join(args[1:], ", ")) + cligger.Success("Domains '%s' added.\n", strings.Join(args[1:], ", ")) } return nil }, diff --git a/cmd/hostctl/actions/add_domains_test.go b/cmd/hostctl/actions/add_domains_test.go index 995be10..1a67a67 100644 --- a/cmd/hostctl/actions/add_domains_test.go +++ b/cmd/hostctl/actions/add_domains_test.go @@ -1,93 +1,63 @@ package actions import ( - "bytes" - "io/ioutil" - "os" "testing" - - "github.com/stretchr/testify/assert" ) func Test_AddDomains(t *testing.T) { cmd := NewRootCmd() - t.Run("Add domains", func(t *testing.T) { - tmp := makeTempHostsFile(t, "addDomainCmd") - defer os.Remove(tmp.Name()) - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"add", "domains", "profile1", "arg.domain.loc", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) + r := NewRunner(t, cmd, "addDomains") + defer r.Clean() - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+-----------+----------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+----------------+ -| profile1 | on | 127.0.0.1 | first.loc | -| profile1 | on | 127.0.0.1 | second.loc | -| profile1 | on | 127.0.0.1 | arg.domain.loc | -+----------+--------+-----------+----------------+ -` - assert.Contains(t, actual, expected) + t.Run("Add domains", func(t *testing.T) { + r.Run("hostctl add domains profile1 arg.domain.loc"). + Containsf(` + [ℹ] Using hosts file: %s + + [✔] Domains 'arg.domain.loc' added. + + +----------+--------+-----------+----------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+----------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + | profile1 | on | 127.0.0.1 | arg.domain.loc | + +----------+--------+-----------+----------------+ +`, r.Hostfile()) }) t.Run("Add domains new profile", func(t *testing.T) { - tmp := makeTempHostsFile(t, "addDomainCmd") - defer os.Remove(tmp.Name()) - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"add", "domains", "newprofile", "arg.domain.loc", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+------------+--------+-----------+----------------+ -| PROFILE | STATUS | IP | DOMAIN | -+------------+--------+-----------+----------------+ -| newprofile | on | 127.0.0.1 | arg.domain.loc | -+------------+--------+-----------+----------------+ -` - assert.Contains(t, actual, expected) + r.Run("hostctl add domains newprofile arg.domain.loc"). + Containsf(` + [ℹ] Using hosts file: %s + + [✔] Domains 'arg.domain.loc' added. + + +------------+--------+-----------+----------------+ + | PROFILE | STATUS | IP | DOMAIN | + +------------+--------+-----------+----------------+ + | newprofile | on | 127.0.0.1 | arg.domain.loc | + +------------+--------+-----------+----------------+ +`, r.Hostfile()) }) t.Run("Add domains with IP", func(t *testing.T) { - tmp := makeTempHostsFile(t, "addDomainCmd") - defer os.Remove(tmp.Name()) - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"add", "domains", "profile1", "--ip", "5.5.5.5", "arg2.domain.loc", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+-----------+-----------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+-----------------+ -| profile1 | on | 127.0.0.1 | first.loc | -| profile1 | on | 127.0.0.1 | second.loc | -| profile1 | on | 5.5.5.5 | arg2.domain.loc | -+----------+--------+-----------+-----------------+ -` - assert.Contains(t, actual, expected) + r := NewRunner(t, cmd, "addWithIP") + defer r.Clean() + r.Run("hostctl add domains profile1 --ip 5.5.5.5 arg.domain.loc"). + Containsf(` + [ℹ] Using hosts file: %s + + [✔] Domains 'arg.domain.loc' added. + + +----------+--------+-----------+----------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+----------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + | profile1 | on | 5.5.5.5 | arg.domain.loc | + +----------+--------+-----------+----------------+ +`, r.Hostfile()) }) } diff --git a/cmd/hostctl/actions/add_replace_test.go b/cmd/hostctl/actions/add_replace_test.go index 3cb9f45..67043e0 100644 --- a/cmd/hostctl/actions/add_replace_test.go +++ b/cmd/hostctl/actions/add_replace_test.go @@ -1,127 +1,104 @@ package actions import ( - "bytes" - "io/ioutil" "os" "strings" "testing" - - "github.com/stretchr/testify/assert" ) func Test_Add(t *testing.T) { - tmp := makeTempHostsFile(t, "addCmd") - defer os.Remove(tmp.Name()) - cmd := NewRootCmd() + r := NewRunner(t, cmd, "add") + defer r.Clean() + + tmp := r.TempHostfile("source") + defer os.Remove(tmp.Name()) + t.Run("Add from file", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"add", "awesome", "--uniq", "--from", tmp.Name(), "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+---------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+---------+--------+-----------+------------+ -| awesome | on | 127.0.0.1 | localhost | -| awesome | on | 127.0.0.1 | first.loc | -| awesome | on | 127.0.0.1 | second.loc | -+---------+--------+-----------+------------+ -` - assert.Contains(t, actual, expected) + r.Runf("hostctl add awesome --uniq --from %s", tmp.Name()). + Contains(` + +---------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +---------+--------+-----------+------------+ + | awesome | on | 127.0.0.1 | localhost | + | awesome | on | 127.0.0.1 | first.loc | + | awesome | on | 127.0.0.1 | second.loc | + +---------+--------+-----------+------------+ + `) }) t.Run("Add from stdin", func(t *testing.T) { - b := bytes.NewBufferString("") + r := NewRunner(t, cmd, "add") + defer r.Clean() + + tmp := r.TempHostfile("source") + defer os.Remove(tmp.Name()) in := strings.NewReader(`3.3.3.3 stdin.loc`) - cmd.SetOut(b) cmd.SetIn(in) - cmd.SetArgs([]string{"add", "awesome", "--host-file", tmp.Name()}) - err := cmd.Execute() - assert.NoError(t, err) + r.Run("hostctl add awesome"). + Containsf(` + [ℹ] Using hosts file: %s - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -` - assert.Contains(t, actual, expected) + +---------+--------+---------+-----------+ + | PROFILE | STATUS | IP | DOMAIN | + +---------+--------+---------+-----------+ + | awesome | on | 3.3.3.3 | stdin.loc | + +---------+--------+---------+-----------+ + `, r.Hostfile()) }) } func Test_ReplaceStdin(t *testing.T) { cmd := NewRootCmd() - tmp := makeTempHostsFile(t, "replaceStdinCmd") - defer os.Remove(tmp.Name()) + r := NewRunner(t, cmd, "replace") + defer r.Clean() - b := bytes.NewBufferString("") - cmd.SetOut(b) + tmp := r.TempHostfile("source") + defer os.Remove(tmp.Name()) in := strings.NewReader(`3.3.3.3 stdin.replaced.loc`) cmd.SetIn(in) - cmd.SetArgs([]string{"replace", "profile1", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) + r.Run("hostctl replace profile1"). + Containsf(` + [ℹ] Using hosts file: %s - actual := "\n" + string(out) - assert.Contains(t, actual, ` -+----------+--------+---------+--------------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+---------+--------------------+ -| profile1 | on | 3.3.3.3 | stdin.replaced.loc | -+----------+--------+---------+--------------------+ -`) + +----------+--------+---------+--------------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+---------+--------------------+ + | profile1 | on | 3.3.3.3 | stdin.replaced.loc | + +----------+--------+---------+--------------------+ + `, r.Hostfile()) } func Test_ReplaceFile(t *testing.T) { cmd := NewRootCmd() in := strings.NewReader(` -5.5.5.5 replaced.loc -5.5.5.6 replaced2.loc + 5.5.5.5 replaced.loc + 5.5.5.6 replaced2.loc `) + cmd.SetIn(in) - tmp := makeTempHostsFile(t, "replaceFileCmd") - defer os.Remove(tmp.Name()) + r := NewRunner(t, cmd, "replace") + defer r.Clean() - b := bytes.NewBufferString("") + tmp := r.TempHostfile("source") + defer os.Remove(tmp.Name()) - cmd.SetIn(in) - cmd.SetOut(b) - cmd.SetArgs([]string{"replace", "awesome", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - assert.Contains(t, actual, ` -+---------+--------+---------+---------------+ -| PROFILE | STATUS | IP | DOMAIN | -+---------+--------+---------+---------------+ -| awesome | on | 5.5.5.5 | replaced.loc | -| awesome | on | 5.5.5.6 | replaced2.loc | -+---------+--------+---------+---------------+ -`) + r.Run("hostctl replace awesome"). + Containsf(` + [ℹ] Using hosts file: %s + + +---------+--------+---------+---------------+ + | PROFILE | STATUS | IP | DOMAIN | + +---------+--------+---------+---------------+ + | awesome | on | 5.5.5.5 | replaced.loc | + | awesome | on | 5.5.5.6 | replaced2.loc | + +---------+--------+---------+---------------+ + `, r.Hostfile()) } diff --git a/cmd/hostctl/actions/backup_test.go b/cmd/hostctl/actions/backup_test.go index 3104b97..56c0d24 100644 --- a/cmd/hostctl/actions/backup_test.go +++ b/cmd/hostctl/actions/backup_test.go @@ -1,43 +1,39 @@ package actions import ( - "bytes" - "io/ioutil" + "fmt" "os" "testing" - - "github.com/stretchr/testify/assert" + "time" ) func Test_Backup(t *testing.T) { cmd := NewRootCmd() - tmp := makeTempHostsFile(t, "backupCmd") - defer os.Remove(tmp.Name()) - - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"backup", "--path", "/tmp", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - assert.Contains(t, actual, ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| default | on | 127.0.0.1 | localhost | -+----------+--------+-----------+------------+ -| profile1 | on | 127.0.0.1 | first.loc | -| profile1 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -| profile2 | off | 127.0.0.1 | first.loc | -| profile2 | off | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -`) + r := NewRunner(t, cmd, "backup") + defer r.Clean() + + date := time.Now().UTC().Format("20060102") + + backupFile := fmt.Sprintf("%s.%s", r.Hostfile(), date) + defer os.Remove(backupFile) + + r.Run("hostctl backup --path /tmp"). + Containsf(` + [ℹ] Using hosts file: %s + + [✔] Backup '%s' created. + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | default | on | 127.0.0.1 | localhost | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + | profile2 | off | 127.0.0.1 | first.loc | + | profile2 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile(), backupFile) } diff --git a/cmd/hostctl/actions/enable_disable_test.go b/cmd/hostctl/actions/enable_disable_test.go index 78ab52b..697b6ec 100644 --- a/cmd/hostctl/actions/enable_disable_test.go +++ b/cmd/hostctl/actions/enable_disable_test.go @@ -1,248 +1,187 @@ package actions import ( - "bytes" - "io/ioutil" - "os" "testing" - "github.com/stretchr/testify/assert" - "github.com/guumaster/hostctl/pkg/types" ) func Test_Disable(t *testing.T) { cmd := NewRootCmd() - tmp := makeTempHostsFile(t, "disableCmd") - defer os.Remove(tmp.Name()) + r := NewRunner(t, cmd, "disable") + defer r.Clean() t.Run("Disable", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"disable", "profile1", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| profile1 | off | 127.0.0.1 | first.loc | -| profile1 | off | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -` - assert.Contains(t, actual, expected) - }) - - t.Run("Disable unknown", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"disable", "unknown", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.EqualError(t, err, types.ErrUnknownProfile.Error()) + r.Run("hostctl disable profile1"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile1 | off | 127.0.0.1 | first.loc | + | profile1 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()) }) t.Run("Disable Only", func(t *testing.T) { - cmd := NewRootCmd() - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"disable", "profile1", "--only", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - cmd.SetArgs([]string{"list", "--host-file", tmp.Name()}) - - err = cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| default | on | 127.0.0.1 | localhost | -+----------+--------+-----------+------------+ -| profile1 | off | 127.0.0.1 | first.loc | -| profile1 | off | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -| profile2 | on | 127.0.0.1 | first.loc | -| profile2 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -` - assert.Contains(t, actual, expected) + r.Run("hostctl disable profile1 --only"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile1 | off | 127.0.0.1 | first.loc | + | profile1 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()). + Run("hostctl list"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | default | on | 127.0.0.1 | localhost | + +----------+--------+-----------+------------+ + | profile1 | off | 127.0.0.1 | first.loc | + | profile1 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + | profile2 | on | 127.0.0.1 | first.loc | + | profile2 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()) }) } -func Test_EnableDisableAll(t *testing.T) { +func Test_EnableDisableErrors(t *testing.T) { cmd := NewRootCmd() - tmp := makeTempHostsFile(t, "disableCmd") - defer os.Remove(tmp.Name()) + r := NewRunner(t, cmd, "enableDisableErrors") + defer r.Clean() - t.Run("Disable All", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"disable", "--all", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| default | on | 127.0.0.1 | localhost | -+----------+--------+-----------+------------+ -| profile1 | off | 127.0.0.1 | first.loc | -| profile1 | off | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -| profile2 | off | 127.0.0.1 | first.loc | -| profile2 | off | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -` - assert.Contains(t, actual, expected) + t.Run("Enable/Disable all error", func(t *testing.T) { + r.RunE("hostctl disable something --all", ErrIncompatibleAllFlag).Empty() + r.RunE("hostctl enable something --all", ErrIncompatibleAllFlag).Empty() }) +} - t.Run("Disable all error", func(t *testing.T) { - b := bytes.NewBufferString("") +func Test_EnableDisableUnknown(t *testing.T) { + cmd := NewRootCmd() - cmd.SetOut(b) - cmd.SetArgs([]string{"disable", "any", "--all", "--host-file", tmp.Name()}) + r := NewRunner(t, cmd, "enableDisableUnknown") + defer r.Clean() - err := cmd.Execute() - assert.EqualError(t, err, "args must be empty with --all flag") + t.Run("Enable unknown", func(t *testing.T) { + r.RunE("hostctl enable unknown", types.ErrUnknownProfile). + Containsf(` + [ℹ] Using hosts file: %s + `, r.Hostfile()) }) - t.Run("Enable All", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"enable", "--all", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| default | on | 127.0.0.1 | localhost | -+----------+--------+-----------+------------+ -| profile1 | on | 127.0.0.1 | first.loc | -| profile1 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -| profile2 | on | 127.0.0.1 | first.loc | -| profile2 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -` - assert.Contains(t, actual, expected) + t.Run("Disable unknown", func(t *testing.T) { + r.RunE("hostctl disable unknown", types.ErrUnknownProfile). + Containsf(` + [ℹ] Using hosts file: %s + `, r.Hostfile()) }) +} + +func Test_EnableDisableAll(t *testing.T) { + cmd := NewRootCmd() - t.Run("Enable all error", func(t *testing.T) { - b := bytes.NewBufferString("") + r := NewRunner(t, cmd, "disable") + defer r.Clean() - cmd.SetOut(b) - cmd.SetArgs([]string{"enable", "any", "--all", "--host-file", tmp.Name()}) + t.Run("Disable All", func(t *testing.T) { + r.Run("hostctl disable --all"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | default | on | 127.0.0.1 | localhost | + +----------+--------+-----------+------------+ + | profile1 | off | 127.0.0.1 | first.loc | + | profile1 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + | profile2 | off | 127.0.0.1 | first.loc | + | profile2 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()) + }) - err := cmd.Execute() - assert.EqualError(t, err, "args must be empty with --all flag") + t.Run("Enable All", func(t *testing.T) { + r.Run("hostctl enable --all"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | default | on | 127.0.0.1 | localhost | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + | profile2 | on | 127.0.0.1 | first.loc | + | profile2 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()) }) } func Test_Enable(t *testing.T) { cmd := NewRootCmd() - tmp := makeTempHostsFile(t, "disableCmd") - defer os.Remove(tmp.Name()) + r := NewRunner(t, cmd, "disable") + defer r.Clean() t.Run("Enable", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"enable", "profile2", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| profile2 | on | 127.0.0.1 | first.loc | -| profile2 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -` - assert.Contains(t, actual, expected) - }) - - t.Run("Enable unknown", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"enable", "unknown", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.EqualError(t, err, types.ErrUnknownProfile.Error()) + r.Run("hostctl enable profile1"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()) }) t.Run("Enable Only", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"enable", "profile2", "--only", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - cmd.SetArgs([]string{"list", "--host-file", tmp.Name()}) - - err = cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| default | on | 127.0.0.1 | localhost | -+----------+--------+-----------+------------+ -| profile1 | off | 127.0.0.1 | first.loc | -| profile1 | off | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -| profile2 | on | 127.0.0.1 | first.loc | -| profile2 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -` - assert.Contains(t, actual, expected) + r.Run("hostctl enable profile1 --only"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()). + Run("hostctl list"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | default | on | 127.0.0.1 | localhost | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + | profile2 | off | 127.0.0.1 | first.loc | + | profile2 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()) }) } diff --git a/cmd/hostctl/actions/helpers.go b/cmd/hostctl/actions/helpers.go index ac69d58..f3b30c7 100644 --- a/cmd/hostctl/actions/helpers.go +++ b/cmd/hostctl/actions/helpers.go @@ -131,7 +131,7 @@ func isValidURL(s string) bool { } func readerFromURL(url string) (io.Reader, error) { - resp, err := http.Get(url) + resp, err := http.Get(url) // nolint:gosec if err != nil { return nil, err } diff --git a/cmd/hostctl/actions/helpers_test.go b/cmd/hostctl/actions/helpers_test.go index bc8bbc2..f656a03 100644 --- a/cmd/hostctl/actions/helpers_test.go +++ b/cmd/hostctl/actions/helpers_test.go @@ -2,54 +2,18 @@ package actions import ( "bytes" - "fmt" - "io/ioutil" "net/http" - "os" + "net/http/httptest" + "sync" "testing" "github.com/stretchr/testify/assert" + "github.com/guumaster/hostctl/pkg/profile" "github.com/guumaster/hostctl/pkg/render" "github.com/guumaster/hostctl/pkg/types" ) -var defaultProfile = "127.0.0.1 localhost\n" - -var testEnabledProfile = ` -# profile.on profile1 -127.0.0.1 first.loc -127.0.0.1 second.loc -# end -` - -var testDisabledProfile = ` -# profile.off profile2 -# 127.0.0.1 first.loc -# 127.0.0.1 second.loc -# end -` - -var listHeader = ` -+---------+--------+-----------+-----------+ -| PROFILE | STATUS | IP | DOMAIN | -+---------+--------+-----------+-----------+ -` - -func makeTempHostsFile(t *testing.T, pattern string) *os.File { - t.Helper() - - file, err := ioutil.TempFile("/tmp", pattern+"_") - if err != nil { - t.Fatal(err) - } - - _, _ = file.WriteString(defaultProfile + testEnabledProfile + testDisabledProfile) - defer file.Close() - - return file -} - func TestContainsDefault(t *testing.T) { err := containsDefault([]string{"default"}) assert.EqualError(t, err, types.ErrDefaultProfile.Error()) @@ -130,22 +94,34 @@ func TestIsValidURL(t *testing.T) { assert.Equal(t, valid, true) } +type handlerFn func(w http.ResponseWriter, r *http.Request) + +type MyHandler struct { + sync.Mutex + fn handlerFn +} + +func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.fn(w, r) +} + func TestReadFromURL(t *testing.T) { - t.SkipNow() - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - _, _ = fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:]) + server := httptest.NewServer(&MyHandler{ + fn: func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`3.3.3.4 some.profile.loc`)) + }, }) + defer server.Close() - go func() { - _ = http.ListenAndServe(":9998", nil) - }() + r, err := readerFromURL(server.URL) + assert.NoError(t, err) - r, err := readerFromURL("http://0.0.0.0:9998/test") + p, err := profile.NewProfileFromReader(r, true) assert.NoError(t, err) - c, _ := ioutil.ReadAll(r) + hosts := p.GetAllHostNames() - assert.Equal(t, c, "Hello, test!") + assert.Equal(t, []string{"some.profile.loc"}, hosts) } func TestHelperCmd(t *testing.T) { diff --git a/cmd/hostctl/actions/info_test.go b/cmd/hostctl/actions/info_test.go index 16b4d13..de393b3 100644 --- a/cmd/hostctl/actions/info_test.go +++ b/cmd/hostctl/actions/info_test.go @@ -1,27 +1,14 @@ package actions import ( - "bytes" - "io/ioutil" "testing" - - "github.com/stretchr/testify/assert" ) func Test_Info(t *testing.T) { cmd := NewRootCmd() - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"info"}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) + r := NewRunner(t, cmd, "info") + defer r.Clean() - actual := "\n" + string(out) - assert.Contains(t, actual, "Your dev tool to manage /etc/hosts like a pro") + r.Run("hostctl info").Contains("Your dev tool to manage /etc/hosts like a pro") } diff --git a/cmd/hostctl/actions/integration_runner_test.go b/cmd/hostctl/actions/integration_runner_test.go new file mode 100644 index 0000000..51d2e07 --- /dev/null +++ b/cmd/hostctl/actions/integration_runner_test.go @@ -0,0 +1,167 @@ +package actions + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/spf13/cobra" + as "github.com/stretchr/testify/assert" +) + +func NewRunner(t *testing.T, root *cobra.Command, pattern string) Runner { + t.Helper() + + c := &cmdRunner{t, root, "", nil} + c.file = c.TempHostfile(pattern) + + return c +} + +type Runner interface { + Run(string) Runner + RunE(string, error) Runner + Runf(string, ...interface{}) Runner + + Equal(string) Runner + Contains(string) Runner + Containsf(string, ...interface{}) Runner + Empty() Runner + + TempHostfile(string) *os.File + Hostfile() string + Clean() +} + +type cmdRunner struct { + t *testing.T + root *cobra.Command + out string + file *os.File +} + +func (c *cmdRunner) Equal(expected string) Runner { + expected = trimLeft(expected) + as.Equal(c.t, expected, c.out) + + return c +} + +func (c *cmdRunner) Hostfile() string { + return c.file.Name() +} + +func trimLeft(s string) string { + lines := strings.Split(s, "\n") + for i, l := range lines { + lines[i] = strings.TrimLeft(strings.ReplaceAll(l, "\t", " "), " ") + } + + return strings.Join(lines, "\n") +} + +func (c *cmdRunner) Contains(expected string) Runner { + expected = trimLeft(expected) + as.Contains(c.t, c.out, expected) + + return c +} + +func (c *cmdRunner) Containsf(expected string, args ...interface{}) Runner { + expected = fmt.Sprintf(expected, args...) + expected = trimLeft(expected) + as.Contains(c.t, c.out, expected) + + return c +} + +func (c *cmdRunner) Empty() Runner { + as.Empty(c.t, strings.ReplaceAll(c.out, "\n", "")) + + return c +} + +func (c *cmdRunner) Runf(format string, args ...interface{}) Runner { + return c.Run(fmt.Sprintf(format, args...)) +} + +func (c *cmdRunner) Run(cmd string) Runner { + assert := as.New(c.t) + b := bytes.NewBufferString("") + + c.out = "" + c.root.SetOut(b) + + if !strings.Contains(cmd, "--host-file") { + cmd += fmt.Sprintf(" --host-file %s", c.file.Name()) + } + + args := strings.Split(cmd, " ") + args = args[1:] + + c.root.SetArgs(args) + + err := c.root.Execute() + assert.NoError(err) + + out, err := ioutil.ReadAll(b) + assert.NoError(err) + + c.out = "\n" + string(out) + + return c +} + +func (c *cmdRunner) RunE(cmd string, expectedErr error) Runner { + assert := as.New(c.t) + b := bytes.NewBufferString("") + + c.out = "" + c.root.SetOut(b) + + cmd += fmt.Sprintf(" --host-file %s", c.file.Name()) + + args := strings.Split(cmd, " ") + args = args[1:] + + c.root.SetArgs(args) + + actualErr := c.root.Execute() + assert.EqualError(actualErr, expectedErr.Error()) + + out, err := ioutil.ReadAll(b) + assert.NoError(err) + + c.out = "\n" + string(out) + + return c +} + +func (c *cmdRunner) TempHostfile(pattern string) *os.File { + file, err := ioutil.TempFile("/tmp", fmt.Sprintf("%s_%s_", c.root.Name(), pattern)) + as.NoError(c.t, err) + + _, _ = file.WriteString(` +127.0.0.1 localhost + +# profile.on profile1 +127.0.0.1 first.loc +127.0.0.1 second.loc +# end + +# profile.off profile2 +# 127.0.0.1 first.loc +# 127.0.0.1 second.loc +# end +`) + + return file +} + +func (c *cmdRunner) Clean() { + _ = c.file.Close() + _ = os.Remove(c.file.Name()) +} diff --git a/cmd/hostctl/actions/list.go b/cmd/hostctl/actions/list.go index c157305..e7d9df7 100644 --- a/cmd/hostctl/actions/list.go +++ b/cmd/hostctl/actions/list.go @@ -42,9 +42,10 @@ The "default" profile is all the content that is not handled by hostctl tool. return listCmd } -var makeListStatusCmd = func(status types.Status) *cobra.Command { +func makeListStatusCmd(status types.Status) *cobra.Command { cmd := "" alias := "" + switch status { case types.Enabled: cmd = "enabled" @@ -53,6 +54,7 @@ var makeListStatusCmd = func(status types.Status) *cobra.Command { cmd = "disabled" alias = "off" } + return &cobra.Command{ Use: cmd, Aliases: []string{alias}, diff --git a/cmd/hostctl/actions/list_test.go b/cmd/hostctl/actions/list_test.go index a7f7b13..41b393f 100644 --- a/cmd/hostctl/actions/list_test.go +++ b/cmd/hostctl/actions/list_test.go @@ -1,88 +1,57 @@ package actions import ( - "bytes" - "io/ioutil" - "os" "testing" - - "github.com/stretchr/testify/assert" ) func Test_List(t *testing.T) { cmd := NewRootCmd() - tmp := makeTempHostsFile(t, "listCmd") - defer os.Remove(tmp.Name()) + r := NewRunner(t, cmd, "list") + defer r.Clean() t.Run("List all", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"list", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - assert.Contains(t, actual, ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| default | on | 127.0.0.1 | localhost | -+----------+--------+-----------+------------+ -| profile1 | on | 127.0.0.1 | first.loc | -| profile1 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -| profile2 | off | 127.0.0.1 | first.loc | -| profile2 | off | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -`) + r.Run("hostctl list"). + Contains(` + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | default | on | 127.0.0.1 | localhost | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + | profile2 | off | 127.0.0.1 | first.loc | + | profile2 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `) }) - for _, f := range []string{"enabled", "disabled"} { - filter := f - t.Run("List "+filter, func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"list", filter, "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) + t.Run("List enabled", func(t *testing.T) { + r.Run("hostctl list enabled"). + Contains(` + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | default | on | 127.0.0.1 | localhost | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `) + }) - expected := "" - actual := "\n" + string(out) - if filter == "enabled" { - expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| default | on | 127.0.0.1 | localhost | -+----------+--------+-----------+------------+ -| profile1 | on | 127.0.0.1 | first.loc | -| profile1 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -` - } else { - expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| default | on | 127.0.0.1 | localhost | -+----------+--------+-----------+------------+ -| profile2 | off | 127.0.0.1 | first.loc | -| profile2 | off | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -` - } - assert.Contains(t, actual, expected) - }) - } + t.Run("List disabled", func(t *testing.T) { + r.Run("hostctl list disabled"). + Contains(` + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | default | on | 127.0.0.1 | localhost | + +----------+--------+-----------+------------+ + | profile2 | off | 127.0.0.1 | first.loc | + | profile2 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `) + }) } diff --git a/cmd/hostctl/actions/post_action.go b/cmd/hostctl/actions/post_action.go index c5f378a..06e4793 100644 --- a/cmd/hostctl/actions/post_action.go +++ b/cmd/hostctl/actions/post_action.go @@ -19,7 +19,7 @@ func postRunListOnly(cmd *cobra.Command, args []string) error { return postActionCmd(cmd, args, nil, true) } -var postActionCmd = func(cmd *cobra.Command, args []string, postCmd *cobra.Command, list bool) error { +func postActionCmd(cmd *cobra.Command, args []string, postCmd *cobra.Command, list bool) error { listCmd := newListCmd() quiet, _ := cmd.Flags().GetBool("quiet") duration, _ := cmd.Flags().GetDuration("wait") @@ -43,6 +43,7 @@ var postActionCmd = func(cmd *cobra.Command, args []string, postCmd *cobra.Comma if !quiet { p := strings.Join(args, ", ") _, _ = fmt.Fprintln(cmd.OutOrStdout()) + if duration == 0 { cligger.Info("Waiting until ctrl+c to %s from profile '%s'\n\n", action, p) } else if duration > 0 { @@ -61,11 +62,14 @@ var postActionCmd = func(cmd *cobra.Command, args []string, postCmd *cobra.Comma if err != nil { return err } + if quiet { return nil } + return listCmd.RunE(cmd, args) } + return nil } diff --git a/cmd/hostctl/actions/post_action_test.go b/cmd/hostctl/actions/post_action_test.go index 1f9ac7b..eac826d 100644 --- a/cmd/hostctl/actions/post_action_test.go +++ b/cmd/hostctl/actions/post_action_test.go @@ -1,8 +1,6 @@ package actions import ( - "bytes" - "io/ioutil" "os" "testing" "time" @@ -13,30 +11,34 @@ import ( func Test_postActionCmd(t *testing.T) { cmd := NewRootCmd() - tmp := makeTempHostsFile(t, "postActionCmd") - defer os.Remove(tmp.Name()) + r := NewRunner(t, cmd, "postAction") + defer r.Clean() t.Run("Wait and disable", func(t *testing.T) { - b := bytes.NewBufferString("") - args := []string{"enable", "profile1", "--host-file", tmp.Name(), "--wait", "10ms"} - - cmd.SetOut(b) - cmd.SetArgs(args) - - err := cmd.Execute() - assert.NoError(t, err) - - out, _ := ioutil.ReadAll(b) - assert.Contains(t, string(out), "Waiting for 10ms or ctrl+c to disable from profile 'profile1'") + r.Run("hostctl enable profile1 --wait 10ms"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + + [ℹ] Waiting for 10ms or ctrl+c to disable from profile 'profile1' + `, r.Hostfile()). + Contains(` + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile1 | off | 127.0.0.1 | first.loc | + | profile1 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `) }) t.Run("Wait and disable on SIGTERM", func(t *testing.T) { - b := bytes.NewBufferString("") - args := []string{"enable", "profile1", "--host-file", tmp.Name(), "--wait", "0"} - - cmd.SetOut(b) - cmd.SetArgs(args) - proc, err := os.FindProcess(os.Getpid()) assert.NoError(t, err) @@ -46,11 +48,27 @@ func Test_postActionCmd(t *testing.T) { assert.NoError(t, err) }() - err = cmd.Execute() - assert.NoError(t, err) - - out, _ := ioutil.ReadAll(b) - assert.Contains(t, string(out), "Waiting until ctrl+c to disable from profile 'profile1'") + r.Run("hostctl enable profile1 --wait 0"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + + [ℹ] Waiting until ctrl+c to disable from profile 'profile1' + `, r.Hostfile()). + Contains(` + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile1 | off | 127.0.0.1 | first.loc | + | profile1 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `) }) } diff --git a/cmd/hostctl/actions/remove_domains.go b/cmd/hostctl/actions/remove_domains.go index 0d4287f..3694624 100644 --- a/cmd/hostctl/actions/remove_domains.go +++ b/cmd/hostctl/actions/remove_domains.go @@ -43,9 +43,9 @@ It cannot be undone unless you have a backup and restore it. } if !quiet { if removed { - cligger.Success("Profile '%s' removed.\n\n", name) + cligger.Success("Profile '%s' removed.\n", name) } else { - cligger.Success("Domains '%s' removed.\n\n", strings.Join(args[1:], ", ")) + cligger.Success("Domains '%s' removed.\n", strings.Join(args[1:], ", ")) } } return nil diff --git a/cmd/hostctl/actions/remove_domains_test.go b/cmd/hostctl/actions/remove_domains_test.go index fe6818f..3bd522b 100644 --- a/cmd/hostctl/actions/remove_domains_test.go +++ b/cmd/hostctl/actions/remove_domains_test.go @@ -1,58 +1,39 @@ package actions import ( - "bytes" - "io/ioutil" - "os" "testing" - - "github.com/stretchr/testify/assert" ) func Test_RemoveDomains(t *testing.T) { cmd := NewRootCmd() t.Run("Remove domains", func(t *testing.T) { - tmp := makeTempHostsFile(t, "removeDomainCmd") - defer os.Remove(tmp.Name()) - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"remove", "domains", "profile1", "first.loc", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| profile1 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -` - assert.Contains(t, actual, expected) - }) + r := NewRunner(t, cmd, "removeDomain") + defer r.Clean() - t.Run("Remove domains and profile", func(t *testing.T) { - tmp := makeTempHostsFile(t, "removeDomainCmd") - defer os.Remove(tmp.Name()) - b := bytes.NewBufferString("") + r.Run("hostctl remove domains profile1 first.loc"). + Containsf(` + [ℹ] Using hosts file: %s - cmd.SetOut(b) - cmd.SetArgs([]string{"remove", "domains", "profile1", "first.loc", "second.loc", "--host-file", tmp.Name()}) + [✔] Domains 'first.loc' removed. - err := cmd.Execute() - assert.NoError(t, err) + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()) + }) - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) + t.Run("Remove domains and profile", func(t *testing.T) { + r := NewRunner(t, cmd, "removeDomain") + defer r.Clean() - actual := "\n" + string(out) + r.Run("hostctl remove domains profile1 first.loc second.loc"). + Containsf(` + [ℹ] Using hosts file: %s - assert.Contains(t, actual, `Profile 'profile1' removed.`) + [✔] Profile 'profile1' removed. + `, r.Hostfile()) }) } diff --git a/cmd/hostctl/actions/remove_test.go b/cmd/hostctl/actions/remove_test.go index 6f724f8..9ade6c5 100644 --- a/cmd/hostctl/actions/remove_test.go +++ b/cmd/hostctl/actions/remove_test.go @@ -1,98 +1,60 @@ package actions import ( - "bytes" - "io/ioutil" - "os" "testing" - "github.com/stretchr/testify/assert" - "github.com/guumaster/hostctl/pkg/types" ) func Test_Remove(t *testing.T) { cmd := NewRootCmd() - t.Run("Remove", func(t *testing.T) { - tmp := makeTempHostsFile(t, "addCmd") - defer os.Remove(tmp.Name()) - b := bytes.NewBufferString("") + r := NewRunner(t, cmd, "remove") + defer r.Clean() - cmd.SetOut(b) - cmd.SetArgs([]string{"remove", "profile2", "--host-file", tmp.Name()}) + t.Run("Remove", func(t *testing.T) { + r.Run("hostctl remove profile2"). + Containsf(` + [ℹ] Using hosts file: %s - err := cmd.Execute() - assert.NoError(t, err) + [✔] Profile(s) 'profile2' removed. + `, r.Hostfile()) + }) - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) + t.Run("Remove unknown", func(t *testing.T) { + r.RunE("hostctl remove unknown", types.ErrUnknownProfile). + Containsf(`[ℹ] Using hosts file: %s`, r.Hostfile()) + }) - actual := "\n" + string(out) - expected := listHeader - assert.NotContains(t, expected, actual) + t.Run("Remove all bad", func(t *testing.T) { + r.RunE("hostctl remove profile1 --all", ErrIncompatibleAllFlag).Empty() }) t.Run("Remove multiple", func(t *testing.T) { - tmp := makeTempHostsFile(t, "addCmd") - defer os.Remove(tmp.Name()) - - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"remove", "profile1", "profile2", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) + cmd := NewRootCmd() - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) + r := NewRunner(t, cmd, "remove") + defer r.Clean() - actual := "\n" + string(out) - expected := listHeader - assert.NotContains(t, actual, expected) - }) - - t.Run("Remove unknown", func(t *testing.T) { - tmp := makeTempHostsFile(t, "addCmd") - defer os.Remove(tmp.Name()) - b := bytes.NewBufferString("") + r.Run("hostctl remove profile1 profile2"). + Containsf(` + [ℹ] Using hosts file: %s - cmd.SetOut(b) - cmd.SetArgs([]string{"remove", "unknown", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.EqualError(t, err, types.ErrUnknownProfile.Error()) + [✔] Profile(s) 'profile1, profile2' removed. + `, r.Hostfile()) }) t.Run("Remove all", func(t *testing.T) { - tmp := makeTempHostsFile(t, "addCmd") - defer os.Remove(tmp.Name()) - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"remove", "--all", "--host-file", tmp.Name()}) + cmd := NewRootCmd() - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - expected := listHeader - assert.NotContains(t, actual, expected) - }) - - t.Run("Remove all bad", func(t *testing.T) { - tmp := makeTempHostsFile(t, "addCmd") - defer os.Remove(tmp.Name()) - b := bytes.NewBufferString("") + r := NewRunner(t, cmd, "remove") + defer r.Clean() - cmd.SetOut(b) - cmd.SetArgs([]string{"remove", "profile1", "--all", "--host-file", tmp.Name()}) + r.Run("hostctl remove --all"). + Containsf(` + [ℹ] Using hosts file: %s - err := cmd.Execute() - assert.EqualError(t, err, "args must be empty with --all flag") + [✔] Profile(s) 'profile1, profile2' removed. + `, r.Hostfile()) }) } diff --git a/cmd/hostctl/actions/restore.go b/cmd/hostctl/actions/restore.go index 69df65d..ed99085 100644 --- a/cmd/hostctl/actions/restore.go +++ b/cmd/hostctl/actions/restore.go @@ -34,7 +34,7 @@ WARNING: the complete hosts file will be overwritten with the backup data. } if !quiet { - cligger.Success("File '%s' restored.\n\n", from) + cligger.Success("File '%s' restored.\n", from) } return nil diff --git a/cmd/hostctl/actions/restore_test.go b/cmd/hostctl/actions/restore_test.go index 35a5372..42e3f2f 100644 --- a/cmd/hostctl/actions/restore_test.go +++ b/cmd/hostctl/actions/restore_test.go @@ -1,7 +1,6 @@ package actions import ( - "bytes" "io/ioutil" "os" "testing" @@ -12,7 +11,10 @@ import ( func Test_Restore(t *testing.T) { cmd := NewRootCmd() - from := makeTempHostsFile(t, "restoreFrom") + r := NewRunner(t, cmd, "remove") + defer r.Clean() + + from := r.TempHostfile("restoreFrom") defer os.Remove(from.Name()) to, err := ioutil.TempFile("/tmp", "restoreTo") @@ -20,33 +22,26 @@ func Test_Restore(t *testing.T) { defer os.Remove(to.Name()) - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"restore", "--from", from.Name(), "--host-file", to.Name()}) - - err = cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) + r.Runf("hostctl restore --from %s --host-file %s", from.Name(), to.Name()). + Containsf(` + [ℹ] Using hosts file: %s + + [✔] File '%s' restored. + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | default | on | 127.0.0.1 | localhost | + +----------+--------+-----------+------------+ + | profile1 | on | 127.0.0.1 | first.loc | + | profile1 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + | profile2 | off | 127.0.0.1 | first.loc | + | profile2 | off | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, to.Name(), from.Name()) toData, _ := ioutil.ReadFile(to.Name()) fromData, _ := ioutil.ReadFile(from.Name()) assert.Equal(t, string(toData), string(fromData)) - - actual := "\n" + string(out) - assert.Contains(t, actual, ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| default | on | 127.0.0.1 | localhost | -+----------+--------+-----------+------------+ -| profile1 | on | 127.0.0.1 | first.loc | -| profile1 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -| profile2 | off | 127.0.0.1 | first.loc | -| profile2 | off | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -`) } diff --git a/cmd/hostctl/actions/root.go b/cmd/hostctl/actions/root.go index 47813dc..fd1d4dd 100644 --- a/cmd/hostctl/actions/root.go +++ b/cmd/hostctl/actions/root.go @@ -8,11 +8,13 @@ import ( "github.com/guumaster/cligger" ) +// nolint:gochecknoglobals var ( version = "dev" snapBuild string ) +// NewRootCmd creates the base command for hostctl func NewRootCmd() *cobra.Command { rootCmd := &cobra.Command{ Use: "hostctl", @@ -162,6 +164,13 @@ func registerCommands(rootCmd *cobra.Command) { syncDockerComposeCmd.Flags().String("project-name", "", "docker compose project name") syncDockerComposeCmd.Flags().Bool("prefix", false, "keep project name prefix from domain name") + // sync minikube + syncMinikubeCmd := newSyncMinikubeCmd(removeCmd) + syncMinikubeCmd.Flags().StringP("profile", "p", "minikube", "minikube profile to read from") + syncMinikubeCmd.Flags().Bool("all-namespaces", false, "read ingresses from all kubernetes namespace") + syncMinikubeCmd.Flags(). + StringP("namespace", "n", "", "kubernetes namespace to read ingress data from") + // list listCmd := newListCmd() @@ -173,6 +182,7 @@ func registerCommands(rootCmd *cobra.Command) { removeCmd.AddCommand(removeDomainsCmd) syncCmd.AddCommand(syncDockerCmd) syncCmd.AddCommand(syncDockerComposeCmd) + syncCmd.AddCommand(syncMinikubeCmd) // register all commands rootCmd.AddCommand(addCmd) diff --git a/cmd/hostctl/actions/status_test.go b/cmd/hostctl/actions/status_test.go index f3963de..0270a10 100644 --- a/cmd/hostctl/actions/status_test.go +++ b/cmd/hostctl/actions/status_test.go @@ -1,41 +1,22 @@ package actions import ( - "bytes" - "io/ioutil" - "os" "testing" - - "github.com/stretchr/testify/assert" ) func Test_Status(t *testing.T) { cmd := NewRootCmd() - tmp := makeTempHostsFile(t, "statusCmd") - defer os.Remove(tmp.Name()) - - t.Run("Status", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"status", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+ -| PROFILE | STATUS | -+----------+--------+ -| profile1 | on | -| profile2 | off | -+----------+--------+ -` - assert.Contains(t, actual, expected) - }) + r := NewRunner(t, cmd, "status") + defer r.Clean() + + r.Run("hostctl status"). + Contains(` + +----------+--------+ + | PROFILE | STATUS | + +----------+--------+ + | profile1 | on | + | profile2 | off | + +----------+--------+ + `) } diff --git a/cmd/hostctl/actions/sync_docker_compose_test.go b/cmd/hostctl/actions/sync_docker_compose_test.go index fa4b116..42951ee 100644 --- a/cmd/hostctl/actions/sync_docker_compose_test.go +++ b/cmd/hostctl/actions/sync_docker_compose_test.go @@ -1,15 +1,45 @@ package actions import ( - "bytes" - "io/ioutil" "os" "testing" - "github.com/stretchr/testify/assert" + "github.com/docker/docker/client" ) -var composeFile = ` +func Test_SyncDockerCompose(t *testing.T) { + cmd := NewRootCmd() + + r := NewRunner(t, cmd, "remove") + defer r.Clean() + + cli := prepareComposeCli(t, r) + + opts := testGetOptions(t, cli) + cmdSync := newSyncDockerCmd(nil, opts) + cmdSync.Use = "test-sync-docker-compose" + + cmd.AddCommand(cmdSync) + + r.Run("hostctl test-sync-docker-compose profile2"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------------------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------------------------+ + | profile2 | on | 172.0.0.2 | testing-app_container1_1.loc | + | profile2 | on | 172.0.0.3 | testing-app_container2_1.loc | + | profile2 | on | 172.0.0.4 | testing-app_db.loc | + +----------+--------+-----------+------------------------------+ + `, r.Hostfile()) +} + +func prepareComposeCli(t *testing.T, r Runner) *client.Client { + c := r.TempHostfile("docker-compose.yml") + defer os.Remove(c.Name()) + + _, _ = c.WriteString(` version: "3" services: @@ -31,13 +61,13 @@ services: networks: networkName1: -` +`) -var dockerComposeResponse = map[string]string{ - "/v1.22/networks": `[ + dockerResponse := map[string]string{ + "/v1.22/networks": `[ {"Id": "testing-app_networkID1", "Name": "testing-app_networkName1" } ]`, - "/v1.22/containers/json": `[{ + "/v1.22/containers/json": `[{ "Id": "container_id1", "Names": ["/testing-app_container1_1"], "NetworkSettings": { "Networks": { "testing-app_networkName1": { "NetworkID": "testing-app_networkID1", "IPAddress": "172.0.0.2" }} @@ -53,49 +83,7 @@ var dockerComposeResponse = map[string]string{ "Networks": { "testing-app_networkName1": { "NetworkID": "testing-app_networkID1", "IPAddress": "172.0.0.4" }} } }]`, -} - -func Test_SyncDockerCompose(t *testing.T) { - c := makeTempHostsFile(t, "docker-compose.yml") - - _, _ = c.WriteString(composeFile) - defer os.Remove(c.Name()) - - cli := newClientWithResponse(t, dockerComposeResponse) - - cmd := NewRootCmd() - - opts := testGetOptions(t, cli) - cmdSync := newSyncDockerCmd(nil, opts) - cmdSync.Use = "test-sync-docker-compose" - - cmd.AddCommand(cmdSync) - - tmp := makeTempHostsFile(t, "syncDockerCmd") - defer os.Remove(tmp.Name()) - - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"test-sync-docker-compose", "profile2", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - - const expected = ` -+----------+--------+-----------+------------------------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------------------------+ -| profile2 | on | 172.0.0.2 | testing-app_container1_1.loc | -| profile2 | on | 172.0.0.3 | testing-app_container2_1.loc | -| profile2 | on | 172.0.0.4 | testing-app_db.loc | -+----------+--------+-----------+------------------------------+ -` + } - assert.Contains(t, actual, expected) + return newClientWithResponse(t, dockerResponse) } diff --git a/cmd/hostctl/actions/sync_docker_test.go b/cmd/hostctl/actions/sync_docker_test.go index 5e65b07..96b93ab 100644 --- a/cmd/hostctl/actions/sync_docker_test.go +++ b/cmd/hostctl/actions/sync_docker_test.go @@ -4,7 +4,6 @@ import ( "bytes" "io/ioutil" "net/http" - "os" "testing" "github.com/docker/docker/client" @@ -61,6 +60,9 @@ func newClientWithResponse(t *testing.T, resp map[string]string) *client.Client func Test_SyncDocker(t *testing.T) { cmd := NewRootCmd() + r := NewRunner(t, cmd, "remove") + defer r.Clean() + cli := newClientWithResponse(t, map[string]string{ "/v1.22/networks": `[ {"Id": "networkID1", "Name": "networkName1" } @@ -80,30 +82,15 @@ func Test_SyncDocker(t *testing.T) { cmd.AddCommand(cmdSync) - tmp := makeTempHostsFile(t, "syncDockerCmd") - defer os.Remove(tmp.Name()) - - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"test-sync-docker", "profile2", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - - const expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| profile2 | on | 172.0.0.2 | first.loc | -| profile2 | on | 172.0.0.3 | second.loc | -+----------+--------+-----------+------------+ -` - - assert.Contains(t, actual, expected) + r.Run("hostctl test-sync-docker profile2"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile2 | on | 172.0.0.2 | first.loc | + | profile2 | on | 172.0.0.3 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()) } diff --git a/cmd/hostctl/actions/sync_minikube.go b/cmd/hostctl/actions/sync_minikube.go new file mode 100644 index 0000000..18ed323 --- /dev/null +++ b/cmd/hostctl/actions/sync_minikube.go @@ -0,0 +1,69 @@ +package actions + +import ( + "github.com/spf13/cobra" + + "github.com/guumaster/hostctl/pkg/file" + "github.com/guumaster/hostctl/pkg/k8s/minikube" + "github.com/guumaster/hostctl/pkg/profile" + "github.com/guumaster/hostctl/pkg/types" +) + +func newSyncMinikubeCmd(removeCmd *cobra.Command) *cobra.Command { + return &cobra.Command{ + Use: "minikube [profile] [flags]", + Short: "Sync a minikube profile with a hostctl profile.", + Long: ` +Reads from Minikube the list of ingresses and add names and IPs to a profile in your hosts file. +`, + Args: commonCheckArgs, + PreRunE: func(cmd *cobra.Command, _ []string) error { + ns, _ := cmd.Flags().GetString("namespace") + allNs, _ := cmd.Flags().GetBool("all-namespaces") + + if ns == "" && !allNs { + return types.ErrKubernetesNamespace + } + return nil + }, + RunE: func(cmd *cobra.Command, profiles []string) error { + src, _ := cmd.Flags().GetString("host-file") + ns, _ := cmd.Flags().GetString("namespace") + allNs, _ := cmd.Flags().GetBool("all-namespace") + + if allNs { + ns = "" + } + + profileName := profiles[0] + + mini, err := minikube.GetProfile(profileName) + if err != nil { + return err + } + + p, err := profile.NewProfileFromMinikube(mini, ns) + if err != nil { + return err + } + + h, err := file.NewFile(src) + if err != nil { + return err + } + + p.Name = mini.Name + p.Status = types.Enabled + + err = h.ReplaceProfile(p) + if err != nil { + return err + } + + return h.Flush() + }, + PostRunE: func(cmd *cobra.Command, args []string) error { + return postActionCmd(cmd, args, removeCmd, true) + }, + } +} diff --git a/cmd/hostctl/actions/toggle_test.go b/cmd/hostctl/actions/toggle_test.go index 41db965..e5223ad 100644 --- a/cmd/hostctl/actions/toggle_test.go +++ b/cmd/hostctl/actions/toggle_test.go @@ -1,53 +1,35 @@ package actions import ( - "bytes" - "io/ioutil" - "os" "testing" - "github.com/stretchr/testify/assert" - "github.com/guumaster/hostctl/pkg/types" ) func Test_Toggle(t *testing.T) { cmd := NewRootCmd() - tmp := makeTempHostsFile(t, "toggleCmd") - defer os.Remove(tmp.Name()) + r := NewRunner(t, cmd, "remove") + defer r.Clean() t.Run("Toggle", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"toggle", "profile2", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.NoError(t, err) - - out, err := ioutil.ReadAll(b) - assert.NoError(t, err) - - actual := "\n" + string(out) - const expected = ` -+----------+--------+-----------+------------+ -| PROFILE | STATUS | IP | DOMAIN | -+----------+--------+-----------+------------+ -| profile2 | on | 127.0.0.1 | first.loc | -| profile2 | on | 127.0.0.1 | second.loc | -+----------+--------+-----------+------------+ -` - assert.Contains(t, actual, expected) + r.Run("hostctl toggle profile2"). + Containsf(` + [ℹ] Using hosts file: %s + + +----------+--------+-----------+------------+ + | PROFILE | STATUS | IP | DOMAIN | + +----------+--------+-----------+------------+ + | profile2 | on | 127.0.0.1 | first.loc | + | profile2 | on | 127.0.0.1 | second.loc | + +----------+--------+-----------+------------+ + `, r.Hostfile()) }) t.Run("Toggle unknown", func(t *testing.T) { - b := bytes.NewBufferString("") - - cmd.SetOut(b) - cmd.SetArgs([]string{"toggle", "unknown", "--host-file", tmp.Name()}) - - err := cmd.Execute() - assert.EqualError(t, err, types.ErrUnknownProfile.Error()) + r.RunE("hostctl toggle unknown", types.ErrUnknownProfile). + Containsf(` + [ℹ] Using hosts file: %s + `, r.Hostfile()) }) } diff --git a/go.mod b/go.mod index b1684a8..11b75fe 100644 --- a/go.mod +++ b/go.mod @@ -16,4 +16,6 @@ require ( github.com/spf13/cobra v1.0.0 github.com/stretchr/testify v1.5.1 gopkg.in/yaml.v2 v2.2.2 + k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d + k8s.io/client-go v0.0.0-20190918200256-06eb1244587a ) diff --git a/go.sum b/go.sum index d41f1cb..22621fe 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= @@ -20,6 +22,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= @@ -30,23 +33,42 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM= +github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -55,10 +77,16 @@ github.com/guumaster/cligger v0.1.0/go.mod h1:jy36TeijWpw11UbItVnXwWqnUyeDGirmfd github.com/guumaster/logsymbols v0.3.0 h1:j27S5MnY2uN3XRIPCwe6sGemdlw7O6tpSUebbIHnpBI= github.com/guumaster/logsymbols v0.3.0/go.mod h1:okXhkQDvvtnxC59vv5PlDFNvHnwdwZ9vQf3nAIMpcGs= github.com/guumaster/tablewriter v0.0.9/go.mod h1:9B1xy1BLPtcVAeYjC1EXPxcklqnzk7dU2c3ywGbUnKY= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE= +github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -77,13 +105,22 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -114,11 +151,14 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -131,46 +171,86 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181025213731-e84da0312774 h1:a4tQYYYuK9QdeO/+kEvNYyuR21S+7ve5EANok6hABhI= +golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20161028155119-f51c12702a4d h1:TnM+PKb3ylGmZvyPXmo9m/wktg7Jn/a/fNmr33HSj8g= +golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gookit/color.v1 v1.1.6 h1:5fB10p6AUFjhd2ayq9JgmJWr9WlTrguFdw3qlYtKNHk= gopkg.in/gookit/color.v1 v1.1.6/go.mod h1:IcEkFGaveVShJ+j8ew+jwe9epHyGpJ9IrptHmW3laVY= +gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o= +gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20190918195907-bd6ac527cfd2 h1:bkwe5LsuANqyOwsBng5Qc4S91D2Tv0JHctAztt3YTQs= +k8s.io/api v0.0.0-20190918195907-bd6ac527cfd2/go.mod h1:AOxZTnaXR/xiarlQL0JUfwQPxjmKDvVYoRp58cA7lUo= +k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d h1:7Kns6qqhMAQWvGkxYOLSLRZ5hJO0/5pcE5lPGP2fxUw= +k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d/go.mod h1:3jediapYqJ2w1BFw7lAZPCx7scubsTfosqHkhXCWJKw= +k8s.io/client-go v0.0.0-20190918200256-06eb1244587a h1:huOvPq1vO7dkuw9rZPYsLGpFmyGvy6L8q6mDItgkdQ4= +k8s.io/client-go v0.0.0-20190918200256-06eb1244587a/go.mod h1:3YAcTbI2ArBRmhHns5vlHRX8YQqvkVYpz+U/N5i1mVU= +k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68= +k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4= +k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/pkg/docker/compose.go b/pkg/docker/compose.go index 43d300d..1a71097 100644 --- a/pkg/docker/compose.go +++ b/pkg/docker/compose.go @@ -29,10 +29,11 @@ func ParseComposeFile(r io.Reader, projectName string) ([]string, error) { err = yaml.Unmarshal(bytes, &data) if err != nil { - return nil, err + return nil, fmt.Errorf("error parsing docker-compose content: %w", err) } - var containers []string + containers := make([]string, len(data.Services)) + i := 0 for serv, data := range data.Services { name := data.ContainerName @@ -40,7 +41,8 @@ func ParseComposeFile(r io.Reader, projectName string) ([]string, error) { name = fmt.Sprintf("%s_%s", projectName, serv) } - containers = append(containers, name) + containers[i] = name + i++ } return containers, nil diff --git a/pkg/docker/compose_test.go b/pkg/docker/compose_test.go new file mode 100644 index 0000000..29306fc --- /dev/null +++ b/pkg/docker/compose_test.go @@ -0,0 +1,43 @@ +package docker + +import ( + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseComposeFile(t *testing.T) { + var composeFile = ` +version: "3" +services: + + container1: + image: some_image:3.5 + networks: + - networkName1 + + container2: + image: some_other_image:1.0 + networks: + - networkName1 + + another-thing: + container_name: db + image: db:1.0 + networks: + - networkName1 + +networks: + networkName1: +` + + r := strings.NewReader(composeFile) + containers, err := ParseComposeFile(r, "test") + assert.NoError(t, err) + + sort.Strings(containers) + + assert.EqualValues(t, []string{"db", "test_container1", "test_container2"}, containers) +} diff --git a/pkg/docker/docker_test.go b/pkg/docker/docker_test.go new file mode 100644 index 0000000..8d363cc --- /dev/null +++ b/pkg/docker/docker_test.go @@ -0,0 +1,112 @@ +package docker + +import ( + "bytes" + "context" + "errors" + "io/ioutil" + "net/http" + "testing" + + dtypes "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/stretchr/testify/assert" + + "github.com/guumaster/hostctl/pkg/types" +) + +func TestGetNetworkID(t *testing.T) { + cli := newClientWithResponse(t, map[string]string{ + "/v1.22/networks": `[ +{"Id": "networkId1", "Name": "networkName1" }, +{"Id": "networkId2", "Name": "networkName2" } +]`, + }) + + t.Run("By Name", func(t *testing.T) { + net, err := GetNetworkID(context.Background(), cli, "networkName2") + assert.NoError(t, err) + + assert.Equal(t, "networkId2", net) + }) + + t.Run("By Name error", func(t *testing.T) { + _, err := GetNetworkID(context.Background(), cli, "invent") + assert.True(t, errors.Is(err, types.ErrUnknownNetworkID)) + }) + + t.Run("By Name empty", func(t *testing.T) { + list, err := GetNetworkID(context.Background(), cli, "") + assert.NoError(t, err) + assert.Empty(t, list) + }) +} + +func TestGetContainerList(t *testing.T) { + cli := newClientWithResponse(t, map[string]string{ + "/v1.22/networks": `[ +{"Id": "networkId1", "Name": "networkName1" }, +{"Id": "networkId2", "Name": "networkName2" } +]`, + "/v1.22/containers/json": `[{ + "Id": "container_id1", "Names": ["container1"], + "NetworkSettings": { "Networks": { "networkName1": { "NetworkID": "networkID1", "IPAddress": "172.0.0.2" }}} +}, { + "Id": "container_id2", "Names": ["container2"], + "NetworkSettings": { "Networks": { "networkName1": { "NetworkID": "networkID1", "IPAddress": "172.0.0.3" }}} +}]`, + }) + + list, err := GetContainerList(context.Background(), cli, "") + assert.NoError(t, err) + + assert.Len(t, list, 2) + + assert.IsType(t, dtypes.Container{}, list[0]) + assert.IsType(t, dtypes.Container{}, list[1]) + assert.Equal(t, "networkID1", list[1].NetworkSettings.Networks["networkName1"].NetworkID) + + assert.Equal(t, dtypes.Container{ + ID: "container_id1", + Names: []string{"container1"}, + + NetworkSettings: list[0].NetworkSettings, // simplify the comparison + }, list[0]) + assert.Equal(t, dtypes.Container{ + ID: "container_id2", + Names: []string{"container2"}, + + NetworkSettings: list[1].NetworkSettings, // simplify the comparison + }, list[1]) +} + +type transportFunc func(*http.Request) (*http.Response, error) + +func (tf transportFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return tf(req) +} + +func newClientWithResponse(t *testing.T, resp map[string]string) *client.Client { + t.Helper() + + v := "1.22" + c, err := client.NewClient("tcp://fake:2345", v, + &http.Client{ + Transport: transportFunc(func(req *http.Request) (*http.Response, error) { + url := req.URL.Path + b, ok := resp[url] + if !ok { + b = "{}" + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader([]byte(b))), + }, nil + }), + }, + map[string]string{}) + + assert.NoError(t, err) + + return c +} diff --git a/pkg/file/add_test.go b/pkg/file/add_test.go index 1860af5..06ca087 100644 --- a/pkg/file/add_test.go +++ b/pkg/file/add_test.go @@ -52,7 +52,7 @@ func TestFile_AddProfile(t *testing.T) { added, err := m.GetProfile("profile1") assert.NoError(t, err) - hosts, err := added.GetHostNames(Localhost.String()) + hosts, err := added.GetHostNames("127.0.0.1") assert.NoError(t, err) assert.Equal(t, hosts, []string{"first.loc", "second.loc", "added.loc"}) diff --git a/pkg/file/enable_disable_test.go b/pkg/file/enable_disable_test.go index 90d4746..91a79f5 100644 --- a/pkg/file/enable_disable_test.go +++ b/pkg/file/enable_disable_test.go @@ -8,9 +8,8 @@ import ( "github.com/guumaster/hostctl/pkg/types" ) -func TestFile_Enable(t *testing.T) { +func TestFile_EnableDisable(t *testing.T) { mem := createBasicFS(t) - f, err := mem.Open("/tmp/etc/hosts") assert.NoError(t, err) @@ -43,4 +42,31 @@ func TestFile_Enable(t *testing.T) { err = m.Enable([]string{"unknown"}) assert.EqualError(t, err, types.ErrUnknownProfile.Error()) }) + + t.Run("Disable", func(t *testing.T) { + err = m.Disable([]string{"profile2"}) + assert.NoError(t, err) + assert.Contains(t, m.GetDisabled(), "profile2") + }) + + t.Run("Disable Only", func(t *testing.T) { + err = m.Disable([]string{"profile1", "profile2"}) + err = m.DisableOnly([]string{"default", "profile2"}) + assert.NoError(t, err) + assert.Contains(t, m.GetEnabled(), "profile1") + assert.Contains(t, m.GetDisabled(), "profile2") + }) + + t.Run("Disable All", func(t *testing.T) { + err = m.DisableAll() + assert.NoError(t, err) + Disabled := m.GetDisabled() + assert.Contains(t, Disabled, "profile1") + assert.Contains(t, Disabled, "profile2") + }) + + t.Run("Disable error", func(t *testing.T) { + err = m.Disable([]string{"unknown"}) + assert.EqualError(t, err, types.ErrUnknownProfile.Error()) + }) } diff --git a/pkg/file/file_test.go b/pkg/file/file_test.go index 06c17ad..f3d18be 100644 --- a/pkg/file/file_test.go +++ b/pkg/file/file_test.go @@ -13,6 +13,63 @@ import ( "github.com/guumaster/hostctl/pkg/types" ) +// nolint:gochecknoglobals +var ( + defaultProfile = "127.0.0.1 localhost\n" + + testEnabledProfile = ` +# profile.on profile1 +127.0.0.1 first.loc +127.0.0.1 second.loc +# end +` + testDisabledProfile = ` +# profile.off profile2 +# 127.0.0.1 first.loc +# 127.0.0.1 second.loc +# end +` + onlyEnabled = defaultProfile + Banner + "\n" + testEnabledProfile + fullHostfile = onlyEnabled + testDisabledProfile +) + +func TestNewWithFs(t *testing.T) { + t.Run("With file", func(t *testing.T) { + src := makeTempHostsFile(t, "etc_hosts") + fs := afero.NewOsFs() + + m, err := NewWithFs(src.Name(), fs) + assert.NoError(t, err) + + err = m.Flush() + assert.NoError(t, err) + + assert.Equal(t, src.Name(), m.src.Name()) + }) + t.Run("With invalid file", func(t *testing.T) { + fs := afero.NewOsFs() + + m, err := NewWithFs("/tmp/invalid_random_name", fs) + assert.Nil(t, m) + assert.True(t, os.IsNotExist(err)) + }) + + t.Run("Without fs", func(t *testing.T) { + src := makeTempHostsFile(t, "etc_hosts") + + m, err := NewWithFs(src.Name(), nil) + assert.NoError(t, err) + + err = m.Flush() + assert.NoError(t, err) + + assert.Equal(t, src.Name(), m.src.Name()) + }) +} + +func TestNewWithFsError(t *testing.T) { +} + func TestManagerStatus(t *testing.T) { t.Run("Get Status", func(t *testing.T) { mem := createBasicFS(t) @@ -72,9 +129,7 @@ func TestManagerRoutes(t *testing.T) { c, err := afero.ReadFile(mem, h.Name()) assert.NoError(t, err) - assert.Contains(t, string(c), defaultProfile) - assert.Contains(t, string(c), Banner) - assert.Contains(t, string(c), testEnabledProfile) + assert.Contains(t, string(c), onlyEnabled) var added = ` # profile.off profile2 # 127.0.0.1 first.loc @@ -103,9 +158,7 @@ func TestManagerRoutes(t *testing.T) { c, err := afero.ReadFile(mem, h.Name()) assert.NoError(t, err) - assert.Contains(t, string(c), defaultProfile) - assert.Contains(t, string(c), Banner) - assert.Contains(t, string(c), testEnabledProfile) + assert.Contains(t, string(c), onlyEnabled) var added = ` # profile.on awesome 3.3.3.4 host1.loc @@ -115,7 +168,7 @@ func TestManagerRoutes(t *testing.T) { assert.Contains(t, string(c), added) }) - t.Run("RemoveRoutes", func(t *testing.T) { + t.Run("RemoveHostnames one", func(t *testing.T) { mem := createBasicFS(t) f, err := NewWithFs("/tmp/etc/hosts", mem) @@ -133,9 +186,8 @@ func TestManagerRoutes(t *testing.T) { c, err := afero.ReadFile(mem, h.Name()) assert.NoError(t, err) - assert.Contains(t, string(c), defaultProfile) - assert.Contains(t, string(c), Banner) - assert.Contains(t, string(c), testEnabledProfile) + assert.Contains(t, string(c), onlyEnabled) + var added = ` # profile.off profile2 # 127.0.0.1 first.loc @@ -144,7 +196,7 @@ func TestManagerRoutes(t *testing.T) { assert.Contains(t, string(c), added) }) - t.Run("RemoveRoutes", func(t *testing.T) { + t.Run("RemoveHostnames multi", func(t *testing.T) { mem := createBasicFS(t) f, err := NewWithFs("/tmp/etc/hosts", mem) @@ -162,9 +214,7 @@ func TestManagerRoutes(t *testing.T) { c, err := afero.ReadFile(mem, h.Name()) assert.NoError(t, err) - assert.Contains(t, string(c), defaultProfile) - assert.Contains(t, string(c), Banner) - assert.Contains(t, string(c), testEnabledProfile) + assert.Contains(t, string(c), onlyEnabled) assert.NotContains(t, string(c), testDisabledProfile) }) } @@ -183,8 +233,7 @@ func TestManagerWrite(t *testing.T) { c, err := afero.ReadFile(mem, h.Name()) assert.NoError(t, err) - assert.Contains(t, string(c), defaultProfile) - assert.Contains(t, string(c), Banner+"\n"+testEnabledProfile+testDisabledProfile) + assert.Contains(t, string(c), fullHostfile) }) t.Run("WriteTo", func(t *testing.T) { @@ -200,8 +249,7 @@ func TestManagerWrite(t *testing.T) { c, err := ioutil.ReadFile(f.Name()) assert.NoError(t, err) - assert.Contains(t, string(c), defaultProfile) - assert.Contains(t, string(c), Banner+"\n"+testEnabledProfile+testDisabledProfile) + assert.Contains(t, string(c), fullHostfile) }) t.Run("writeBanner", func(t *testing.T) { diff --git a/pkg/file/helpers_test.go b/pkg/file/helpers_test.go index d454f0c..36f4121 100644 --- a/pkg/file/helpers_test.go +++ b/pkg/file/helpers_test.go @@ -2,38 +2,31 @@ package file import ( "io/ioutil" - "net" "os" "testing" "github.com/spf13/afero" ) -var Localhost = net.ParseIP("127.0.0.1") +func makeTempHostsFile(t *testing.T, pattern string) *os.File { + t.Helper() + + file, err := ioutil.TempFile("/tmp", pattern+"_") + if err != nil { + t.Fatal(err) + } -var defaultProfile = "127.0.0.1 localhost\n" -var testEnabledProfile = ` + _, _ = file.WriteString(` +127.0.0.1 localhost # profile.on profile1 127.0.0.1 first.loc 127.0.0.1 second.loc # end -` -var testDisabledProfile = ` # profile.off profile2 # 127.0.0.1 first.loc # 127.0.0.1 second.loc # end -` - -func makeTempHostsFile(t *testing.T, pattern string) *os.File { - t.Helper() - - file, err := ioutil.TempFile("/tmp", pattern+"_") - if err != nil { - t.Fatal(err) - } - - _, _ = file.WriteString(defaultProfile + testEnabledProfile + testDisabledProfile) +`) defer file.Close() return file @@ -48,7 +41,18 @@ func createBasicFS(t *testing.T) afero.Fs { f, _ := appFS.Create("/tmp/etc/hosts") defer f.Close() - _, _ = f.WriteString(defaultProfile + Banner + testEnabledProfile + testDisabledProfile) + _, _ = f.WriteString(` +127.0.0.1 localhost +` + Banner + ` +# profile.on profile1 +127.0.0.1 first.loc +127.0.0.1 second.loc +# end +# profile.off profile2 +# 127.0.0.1 first.loc +# 127.0.0.1 second.loc +# end +`) return appFS } diff --git a/pkg/file/merge.go b/pkg/file/merge.go index b2f2360..17b8fb3 100644 --- a/pkg/file/merge.go +++ b/pkg/file/merge.go @@ -6,9 +6,9 @@ import ( // MergeFile joins new content with existing content func (f *File) MergeFile(from *File) { - var ps []*types.Profile - for _, p := range from.data.Profiles { - ps = append(ps, p) + ps := make([]*types.Profile, len(from.data.Profiles)) + for i, name := range from.data.ProfileNames { + ps[i] = from.data.Profiles[name] } f.MergeProfiles(ps) diff --git a/pkg/file/merge_test.go b/pkg/file/merge_test.go index 24c39be..30a8c6b 100644 --- a/pkg/file/merge_test.go +++ b/pkg/file/merge_test.go @@ -82,9 +82,10 @@ func TestFile_MergeProfiles(t *testing.T) { p2, err := m.GetProfile("profile2") assert.NoError(t, err) + ip := net.ParseIP("127.0.0.1") modP2 := profiles[0] modP2.IPList = []string{"127.0.0.1", "2.2.2.2"} - modP2.Routes[Localhost.String()] = &types.Route{IP: Localhost, HostNames: []string{"first.loc", "second.loc"}} + modP2.Routes[ip.String()] = &types.Route{IP: ip, HostNames: []string{"first.loc", "second.loc"}} modP2.Status = types.Disabled assert.Equal(t, modP2, p2) } diff --git a/pkg/file/types.go b/pkg/file/types.go index a788ba6..edd0ede 100644 --- a/pkg/file/types.go +++ b/pkg/file/types.go @@ -1,5 +1,6 @@ package file +// Banner is the mark added to hosts file const Banner = ` ################################################################## # Content under this line is handled by hostctl. DO NOT EDIT. diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go new file mode 100644 index 0000000..e4c4d4e --- /dev/null +++ b/pkg/k8s/client.go @@ -0,0 +1,34 @@ +package k8s + +import ( + "os" + "path/filepath" + + "github.com/guumaster/cligger" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +// NewClientset returns a new clienset to interact with Kubernetes +func NewClientset() (*kubernetes.Clientset, error) { + kubeConfigPath := filepath.Join(os.Getenv("HOME"), ".kube", "config") + + if fromEnv := os.Getenv("KUBECONFIG"); fromEnv != "" { + kubeConfigPath = fromEnv + cligger.Info("Using config from %s", kubeConfigPath) + } + + // use the current context in kubeConfigPath + kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath) + if err != nil { + return nil, cligger.Errorf("fatal error kubernetes config: %s", err) + } + + // create the clientset + clientset, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + return nil, cligger.Errorf("error with kubernetes config:", err) + } + + return clientset, nil +} diff --git a/pkg/k8s/ingress.go b/pkg/k8s/ingress.go new file mode 100644 index 0000000..cdf10f3 --- /dev/null +++ b/pkg/k8s/ingress.go @@ -0,0 +1,41 @@ +package k8s + +import ( + "net" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// Ingress contains information about an ingress rule +type Ingress struct { + IP net.IP + Hostname string +} + +// GetIngresses returns a list of ingresses presents on a namespace +func GetIngresses(cli *kubernetes.Clientset, ns string) ([]Ingress, error) { + if ns == "" { + ns = v1.NamespaceAll + } + + ing, err := cli.ExtensionsV1beta1().Ingresses(ns).List(v1.ListOptions{}) + if err != nil { + return nil, err + } + + var list []Ingress + + for _, i := range ing.Items { + ip := i.Status.LoadBalancer.Ingress[0].IP + + for _, r := range i.Spec.Rules { + list = append(list, Ingress{ + IP: net.ParseIP(ip), + Hostname: r.Host, + }) + } + } + + return list, nil +} diff --git a/pkg/k8s/minikube/minikube.go b/pkg/k8s/minikube/minikube.go new file mode 100644 index 0000000..49a6699 --- /dev/null +++ b/pkg/k8s/minikube/minikube.go @@ -0,0 +1,98 @@ +package minikube + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os/exec" +) + +// Profile contains information about a minikube profile +type Profile struct { + Name string + IP string + Status Status + Driver string + + IngressEnabled bool + IngressDNSEnabled bool +} + +// Status represents the current state of a profile +type Status string + +const ( + // Running status of minikube profile + Running Status = "Running" +) + +// GetProfile returns information about a minikube profile +func GetProfile(name string) (*Profile, error) { + mini, err := exec.LookPath("minikube") + if err != nil { + return nil, err + } + + b := bytes.NewBufferString("") + c := exec.Command(mini, "profile", "list", "-o", "json") + c.Stdout = b + + err = c.Run() + if err != nil { + return nil, err + } + + data, err := ioutil.ReadAll(b) + if err != nil { + return nil, err + } + + var m ProfileListResponse + + _ = json.Unmarshal(data, &m) + + var profile *Profile + + for _, p := range m.Valid { + if p.Name == name { + profile = &Profile{ + Name: p.Name, + IP: p.Config.Nodes[0].IP, + Status: Status(p.Status), + Driver: p.Config.Driver, + } + } + } + + if profile == nil { + return nil, fmt.Errorf("can't find profile '%s' on minikube", name) + } + + b = bytes.NewBufferString("") + c = exec.Command(mini, "addons", "list", "-o", "json", "-p", profile.Name) + c.Stdout = b + + err = c.Run() + if err != nil { + return nil, err + } + + data, err = ioutil.ReadAll(b) + if err != nil { + return nil, err + } + + var a *AddonsResponse + + _ = json.Unmarshal(data, &a) + + if a == nil { + return profile, nil + } + + profile.IngressDNSEnabled = a.IngressDNS.Status == "enabled" + profile.IngressEnabled = a.Ingress.Status == "enabled" + + return profile, nil +} diff --git a/pkg/k8s/minikube/responses.go b/pkg/k8s/minikube/responses.go new file mode 100644 index 0000000..9667ca3 --- /dev/null +++ b/pkg/k8s/minikube/responses.go @@ -0,0 +1,27 @@ +package minikube + +// ProfileListResponse represents the json response of `minikube profile list` +type ProfileListResponse struct { + Valid []struct { + Config struct { + Driver string `json:"Driver"` + Name string `json:"Name"` + Nodes []struct { + IP string `json:"IP"` + Name string `json:"Name"` + } `json:"Nodes"` + } `json:"Config"` + Name string `json:"Name"` + Status string `json:"Status"` + } `json:"valid"` +} + +// AddonsResponse represents the json response of `minikube addons list` +type AddonsResponse struct { + Ingress struct { + Status string `json:"Status"` + } `json:"ingress"` + IngressDNS struct { + Status string `json:"Status"` + } `json:"ingress-dns"` +} diff --git a/pkg/profile/docker.go b/pkg/profile/docker.go index eb699f6..b589a1c 100644 --- a/pkg/profile/docker.go +++ b/pkg/profile/docker.go @@ -47,6 +47,11 @@ func NewProfileFromDockerCompose(opts *DockerOptions) (*types.Profile, error) { func NewProfileFromDocker(opts *DockerOptions) (*types.Profile, error) { p := &types.Profile{} + err := checkCli(opts) + if err != nil { + return nil, err + } + containers, err := getContainerList(opts) if err != nil { return nil, err @@ -57,26 +62,30 @@ func NewProfileFromDocker(opts *DockerOptions) (*types.Profile, error) { return p, err } -func getContainerList(opts *DockerOptions) ([]dtypes.Container, error) { - var ( - networkID string - err error - ) - +func checkCli(opts *DockerOptions) error { cli := opts.Cli if cli == nil { - cli, err = client.NewEnvClient() + cli, err := client.NewEnvClient() if err != nil { - return nil, err + return err } - defer cli.Close() + opts.Cli = cli } + return nil +} + +func getContainerList(opts *DockerOptions) ([]dtypes.Container, error) { + var ( + networkID string + err error + ) + ctx := context.Background() if opts.NetworkID == "" && opts.Network != "" { - networkID, err = docker.GetNetworkID(ctx, cli, opts.Network) + networkID, err = docker.GetNetworkID(ctx, opts.Cli, opts.Network) if err != nil { return nil, err } @@ -84,7 +93,7 @@ func getContainerList(opts *DockerOptions) ([]dtypes.Container, error) { opts.NetworkID = networkID } - return docker.GetContainerList(ctx, cli, networkID) + return docker.GetContainerList(ctx, opts.Cli, networkID) } func addFromContainer(profile *types.Profile, containers []dtypes.Container, opts *DockerOptions) { diff --git a/pkg/profile/docker_test.go b/pkg/profile/docker_test.go index c261759..9b4543d 100644 --- a/pkg/profile/docker_test.go +++ b/pkg/profile/docker_test.go @@ -11,6 +11,16 @@ import ( "github.com/stretchr/testify/assert" ) +func TestNew(t *testing.T) { + opts := &DockerOptions{ + Domain: "test", + } + err := checkCli(opts) + assert.NoError(t, err) + assert.NotNil(t, opts) + assert.NotNil(t, opts.Cli) +} + func TestNewProfileFromDocker(t *testing.T) { t.Run("All containers", func(t *testing.T) { c := newClientWithResponse(t, map[string]string{ @@ -34,8 +44,7 @@ func TestNewProfileFromDocker(t *testing.T) { assert.NoError(t, err) - hosts, err := p.GetAllHostNames() - assert.NoError(t, err) + hosts := p.GetAllHostNames() assert.Equal(t, []string{"172.0.0.2", "172.0.0.3"}, p.IPList) assert.Equal(t, []string{"container1.test", "container2.test"}, hosts) @@ -60,8 +69,7 @@ func TestNewProfileFromDocker(t *testing.T) { assert.NoError(t, err) - hosts, err := p.GetAllHostNames() - assert.NoError(t, err) + hosts := p.GetAllHostNames() assert.Equal(t, []string{"172.0.0.3"}, p.IPList) assert.Equal(t, []string{"container2.test"}, hosts) @@ -113,8 +121,7 @@ networks: assert.NoError(t, err) - hosts, err := p.GetAllHostNames() - assert.NoError(t, err) + hosts := p.GetAllHostNames() assert.Equal(t, []string{"172.0.0.2", "172.0.0.3"}, p.IPList) assert.Equal(t, []string{"container1_1.loc", "container2_1.loc"}, hosts) diff --git a/pkg/profile/k8s.go b/pkg/profile/k8s.go new file mode 100644 index 0000000..87fed55 --- /dev/null +++ b/pkg/profile/k8s.go @@ -0,0 +1,36 @@ +package profile + +import ( + "github.com/guumaster/hostctl/pkg/k8s" + "github.com/guumaster/hostctl/pkg/k8s/minikube" + "github.com/guumaster/hostctl/pkg/types" +) + +// NewProfileFromMinikube creates a new profile from a minikube profile and k8s namespace +func NewProfileFromMinikube(mini *minikube.Profile, ns string) (*types.Profile, error) { + if mini.Status != minikube.Running { + return nil, types.ErrMinikubeStatus + } + + if !mini.IngressEnabled { + return nil, types.ErrMinikubeIngress + } + + cli, err := k8s.NewClientset() + if err != nil { + return nil, err + } + + list, err := k8s.GetIngresses(cli, ns) + if err != nil { + return nil, err + } + + p := &types.Profile{} + + for _, in := range list { + p.AddRoute(types.NewRoute(in.IP.String(), in.Hostname)) + } + + return p, nil +} diff --git a/pkg/profile/parser.go b/pkg/profile/parser.go index a14d179..bc5e1ba 100644 --- a/pkg/profile/parser.go +++ b/pkg/profile/parser.go @@ -10,6 +10,7 @@ import ( "github.com/guumaster/hostctl/pkg/types" ) +// nolint:gochecknoglobals var ( profileNameRe = regexp.MustCompile(`# profile(?:.(on|off))?\s+([a-z0-9-_.\s]+)`) profileEnd = regexp.MustCompile(`(?i)# end\s*`) @@ -17,6 +18,7 @@ var ( tabReplacer = regexp.MustCompile(`\t+`) ) +// Parser is the interface for content parsers type Parser interface { Parse(reader io.Reader) types.Content } diff --git a/pkg/profile/reader_test.go b/pkg/profile/reader_test.go index e3227b4..1003b0f 100644 --- a/pkg/profile/reader_test.go +++ b/pkg/profile/reader_test.go @@ -19,4 +19,18 @@ func TestNewProfile(t *testing.T) { assert.NoError(t, err) assert.Equal(t, []string{"some.profile.loc", "first.loc"}, hosts) }) + + t.Run("NewProfileFromReader non-uniq", func(t *testing.T) { + r := strings.NewReader(` +3.3.3.4 some.profile.loc +# non-route-line +3.3.3.4 first.loc +3.3.3.4 first.loc +`) + p, err := NewProfileFromReader(r, false) + assert.NoError(t, err) + hosts, err := p.GetHostNames("3.3.3.4") + assert.NoError(t, err) + assert.Equal(t, []string{"some.profile.loc", "first.loc", "first.loc"}, hosts) + }) } diff --git a/pkg/render/json.go b/pkg/render/json.go index b3599dc..21d46b7 100644 --- a/pkg/render/json.go +++ b/pkg/render/json.go @@ -7,12 +7,14 @@ import ( "github.com/guumaster/hostctl/pkg/types" ) +// JSONRendererOptions contains options to render JSON content type JSONRendererOptions struct { Writer io.Writer Columns []string OnlyEnabled bool } +// JSONRenderer is the Renderer used to output JSON type JSONRenderer struct { Type RendererType Columns []string @@ -24,6 +26,7 @@ type data struct { lines []line } +// NewJSONRenderer creates an instance of JSONRenderer func NewJSONRenderer(opts *JSONRendererOptions) JSONRenderer { if len(opts.Columns) == 0 { opts.Columns = types.DefaultColumns @@ -37,6 +40,7 @@ func NewJSONRenderer(opts *JSONRendererOptions) JSONRenderer { } } +// AddSeparator not used on JSONRenderer func (j JSONRenderer) AddSeparator() { // not used } @@ -48,6 +52,7 @@ type line struct { Host string } +// AppendRow adds a new row to the list func (j JSONRenderer) AppendRow(row *types.Row) { if row.Comment != "" { return @@ -62,6 +67,7 @@ func (j JSONRenderer) AppendRow(row *types.Row) { j.data.lines = append(j.data.lines, l) } +// Render returns a JSON representation of the list content func (j JSONRenderer) Render() error { enc := json.NewEncoder(j.w) diff --git a/pkg/render/markdown.go b/pkg/render/markdown.go index adcaa68..47d28c0 100644 --- a/pkg/render/markdown.go +++ b/pkg/render/markdown.go @@ -2,10 +2,9 @@ package render import ( "github.com/guumaster/tablewriter" - - "github.com/guumaster/hostctl/pkg/types" ) +// NewMarkdownRenderer creates an instance of TableRenderer func NewMarkdownRenderer(opts *TableRendererOptions) TableRenderer { table := createTableWriter(opts) @@ -21,7 +20,7 @@ func NewMarkdownRenderer(opts *TableRendererOptions) TableRenderer { Columns: opts.Columns, table: table, opts: opts, - meta: &types.Meta{ + meta: &meta{ Rows: 0, }, } diff --git a/pkg/render/raw.go b/pkg/render/raw.go index c9dff4b..b2eb038 100644 --- a/pkg/render/raw.go +++ b/pkg/render/raw.go @@ -2,10 +2,9 @@ package render import ( "github.com/guumaster/tablewriter" - - "github.com/guumaster/hostctl/pkg/types" ) +// NewRawRenderer creates an instance of TableRenderer without borders func NewRawRenderer(opts *TableRendererOptions) TableRenderer { table := createTableWriter(opts) @@ -25,7 +24,7 @@ func NewRawRenderer(opts *TableRendererOptions) TableRenderer { Columns: opts.Columns, table: table, opts: opts, - meta: &types.Meta{ + meta: &meta{ Rows: 0, Raw: true, }, diff --git a/pkg/render/table.go b/pkg/render/table.go index b5018df..b7c6727 100644 --- a/pkg/render/table.go +++ b/pkg/render/table.go @@ -9,13 +9,16 @@ import ( "github.com/guumaster/hostctl/pkg/types" ) +// TableRendererOptions contains options to render a table type TableRendererOptions struct { Writer io.Writer Columns []string } +// RendererType represents all the existing renderers type RendererType string +// nolint:gochecknoglobals var ( Markdown RendererType = "markdown" Table RendererType = "table" @@ -23,12 +26,18 @@ var ( JSON RendererType = "json" ) +type meta struct { + Rows int + Raw bool +} + +// TableRenderer is the Renderer used to output tables type TableRenderer struct { Type RendererType Columns []string table *tablewriter.Table opts *TableRendererOptions - meta *types.Meta + meta *meta } func createTableWriter(opts *TableRendererOptions) *tablewriter.Table { @@ -47,6 +56,7 @@ func createTableWriter(opts *TableRendererOptions) *tablewriter.Table { return table } +// NewTableRenderer creates an instance of TableRenderer func NewTableRenderer(opts *TableRendererOptions) TableRenderer { table := createTableWriter(opts) @@ -55,12 +65,13 @@ func NewTableRenderer(opts *TableRendererOptions) TableRenderer { Columns: opts.Columns, table: table, opts: opts, - meta: &types.Meta{ + meta: &meta{ Rows: 0, }, } } +// AppendRow adds a new row to the list func (t TableRenderer) AppendRow(row *types.Row) { r := []string{} @@ -87,13 +98,15 @@ func (t TableRenderer) AppendRow(row *types.Row) { } } +// AddSeparator adds a separator line to the list func (t TableRenderer) AddSeparator() { if !t.meta.Raw && t.meta.Rows > 0 { t.table.AddSeparator() } } -func (t TableRenderer) Render() error { +// Render prints a table representation of row content +func (t TableRenderer) Render() error { // nolint:unparam if t.meta.Rows > 0 { t.table.Render() } diff --git a/pkg/types/Route.go b/pkg/types/Route.go index e52d147..b3ee402 100644 --- a/pkg/types/Route.go +++ b/pkg/types/Route.go @@ -10,6 +10,7 @@ type Route struct { HostNames []string } +// NewRoute creates an new Route func NewRoute(ip string, hostnames ...string) *Route { return &Route{ IP: net.ParseIP(ip), diff --git a/pkg/types/errors.go b/pkg/types/errors.go index 24b1ea9..7df4279 100644 --- a/pkg/types/errors.go +++ b/pkg/types/errors.go @@ -32,4 +32,13 @@ var ( // ErrSnapConfinement when trying to read files on snap installation ErrSnapConfinement = errors.New("can't use --from or --host-file. " + "Snap confinement restrictions doesn't allow to read other than /etc/hosts file") + + // ErrMinikubeStatus when minikube profile is not running + ErrMinikubeStatus = errors.New("minikube profile has to be running") + + // ErrMinikubeIngress when minikube doesn't have ingress addon enabled + ErrMinikubeIngress = errors.New("minikube profile doesn't have ingress addon enabled") + + // ErrKubernetesNamespace when no namespace is given + ErrKubernetesNamespace = errors.New("namespace parameter is required or use --all-namespaces") ) diff --git a/pkg/types/profile.go b/pkg/types/profile.go index dfaf01a..35a608b 100644 --- a/pkg/types/profile.go +++ b/pkg/types/profile.go @@ -25,7 +25,7 @@ func (p *Profile) GetStatus() string { return string(p.Status) } -func (p *Profile) AppendIP(n string) { +func (p *Profile) appendIP(n string) { for _, c := range p.IPList { if c == n { return @@ -40,7 +40,7 @@ func (p *Profile) AddRoute(route *Route) { p.AddRoutes([]*Route{route}) } -// AddRoute adds a single route to the profile +// AddRouteUniq adds a single route to the profile and removes duplicates func (p *Profile) AddRouteUniq(route *Route) { p.AddRoutesUniq([]*Route{route}) } @@ -54,7 +54,7 @@ func (p *Profile) AddRoutes(routes []*Route) { for _, r := range routes { ip := r.IP.String() if p.Routes[ip] == nil { - p.AppendIP(ip) + p.appendIP(ip) p.Routes[ip] = &Route{ IP: net.ParseIP(ip), HostNames: r.HostNames, @@ -110,18 +110,18 @@ func (p *Profile) GetHostNames(ip string) ([]string, error) { } // GetAllHostNames returns all hostnames of the profile. -func (p *Profile) GetAllHostNames() ([]string, error) { +func (p *Profile) GetAllHostNames() []string { list := []string{} if p.IPList == nil { - return list, nil + return list } for _, ip := range p.IPList { list = append(list, p.Routes[ip].HostNames...) } - return list, nil + return list } // Render writes the profile content to the given StringWriter diff --git a/pkg/types/profile_test.go b/pkg/types/profile_test.go index e8f763d..5cd1480 100644 --- a/pkg/types/profile_test.go +++ b/pkg/types/profile_test.go @@ -106,8 +106,7 @@ func TestProfile(t *testing.T) { } p.RemoveHostnames([]string{"another.profile.loc"}) - names, err := p.GetAllHostNames() - assert.NoError(t, err) + names := p.GetAllHostNames() assert.Equal(t, []string{"some.profile.loc"}, names) }) @@ -132,9 +131,9 @@ func Test_appendIP(t *testing.T) { p.AddRoute(NewRoute("3.3.3.4", "some.profile.loc")) - p.AppendIP("3.3.3.4") - p.AppendIP("3.3.3.4") - p.AppendIP("3.3.3.5") + p.appendIP("3.3.3.4") + p.appendIP("3.3.3.4") + p.appendIP("3.3.3.5") assert.Equal(t, p.IPList, []string{"3.3.3.4", "3.3.3.5"}) } diff --git a/pkg/types/renderer.go b/pkg/types/renderer.go index 3d75308..daf1319 100644 --- a/pkg/types/renderer.go +++ b/pkg/types/renderer.go @@ -1,10 +1,13 @@ package types -// DefaultColumns is the list of default columns to use when showing table list -var DefaultColumns = []string{"profile", "status", "ip", "domain"} +// nolint:gochecknoglobals +var ( + // DefaultColumns is the list of default columns to use when showing table list + DefaultColumns = []string{"profile", "status", "ip", "domain"} -// ProfilesOnlyColumns are the columns used for profile status list -var ProfilesOnlyColumns = []string{"profile", "status"} + // ProfilesOnlyColumns are the columns used for profile status list + ProfilesOnlyColumns = []string{"profile", "status"} +) // Renderer is the interface to render hosts file content type Renderer interface { @@ -21,8 +24,3 @@ type Row struct { IP string Host string } - -type Meta struct { - Rows int - Raw bool -} diff --git a/pkg/types/types.go b/pkg/types/types.go index ccd2f09..5e27418 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -14,7 +14,7 @@ type Content struct { Profiles map[string]*Profile } -// ProfileStatus represents the status of a Profile +// Status represents the status of a Profile type Status string const ( diff --git a/sonar-project.properties b/sonar-project.properties index 502b2cb..2eec511 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,11 +4,11 @@ sonar.projectKey=guumaster_hostctl sonar.projectName=hostctl sonar.projectVersion=1.0.0 sonar.sources=. -sonar.exclusions=**/*_test.go,**/cmd/*docker*.go,*.go,**/*doc?.go,**/types.go,**/errors.go,**/renderer.go +sonar.exclusions=**/*_test.go,**/cmd/*docker*.go,*.go,**/*doc?.go,**/types.go,**/errors.go,**/renderer.go,**/k8s*,**/minikube* sonar.tests=. sonar.test.inclusions=**/*_test.go -sonar.test.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,**/renderer.go -sonar.coverage.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,cmd/**,**/renderer.go -sonar.go.coverage.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,cmd/**,**/renderer.go +sonar.test.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,**/renderer.go,**/k8s*,**/minikube* +sonar.coverage.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,cmd/**,**/renderer.go,**/k8s*,**/minikube* +sonar.go.coverage.exclusions=**/cmd/*docker*.go,**/*doc?.go,**/types.go,**/errors.go,cmd/**,**/renderer.go,**/k8s*,**/minikube* sonar.go.coverage.reportPaths=/github/workspace/ubuntu-latest_coverage/ubuntu-latest_coverage.out #sonar.go.golangci-lint.reportPaths=/github/workspace/golangci-report.xml