Welcome to mirror list, hosted at ThFree Co, Russian Federation.

config.go « repository « service « gitaly « internal - gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 452eabb82e503c8db4a3939161725bf0efa77058 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
package repository

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"

	"gitlab.com/gitlab-org/gitaly/v14/internal/command"
	"gitlab.com/gitlab-org/gitaly/v14/internal/git"
	"gitlab.com/gitlab-org/gitaly/v14/internal/git2go"
	"gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/rubyserver"
	"gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/transaction"
	"gitlab.com/gitlab-org/gitaly/v14/internal/helper"
	"gitlab.com/gitlab-org/gitaly/v14/internal/metadata/featureflag"
	"gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo"
	"gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting"
	"gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb"
	"gitlab.com/gitlab-org/gitaly/v14/streamio"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// GetConfig reads the repository's gitconfig file and returns its contents.
func (s *server) GetConfig(
	request *gitalypb.GetConfigRequest,
	stream gitalypb.RepositoryService_GetConfigServer,
) error {
	repoPath, err := s.locator.GetPath(request.GetRepository())
	if err != nil {
		return err
	}

	configPath := filepath.Join(repoPath, "config")

	gitconfig, err := os.Open(configPath)
	if err != nil {
		if os.IsNotExist(err) {
			return status.Errorf(codes.NotFound, "opening gitconfig: %v", err)
		}
		return helper.ErrInternalf("opening gitconfig: %v", err)
	}

	writer := streamio.NewWriter(func(p []byte) error {
		return stream.Send(&gitalypb.GetConfigResponse{
			Data: p,
		})
	})

	if _, err := io.Copy(writer, gitconfig); err != nil {
		return helper.ErrInternalf("sending config: %v", err)
	}

	return nil
}

func (s *server) DeleteConfig(ctx context.Context, req *gitalypb.DeleteConfigRequest) (*gitalypb.DeleteConfigResponse, error) {
	/*
	 * We need to vote both before and after the change because we don't have proper commit
	 * semantics: it's not easily feasible to lock the config manually, vote on it and only
	 * commit the change if the vote was successful. Git automatically does this for us for ref
	 * updates via the reference-transaction hook, but here we'll need to use an approximation.
	 *
	 * As an approximation, we thus vote both before and after the change. Praefect requires the
	 * vote up front because if an RPC failed and no vote exists, it assumes no change was
	 * performed, and that's bad for us if we fail _after_ the modification but _before_ the
	 * vote on changed data. And the second vote is required such that we can assert that all
	 * Gitaly nodes actually did perform the same change.
	 */
	if err := s.voteOnConfig(ctx, req.GetRepository()); err != nil {
		return nil, helper.ErrInternal(fmt.Errorf("preimage vote on config: %w", err))
	}

	for _, k := range req.Keys {
		// We assume k does not contain any secrets; it is leaked via 'ps'.
		cmd, err := s.gitCmdFactory.New(ctx, req.Repository, git.SubCmd{
			Name:  "config",
			Flags: []git.Option{git.Flag{Name: "--unset-all"}},
			Args:  []string{k},
		})
		if err != nil {
			return nil, err
		}

		if err := cmd.Wait(); err != nil {
			if code, ok := command.ExitStatus(err); ok && code == 5 {
				// Status code 5 means 'key not in config', see 'git help config'
				continue
			}

			return nil, status.Errorf(codes.Internal, "command failed: %v", err)
		}
	}

	if err := s.voteOnConfig(ctx, req.GetRepository()); err != nil {
		return nil, helper.ErrInternal(fmt.Errorf("postimage vote on config: %w", err))
	}

	return &gitalypb.DeleteConfigResponse{}, nil
}

