Repository: m3db/m3db-operator
Pull request #255 (2020-12-11 21:01:10)

Add HealthClient for checking if node is bootstrapped

codecov[bot]: # [Codecov](https://codecov.io/gh/m3db/m3db-operator/pull/255?src=pr&el=h1) Report
> Merging [#255](https://codecov.io/gh/m3db/m3db-operator/pull/255?src=pr&el=desc) (f8f2191) into [master](https://codecov.io/gh/m3db/m3db-operator/commit/dcdd974ccf891d9fc4fa9849d55191c1132aa0e2?el=desc) (dcdd974) will **increase** coverage by `0.05%`.
> The diff coverage is `87.09%`.


[![Impacted file tree graph](https://codecov.io/gh/m3db/m3db-operator/pull/255/graphs/tree.svg?width=650&height=150&src=pr&token=jtHDyWhsto)](https://codecov.io/gh/m3db/m3db-operator/pull/255?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #255      +/-   ##
==========================================
+ Coverage   76.31%   76.37%   +0.05%     
==========================================
  Files          30       31       +1     
  Lines        2246     2277      +31     
==========================================
+ Hits         1714     1739      +25     
- Misses        392      395       +3     
- Partials      140      143       +3     
```


------

[Continue to review full report at Codecov](https://codecov.io/gh/m3db/m3db-operator/pull/255?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/m3db/m3db-operator/pull/255?src=pr&el=footer). Last update [dcdd974...f8f2191](https://codecov.io/gh/m3db/m3db-operator/pull/255?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/pkg/m3admin/health/client.go b/pkg/m3admin/health/client.go
new file mode 100644
index 0000000..d70e988
--- /dev/null
+++ b/pkg/m3admin/health/client.go
@@ -0,0 +1,103 @@
+// Copyright (c) 2018 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package health
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"github.com/m3db/m3db-operator/pkg/m3admin"
+)
+
+const (
+	bootstrappedPath = "/bootstrapped"
+	m3dbServiceName  = "m3dbnode-m3-cluster-short"
+)
+
+type healthClient struct {
+	client m3admin.Client
+	url    string
+}
+
+// Option provides an interface that can be used for setter options with the
+// constructor
+type Option interface {
+	execute(*healthClient) error
+}
+
+type optionFn func(h *healthClient) error
+
+func (fn optionFn) execute(h *healthClient) error {
+	return fn(h)
+}
+
+// WithURL is a setter to override the default base url for a node.
+func WithURL(u string) Option {
+	return optionFn(func(h *healthClient) error {
+		if u != "" {
+			if _, err := url.ParseRequestURI(u); err != nil {
+				return err
+			}
+		}
+		h.url = u
+		return nil
+	})
+}
+
+// WithClient configures an m3admin client.
+func WithClient(cl m3admin.Client) Option {
+	return optionFn(func(h *healthClient) error {
+		h.client = cl
+		return nil
+	})
+}
+
+// NewClient constructs a new health client.
+func NewClient(opts ...Option) (Client, error) {
+	hc := &healthClient{
+		client: m3admin.NewClient(),
+	}
+	for _, o := range opts {
+		if err := o.execute(hc); err != nil {
+			return nil, err
+		}
+	}
+	return hc, nil
+}
+
+func (h *healthClient) Bootstrapped(namespace string, podName string) (bool, error) {
+	url := h.url
+	if url == "" {
+		url = getPodURL(namespace, podName)
+	}
+
+	_, err := h.client.DoHTTPRequest(http.MethodGet, url+bootstrappedPath, nil)
+	if err != nil {
+		return false, err
+	}
+
+	return true, nil
+}
+
+func getPodURL(namespace string, podName string) string {
+	return fmt.Sprintf("http://%s.%s.%s", podName, m3dbServiceName, namespace)
+}
diff --git a/pkg/m3admin/health/types.go b/pkg/m3admin/health/types.go
new file mode 100644
index 0000000..999a433
--- /dev/null
+++ b/pkg/m3admin/health/types.go
@@ -0,0 +1,28 @@
+// Copyright (c) 2018 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package health
+
+// Client provides the interface for checking cluster health.
+type Client interface {
+	// Bootstrapped returns true or false depending on the/ bootstrapped state
+	// of the provided dbnode.
+	Bootstrapped(namespace string, podName string) (bool, error)
+}


Test patch:
diff --git a/pkg/m3admin/health/client_test.go b/pkg/m3admin/health/client_test.go
new file mode 100644
index 0000000..3b9c777
--- /dev/null
+++ b/pkg/m3admin/health/client_test.go
@@ -0,0 +1,115 @@
+// Copyright (c) 2018 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package health
+
+import (
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/m3db/m3db-operator/pkg/m3admin"
+
+	"github.com/golang/mock/gomock"
+	retryhttp "github.com/hashicorp/go-retryablehttp"
+	"github.com/stretchr/testify/require"
+)
+
+func TestBootstrapped(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, err := ioutil.ReadAll(r.Body)
+		require.NoError(t, err)
+
+		w.WriteHeader(200)
+		w.Write([]byte("{}"))
+	}))
+
+	defer s.Close()
+	client := newHealthClientWithURL(t, newM3adminClient(s.Client()), s.URL)
+
+	bootstrapped, err := client.Bootstrapped("foo", "bar")
+
+	require.NoError(t, err)
+	require.True(t, bootstrapped)
+}
+
+func TestBootstrapped_ValidateURL(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	adminClient := m3admin.NewMockClient(ctrl)
+	adminClient.EXPECT().DoHTTPRequest("GET", "http://bar.m3dbnode-m3-cluster-short.foo/bootstrapped", nil).
+		Return(&http.Response{}, nil)
+
+	client := newHealthClient(t, adminClient)
+
+	bootstrapped, err := client.Bootstrapped("foo", "bar")
+
+	require.NoError(t, err)
+	require.True(t, bootstrapped)
+}
+
+func TestBootstrapped_NotReady(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, err := ioutil.ReadAll(r.Body)
+		require.NoError(t, err)
+
+		w.WriteHeader(500)
+		w.Write([]byte(notReadyJSON))
+	}))
+
+	defer s.Close()
+	client := newHealthClientWithURL(t, newM3adminClient(s.Client()), s.URL)
+
+	bootstrapped, err := client.Bootstrapped("foo", "bar")
+
+	require.Error(t, err)
+	require.False(t, bootstrapped)
+}
+
+const (
+	notReadyJSON = "{\"error\":{\"message\":\"Error({Type:INTERNAL_ERROR Message:node is not bootstrapped}" +
+		")\",\"data\":{\"type\":\"INTERNAL_ERROR\",\"message\":\"node is not bootstrapped\"}}}"
+)
+
+func newM3adminClient(client *http.Client) m3admin.Client {
+	retry := retryhttp.NewClient()
+	retry.HTTPClient = client
+	retry.RetryMax = 0
+
+	return m3admin.NewClient(m3admin.WithHTTPClient(retry))
+}
+
+func newHealthClient(t *testing.T, client m3admin.Client) Client {
+	return newHealthClientWithURL(t, client, "")
+}
+
+func newHealthClientWithURL(t *testing.T, client m3admin.Client, url string) Client {
+	cl, err := NewClient(
+		WithClient(client),
+		WithURL(url),
+	)
+
+	require.NoError(t, err)
+	require.NotNil(t, cl)
+
+	return cl
+}