1package main
2
3import (
4	"flag"
5	"fmt"
6	"path/filepath"
7	"testing"
8	"time"
9
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12	"gitlab.com/gitlab-org/gitaly/v14/client"
13	"gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/service/setup"
14	"gitlab.com/gitlab-org/gitaly/v14/internal/praefect/config"
15	"gitlab.com/gitlab-org/gitaly/v14/internal/praefect/datastore"
16	"gitlab.com/gitlab-org/gitaly/v14/internal/praefect/datastore/glsql"
17	"gitlab.com/gitlab-org/gitaly/v14/internal/praefect/nodes"
18	"gitlab.com/gitlab-org/gitaly/v14/internal/praefect/protoregistry"
19	"gitlab.com/gitlab-org/gitaly/v14/internal/testhelper"
20	"gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/promtest"
21	"gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testcfg"
22	"gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testserver"
23	"gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb"
24)
25
26func TestAddRepository_FlagSet(t *testing.T) {
27	t.Parallel()
28	cmd := &trackRepository{}
29	fs := cmd.FlagSet()
30	require.NoError(t, fs.Parse([]string{"--virtual-storage", "vs", "--repository", "repo", "--authoritative-storage", "storage-0"}))
31	require.Equal(t, "vs", cmd.virtualStorage)
32	require.Equal(t, "repo", cmd.relativePath)
33	require.Equal(t, "storage-0", cmd.authoritativeStorage)
34}
35
36func TestAddRepository_Exec_invalidArgs(t *testing.T) {
37	t.Parallel()
38	t.Run("not all flag values processed", func(t *testing.T) {
39		cmd := trackRepository{}
40		flagSet := flag.NewFlagSet("cmd", flag.PanicOnError)
41		require.NoError(t, flagSet.Parse([]string{"stub"}))
42		err := cmd.Exec(flagSet, config.Config{})
43		require.EqualError(t, err, "cmd doesn't accept positional arguments")
44	})
45
46	t.Run("virtual-storage is not set", func(t *testing.T) {
47		cmd := trackRepository{}
48		err := cmd.Exec(flag.NewFlagSet("", flag.PanicOnError), config.Config{})
49		require.EqualError(t, err, `"virtual-storage" is a required parameter`)
50	})
51
52	t.Run("repository is not set", func(t *testing.T) {
53		cmd := trackRepository{virtualStorage: "stub"}
54		err := cmd.Exec(flag.NewFlagSet("", flag.PanicOnError), config.Config{})
55		require.EqualError(t, err, `"repository" is a required parameter`)
56	})
57
58	t.Run("authoritative-storage is not set", func(t *testing.T) {
59		cmd := trackRepository{virtualStorage: "stub", relativePath: "path/to/repo"}
60		err := cmd.Exec(flag.NewFlagSet("", flag.PanicOnError), config.Config{Failover: config.Failover{ElectionStrategy: config.ElectionStrategyPerRepository}})
61		require.EqualError(t, err, `"authoritative-storage" is a required parameter`)
62	})
63
64	t.Run("db connection error", func(t *testing.T) {
65		cmd := trackRepository{virtualStorage: "stub", relativePath: "stub", authoritativeStorage: "storage-0"}
66		cfg := config.Config{DB: config.DB{Host: "stub", SSLMode: "disable"}}
67		err := cmd.Exec(flag.NewFlagSet("", flag.PanicOnError), cfg)
68		require.Error(t, err)
69		require.Contains(t, err.Error(), "connect to database: dial tcp: lookup stub")
70	})
71}
72
73func TestAddRepository_Exec(t *testing.T) {
74	t.Parallel()
75	g1Cfg := testcfg.Build(t, testcfg.WithStorages("gitaly-1"))
76	g2Cfg := testcfg.Build(t, testcfg.WithStorages("gitaly-2"))
77
78	g1Srv := testserver.StartGitalyServer(t, g1Cfg, nil, setup.RegisterAll, testserver.WithDisablePraefect())
79	g2Srv := testserver.StartGitalyServer(t, g2Cfg, nil, setup.RegisterAll, testserver.WithDisablePraefect())
80	defer g2Srv.Shutdown()
81	defer g1Srv.Shutdown()
82
83	g1Addr := g1Srv.Address()
84
85	db := glsql.NewDB(t)
86	var database string
87	require.NoError(t, db.QueryRow(`SELECT current_database()`).Scan(&database))
88	dbConf := glsql.GetDBConfig(t, database)
89
90	virtualStorageName := "praefect"
91	conf := config.Config{
92		AllowLegacyElectors: true,
93		SocketPath:          testhelper.GetTemporaryGitalySocketFileName(t),
94		VirtualStorages: []*config.VirtualStorage{
95			{
96				Name: virtualStorageName,
97				Nodes: []*config.Node{
98					{Storage: g1Cfg.Storages[0].Name, Address: g1Addr},
99					{Storage: g2Cfg.Storages[0].Name, Address: g2Srv.Address()},
100				},
101				DefaultReplicationFactor: 2,
102			},
103		},
104		DB: dbConf,
105	}
106
107	gitalyCC, err := client.Dial(g1Addr, nil)
108	require.NoError(t, err)
109	defer func() { require.NoError(t, gitalyCC.Close()) }()
110	ctx, cancel := testhelper.Context()
111	defer cancel()
112
113	gitaly1RepositoryClient := gitalypb.NewRepositoryServiceClient(gitalyCC)
114
115	createRepoThroughGitaly1 := func(relativePath string) error {
116		_, err := gitaly1RepositoryClient.CreateRepository(
117			ctx,
118			&gitalypb.CreateRepositoryRequest{
119				Repository: &gitalypb.Repository{
120					StorageName:  g1Cfg.Storages[0].Name,
121					RelativePath: relativePath,
122				},
123			})
124		return err
125	}
126
127	testCases := map[string]struct {
128		failoverConfig       config.Failover
129		authoritativeStorage string
130	}{
131		"sql election": {
132			failoverConfig: config.Failover{
133				Enabled:          true,
134				ElectionStrategy: config.ElectionStrategySQL,
135			},
136			authoritativeStorage: "",
137		},
138		"per repository election": {
139			failoverConfig: config.Failover{
140				Enabled:          true,
141				ElectionStrategy: config.ElectionStrategyPerRepository,
142			},
143			authoritativeStorage: g1Cfg.Storages[0].Name,
144		},
145	}
146
147	logger := testhelper.NewTestLogger(t)
148	for tn, tc := range testCases {
149		t.Run(tn, func(t *testing.T) {
150			addCmdConf := conf
151			addCmdConf.Failover = tc.failoverConfig
152
153			t.Run("ok", func(t *testing.T) {
154				nodeMgr, err := nodes.NewManager(testhelper.DiscardTestEntry(t), addCmdConf, db.DB, nil, promtest.NewMockHistogramVec(), protoregistry.GitalyProtoPreregistered, nil, nil, nil)
155				require.NoError(t, err)
156				nodeMgr.Start(0, time.Hour)
157				defer nodeMgr.Stop()
158
159				relativePath := fmt.Sprintf("path/to/test/repo_%s", tn)
160				repoDS := datastore.NewPostgresRepositoryStore(db, conf.StorageNames())
161
162				require.NoError(t, createRepoThroughGitaly1(relativePath))
163
164				rmRepoCmd := &removeRepository{
165					logger:         logger,
166					virtualStorage: virtualStorageName,
167					relativePath:   relativePath,
168				}
169
170				require.NoError(t, rmRepoCmd.Exec(flag.NewFlagSet("", flag.PanicOnError), conf))
171
172				// create the repo on Gitaly without Praefect knowing
173				require.NoError(t, createRepoThroughGitaly1(relativePath))
174				require.DirExists(t, filepath.Join(g1Cfg.Storages[0].Path, relativePath))
175				require.NoDirExists(t, filepath.Join(g2Cfg.Storages[0].Path, relativePath))
176
177				addRepoCmd := &trackRepository{
178					logger:               logger,
179					virtualStorage:       virtualStorageName,
180					relativePath:         relativePath,
181					authoritativeStorage: tc.authoritativeStorage,
182				}
183
184				require.NoError(t, addRepoCmd.Exec(flag.NewFlagSet("", flag.PanicOnError), addCmdConf))
185				as := datastore.NewAssignmentStore(db, conf.StorageNames())
186
187				assignments, err := as.GetHostAssignments(ctx, virtualStorageName, relativePath)
188				require.NoError(t, err)
189				require.Len(t, assignments, 2)
190				assert.Contains(t, assignments, g1Cfg.Storages[0].Name)
191				assert.Contains(t, assignments, g2Cfg.Storages[0].Name)
192
193				exists, err := repoDS.RepositoryExists(ctx, virtualStorageName, relativePath)
194				require.NoError(t, err)
195				assert.True(t, exists)
196			})
197
198			t.Run("repository does not exist", func(t *testing.T) {
199				relativePath := fmt.Sprintf("path/to/test/repo_1_%s", tn)
200
201				cmd := &trackRepository{
202					logger:               testhelper.NewTestLogger(t),
203					virtualStorage:       "praefect",
204					relativePath:         relativePath,
205					authoritativeStorage: tc.authoritativeStorage,
206				}
207
208				assert.ErrorIs(t, cmd.Exec(flag.NewFlagSet("", flag.PanicOnError), addCmdConf), errAuthoritativeRepositoryNotExist)
209			})
210
211			t.Run("records already exist", func(t *testing.T) {
212				relativePath := fmt.Sprintf("path/to/test/repo_2_%s", tn)
213
214				require.NoError(t, createRepoThroughGitaly1(relativePath))
215				require.DirExists(t, filepath.Join(g1Cfg.Storages[0].Path, relativePath))
216				require.NoDirExists(t, filepath.Join(g2Cfg.Storages[0].Path, relativePath))
217
218				ds := datastore.NewPostgresRepositoryStore(db, conf.StorageNames())
219				id, err := ds.ReserveRepositoryID(ctx, virtualStorageName, relativePath)
220				require.NoError(t, err)
221				require.NoError(t, ds.CreateRepository(ctx, id, virtualStorageName, relativePath, g1Cfg.Storages[0].Name, nil, nil, true, true))
222
223				cmd := &trackRepository{
224					logger:               testhelper.NewTestLogger(t),
225					virtualStorage:       virtualStorageName,
226					relativePath:         relativePath,
227					authoritativeStorage: tc.authoritativeStorage,
228				}
229
230				assert.NoError(t, cmd.Exec(flag.NewFlagSet("", flag.PanicOnError), addCmdConf))
231			})
232		})
233	}
234}
235