func (s *server) setConfigGit2Go(ctx context.Context, req *gitalypb.SetConfigRequest) (*gitalypb.SetConfigResponse, error) {
	reqRepo := req.GetRepository()
	if reqRepo == nil {
		return nil, status.Error(codes.InvalidArgument, "no repository")
	}

	path, err := s.locator.GetRepoPath(reqRepo)
	if err != nil {
		return nil, err
	}

	entries := make(map[string]git2go.ConfigEntry)

	for _, el := range req.Entries {
		switch el.GetValue().(type) {
		case *gitalypb.SetConfigRequest_Entry_ValueStr:
			entries[el.Key] = git2go.ConfigEntry{Value: el.GetValueStr()}
		case *gitalypb.SetConfigRequest_Entry_ValueInt32:
			entries[el.Key] = git2go.ConfigEntry{Value: el.GetValueInt32()}
		case *gitalypb.SetConfigRequest_Entry_ValueBool:
			entries[el.Key] = git2go.ConfigEntry{Value: el.GetValueBool()}
		default:
			return nil, status.Error(codes.InvalidArgument, "unknown entry type")
		}
	}

	/*
	 * We're voting twice, once on the preimage and once on the postimage. Please refer to the
	 * comment in DeleteConfig() for the reason.
	 */
	if err := s.voteOnConfig(ctx, req.GetRepository()); err != nil {
		return nil, helper.ErrInternalf("preimage vote on config: %v", err)
	}

	if err := s.git2go.SetConfig(ctx, reqRepo, git2go.SetConfigCommand{Repository: path, Entries: entries}); err != nil {
		return nil, status.Errorf(codes.Internal, "SetConfig git2go error")
	}

	if err := s.voteOnConfig(ctx, req.GetRepository()); err != nil {
		return nil, helper.ErrInternalf("postimage vote on config: %v", err)
	}

	return &gitalypb.SetConfigResponse{}, nil
}

func (s *server) SetConfig(ctx context.Context, req *gitalypb.SetConfigRequest) (*gitalypb.SetConfigResponse, error) {
	// We use gitaly-ruby here because in gitaly-ruby we can use Rugged, and
	// Rugged lets us set config values without leaking secrets via 'ps'. We
	// can't use `git config foo.bar secret` because that leaks secrets.
	// Also we can use git2go implementation of SetConfig

	if featureflag.GoSetConfig.IsEnabled(ctx) {
		return s.setConfigGit2Go(ctx, req)
	}

	client, err := s.ruby.RepositoryServiceClient(ctx)
	if err != nil {
		return nil, err
	}

	clientCtx, err := rubyserver.SetHeaders(ctx, s.locator, req.GetRepository())
	if err != nil {
		return nil, err
	}

	/*
	 * We're voting twice, once on the preimage and once on the postimage. Please refer to the
	 * comment in DeleteConfig() for the reason.
	 */
	if err := s.voteOnConfig(ctx, req.GetRepository()); err != nil {
		return nil, helper.ErrInternalf("preimage vote on config: %v", err)
	}

	response, err := client.SetConfig(clientCtx, req)
	if err != nil {
		return nil, err
	}

	if err := s.voteOnConfig(ctx, req.GetRepository()); err != nil {
		return nil, helper.ErrInternalf("postimage vote on config: %v", err)
	}

	return response, nil
}

func (s *server) voteOnConfig(ctx context.Context, repo *gitalypb.Repository) error {
	return transaction.RunOnContext(ctx, func(tx txinfo.Transaction) error {
		repoPath, err := s.locator.GetPath(repo)
		if err != nil {
			return fmt.Errorf("get repo path: %w", err)
		}

		var vote voting.Vote

		config, err := os.Open(filepath.Join(repoPath, "config"))
		switch {
		case err == nil:
			hash := voting.NewVoteHash()
			if _, err := io.Copy(hash, config); err != nil {
				return fmt.Errorf("seeding vote: %w", err)
			}

			vote, err = hash.Vote()
			if err != nil {
				return fmt.Errorf("computing vote: %w", err)
			}
		case os.IsNotExist(err):
			vote = voting.VoteFromData([]byte("notfound"))
		default:
			return fmt.Errorf("open repo config: %w", err)
		}

		if err := s.txManager.Vote(ctx, tx, vote); err != nil {
			return fmt.Errorf("casting vote: %w", err)
		}

		return nil
	})
}