Skip to content

Commit 4d1c005

Browse files
feat(delete): delete replica indices in one go (#126)
* feat(delete): delete replica indices in one go This commit makes deleting replicas possible. By default, the API returns a 403 if you try to delete a replica index. Since this is annoying, I added the multiple steps it takes to delete a replica index to the `delete` command. --------- Co-authored-by: Clement Denoix <[email protected]>
1 parent 9f0eb20 commit 4d1c005

File tree

4 files changed

+121
-6
lines changed

4 files changed

+121
-6
lines changed

pkg/cmd/indices/delete/delete.go

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66

77
"github.com/MakeNowJust/heredoc"
8+
"github.com/algolia/algoliasearch-client-go/v3/algolia/opt"
89
"github.com/algolia/algoliasearch-client-go/v3/algolia/search"
910
"github.com/spf13/cobra"
1011

@@ -105,7 +106,12 @@ func runDeleteCmd(opts *DeleteOptions) error {
105106

106107
for _, index := range indices {
107108
if _, err := index.Delete(); err != nil {
108-
return fmt.Errorf("failed to delete index %q: %w", index.GetName(), err)
109+
opts.IO.StartProgressIndicatorWithLabel(fmt.Sprint("Deleting replica index ", index.GetName()))
110+
err := deleteReplicaIndex(client, index)
111+
opts.IO.StopProgressIndicator()
112+
if err != nil {
113+
return fmt.Errorf("failed to delete index %q: %w", index.GetName(), err)
114+
}
109115
}
110116
}
111117

@@ -116,3 +122,81 @@ func runDeleteCmd(opts *DeleteOptions) error {
116122

117123
return nil
118124
}
125+
126+
// Delete a replica index.
127+
func deleteReplicaIndex(client *search.Client, replicaIndex *search.Index) error {
128+
replicaName := replicaIndex.GetName()
129+
primaryName, err := findPrimaryIndex(replicaIndex)
130+
if err != nil {
131+
return fmt.Errorf("can't find primary index for %q: %w", replicaName, err)
132+
}
133+
134+
err = detachReplicaIndex(replicaName, primaryName, client)
135+
if err != nil {
136+
return fmt.Errorf("can't unlink replica index %s from primary index %s: %w", replicaName, primaryName, err)
137+
}
138+
139+
_, err = replicaIndex.Delete()
140+
if err != nil {
141+
return fmt.Errorf("can't delete replica index %q: %w", replicaName, err)
142+
}
143+
144+
return nil
145+
}
146+
147+
// Find the primary index of a replica index
148+
func findPrimaryIndex(replicaIndex *search.Index) (string, error) {
149+
replicaName := replicaIndex.GetName()
150+
settings, err := replicaIndex.GetSettings()
151+
152+
if err != nil {
153+
return "", fmt.Errorf("can't get settings of replica index %q: %w", replicaName, err)
154+
}
155+
156+
primary := settings.Primary
157+
if primary == nil {
158+
return "", fmt.Errorf("index %s doesn't have a primary", replicaName)
159+
}
160+
161+
return primary.Get(), nil
162+
}
163+
164+
// Remove replica from `replicas` settings of the primary index
165+
func detachReplicaIndex(replicaName string, primaryName string, client *search.Client) error {
166+
primaryIndex := client.InitIndex(primaryName)
167+
settings, err := primaryIndex.GetSettings()
168+
169+
if err != nil {
170+
return fmt.Errorf("can't get settings of primary index %q: %w", primaryName, err)
171+
}
172+
173+
replicas := settings.Replicas.Get()
174+
indexOfReplica := findIndex(replicas, replicaName)
175+
176+
// Delete the replica at position `indexOfReplica` from the array
177+
replicas = append(replicas[:indexOfReplica], replicas[indexOfReplica+1:]...)
178+
179+
res, err := primaryIndex.SetSettings(
180+
search.Settings{
181+
Replicas: opt.Replicas(replicas...),
182+
},
183+
)
184+
185+
if err != nil {
186+
return fmt.Errorf("can't update settings of index %q: %w", primaryName, err)
187+
}
188+
189+
// Wait until the settings are updated, else a subsequent `delete` will fail.
190+
_ = res.Wait()
191+
return nil
192+
}
193+
194+
// Find the index of the string `target` in the array `arr`
195+
func findIndex(arr []string, target string) int {
196+
for i, v := range arr {
197+
if v == target {
198+
return i
199+
}
200+
}
201+
return -1
202+
}

pkg/cmd/indices/delete/delete_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"testing"
66

7+
"github.com/algolia/algoliasearch-client-go/v3/algolia/opt"
78
"github.com/algolia/algoliasearch-client-go/v3/algolia/search"
89
"github.com/google/shlex"
910
"github.com/stretchr/testify/assert"
@@ -20,6 +21,7 @@ func TestNewDeleteCmd(t *testing.T) {
2021
name string
2122
tty bool
2223
cli string
24+
isReplica bool
2325
wantsErr bool
2426
wantsOpts DeleteOptions
2527
}{
@@ -98,6 +100,7 @@ func Test_runDeleteCmd(t *testing.T) {
98100
name string
99101
cli string
100102
indices []string
103+
replica bool
101104
isTTY bool
102105
wantOut string
103106
}{
@@ -129,13 +132,37 @@ func Test_runDeleteCmd(t *testing.T) {
129132
isTTY: true,
130133
wantOut: "✓ Deleted indices foo, bar\n",
131134
},
135+
{
136+
name: "TTY, replica indice",
137+
cli: "foo --confirm",
138+
indices: []string{"foo"},
139+
replica: true,
140+
isTTY: true,
141+
wantOut: "✓ Deleted indices foo\n",
142+
},
132143
}
133144

134145
for _, tt := range tests {
135146
t.Run(tt.name, func(t *testing.T) {
136147
r := httpmock.Registry{}
137148
for _, index := range tt.indices {
138-
r.Register(httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/settings", index)), httpmock.JSONResponse(search.DeleteKeyRes{}))
149+
// First settings call with `Exists()`
150+
r.Register(httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/settings", index)), httpmock.JSONResponse(search.Settings{}))
151+
if tt.replica {
152+
// We want the first `Delete()` call to fail
153+
r.Register(httpmock.REST("DELETE", fmt.Sprintf("1/indexes/%s", index)), httpmock.ErrorResponse())
154+
// Second settings call to fetch the primary index name
155+
r.Register(httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/settings", index)), httpmock.JSONResponse(search.Settings{
156+
Primary: opt.Primary("bar"),
157+
}))
158+
// Third settings call to fetch the primary index settings
159+
r.Register(httpmock.REST("GET", "1/indexes/bar/settings"), httpmock.JSONResponse(search.Settings{
160+
Replicas: opt.Replicas(index),
161+
}))
162+
// Fourth settings call to update the primary settings
163+
r.Register(httpmock.REST("PUT", "1/indexes/bar/settings"), httpmock.JSONResponse(search.Settings{}))
164+
}
165+
// Final `Delete()` call
139166
r.Register(httpmock.REST("DELETE", fmt.Sprintf("1/indexes/%s", index)), httpmock.JSONResponse(search.DeleteKeyRes{}))
140167
}
141168
defer r.Verify(t)

pkg/httpmock/registry.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,9 @@ func (r *Registry) Request(req *http.Request) (*http.Response, error) {
4646
if s.matched || !s.Matcher(req) {
4747
continue
4848
}
49-
if stub != nil {
50-
r.mu.Unlock()
51-
return nil, fmt.Errorf("more than 1 stub matched %v", req)
52-
}
49+
5350
stub = s
51+
break
5452
}
5553
if stub != nil {
5654
stub.matched = true

pkg/httpmock/stub.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ func JSONResponse(body interface{}) Responder {
4242
}
4343
}
4444

45+
func ErrorResponse() Responder {
46+
return func(req *http.Request) (*http.Response, error) {
47+
return httpResponse(404, req, bytes.NewBufferString("")), nil
48+
}
49+
}
50+
4551
func httpResponse(status int, req *http.Request, body io.Reader) *http.Response {
4652
return &http.Response{
4753
StatusCode: status,

0 commit comments

Comments
 (0)