diff --git a/internal/analytics/ga.go b/internal/analytics/ga.go deleted file mode 100644 index 5be07b24..00000000 --- a/internal/analytics/ga.go +++ /dev/null @@ -1,73 +0,0 @@ -package analytics - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/http/httputil" - - log "github.com/sirupsen/logrus" -) - -func SendStartEvent(se StartEvent) error { - postBody := map[string]interface{}{ - "client_id": se.ClientId, - "events": []map[string]interface{}{ - { - "name": "start", - "params": se, - }, - }, - } - - return doRequest(postBody) -} - -func SendRequestEvent(re RequestEvent) error { - postBody := map[string]interface{}{ - "client_id": re.ClientId, - "events": []map[string]interface{}{ - { - "name": "request", - "params": re, - }, - }, - } - - return doRequest(postBody) -} - -func doRequest(body map[string]interface{}) error { - jsonValue, err := json.Marshal(body) - if err != nil { - return err - } - - req, err := http.NewRequest("POST", "https://www.google-analytics.com/mp/collect", bytes.NewBuffer(jsonValue)) - if err != nil { - return err - } - - q := req.URL.Query() - q.Add("measurement_id", "G-S6NT05VXK9") - q.Add("api_secret", "7FFhe65HQK-bXvujpQMquQ") - req.URL.RawQuery = q.Encode() - - response, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer response.Body.Close() - - if response.StatusCode/100 != 2 { - dump, err := httputil.DumpResponse(response, true) - if err != nil { - return err - } - log.Debugf("%v", string(dump)) - return fmt.Errorf("google analytics returned non-2xx status code: %v", response.Status) - } - - return nil -} diff --git a/internal/analytics/http_beacon.go b/internal/analytics/http_beacon.go index 825ed4f0..09574b4b 100644 --- a/internal/analytics/http_beacon.go +++ b/internal/analytics/http_beacon.go @@ -11,6 +11,7 @@ import ( ) func SendBeacon(e BeaconEvent) error { + log.Tracef("sending beacon: %+v", e) jsonValue, err := json.Marshal(e) if err != nil { return err diff --git a/internal/analytics/types.go b/internal/analytics/types.go index 1b184f43..001d6e9d 100644 --- a/internal/analytics/types.go +++ b/internal/analytics/types.go @@ -1,22 +1,5 @@ package analytics -type StartEvent struct { - ClientId string `json:"-"` - Version string `json:"version"` - FilterLength int `json:"filterLength"` - RemoteHostLength int `json:"remoteHostLength"` - CustomAddress bool `json:"customAddress"` - CustomBase bool `json:"customBase"` - Protected bool `json:"protected"` - HasHostname bool `json:"hasHostname"` -} - -type RequestEvent struct { - ClientId string `json:"-"` - TotalContainers int `json:"totalContainers"` - RunningContainers int `json:"runningContainers"` -} - type BeaconEvent struct { Name string `json:"name"` Version string `json:"version"` @@ -24,10 +7,10 @@ type BeaconEvent struct { AuthProvider string `json:"authProvider"` FilterLength int `json:"filterLength"` Clients int `json:"clients"` - HasDocumentation bool `json:"hasDocumentation"` HasCustomAddress bool `json:"hasCustomAddress"` HasCustomBase bool `json:"hasCustomBase"` HasHostname bool `json:"hasHostname"` RunningContainers int `json:"runningContainers"` HasActions bool `json:"hasActions"` + IsSwarmMode bool `json:"isSwarmMode"` } diff --git a/internal/docker/client.go b/internal/docker/client.go index 4d503b76..37c6e28c 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -18,6 +18,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" log "github.com/sirupsen/logrus" @@ -67,16 +68,18 @@ type Client interface { Ping(context.Context) (types.Ping, error) Host() *Host ContainerActions(action string, containerID string) error + IsSwarmMode() bool } -type _client struct { - cli DockerCLI - filters filters.Args - host *Host +type httpClient struct { + cli DockerCLI + filters filters.Args + host *Host + SwarmMode bool } -func NewClient(cli DockerCLI, filters filters.Args, host *Host) Client { - return &_client{cli, filters, host} +func NewClient(cli DockerCLI, filters filters.Args, host *Host, swarm bool) Client { + return &httpClient{cli, filters, host, swarm} } // NewClientWithFilters creates a new instance of Client with docker filters @@ -96,7 +99,10 @@ func NewClientWithFilters(f map[string][]string) (Client, error) { return nil, err } - return NewClient(cli, filterArgs, &Host{Name: "localhost", ID: "localhost"}), nil + info, _ := cli.Info(context.Background()) + swarm := info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive + + return NewClient(cli, filterArgs, &Host{Name: "localhost", ID: "localhost"}, swarm), nil } func NewClientWithTlsAndFilter(f map[string][]string, host Host) (Client, error) { @@ -132,10 +138,13 @@ func NewClientWithTlsAndFilter(f map[string][]string, host Host) (Client, error) return nil, err } - return NewClient(cli, filterArgs, &host), nil + info, _ := cli.Info(context.Background()) + swarm := info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive + + return NewClient(cli, filterArgs, &host, swarm), nil } -func (d *_client) FindContainer(id string) (Container, error) { +func (d *httpClient) FindContainer(id string) (Container, error) { var container Container containers, err := d.ListContainers() if err != nil { @@ -163,7 +172,7 @@ func (d *_client) FindContainer(id string) (Container, error) { return container, nil } -func (d *_client) ContainerActions(action string, containerID string) error { +func (d *httpClient) ContainerActions(action string, containerID string) error { switch action { case "start": return d.cli.ContainerStart(context.Background(), containerID, container.StartOptions{}) @@ -176,7 +185,7 @@ func (d *_client) ContainerActions(action string, containerID string) error { } } -func (d *_client) ListContainers() ([]Container, error) { +func (d *httpClient) ListContainers() ([]Container, error) { containerListOptions := container.ListOptions{ Filters: d.filters, All: true, @@ -217,7 +226,7 @@ func (d *_client) ListContainers() ([]Container, error) { return containers, nil } -func (d *_client) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error { +func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error { response, err := d.cli.ContainerStats(ctx, id, true) if err != nil { @@ -252,8 +261,6 @@ func (d *_client) ContainerStats(ctx context.Context, id string, stats chan<- Co mem = float64(v.MemoryStats.PrivateWorkingSet) } - log.Tracef("containerId = %s, cpuPercent = %f, memPercent = %f, memUsage = %f, daemonOSType = %s", id, cpuPercent, memPercent, mem, daemonOSType) - if cpuPercent > 0 || mem > 0 { select { case <-ctx.Done(): @@ -269,7 +276,7 @@ func (d *_client) ContainerStats(ctx context.Context, id string, stats chan<- Co } } -func (d *_client) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) { +func (d *httpClient) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) { log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container") if since != "" { @@ -297,7 +304,7 @@ func (d *_client) ContainerLogs(ctx context.Context, id string, since string, st return reader, nil } -func (d *_client) Events(ctx context.Context, messages chan<- ContainerEvent) error { +func (d *httpClient) Events(ctx context.Context, messages chan<- ContainerEvent) error { dockerMessages, err := d.cli.Events(ctx, types.EventsOptions{}) for { @@ -320,7 +327,7 @@ func (d *_client) Events(ctx context.Context, messages chan<- ContainerEvent) er } -func (d *_client) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) { +func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) { options := container.LogsOptions{ ShowStdout: stdType&STDOUT != 0, ShowStderr: stdType&STDERR != 0, @@ -339,14 +346,18 @@ func (d *_client) ContainerLogsBetweenDates(ctx context.Context, id string, from return reader, nil } -func (d *_client) Ping(ctx context.Context) (types.Ping, error) { +func (d *httpClient) Ping(ctx context.Context) (types.Ping, error) { return d.cli.Ping(ctx) } -func (d *_client) Host() *Host { +func (d *httpClient) Host() *Host { return d.host } +func (d *httpClient) IsSwarmMode() bool { + return d.SwarmMode +} + var PARENTHESIS_RE = regexp.MustCompile(`\(([a-zA-Z]+)\)`) func findBetweenParentheses(s string) string { diff --git a/internal/docker/client_test.go b/internal/docker/client_test.go index 466de53e..774d8b2c 100644 --- a/internal/docker/client_test.go +++ b/internal/docker/client_test.go @@ -89,7 +89,7 @@ func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string, func Test_dockerClient_ListContainers_null(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil) - client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} list, err := client.ListContainers() assert.Empty(t, list, "list should be empty") @@ -101,7 +101,7 @@ func Test_dockerClient_ListContainers_null(t *testing.T) { func Test_dockerClient_ListContainers_error(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test")) - client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} list, err := client.ListContainers() assert.Nil(t, list, "list should be nil") @@ -124,7 +124,7 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) - client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} list, err := client.ListContainers() require.NoError(t, err, "error should not return an error.") @@ -151,7 +151,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) { options := container.LogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"} proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil) - client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL) actual, _ := io.ReadAll(logReader) @@ -165,7 +165,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) { proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test")) - client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} reader, err := client.ContainerLogs(context.Background(), id, "", STDALL) @@ -192,7 +192,7 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) { json := types.ContainerJSON{Config: &container.Config{Tty: false}} proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil) - client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} container, err := client.FindContainer("abcdefghijkl") require.NoError(t, err, "error should not be thrown") @@ -215,7 +215,7 @@ func Test_dockerClient_FindContainer_error(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) - client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} _, err := client.FindContainer("not_valid") require.Error(t, err, "error should be thrown") @@ -236,7 +236,7 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) { } proxy := new(mockedProxy) - client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} json := types.ContainerJSON{Config: &container.Config{Tty: false}} proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil) @@ -272,7 +272,7 @@ func Test_dockerClient_ContainerActions_error(t *testing.T) { } proxy := new(mockedProxy) - client := &_client{proxy, filters.NewArgs(), &Host{ID: "localhost"}} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test")) diff --git a/internal/web/events.go b/internal/web/events.go index bd3534bb..bf9bd843 100644 --- a/internal/web/events.go +++ b/internal/web/events.go @@ -42,6 +42,7 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) { allContainers := make([]docker.Container, 0) events := make(chan docker.ContainerEvent) stats := make(chan docker.ContainerStat) + hasSwarm := false for _, store := range h.stores { if containers, err := store.List(); err == nil { @@ -55,6 +56,10 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) { } store.SubscribeStats(ctx, stats) store.Subscribe(ctx, events) + + if store.Client().IsSwarmMode() { + hasSwarm = true + } } defer func() { @@ -67,6 +72,8 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) { log.Errorf("error writing containers to event stream: %v", err) } b.RunningContainers = len(allContainers) + b.IsSwarmMode = hasSwarm + f.Flush() if !h.config.NoAnalytics { diff --git a/internal/web/routes_test.go b/internal/web/routes_test.go index bef64b72..980fe497 100644 --- a/internal/web/routes_test.go +++ b/internal/web/routes_test.go @@ -59,6 +59,10 @@ func (m *MockedClient) Host() *docker.Host { return args.Get(0).(*docker.Host) } +func (m *MockedClient) IsSwarmMode() bool { + return false +} + func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux { if client == nil { client = new(MockedClient) diff --git a/main_test.go b/main_test.go index e2733d9e..3df1214e 100644 --- a/main_test.go +++ b/main_test.go @@ -29,7 +29,7 @@ func Test_valid_localhost(t *testing.T) { fakeClientFactory := func(filter map[string][]string) (docker.Client, error) { return docker.NewClient(client, filters.NewArgs(), &docker.Host{ ID: "localhost", - }), nil + }, false), nil } args := args{} @@ -46,7 +46,7 @@ func Test_invalid_localhost(t *testing.T) { fakeClientFactory := func(filter map[string][]string) (docker.Client, error) { return docker.NewClient(client, filters.NewArgs(), &docker.Host{ ID: "localhost", - }), nil + }, false), nil } args := args{} @@ -63,7 +63,7 @@ func Test_valid_remote(t *testing.T) { fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) { return docker.NewClient(local, filters.NewArgs(), &docker.Host{ ID: "localhost", - }), nil + }, false), nil } remote := new(fakeCLI) @@ -71,7 +71,7 @@ func Test_valid_remote(t *testing.T) { fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) { return docker.NewClient(remote, filters.NewArgs(), &docker.Host{ ID: "test", - }), nil + }, false), nil } args := args{ @@ -93,7 +93,7 @@ func Test_valid_remote_and_local(t *testing.T) { fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) { return docker.NewClient(local, filters.NewArgs(), &docker.Host{ ID: "localhost", - }), nil + }, false), nil } remote := new(fakeCLI) @@ -101,7 +101,7 @@ func Test_valid_remote_and_local(t *testing.T) { fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) { return docker.NewClient(remote, filters.NewArgs(), &docker.Host{ ID: "test", - }), nil + }, false), nil } args := args{ RemoteHost: []string{"tcp://test:2375"}, @@ -123,13 +123,13 @@ func Test_no_clients(t *testing.T) { return docker.NewClient(local, filters.NewArgs(), &docker.Host{ ID: "localhost", - }), nil + }, false), nil } fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) { client := new(fakeCLI) return docker.NewClient(client, filters.NewArgs(), &docker.Host{ ID: "test", - }), nil + }, false), nil } args := args{}