mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:51:43 +00:00
gitserver: Replace P4Exec endpoint with properly typed and individually tested RPC calls (#57247)
This exposes a proper API that we exactly know the surface of and know what to test for. If there are any issues with this endpoint, it will be very clear what that is, vs. a user-error calling p4exec with invalid arguments or so. Also, this reduces the risk of accidentally exposing a p4 command that should not be exposed.
This commit is contained in:
parent
c955a571e0
commit
bff2e222b7
@ -14,6 +14,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
)
|
||||
|
||||
func TestGitserverResolver(t *testing.T) {
|
||||
@ -31,7 +32,7 @@ func TestGitserverResolver(t *testing.T) {
|
||||
userCtx := actor.WithActor(ctx, actor.FromUser(user.ID))
|
||||
adminCtx := actor.WithActor(ctx, actor.FromUser(admin.ID))
|
||||
|
||||
gitserverInstances := []gitserver.SystemInfo{
|
||||
gitserverInstances := []protocol.SystemInfo{
|
||||
{
|
||||
Address: "127.0.0.1:3501",
|
||||
FreeSpace: 10240,
|
||||
|
||||
@ -11,6 +11,7 @@ go_library(
|
||||
"list_gitolite.go",
|
||||
"lock.go",
|
||||
"observability.go",
|
||||
"p4exec.go",
|
||||
"patch.go",
|
||||
"repo_info.go",
|
||||
"search.go",
|
||||
@ -94,6 +95,7 @@ go_test(
|
||||
"cleanup_test.go",
|
||||
"list_gitolite_test.go",
|
||||
"main_test.go",
|
||||
"p4exec_test.go",
|
||||
"server_test.go",
|
||||
"serverutil_test.go",
|
||||
],
|
||||
|
||||
@ -216,10 +216,10 @@ func TestClient_ArchiveReader(t *testing.T) {
|
||||
base := proto.NewGitserverServiceClient(cc)
|
||||
return base.RepoUpdate(ctx, in, opts...)
|
||||
}
|
||||
return &gitserver.MockGRPCClient{
|
||||
MockArchive: mockArchive,
|
||||
MockRepoUpdate: mockRepoUpdate,
|
||||
}
|
||||
cli := gitserver.NewMockGitserverServiceClient()
|
||||
cli.ArchiveFunc.SetDefaultHook(mockArchive)
|
||||
cli.RepoUpdateFunc.SetDefaultHook(mockRepoUpdate)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
|
||||
@ -260,7 +260,9 @@ func TestClient_ArchiveReader(t *testing.T) {
|
||||
base := proto.NewGitserverServiceClient(cc)
|
||||
return base.Archive(ctx, in, opts...)
|
||||
}
|
||||
return &gitserver.MockGRPCClient{MockArchive: mockArchive}
|
||||
cli := gitserver.NewMockGitserverServiceClient()
|
||||
cli.ArchiveFunc.SetDefaultHook(mockArchive)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
346
cmd/gitserver/internal/p4exec.go
Normal file
346
cmd/gitserver/internal/p4exec.go
Normal file
@ -0,0 +1,346 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/accesslog"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/gitserverfs"
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
|
||||
"github.com/sourcegraph/sourcegraph/internal/grpc/streamio"
|
||||
"github.com/sourcegraph/sourcegraph/internal/honey"
|
||||
"github.com/sourcegraph/sourcegraph/internal/trace"
|
||||
)
|
||||
|
||||
func (gs *GRPCServer) P4Exec(req *proto.P4ExecRequest, ss proto.GitserverService_P4ExecServer) error {
|
||||
arguments := byteSlicesToStrings(req.GetArgs()) //nolint:staticcheck
|
||||
|
||||
if len(arguments) < 1 {
|
||||
return status.Error(codes.InvalidArgument, "args must be greater than or equal to 1")
|
||||
}
|
||||
|
||||
subCommand := arguments[0]
|
||||
|
||||
// Make sure the subcommand is explicitly allowed
|
||||
allowlist := []string{"protects", "groups", "users", "group", "changes"}
|
||||
allowed := false
|
||||
for _, c := range allowlist {
|
||||
if subCommand == c {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return status.Error(codes.InvalidArgument, fmt.Sprintf("subcommand %q is not allowed", subCommand))
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
|
||||
if err != nil {
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
// Log which actor is accessing p4-exec.
|
||||
//
|
||||
// p4-exec is currently only used for fetching user based permissions information
|
||||
// so, we don't have a repo name.
|
||||
accesslog.Record(ss.Context(), "<no-repo>",
|
||||
log.String("p4user", req.GetP4User()), //nolint:staticcheck
|
||||
log.String("p4port", req.GetP4Port()), //nolint:staticcheck
|
||||
log.Strings("args", arguments),
|
||||
)
|
||||
|
||||
// Make sure credentials are valid before heavier operation
|
||||
err = perforce.P4TestWithTrust(ss.Context(), p4home, req.GetP4Port(), req.GetP4User(), req.GetP4Passwd()) //nolint:staticcheck
|
||||
if err != nil {
|
||||
if ctxErr := ss.Context().Err(); ctxErr != nil {
|
||||
return status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
w := streamio.NewWriter(func(p []byte) error {
|
||||
return ss.Send(&proto.P4ExecResponse{
|
||||
Data: p,
|
||||
})
|
||||
})
|
||||
|
||||
var r p4ExecRequest
|
||||
r.FromProto(req)
|
||||
|
||||
return gs.doP4Exec(ss.Context(), gs.Server.Logger, &r, "unknown-grpc-client", w)
|
||||
}
|
||||
|
||||
func (gs *GRPCServer) doP4Exec(ctx context.Context, logger log.Logger, req *p4ExecRequest, userAgent string, w io.Writer) error {
|
||||
execStatus := gs.Server.p4Exec(ctx, logger, req, userAgent, w)
|
||||
|
||||
if execStatus.ExitStatus != 0 || execStatus.Err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
gRPCStatus := codes.Unknown
|
||||
if strings.Contains(execStatus.Err.Error(), "signal: killed") {
|
||||
gRPCStatus = codes.Aborted
|
||||
}
|
||||
|
||||
s, err := status.New(gRPCStatus, execStatus.Err.Error()).WithDetails(&proto.ExecStatusPayload{
|
||||
StatusCode: int32(execStatus.ExitStatus),
|
||||
Stderr: execStatus.Stderr,
|
||||
})
|
||||
if err != nil {
|
||||
gs.Server.Logger.Error("failed to marshal status", log.Error(err))
|
||||
return err
|
||||
}
|
||||
return s.Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleP4Exec(w http.ResponseWriter, r *http.Request) {
|
||||
var req p4ExecRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Args) < 1 {
|
||||
http.Error(w, "args must be greater than or equal to 1", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the subcommand is explicitly allowed
|
||||
allowlist := []string{"protects", "groups", "users", "group", "changes"}
|
||||
allowed := false
|
||||
for _, arg := range allowlist {
|
||||
if req.Args[0] == arg {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
http.Error(w, fmt.Sprintf("subcommand %q is not allowed", req.Args[0]), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Log which actor is accessing p4-exec.
|
||||
//
|
||||
// p4-exec is currently only used for fetching user based permissions information
|
||||
// so, we don't have a repo name.
|
||||
accesslog.Record(r.Context(), "<no-repo>",
|
||||
log.String("p4user", req.P4User),
|
||||
log.String("p4port", req.P4Port),
|
||||
log.Strings("args", req.Args),
|
||||
)
|
||||
|
||||
// Make sure credentials are valid before heavier operation
|
||||
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.p4execHTTP(w, r, &req)
|
||||
}
|
||||
|
||||
func (s *Server) p4execHTTP(w http.ResponseWriter, r *http.Request, req *p4ExecRequest) {
|
||||
logger := s.Logger.Scoped("p4exec", "")
|
||||
|
||||
// Flush writes more aggressively than standard net/http so that clients
|
||||
// with a context deadline see as much partial response body as possible.
|
||||
if fw := newFlushingResponseWriter(logger, w); fw != nil {
|
||||
w = fw
|
||||
defer fw.Close()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
w.Header().Set("Trailer", "X-Exec-Error")
|
||||
w.Header().Add("Trailer", "X-Exec-Exit-Status")
|
||||
w.Header().Add("Trailer", "X-Exec-Stderr")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
execStatus := s.p4Exec(ctx, logger, req, r.UserAgent(), w)
|
||||
w.Header().Set("X-Exec-Error", errorString(execStatus.Err))
|
||||
w.Header().Set("X-Exec-Exit-Status", strconv.Itoa(execStatus.ExitStatus))
|
||||
w.Header().Set("X-Exec-Stderr", execStatus.Stderr)
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) p4Exec(ctx context.Context, logger log.Logger, req *p4ExecRequest, userAgent string, w io.Writer) execStatus {
|
||||
start := time.Now()
|
||||
var cmdStart time.Time // set once we have ensured commit
|
||||
exitStatus := executil.UnsetExitStatus
|
||||
var stdoutN, stderrN int64
|
||||
var status string
|
||||
var execErr error
|
||||
|
||||
// Instrumentation
|
||||
{
|
||||
cmd := ""
|
||||
if len(req.Args) > 0 {
|
||||
cmd = req.Args[0]
|
||||
}
|
||||
args := strings.Join(req.Args, " ")
|
||||
|
||||
var tr trace.Trace
|
||||
tr, ctx = trace.New(ctx, "p4exec."+cmd, attribute.String("port", req.P4Port))
|
||||
tr.SetAttributes(attribute.String("args", args))
|
||||
logger = logger.WithTrace(trace.Context(ctx))
|
||||
|
||||
execRunning.WithLabelValues(cmd).Inc()
|
||||
defer func() {
|
||||
tr.AddEvent("done",
|
||||
attribute.String("status", status),
|
||||
attribute.Int64("stdout", stdoutN),
|
||||
attribute.Int64("stderr", stderrN),
|
||||
)
|
||||
tr.SetError(execErr)
|
||||
tr.End()
|
||||
|
||||
duration := time.Since(start)
|
||||
execRunning.WithLabelValues(cmd).Dec()
|
||||
execDuration.WithLabelValues(cmd, status).Observe(duration.Seconds())
|
||||
|
||||
var cmdDuration time.Duration
|
||||
if !cmdStart.IsZero() {
|
||||
cmdDuration = time.Since(cmdStart)
|
||||
}
|
||||
|
||||
isSlow := cmdDuration > 30*time.Second
|
||||
if honey.Enabled() || traceLogs || isSlow {
|
||||
act := actor.FromContext(ctx)
|
||||
ev := honey.NewEvent("gitserver-p4exec")
|
||||
ev.SetSampleRate(honeySampleRate(cmd, act))
|
||||
ev.AddField("p4port", req.P4Port)
|
||||
ev.AddField("cmd", cmd)
|
||||
ev.AddField("args", args)
|
||||
ev.AddField("actor", act.UIDString())
|
||||
ev.AddField("client", userAgent)
|
||||
ev.AddField("duration_ms", duration.Milliseconds())
|
||||
ev.AddField("stdout_size", stdoutN)
|
||||
ev.AddField("stderr_size", stderrN)
|
||||
ev.AddField("exit_status", exitStatus)
|
||||
ev.AddField("status", status)
|
||||
if execErr != nil {
|
||||
ev.AddField("error", execErr.Error())
|
||||
}
|
||||
if !cmdStart.IsZero() {
|
||||
ev.AddField("cmd_duration_ms", cmdDuration.Milliseconds())
|
||||
}
|
||||
|
||||
if traceID := trace.ID(ctx); traceID != "" {
|
||||
ev.AddField("traceID", traceID)
|
||||
ev.AddField("trace", trace.URL(traceID, conf.DefaultClient()))
|
||||
}
|
||||
|
||||
_ = ev.Send()
|
||||
|
||||
if traceLogs {
|
||||
logger.Debug("TRACE gitserver p4exec", log.Object("ev.Fields", mapToLoggerField(ev.Fields())...))
|
||||
}
|
||||
if isSlow {
|
||||
logger.Warn("Long p4exec request", log.Object("ev.Fields", mapToLoggerField(ev.Fields())...))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
|
||||
if err != nil {
|
||||
return execStatus{ExitStatus: -1, Err: err}
|
||||
}
|
||||
|
||||
var stderrBuf bytes.Buffer
|
||||
stdoutW := &writeCounter{w: w}
|
||||
stderrW := &writeCounter{w: &limitWriter{W: &stderrBuf, N: 1024}}
|
||||
|
||||
cmdStart = time.Now()
|
||||
cmd := exec.CommandContext(ctx, "p4", req.Args...)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"P4PORT="+req.P4Port,
|
||||
"P4USER="+req.P4User,
|
||||
"P4PASSWD="+req.P4Passwd,
|
||||
"HOME="+p4home,
|
||||
)
|
||||
cmd.Stdout = stdoutW
|
||||
cmd.Stderr = stderrW
|
||||
|
||||
exitStatus, execErr = executil.RunCommand(ctx, s.RecordingCommandFactory.Wrap(ctx, s.Logger, cmd))
|
||||
|
||||
status = strconv.Itoa(exitStatus)
|
||||
stdoutN = stdoutW.n
|
||||
stderrN = stderrW.n
|
||||
|
||||
stderr := stderrBuf.String()
|
||||
|
||||
return execStatus{
|
||||
ExitStatus: exitStatus,
|
||||
Stderr: stderr,
|
||||
Err: execErr,
|
||||
}
|
||||
}
|
||||
|
||||
// p4ExecRequest is a request to execute a p4 command with given arguments.
|
||||
//
|
||||
// Note that this request is deserialized by both gitserver and the frontend's
|
||||
// internal proxy route and any major change to this structure will need to be
|
||||
// reconciled in both places.
|
||||
type p4ExecRequest struct {
|
||||
P4Port string `json:"p4port"`
|
||||
P4User string `json:"p4user"`
|
||||
P4Passwd string `json:"p4passwd"`
|
||||
Args []string `json:"args"`
|
||||
}
|
||||
|
||||
func (r *p4ExecRequest) ToProto() *proto.P4ExecRequest {
|
||||
return &proto.P4ExecRequest{
|
||||
P4Port: r.P4Port,
|
||||
P4User: r.P4User,
|
||||
P4Passwd: r.P4Passwd,
|
||||
Args: stringsToByteSlices(r.Args),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *p4ExecRequest) FromProto(p *proto.P4ExecRequest) {
|
||||
*r = p4ExecRequest{
|
||||
P4Port: p.GetP4Port(), //nolint:staticcheck
|
||||
P4User: p.GetP4User(), //nolint:staticcheck
|
||||
P4Passwd: p.GetP4Passwd(), //nolint:staticcheck
|
||||
Args: byteSlicesToStrings(p.GetArgs()), //nolint:staticcheck
|
||||
}
|
||||
}
|
||||
|
||||
func stringsToByteSlices(in []string) [][]byte {
|
||||
res := make([][]byte, len(in))
|
||||
for i, s := range in {
|
||||
res[i] = []byte(s)
|
||||
}
|
||||
return res
|
||||
}
|
||||
260
cmd/gitserver/internal/p4exec_test.go
Normal file
260
cmd/gitserver/internal/p4exec_test.go
Normal file
@ -0,0 +1,260 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/sourcegraph/log/logtest"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbmocks"
|
||||
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
|
||||
"github.com/sourcegraph/sourcegraph/internal/grpc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/grpc/defaults"
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
"github.com/sourcegraph/sourcegraph/internal/wrexec"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
func TestServer_handleP4Exec(t *testing.T) {
|
||||
defaultMockRunCommand := func(ctx context.Context, cmd *exec.Cmd) (int, error) {
|
||||
switch cmd.Args[1] {
|
||||
case "users":
|
||||
_, _ = cmd.Stdout.Write([]byte("admin <admin@joe-perforce-server> (admin) accessed 2021/01/31"))
|
||||
_, _ = cmd.Stderr.Write([]byte("teststderr"))
|
||||
return 42, errors.New("the answer to life the universe and everything")
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
executil.UpdateRunCommandMock(nil)
|
||||
})
|
||||
|
||||
startServer := func(t *testing.T) (handler http.Handler, client proto.GitserverServiceClient, cleanup func()) {
|
||||
t.Helper()
|
||||
|
||||
logger := logtest.Scoped(t)
|
||||
|
||||
s := &Server{
|
||||
Logger: logger,
|
||||
ReposDir: t.TempDir(),
|
||||
ObservationCtx: observation.TestContextTB(t),
|
||||
skipCloneForTests: true,
|
||||
DB: dbmocks.NewMockDB(),
|
||||
RecordingCommandFactory: wrexec.NewNoOpRecordingCommandFactory(),
|
||||
Locker: NewRepositoryLocker(),
|
||||
}
|
||||
|
||||
server := defaults.NewServer(logger)
|
||||
proto.RegisterGitserverServiceServer(server, &GRPCServer{Server: s})
|
||||
handler = grpc.MultiplexHandlers(server, s.Handler())
|
||||
|
||||
srv := httptest.NewServer(handler)
|
||||
|
||||
u, _ := url.Parse(srv.URL)
|
||||
conn, err := defaults.Dial(u.Host, logger.Scoped("gRPC client", ""))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial: %v", err)
|
||||
}
|
||||
|
||||
client = proto.NewGitserverServiceClient(conn)
|
||||
|
||||
return handler, client, func() {
|
||||
srv.Close()
|
||||
conn.Close()
|
||||
server.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("gRPC", func(t *testing.T) {
|
||||
readAll := func(execClient proto.GitserverService_P4ExecClient) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
for {
|
||||
resp, err := execClient.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
_, err = buf.Write(resp.GetData())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write data: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("users", func(t *testing.T) {
|
||||
executil.UpdateRunCommandMock(defaultMockRunCommand)
|
||||
|
||||
_, client, closeFunc := startServer(t)
|
||||
t.Cleanup(closeFunc)
|
||||
|
||||
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{
|
||||
Args: [][]byte{[]byte("users")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to call P4Exec: %v", err)
|
||||
}
|
||||
|
||||
data, err := readAll(stream)
|
||||
s, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatal("received non-status error from p4exec call")
|
||||
}
|
||||
|
||||
if diff := cmp.Diff("the answer to life the universe and everything", s.Message()); diff != "" {
|
||||
t.Fatalf("unexpected error in stream (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
expectedData := []byte("admin <admin@joe-perforce-server> (admin) accessed 2021/01/31")
|
||||
|
||||
if diff := cmp.Diff(expectedData, data); diff != "" {
|
||||
t.Fatalf("unexpected data (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty request", func(t *testing.T) {
|
||||
executil.UpdateRunCommandMock(defaultMockRunCommand)
|
||||
|
||||
_, client, closeFunc := startServer(t)
|
||||
t.Cleanup(closeFunc)
|
||||
|
||||
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to call P4Exec: %v", err)
|
||||
}
|
||||
|
||||
_, err = readAll(stream)
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("expected InvalidArgument error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("disallowed command", func(t *testing.T) {
|
||||
|
||||
executil.UpdateRunCommandMock(defaultMockRunCommand)
|
||||
|
||||
_, client, closeFunc := startServer(t)
|
||||
t.Cleanup(closeFunc)
|
||||
|
||||
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{
|
||||
Args: [][]byte{[]byte("bad_command")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to call P4Exec: %v", err)
|
||||
}
|
||||
|
||||
_, err = readAll(stream)
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("expected InvalidArgument error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("context cancelled", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
executil.UpdateRunCommandMock(func(ctx context.Context, _ *exec.Cmd) (int, error) {
|
||||
// fake a context cancellation that occurs while the process is running
|
||||
|
||||
cancel()
|
||||
return 0, ctx.Err()
|
||||
})
|
||||
|
||||
_, client, closeFunc := startServer(t)
|
||||
t.Cleanup(closeFunc)
|
||||
|
||||
stream, err := client.P4Exec(ctx, &proto.P4ExecRequest{
|
||||
Args: [][]byte{[]byte("users")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to call P4Exec: %v", err)
|
||||
}
|
||||
|
||||
_, err = readAll(stream)
|
||||
if !(errors.Is(err, context.Canceled) || status.Code(err) == codes.Canceled) {
|
||||
t.Fatalf("expected context cancelation error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
t.Run("HTTP", func(t *testing.T) {
|
||||
|
||||
tests := []Test{
|
||||
{
|
||||
Name: "Command",
|
||||
Request: newRequest("POST", "/p4-exec", strings.NewReader(`{"args": ["users"]}`)),
|
||||
ExpectedCode: http.StatusOK,
|
||||
ExpectedBody: "admin <admin@joe-perforce-server> (admin) accessed 2021/01/31",
|
||||
ExpectedTrailers: http.Header{
|
||||
"X-Exec-Error": {"the answer to life the universe and everything"},
|
||||
"X-Exec-Exit-Status": {"42"},
|
||||
"X-Exec-Stderr": {"teststderr"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Error",
|
||||
Request: newRequest("POST", "/p4-exec", strings.NewReader(`{"args": ["bad_command"]}`)),
|
||||
ExpectedCode: http.StatusBadRequest,
|
||||
ExpectedBody: "subcommand \"bad_command\" is not allowed",
|
||||
},
|
||||
{
|
||||
Name: "EmptyBody",
|
||||
Request: newRequest("POST", "/p4-exec", nil),
|
||||
ExpectedCode: http.StatusBadRequest,
|
||||
ExpectedBody: `EOF`,
|
||||
},
|
||||
{
|
||||
Name: "EmptyInput",
|
||||
Request: newRequest("POST", "/p4-exec", strings.NewReader("{}")),
|
||||
ExpectedCode: http.StatusBadRequest,
|
||||
ExpectedBody: `args must be greater than or equal to 1`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
executil.UpdateRunCommandMock(defaultMockRunCommand)
|
||||
|
||||
handler, _, closeFunc := startServer(t)
|
||||
t.Cleanup(closeFunc)
|
||||
|
||||
w := httptest.ResponseRecorder{Body: new(bytes.Buffer)}
|
||||
handler.ServeHTTP(&w, test.Request)
|
||||
|
||||
res := w.Result()
|
||||
if res.StatusCode != test.ExpectedCode {
|
||||
t.Errorf("wrong status: expected %d, got %d", test.ExpectedCode, w.Code)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.TrimSpace(string(body)) != test.ExpectedBody {
|
||||
t.Errorf("wrong body: expected %q, got %q", test.ExpectedBody, string(body))
|
||||
}
|
||||
|
||||
for k, v := range test.ExpectedTrailers {
|
||||
if got := res.Trailer.Get(k); got != v[0] {
|
||||
t.Errorf("wrong trailer %q: expected %q, got %q", k, v[0], got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -469,9 +469,9 @@ func (s *Server) shelveChangelist(ctx context.Context, req protocol.CreateCommit
|
||||
}
|
||||
|
||||
// check to see if there's a changelist for this target branch already
|
||||
cid, err := p4Cmd.changeListIDFromClientSpecName(p4client)
|
||||
if err == nil && cid != "" {
|
||||
return cid, nil
|
||||
cl, err := perforce.GetChangelistByClient(ctx, p4port, p4user, p4passwd, tmpClientDir, p4client)
|
||||
if err == nil && cl.ID != "" {
|
||||
return cl.ID, nil
|
||||
}
|
||||
|
||||
// extract the base changelist id from the base commit
|
||||
@ -584,7 +584,7 @@ func (s *Server) shelveChangelist(ctx context.Context, req protocol.CreateCommit
|
||||
|
||||
// feed the changelist form into `p4 shelve`
|
||||
// capture the output to parse for a changelist id
|
||||
cid, err = p4Cmd.shelveChangelist(changeForm)
|
||||
cid, err := p4Cmd.shelveChangelist(changeForm)
|
||||
if err != nil {
|
||||
errorMessage := "failed shelving the changelist"
|
||||
logger.Error(errorMessage, log.String("output", digErrorMessage(err)), log.Error(errors.New(errorMessage)))
|
||||
@ -656,25 +656,6 @@ func (p p4Command) commandContext(args ...string) *exec.Cmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Uses `p4 changes` to see if there is a changelist already associated with the given client spec
|
||||
func (p p4Command) changeListIDFromClientSpecName(p4client string) (string, error) {
|
||||
cmd := p.commandContext("changes",
|
||||
"-r", // list in reverse order, which means that the given changelist id will be the first one listed
|
||||
"-m", "1", // limit output to one record, so that the given changelist is the only one listed
|
||||
"-l", // use a long listing, which includes the whole commit message
|
||||
"-c", p4client,
|
||||
)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, string(out))
|
||||
}
|
||||
pcl, err := internalperforce.ParseChangelistOutput(string(out))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, string(out))
|
||||
}
|
||||
return pcl.ID, nil
|
||||
}
|
||||
|
||||
const clientSpecForm = `Client: %s
|
||||
Owner: %s
|
||||
Description:
|
||||
|
||||
@ -4,11 +4,15 @@ load("//dev:go_defs.bzl", "go_test")
|
||||
go_library(
|
||||
name = "perforce",
|
||||
srcs = [
|
||||
"changelist.go",
|
||||
"cloneable.go",
|
||||
"depots.go",
|
||||
"groups.go",
|
||||
"login.go",
|
||||
"perforce.go",
|
||||
"protects.go",
|
||||
"url.go",
|
||||
"users.go",
|
||||
"util.go",
|
||||
],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/perforce",
|
||||
@ -17,6 +21,7 @@ go_library(
|
||||
"//cmd/gitserver/internal/common",
|
||||
"//cmd/gitserver/internal/executil",
|
||||
"//internal/api",
|
||||
"//internal/byteutils",
|
||||
"//internal/conf",
|
||||
"//internal/database",
|
||||
"//internal/extsvc",
|
||||
@ -34,7 +39,10 @@ go_library(
|
||||
go_test(
|
||||
name = "perforce_test",
|
||||
srcs = [
|
||||
"changelist_test.go",
|
||||
"groups_test.go",
|
||||
"perforce_test.go",
|
||||
"protects_test.go",
|
||||
"url_test.go",
|
||||
"util_test.go",
|
||||
],
|
||||
@ -48,6 +56,7 @@ go_test(
|
||||
"//internal/extsvc",
|
||||
"//internal/gitserver",
|
||||
"//internal/observation",
|
||||
"//internal/perforce",
|
||||
"//internal/types",
|
||||
"//internal/vcs",
|
||||
"//schema",
|
||||
|
||||
166
cmd/gitserver/internal/perforce/changelist.go
Normal file
166
cmd/gitserver/internal/perforce/changelist.go
Normal file
@ -0,0 +1,166 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
|
||||
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/wrexec"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
func GetChangelistByID(ctx context.Context, p4home, p4port, p4user, p4passwd, changelistID string) (*p4types.Changelist, error) {
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
"p4",
|
||||
"-Mj",
|
||||
"-z", "tag",
|
||||
"changes",
|
||||
"-r", // list in reverse order, which means that the given changelist id will be the first one listed
|
||||
"-m", "1", // limit output to one record, so that the given changelist is the only one listed
|
||||
"-l", // use a long listing, which includes the whole commit message
|
||||
"-e", changelistID, // start from this changelist and go up
|
||||
)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"P4PORT="+p4port,
|
||||
"P4USER="+p4user,
|
||||
"P4PASSWD="+p4passwd,
|
||||
"HOME="+p4home,
|
||||
)
|
||||
|
||||
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
|
||||
if err != nil {
|
||||
if ctxerr := ctx.Err(); ctxerr != nil {
|
||||
err = errors.Wrap(ctxerr, "p4 changes context error")
|
||||
}
|
||||
if len(out) > 0 {
|
||||
err = errors.Wrapf(err, `failed to run command "p4 changes" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output := bytes.TrimSpace(out)
|
||||
|
||||
if len(output) == 0 {
|
||||
return nil, errors.New("invalid changelist " + changelistID)
|
||||
}
|
||||
|
||||
pcl, err := parseChangelistOutput(output)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to parse change output")
|
||||
}
|
||||
|
||||
return pcl, nil
|
||||
}
|
||||
|
||||
func GetChangelistByClient(ctx context.Context, p4port, p4user, p4passwd, workDir, client string) (*p4types.Changelist, error) {
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
"p4",
|
||||
"-Mj",
|
||||
"-z", "tag",
|
||||
"changes",
|
||||
"-r", // list in reverse order, which means that the given changelist id will be the first one listed
|
||||
"-m", "1", // limit output to one record, so that the given changelist is the only one listed
|
||||
"-l", // use a long listing, which includes the whole commit message
|
||||
"-c", client,
|
||||
)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"P4PORT="+p4port,
|
||||
"P4USER="+p4user,
|
||||
"P4PASSWD="+p4passwd,
|
||||
"P4CLIENT="+client,
|
||||
)
|
||||
cmd.Dir = workDir
|
||||
|
||||
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
|
||||
if err != nil {
|
||||
if ctxerr := ctx.Err(); ctxerr != nil {
|
||||
err = errors.Wrap(ctxerr, "p4 changes context error")
|
||||
}
|
||||
if len(out) > 0 {
|
||||
err = errors.Wrapf(err, `failed to run command "p4 changes" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output := bytes.TrimSpace(out)
|
||||
|
||||
if len(output) == 0 {
|
||||
return nil, errors.New("no changelist found for client " + client)
|
||||
}
|
||||
|
||||
pcl, err := parseChangelistOutput(output)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to parse change output")
|
||||
}
|
||||
|
||||
return pcl, nil
|
||||
}
|
||||
|
||||
type changelistJson struct {
|
||||
// Change is the number of the changelist.
|
||||
Change string `json:"change"`
|
||||
ChangeType string `json:"changeType"`
|
||||
Client string `json:"client"`
|
||||
Desc string `json:"desc"`
|
||||
Path string `json:"path"`
|
||||
Status string `json:"status"`
|
||||
Time string `json:"time"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
// parseChangelistOutput parses one JSON line of p4 changes output.
|
||||
// output should be whitespace-trimmed and not empty.
|
||||
func parseChangelistOutput(output []byte) (*p4types.Changelist, error) {
|
||||
var cidj changelistJson
|
||||
err := json.Unmarshal(output, &cidj)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to unmarshal change output")
|
||||
}
|
||||
|
||||
state, err := parseChangelistState(cidj.Status)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to parse changelist state")
|
||||
}
|
||||
|
||||
intTime, err := strconv.Atoi(cidj.Time)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid time: "+cidj.Time)
|
||||
}
|
||||
|
||||
creationDate := time.Unix(int64(intTime), 0)
|
||||
|
||||
return &p4types.Changelist{
|
||||
ID: cidj.Change,
|
||||
State: state,
|
||||
Author: cidj.User,
|
||||
CreationDate: creationDate,
|
||||
Title: cidj.Client,
|
||||
Message: strings.TrimSpace(cidj.Desc),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseChangelistState(state string) (p4types.ChangelistState, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(state)) {
|
||||
case "submitted":
|
||||
return p4types.ChangelistStateSubmitted, nil
|
||||
case "pending":
|
||||
return p4types.ChangelistStatePending, nil
|
||||
case "shelved":
|
||||
return p4types.ChangelistStateShelved, nil
|
||||
case "closed":
|
||||
return p4types.ChangelistStateClosed, nil
|
||||
default:
|
||||
return "", errors.Newf("invalid Perforce changelist state: %s", state)
|
||||
}
|
||||
}
|
||||
59
cmd/gitserver/internal/perforce/changelist_test.go
Normal file
59
cmd/gitserver/internal/perforce/changelist_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
)
|
||||
|
||||
func TestParseChangelistOutput(t *testing.T) {
|
||||
created := time.Unix(1629179137, 0)
|
||||
testCases := []struct {
|
||||
output string
|
||||
expectedChangelist *p4types.Changelist
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
output: `{"change":"10","changeType":"public","client":"tester","desc":"test-first-one\nAppend still another line to all SECOND.md files\nHere's a second line of message\n","path":"//go/src/...","status":"pending","time":"1629179137","user":"admin"}`,
|
||||
expectedChangelist: &p4types.Changelist{
|
||||
ID: "10",
|
||||
CreationDate: created,
|
||||
Author: "admin",
|
||||
Title: "tester",
|
||||
State: p4types.ChangelistStatePending,
|
||||
Message: "test-first-one\nAppend still another line to all SECOND.md files\nHere's a second line of message",
|
||||
},
|
||||
},
|
||||
{
|
||||
output: `{"change":123}`,
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
output: `definitelynotjson`,
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
output: `{"change":"1188","changeType":"public","client":"tester","desc":"test","path":"//go/src/...","status":"INVALID","time":"1629179137","user":"admin"}`,
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
changelist, err := parseChangelistOutput([]byte(testCase.output))
|
||||
if testCase.shouldError {
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got nil")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(testCase.expectedChangelist, changelist); diff != "" {
|
||||
t.Errorf("parsed changelist did not match expected (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,35 +16,51 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
type PerforceDepotType string
|
||||
type perforceDepotType string
|
||||
|
||||
func (t perforceDepotType) Valid() bool {
|
||||
switch t {
|
||||
case perforceDepotTypeLocal,
|
||||
perforceDepotTypeRemote,
|
||||
perforceDepotTypeStream,
|
||||
perforceDepotTypeSpec,
|
||||
perforceDepotTypeUnload,
|
||||
perforceDepotTypeArchive,
|
||||
perforceDepotTypeTangent,
|
||||
perforceDepotTypeGraph:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
Local PerforceDepotType = "local"
|
||||
Remote PerforceDepotType = "remote"
|
||||
Stream PerforceDepotType = "stream"
|
||||
Spec PerforceDepotType = "spec"
|
||||
Unload PerforceDepotType = "unload"
|
||||
Archive PerforceDepotType = "archive"
|
||||
Tangent PerforceDepotType = "tangent"
|
||||
Graph PerforceDepotType = "graph"
|
||||
perforceDepotTypeLocal perforceDepotType = "local"
|
||||
perforceDepotTypeRemote perforceDepotType = "remote"
|
||||
perforceDepotTypeStream perforceDepotType = "stream"
|
||||
perforceDepotTypeSpec perforceDepotType = "spec"
|
||||
perforceDepotTypeUnload perforceDepotType = "unload"
|
||||
perforceDepotTypeArchive perforceDepotType = "archive"
|
||||
perforceDepotTypeTangent perforceDepotType = "tangent"
|
||||
perforceDepotTypeGraph perforceDepotType = "graph"
|
||||
)
|
||||
|
||||
// PerforceDepot is a definiton of a depot that matches the format
|
||||
// perforceDepot is a definiton of a depot that matches the format
|
||||
// returned from `p4 -Mj -ztag depots`
|
||||
type PerforceDepot struct {
|
||||
type perforceDepot struct {
|
||||
Desc string `json:"desc,omitempty"`
|
||||
Map string `json:"map,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
// Time is seconds since the Epoch, but p4 quotes it in the output, so it's a string
|
||||
Time string `json:"time,omitempty"`
|
||||
// Type is local, remote, stream, spec, unload, archive, tangent, graph
|
||||
Type PerforceDepotType `json:"type,omitempty"`
|
||||
Type perforceDepotType `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// P4Depots returns all of the depots to which the user has access on the host
|
||||
// and whose names match the given nameFilter, which can contain asterisks (*) for wildcards
|
||||
// if nameFilter is blank, return all depots
|
||||
func P4Depots(ctx context.Context, p4home, p4port, p4user, p4passwd, nameFilter string) ([]PerforceDepot, error) {
|
||||
// if nameFilter is blank, return all depots.
|
||||
func P4Depots(ctx context.Context, p4home, p4port, p4user, p4passwd, nameFilter string) ([]perforceDepot, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@ -71,12 +87,12 @@ func P4Depots(ctx context.Context, p4home, p4port, p4user, p4passwd, nameFilter
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
depots := make([]PerforceDepot, 0)
|
||||
depots := make([]perforceDepot, 0)
|
||||
if len(out) > 0 {
|
||||
// the output of `p4 -Mj -ztag depots` is a series of JSON-formatted depot definitions, one per line
|
||||
buf := bufio.NewScanner(bytes.NewBuffer(out))
|
||||
for buf.Scan() {
|
||||
depot := PerforceDepot{}
|
||||
depot := perforceDepot{}
|
||||
err := json.Unmarshal(buf.Bytes(), &depot)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "malformed output from p4 depots")
|
||||
|
||||
71
cmd/gitserver/internal/perforce/groups.go
Normal file
71
cmd/gitserver/internal/perforce/groups.go
Normal file
@ -0,0 +1,71 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/wrexec"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
// P4GroupMembers returns all usernames that are members of the given group.
|
||||
func P4GroupMembers(ctx context.Context, p4home, p4port, p4user, p4passwd, group string) ([]string, error) {
|
||||
cmd := exec.CommandContext(ctx, "p4", "-Mj", "-ztag", "group", "-o", group)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"P4PORT="+p4port,
|
||||
"P4USER="+p4user,
|
||||
"P4PASSWD="+p4passwd,
|
||||
"HOME="+p4home,
|
||||
)
|
||||
|
||||
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
|
||||
if err != nil {
|
||||
if ctxerr := ctx.Err(); ctxerr != nil {
|
||||
err = errors.Wrap(ctxerr, "p4 group context error")
|
||||
}
|
||||
|
||||
if len(out) > 0 {
|
||||
err = errors.Wrapf(err, `failed to run command "p4 group" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
// no error, but also no members. Maybe the group doesn't have any members?
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return parseP4GroupMembers(out)
|
||||
}
|
||||
|
||||
func parseP4GroupMembers(out []byte) ([]string, error) {
|
||||
var jsonGroup map[string]any
|
||||
err := json.Unmarshal(out, &jsonGroup)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "malformed output from p4 group")
|
||||
}
|
||||
|
||||
users := make([]string, 0)
|
||||
currentUserIdx := 0
|
||||
for {
|
||||
user, ok := jsonGroup[fmt.Sprintf("Users%d", currentUserIdx)]
|
||||
currentUserIdx++
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
username, ok := user.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
users = append(users, username)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
16
cmd/gitserver/internal/perforce/groups_test.go
Normal file
16
cmd/gitserver/internal/perforce/groups_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseP4GroupMembers(t *testing.T) {
|
||||
groupOut := []byte(`{"Group":"all","MaxLockTime":"unset","MaxOpenFiles":"unset","MaxResults":"unset","MaxScanRows":"unset","Owners0":"admin","PasswordTimeout":"unset","Timeout":"43200","Users0":"admin","Users1":"alice","Users2":"bob","Users3":"buildkite","Users4":"test-perforce"}`)
|
||||
|
||||
users, err := parseP4GroupMembers(groupOut)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, []string{"admin", "alice", "bob", "buildkite", "test-perforce"}, users)
|
||||
}
|
||||
@ -40,6 +40,46 @@ func P4TestWithTrust(ctx context.Context, p4home, p4port, p4user, p4passwd strin
|
||||
return err
|
||||
}
|
||||
|
||||
// P4UserIsSuperUser checks if the given credentials are for a super level user.
|
||||
// If the user is a super user, no error is returned. If not, ErrIsNotSuperUser
|
||||
// is returned.
|
||||
// Other errors may occur.
|
||||
func P4UserIsSuperUser(ctx context.Context, p4home, p4port, p4user, p4passwd string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Validate the user has "super" access with "-u" option, see https://www.perforce.com/perforce/r12.1/manuals/cmdref/protects.html
|
||||
cmd := exec.CommandContext(ctx, "p4", "protects", "-u", p4user)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"P4PORT="+p4port,
|
||||
"P4USER="+p4user,
|
||||
"P4PASSWD="+p4passwd,
|
||||
"HOME="+p4home,
|
||||
)
|
||||
|
||||
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
|
||||
if err != nil {
|
||||
if ctxerr := ctx.Err(); ctxerr != nil {
|
||||
err = ctxerr
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "You don't have permission for this operation.") {
|
||||
return ErrIsNotSuperUser
|
||||
}
|
||||
|
||||
if len(out) > 0 {
|
||||
err = errors.Errorf("%s (output follows)\n\n%s", err, out)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
var ErrIsNotSuperUser = errors.New("the user does not have super access")
|
||||
|
||||
// P4Trust blindly accepts fingerprint of the Perforce server.
|
||||
func P4Trust(ctx context.Context, p4home, host string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
|
||||
@ -221,6 +221,7 @@ func TestServicePipeline(t *testing.T) {
|
||||
|
||||
db := dbmocks.NewMockDB()
|
||||
db.ReposFunc.SetDefaultReturn(repos)
|
||||
db.RepoCommitsChangelistsFunc.SetDefaultReturn(dbmocks.NewMockRepoCommitsChangelistsStore())
|
||||
|
||||
logger := logtest.NoOp(t)
|
||||
svc := NewService(ctx, observation.NewContext(logger), logger, db, list.New())
|
||||
|
||||
126
cmd/gitserver/internal/perforce/protects.go
Normal file
126
cmd/gitserver/internal/perforce/protects.go
Normal file
@ -0,0 +1,126 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/byteutils"
|
||||
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/wrexec"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
// P4ProtectsForUser returns all protect definitions that apply to the given username.
|
||||
func P4ProtectsForUser(ctx context.Context, p4home, p4port, p4user, p4passwd, username string) ([]*p4types.Protect, error) {
|
||||
// -u User : Displays protection lines that apply to the named user. This option
|
||||
// requires super access.
|
||||
cmd := exec.CommandContext(ctx, "p4", "-Mj", "-ztag", "protects", "-u", username)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"P4PORT="+p4port,
|
||||
"P4USER="+p4user,
|
||||
"P4PASSWD="+p4passwd,
|
||||
"HOME="+p4home,
|
||||
)
|
||||
|
||||
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
|
||||
if err != nil {
|
||||
if ctxerr := ctx.Err(); ctxerr != nil {
|
||||
err = errors.Wrap(ctxerr, "p4 protects context error")
|
||||
}
|
||||
|
||||
if len(out) > 0 {
|
||||
err = errors.Wrapf(err, `failed to run command "p4 protects" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
// no error, but also no protects.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return parseP4Protects(out)
|
||||
}
|
||||
|
||||
// P4ProtectsForUser returns all protect definitions that apply to the given depot.
|
||||
func P4ProtectsForDepot(ctx context.Context, p4home, p4port, p4user, p4passwd, depot string) ([]*p4types.Protect, error) {
|
||||
// -a : Displays protection lines for all users. This option requires super
|
||||
// access.
|
||||
cmd := exec.CommandContext(ctx, "p4", "-Mj", "-ztag", "protects", "-a", depot)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"P4PORT="+p4port,
|
||||
"P4USER="+p4user,
|
||||
"P4PASSWD="+p4passwd,
|
||||
"HOME="+p4home,
|
||||
)
|
||||
|
||||
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
|
||||
if err != nil {
|
||||
if ctxerr := ctx.Err(); ctxerr != nil {
|
||||
err = errors.Wrap(ctxerr, "p4 protects context error")
|
||||
}
|
||||
|
||||
if len(out) > 0 {
|
||||
err = errors.Wrapf(err, `failed to run command "p4 protects" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
// no error, but also no protects.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return parseP4Protects(out)
|
||||
}
|
||||
|
||||
type perforceJSONProtect struct {
|
||||
DepotFile string `json:"depotFile"`
|
||||
Host string `json:"host"`
|
||||
Line string `json:"line"`
|
||||
Perm string `json:"perm"`
|
||||
IsGroup *string `json:"isgroup,omitempty"`
|
||||
Unmap *string `json:"unmap,omitempty"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
func parseP4Protects(out []byte) ([]*p4types.Protect, error) {
|
||||
protects := make([]*p4types.Protect, 0)
|
||||
|
||||
lr := byteutils.NewLineReader(out)
|
||||
for lr.Scan() {
|
||||
line := lr.Line()
|
||||
|
||||
// Trim whitespace
|
||||
line = bytes.TrimSpace(line)
|
||||
|
||||
var parsedLine perforceJSONProtect
|
||||
if err := json.Unmarshal(line, &parsedLine); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal protect line")
|
||||
}
|
||||
|
||||
entityType := "user"
|
||||
if parsedLine.IsGroup != nil {
|
||||
entityType = "group"
|
||||
}
|
||||
|
||||
protects = append(protects, &p4types.Protect{
|
||||
Host: parsedLine.Host,
|
||||
EntityType: entityType,
|
||||
EntityName: parsedLine.User,
|
||||
Match: parsedLine.DepotFile,
|
||||
IsExclusion: parsedLine.Unmap != nil,
|
||||
Level: parsedLine.Perm,
|
||||
})
|
||||
}
|
||||
|
||||
return protects, nil
|
||||
}
|
||||
84
cmd/gitserver/internal/perforce/protects_test.go
Normal file
84
cmd/gitserver/internal/perforce/protects_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
)
|
||||
|
||||
func TestParseP4Protects(t *testing.T) {
|
||||
protectsOut := []byte(`{"depotFile":"//...","host":"*","line":"1","perm":"list","unmap":"","user":"*"}
|
||||
{"depotFile":"//test-perms/Frontend/...","host":"*","isgroup":"","line":"6","perm":"write","user":"Frontend"}
|
||||
{"depotFile":"//integration-test-depot/...","host":"*","isgroup":"","line":"14","perm":"=read","unmap":"","user":"all"}
|
||||
{"depotFile":"//go/...","host":"*","line":"24","perm":"read","user":"bob"}
|
||||
{"depotFile":"//go/api/...","host":"*","line":"25","perm":"=read","unmap":"","user":"bob"}
|
||||
{"depotFile":"//go/*/except.txt","host":"*","isgroup":"","line":"26","perm":"read","user":"Frontend"}
|
||||
{"depotFile":"//go/...","host":"192.168.10.1/24","line":"27","perm":"=read","unmap":"","user":"bob"}
|
||||
`)
|
||||
|
||||
protects, err := parseP4Protects(protectsOut)
|
||||
require.NoError(t, err)
|
||||
|
||||
want := []*perforce.Protect{
|
||||
{
|
||||
Level: "list",
|
||||
EntityType: "user",
|
||||
EntityName: "*",
|
||||
Match: "//...",
|
||||
IsExclusion: true,
|
||||
Host: "*",
|
||||
},
|
||||
{
|
||||
Level: "write",
|
||||
EntityType: "group",
|
||||
EntityName: "Frontend",
|
||||
Match: "//test-perms/Frontend/...",
|
||||
IsExclusion: false,
|
||||
Host: "*",
|
||||
},
|
||||
{
|
||||
Level: "=read",
|
||||
EntityType: "group",
|
||||
EntityName: "all",
|
||||
Match: "//integration-test-depot/...",
|
||||
IsExclusion: true,
|
||||
Host: "*",
|
||||
},
|
||||
{
|
||||
Level: "read",
|
||||
EntityType: "user",
|
||||
EntityName: "bob",
|
||||
Match: "//go/...",
|
||||
IsExclusion: false,
|
||||
Host: "*",
|
||||
},
|
||||
{
|
||||
Level: "=read",
|
||||
EntityType: "user",
|
||||
EntityName: "bob",
|
||||
Match: "//go/api/...",
|
||||
IsExclusion: true,
|
||||
Host: "*",
|
||||
},
|
||||
{
|
||||
Level: "read",
|
||||
EntityType: "group",
|
||||
EntityName: "Frontend",
|
||||
Match: "//go/*/except.txt",
|
||||
IsExclusion: false,
|
||||
Host: "*",
|
||||
},
|
||||
{
|
||||
Level: "=read",
|
||||
EntityType: "user",
|
||||
EntityName: "bob",
|
||||
Match: "//go/...",
|
||||
IsExclusion: true,
|
||||
Host: "192.168.10.1/24",
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, want, protects)
|
||||
}
|
||||
93
cmd/gitserver/internal/perforce/users.go
Normal file
93
cmd/gitserver/internal/perforce/users.go
Normal file
@ -0,0 +1,93 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/gitserver/internal/executil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/byteutils"
|
||||
"github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/wrexec"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
// P4Users returns all of users known to the Perforce server.
|
||||
func P4Users(ctx context.Context, p4home, p4port, p4user, p4passwd string) ([]perforce.User, error) {
|
||||
cmd := exec.CommandContext(ctx, "p4", "-Mj", "-ztag", "users")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"P4PORT="+p4port,
|
||||
"P4USER="+p4user,
|
||||
"P4PASSWD="+p4passwd,
|
||||
"HOME="+p4home,
|
||||
)
|
||||
|
||||
out, err := executil.RunCommandCombinedOutput(ctx, wrexec.Wrap(ctx, log.NoOp(), cmd))
|
||||
if err != nil {
|
||||
if ctxerr := ctx.Err(); ctxerr != nil {
|
||||
err = errors.Wrap(ctxerr, "p4 users context error")
|
||||
}
|
||||
if len(out) > 0 {
|
||||
err = errors.Wrapf(err, `failed to run command "p4 users" (output follows)\n\n%s`, specifyCommandInErrorMessage(string(out), cmd))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
// no error, but also no users. Maybe the user doesn't have access to any users?
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
users := make([]perforce.User, 0)
|
||||
lr := byteutils.NewLineReader(out)
|
||||
for lr.Scan() {
|
||||
line := lr.Line()
|
||||
// the output of `p4 -Mj -ztag users` is a series of JSON-formatted user definitions, one per line.
|
||||
var user perforceSJONUser
|
||||
err := json.Unmarshal(line, &user)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "malformed output from p4 users")
|
||||
}
|
||||
users = append(users, perforce.User{
|
||||
Username: user.User,
|
||||
Email: user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
type perforceUserType string
|
||||
|
||||
func (t perforceUserType) Valid() bool {
|
||||
switch t {
|
||||
case perforceUserTypeStandard,
|
||||
perforceUserTypeOperator,
|
||||
perforceUserTypeService:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
perforceUserTypeStandard perforceUserType = "standard"
|
||||
perforceUserTypeOperator perforceUserType = "operator"
|
||||
perforceUserTypeService perforceUserType = "service"
|
||||
)
|
||||
|
||||
// perforceSJONUser is a definition of a user that matches the format returned from
|
||||
// `p4 -Mj -ztag users`.
|
||||
type perforceSJONUser struct {
|
||||
Email string `json:"Email,omitempty"`
|
||||
User string `json:"User,omitempty"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
FullName string `json:"FullName,omitempty"`
|
||||
// Access is seconds since the Epoch, but p4 quotes it in the output, so it's a string
|
||||
Access string `json:"Access,omitempty"`
|
||||
Update string `json:"Update,omitempty"`
|
||||
Type perforceUserType `json:"Type,omitempty"`
|
||||
}
|
||||
@ -307,6 +307,12 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("/is-perforce-path-cloneable", trace.WithRouteName("is-perforce-path-cloneable", s.handleIsPerforcePathCloneable))
|
||||
mux.HandleFunc("/check-perforce-credentials", trace.WithRouteName("check-perforce-credentials", s.handleCheckPerforceCredentials))
|
||||
mux.HandleFunc("/commands/get-object", trace.WithRouteName("commands/get-object", s.handleGetObject))
|
||||
mux.HandleFunc("/perforce-users", trace.WithRouteName("perforce-users", s.handlePerforceUsers))
|
||||
mux.HandleFunc("/perforce-protects-for-user", trace.WithRouteName("perforce-protects-for-user", s.handlePerforceProtectsForUser))
|
||||
mux.HandleFunc("/perforce-protects-for-depot", trace.WithRouteName("perforce-protects-for-depot", s.handlePerforceProtectsForDepot))
|
||||
mux.HandleFunc("/perforce-group-members", trace.WithRouteName("perforce-group-members", s.handlePerforceGroupMembers))
|
||||
mux.HandleFunc("/is-perforce-super-user", trace.WithRouteName("is-perforce-super-user", s.handleIsPerforceSuperUser))
|
||||
mux.HandleFunc("/perforce-get-changelist", trace.WithRouteName("perforce-get-changelist", s.handlePerforceGetChangelist))
|
||||
mux.HandleFunc("/ping", trace.WithRouteName("ping", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
@ -1216,197 +1222,6 @@ func (s *Server) execHTTP(w http.ResponseWriter, r *http.Request, req *protocol.
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleP4Exec(w http.ResponseWriter, r *http.Request) {
|
||||
var req protocol.P4ExecRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Args) < 1 {
|
||||
http.Error(w, "args must be greater than or equal to 1", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the subcommand is explicitly allowed
|
||||
allowlist := []string{"protects", "groups", "users", "group", "changes"}
|
||||
allowed := false
|
||||
for _, arg := range allowlist {
|
||||
if req.Args[0] == arg {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
http.Error(w, fmt.Sprintf("subcommand %q is not allowed", req.Args[0]), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Log which actor is accessing p4-exec.
|
||||
//
|
||||
// p4-exec is currently only used for fetching user based permissions information
|
||||
// so, we don't have a repo name.
|
||||
accesslog.Record(r.Context(), "<no-repo>",
|
||||
log.String("p4user", req.P4User),
|
||||
log.String("p4port", req.P4Port),
|
||||
log.Strings("args", req.Args),
|
||||
)
|
||||
|
||||
// Make sure credentials are valid before heavier operation
|
||||
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.p4execHTTP(w, r, &req)
|
||||
}
|
||||
|
||||
func (s *Server) p4execHTTP(w http.ResponseWriter, r *http.Request, req *protocol.P4ExecRequest) {
|
||||
logger := s.Logger.Scoped("p4exec", "")
|
||||
|
||||
// Flush writes more aggressively than standard net/http so that clients
|
||||
// with a context deadline see as much partial response body as possible.
|
||||
if fw := newFlushingResponseWriter(logger, w); fw != nil {
|
||||
w = fw
|
||||
defer fw.Close()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
w.Header().Set("Trailer", "X-Exec-Error")
|
||||
w.Header().Add("Trailer", "X-Exec-Exit-Status")
|
||||
w.Header().Add("Trailer", "X-Exec-Stderr")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
execStatus := s.p4Exec(ctx, logger, req, r.UserAgent(), w)
|
||||
w.Header().Set("X-Exec-Error", errorString(execStatus.Err))
|
||||
w.Header().Set("X-Exec-Exit-Status", strconv.Itoa(execStatus.ExitStatus))
|
||||
w.Header().Set("X-Exec-Stderr", execStatus.Stderr)
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) p4Exec(ctx context.Context, logger log.Logger, req *protocol.P4ExecRequest, userAgent string, w io.Writer) execStatus {
|
||||
start := time.Now()
|
||||
var cmdStart time.Time // set once we have ensured commit
|
||||
exitStatus := executil.UnsetExitStatus
|
||||
var stdoutN, stderrN int64
|
||||
var status string
|
||||
var execErr error
|
||||
|
||||
// Instrumentation
|
||||
{
|
||||
cmd := ""
|
||||
if len(req.Args) > 0 {
|
||||
cmd = req.Args[0]
|
||||
}
|
||||
args := strings.Join(req.Args, " ")
|
||||
|
||||
var tr trace.Trace
|
||||
tr, ctx = trace.New(ctx, "p4exec."+cmd, attribute.String("port", req.P4Port))
|
||||
tr.SetAttributes(attribute.String("args", args))
|
||||
logger = logger.WithTrace(trace.Context(ctx))
|
||||
|
||||
execRunning.WithLabelValues(cmd).Inc()
|
||||
defer func() {
|
||||
tr.AddEvent("done",
|
||||
attribute.String("status", status),
|
||||
attribute.Int64("stdout", stdoutN),
|
||||
attribute.Int64("stderr", stderrN),
|
||||
)
|
||||
tr.SetError(execErr)
|
||||
tr.End()
|
||||
|
||||
duration := time.Since(start)
|
||||
execRunning.WithLabelValues(cmd).Dec()
|
||||
execDuration.WithLabelValues(cmd, status).Observe(duration.Seconds())
|
||||
|
||||
var cmdDuration time.Duration
|
||||
if !cmdStart.IsZero() {
|
||||
cmdDuration = time.Since(cmdStart)
|
||||
}
|
||||
|
||||
isSlow := cmdDuration > 30*time.Second
|
||||
if honey.Enabled() || traceLogs || isSlow {
|
||||
act := actor.FromContext(ctx)
|
||||
ev := honey.NewEvent("gitserver-p4exec")
|
||||
ev.SetSampleRate(honeySampleRate(cmd, act))
|
||||
ev.AddField("p4port", req.P4Port)
|
||||
ev.AddField("cmd", cmd)
|
||||
ev.AddField("args", args)
|
||||
ev.AddField("actor", act.UIDString())
|
||||
ev.AddField("client", userAgent)
|
||||
ev.AddField("duration_ms", duration.Milliseconds())
|
||||
ev.AddField("stdout_size", stdoutN)
|
||||
ev.AddField("stderr_size", stderrN)
|
||||
ev.AddField("exit_status", exitStatus)
|
||||
ev.AddField("status", status)
|
||||
if execErr != nil {
|
||||
ev.AddField("error", execErr.Error())
|
||||
}
|
||||
if !cmdStart.IsZero() {
|
||||
ev.AddField("cmd_duration_ms", cmdDuration.Milliseconds())
|
||||
}
|
||||
|
||||
if traceID := trace.ID(ctx); traceID != "" {
|
||||
ev.AddField("traceID", traceID)
|
||||
ev.AddField("trace", trace.URL(traceID, conf.DefaultClient()))
|
||||
}
|
||||
|
||||
_ = ev.Send()
|
||||
|
||||
if traceLogs {
|
||||
logger.Debug("TRACE gitserver p4exec", log.Object("ev.Fields", mapToLoggerField(ev.Fields())...))
|
||||
}
|
||||
if isSlow {
|
||||
logger.Warn("Long p4exec request", log.Object("ev.Fields", mapToLoggerField(ev.Fields())...))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
|
||||
if err != nil {
|
||||
return execStatus{ExitStatus: -1, Err: err}
|
||||
}
|
||||
|
||||
var stderrBuf bytes.Buffer
|
||||
stdoutW := &writeCounter{w: w}
|
||||
stderrW := &writeCounter{w: &limitWriter{W: &stderrBuf, N: 1024}}
|
||||
|
||||
cmdStart = time.Now()
|
||||
cmd := exec.CommandContext(ctx, "p4", req.Args...)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"P4PORT="+req.P4Port,
|
||||
"P4USER="+req.P4User,
|
||||
"P4PASSWD="+req.P4Passwd,
|
||||
"HOME="+p4home,
|
||||
)
|
||||
cmd.Stdout = stdoutW
|
||||
cmd.Stderr = stderrW
|
||||
|
||||
exitStatus, execErr = executil.RunCommand(ctx, s.RecordingCommandFactory.Wrap(ctx, s.Logger, cmd))
|
||||
|
||||
status = strconv.Itoa(exitStatus)
|
||||
stdoutN = stdoutW.n
|
||||
stderrN = stderrW.n
|
||||
|
||||
stderr := stderrBuf.String()
|
||||
|
||||
return execStatus{
|
||||
ExitStatus: exitStatus,
|
||||
Stderr: stderr,
|
||||
Err: execErr,
|
||||
}
|
||||
}
|
||||
|
||||
func setLastFetched(ctx context.Context, db database.DB, shardID string, dir common.GitDir, name api.RepoName) error {
|
||||
lastFetched, err := repoLastFetched(dir)
|
||||
if err != nil {
|
||||
@ -2263,3 +2078,297 @@ func (s *Server) handleGetObject(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handlePerforceUsers(w http.ResponseWriter, r *http.Request) {
|
||||
var req protocol.PerforceUsersRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
r.Context(),
|
||||
"<no-repo>",
|
||||
log.String("p4user", req.P4User),
|
||||
log.String("p4port", req.P4Port),
|
||||
)
|
||||
|
||||
users, err := perforce.P4Users(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &protocol.PerforceUsersResponse{
|
||||
Users: make([]protocol.PerforceUser, 0, len(users)),
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
resp.Users = append(resp.Users, protocol.PerforceUser{
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handlePerforceProtectsForUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req protocol.PerforceProtectsForUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
r.Context(),
|
||||
"<no-repo>",
|
||||
log.String("p4user", req.P4User),
|
||||
log.String("p4port", req.P4Port),
|
||||
)
|
||||
|
||||
protects, err := perforce.P4ProtectsForUser(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd, req.Username)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
jsonProtects := make([]protocol.PerforceProtect, len(protects))
|
||||
for i, p := range protects {
|
||||
jsonProtects[i] = protocol.PerforceProtect{
|
||||
Level: p.Level,
|
||||
EntityType: p.EntityType,
|
||||
EntityName: p.EntityName,
|
||||
Match: p.Match,
|
||||
IsExclusion: p.IsExclusion,
|
||||
Host: p.Host,
|
||||
}
|
||||
}
|
||||
|
||||
resp := &protocol.PerforceProtectsForUserResponse{
|
||||
Protects: jsonProtects,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handlePerforceProtectsForDepot(w http.ResponseWriter, r *http.Request) {
|
||||
var req protocol.PerforceProtectsForDepotRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
r.Context(),
|
||||
"<no-repo>",
|
||||
log.String("p4user", req.P4User),
|
||||
log.String("p4port", req.P4Port),
|
||||
)
|
||||
|
||||
protects, err := perforce.P4ProtectsForDepot(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd, req.Depot)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
jsonProtects := make([]protocol.PerforceProtect, len(protects))
|
||||
for i, p := range protects {
|
||||
jsonProtects[i] = protocol.PerforceProtect{
|
||||
Level: p.Level,
|
||||
EntityType: p.EntityType,
|
||||
EntityName: p.EntityName,
|
||||
Match: p.Match,
|
||||
IsExclusion: p.IsExclusion,
|
||||
Host: p.Host,
|
||||
}
|
||||
}
|
||||
|
||||
resp := &protocol.PerforceProtectsForDepotResponse{
|
||||
Protects: jsonProtects,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) handlePerforceGroupMembers(w http.ResponseWriter, r *http.Request) {
|
||||
var req protocol.PerforceGroupMembersRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
r.Context(),
|
||||
"<no-repo>",
|
||||
log.String("p4user", req.P4User),
|
||||
log.String("p4port", req.P4Port),
|
||||
)
|
||||
|
||||
members, err := perforce.P4GroupMembers(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd, req.Group)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &protocol.PerforceGroupMembersResponse{
|
||||
Usernames: members,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleIsPerforceSuperUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req protocol.IsPerforceSuperUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
r.Context(),
|
||||
"<no-repo>",
|
||||
log.String("p4user", req.P4User),
|
||||
log.String("p4port", req.P4Port),
|
||||
)
|
||||
|
||||
err = perforce.P4UserIsSuperUser(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
|
||||
if err != nil {
|
||||
if err == perforce.ErrIsNotSuperUser {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &protocol.IsPerforceSuperUserResponse{}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handlePerforceGetChangelist(w http.ResponseWriter, r *http.Request) {
|
||||
var req protocol.PerforceGetChangelistRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(s.ReposDir)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = perforce.P4TestWithTrust(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
r.Context(),
|
||||
"<no-repo>",
|
||||
log.String("p4user", req.P4User),
|
||||
log.String("p4port", req.P4Port),
|
||||
)
|
||||
|
||||
changelist, err := perforce.GetChangelistByID(r.Context(), p4home, req.P4Port, req.P4User, req.P4Passwd, req.ChangelistID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &protocol.PerforceGetChangelistResponse{
|
||||
Changelist: protocol.PerforceChangelist{
|
||||
ID: changelist.ID,
|
||||
CreationDate: changelist.CreationDate,
|
||||
State: string(changelist.State),
|
||||
Author: changelist.Author,
|
||||
Title: changelist.Title,
|
||||
Message: changelist.Message,
|
||||
},
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
@ -30,6 +29,8 @@ type GRPCServer struct {
|
||||
proto.UnimplementedGitserverServiceServer
|
||||
}
|
||||
|
||||
var _ proto.GitserverServiceServer = &GRPCServer{}
|
||||
|
||||
func (gs *GRPCServer) BatchLog(ctx context.Context, req *proto.BatchLogRequest) (*proto.BatchLogResponse, error) {
|
||||
gs.Server.operations = gs.Server.ensureOperations()
|
||||
|
||||
@ -251,92 +252,6 @@ func (gs *GRPCServer) GetObject(ctx context.Context, req *proto.GetObjectRequest
|
||||
return resp.ToProto(), nil
|
||||
}
|
||||
|
||||
func (gs *GRPCServer) P4Exec(req *proto.P4ExecRequest, ss proto.GitserverService_P4ExecServer) error {
|
||||
arguments := byteSlicesToStrings(req.GetArgs())
|
||||
|
||||
if len(arguments) < 1 {
|
||||
return status.Error(codes.InvalidArgument, "args must be greater than or equal to 1")
|
||||
}
|
||||
|
||||
subCommand := arguments[0]
|
||||
|
||||
// Make sure the subcommand is explicitly allowed
|
||||
allowlist := []string{"protects", "groups", "users", "group", "changes"}
|
||||
allowed := false
|
||||
for _, c := range allowlist {
|
||||
if subCommand == c {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return status.Error(codes.InvalidArgument, fmt.Sprintf("subcommand %q is not allowed", subCommand))
|
||||
}
|
||||
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
|
||||
if err != nil {
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
// Log which actor is accessing p4-exec.
|
||||
//
|
||||
// p4-exec is currently only used for fetching user based permissions information
|
||||
// so, we don't have a repo name.
|
||||
accesslog.Record(ss.Context(), "<no-repo>",
|
||||
log.String("p4user", req.GetP4User()),
|
||||
log.String("p4port", req.GetP4Port()),
|
||||
log.Strings("args", arguments),
|
||||
)
|
||||
|
||||
// Make sure credentials are valid before heavier operation
|
||||
err = perforce.P4TestWithTrust(ss.Context(), p4home, req.GetP4Port(), req.GetP4User(), req.GetP4Passwd())
|
||||
if err != nil {
|
||||
if ctxErr := ss.Context().Err(); ctxErr != nil {
|
||||
return status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
w := streamio.NewWriter(func(p []byte) error {
|
||||
return ss.Send(&proto.P4ExecResponse{
|
||||
Data: p,
|
||||
})
|
||||
})
|
||||
|
||||
var r protocol.P4ExecRequest
|
||||
r.FromProto(req)
|
||||
|
||||
return gs.doP4Exec(ss.Context(), gs.Server.Logger, &r, "unknown-grpc-client", w)
|
||||
}
|
||||
|
||||
func (gs *GRPCServer) doP4Exec(ctx context.Context, logger log.Logger, req *protocol.P4ExecRequest, userAgent string, w io.Writer) error {
|
||||
execStatus := gs.Server.p4Exec(ctx, logger, req, userAgent, w)
|
||||
|
||||
if execStatus.ExitStatus != 0 || execStatus.Err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
gRPCStatus := codes.Unknown
|
||||
if strings.Contains(execStatus.Err.Error(), "signal: killed") {
|
||||
gRPCStatus = codes.Aborted
|
||||
}
|
||||
|
||||
s, err := status.New(gRPCStatus, execStatus.Err.Error()).WithDetails(&proto.ExecStatusPayload{
|
||||
StatusCode: int32(execStatus.ExitStatus),
|
||||
Stderr: execStatus.Stderr,
|
||||
})
|
||||
if err != nil {
|
||||
gs.Server.Logger.Error("failed to marshal status", log.Error(err))
|
||||
return err
|
||||
}
|
||||
return s.Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gs *GRPCServer) ListGitolite(ctx context.Context, req *proto.ListGitoliteRequest) (*proto.ListGitoliteResponse, error) {
|
||||
host := req.GetGitoliteHost()
|
||||
repos, err := defaultGitolite.listRepos(ctx, host)
|
||||
@ -459,7 +374,8 @@ func (gs *GRPCServer) IsPerforcePathCloneable(ctx context.Context, req *proto.Is
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
err = perforce.IsDepotPathCloneable(ctx, p4home, req.GetP4Port(), req.GetP4User(), req.GetP4Passwd(), req.GetDepotPath())
|
||||
conn := req.GetConnectionDetails()
|
||||
err = perforce.IsDepotPathCloneable(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd(), req.DepotPath)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
@ -473,14 +389,232 @@ func (gs *GRPCServer) CheckPerforceCredentials(ctx context.Context, req *proto.C
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
err = perforce.P4TestWithTrust(ctx, p4home, req.GetP4Port(), req.GetP4User(), req.GetP4Passwd())
|
||||
conn := req.GetConnectionDetails()
|
||||
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return nil, status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
return &proto.CheckPerforceCredentialsResponse{}, nil
|
||||
}
|
||||
|
||||
func (gs *GRPCServer) PerforceUsers(ctx context.Context, req *proto.PerforceUsersRequest) (*proto.PerforceUsersResponse, error) {
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
conn := req.GetConnectionDetails()
|
||||
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return nil, status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
ctx,
|
||||
"<no-repo>",
|
||||
log.String("p4user", conn.GetP4User()),
|
||||
log.String("p4port", conn.GetP4Port()),
|
||||
)
|
||||
|
||||
users, err := perforce.P4Users(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
resp := &proto.PerforceUsersResponse{
|
||||
Users: make([]*proto.PerforceUser, 0, len(users)),
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
resp.Users = append(resp.Users, user.ToProto())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (gs *GRPCServer) PerforceProtectsForUser(ctx context.Context, req *proto.PerforceProtectsForUserRequest) (*proto.PerforceProtectsForUserResponse, error) {
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
conn := req.GetConnectionDetails()
|
||||
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return nil, status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
ctx,
|
||||
"<no-repo>",
|
||||
log.String("p4user", conn.GetP4User()),
|
||||
log.String("p4port", conn.GetP4Port()),
|
||||
)
|
||||
|
||||
protects, err := perforce.P4ProtectsForUser(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd(), req.GetUsername())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
protoProtects := make([]*proto.PerforceProtect, len(protects))
|
||||
for i, p := range protects {
|
||||
protoProtects[i] = p.ToProto()
|
||||
}
|
||||
|
||||
return &proto.PerforceProtectsForUserResponse{
|
||||
Protects: protoProtects,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (gs *GRPCServer) PerforceProtectsForDepot(ctx context.Context, req *proto.PerforceProtectsForDepotRequest) (*proto.PerforceProtectsForDepotResponse, error) {
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
conn := req.GetConnectionDetails()
|
||||
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return nil, status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
ctx,
|
||||
"<no-repo>",
|
||||
log.String("p4user", conn.GetP4User()),
|
||||
log.String("p4port", conn.GetP4Port()),
|
||||
)
|
||||
|
||||
protects, err := perforce.P4ProtectsForDepot(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd(), req.GetDepot())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
protoProtects := make([]*proto.PerforceProtect, len(protects))
|
||||
for i, p := range protects {
|
||||
protoProtects[i] = p.ToProto()
|
||||
}
|
||||
|
||||
return &proto.PerforceProtectsForDepotResponse{
|
||||
Protects: protoProtects,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (gs *GRPCServer) PerforceGroupMembers(ctx context.Context, req *proto.PerforceGroupMembersRequest) (*proto.PerforceGroupMembersResponse, error) {
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
conn := req.GetConnectionDetails()
|
||||
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return nil, status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
ctx,
|
||||
"<no-repo>",
|
||||
log.String("p4user", conn.GetP4User()),
|
||||
log.String("p4port", conn.GetP4Port()),
|
||||
)
|
||||
|
||||
members, err := perforce.P4GroupMembers(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd(), req.GetGroup())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &proto.PerforceGroupMembersResponse{
|
||||
Usernames: members,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (gs *GRPCServer) IsPerforceSuperUser(ctx context.Context, req *proto.IsPerforceSuperUserRequest) (*proto.IsPerforceSuperUserResponse, error) {
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
conn := req.GetConnectionDetails()
|
||||
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return nil, status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
err = perforce.P4UserIsSuperUser(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return nil, status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
if err == perforce.ErrIsNotSuperUser {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &proto.IsPerforceSuperUserResponse{}, nil
|
||||
}
|
||||
|
||||
func (gs *GRPCServer) PerforceGetChangelist(ctx context.Context, req *proto.PerforceGetChangelistRequest) (*proto.PerforceGetChangelistResponse, error) {
|
||||
p4home, err := gitserverfs.MakeP4HomeDir(gs.Server.ReposDir)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
conn := req.GetConnectionDetails()
|
||||
err = perforce.P4TestWithTrust(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd())
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return nil, status.FromContextError(ctxErr).Err()
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
accesslog.Record(
|
||||
ctx,
|
||||
"<no-repo>",
|
||||
log.String("p4user", conn.GetP4User()),
|
||||
log.String("p4port", conn.GetP4Port()),
|
||||
)
|
||||
|
||||
changelist, err := perforce.GetChangelistByID(ctx, p4home, conn.GetP4Port(), conn.GetP4User(), conn.GetP4Passwd(), req.GetChangelistId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &proto.PerforceGetChangelistResponse{
|
||||
Changelist: changelist.ToProto(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func byteSlicesToStrings(in [][]byte) []string {
|
||||
res := make([]string, len(in))
|
||||
for i, b := range in {
|
||||
|
||||
@ -9,7 +9,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@ -23,8 +22,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"golang.org/x/time/rate"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/sourcegraph/log/logtest"
|
||||
|
||||
@ -39,9 +36,6 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
proto "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
|
||||
"github.com/sourcegraph/sourcegraph/internal/grpc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/grpc/defaults"
|
||||
"github.com/sourcegraph/sourcegraph/internal/limiter"
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
"github.com/sourcegraph/sourcegraph/internal/ratelimit"
|
||||
@ -267,239 +261,6 @@ func TestExecRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_handleP4Exec(t *testing.T) {
|
||||
defaultMockRunCommand := func(ctx context.Context, cmd *exec.Cmd) (int, error) {
|
||||
switch cmd.Args[1] {
|
||||
case "users":
|
||||
_, _ = cmd.Stdout.Write([]byte("admin <admin@joe-perforce-server> (admin) accessed 2021/01/31"))
|
||||
_, _ = cmd.Stderr.Write([]byte("teststderr"))
|
||||
return 42, errors.New("the answer to life the universe and everything")
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
executil.UpdateRunCommandMock(nil)
|
||||
})
|
||||
|
||||
startServer := func(t *testing.T) (handler http.Handler, client proto.GitserverServiceClient, cleanup func()) {
|
||||
t.Helper()
|
||||
|
||||
logger := logtest.Scoped(t)
|
||||
|
||||
s := &Server{
|
||||
Logger: logger,
|
||||
ReposDir: t.TempDir(),
|
||||
ObservationCtx: observation.TestContextTB(t),
|
||||
skipCloneForTests: true,
|
||||
DB: dbmocks.NewMockDB(),
|
||||
RecordingCommandFactory: wrexec.NewNoOpRecordingCommandFactory(),
|
||||
Locker: NewRepositoryLocker(),
|
||||
}
|
||||
|
||||
server := defaults.NewServer(logger)
|
||||
proto.RegisterGitserverServiceServer(server, &GRPCServer{Server: s})
|
||||
handler = grpc.MultiplexHandlers(server, s.Handler())
|
||||
|
||||
srv := httptest.NewServer(handler)
|
||||
|
||||
u, _ := url.Parse(srv.URL)
|
||||
conn, err := defaults.Dial(u.Host, logger.Scoped("gRPC client", ""))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial: %v", err)
|
||||
}
|
||||
|
||||
client = proto.NewGitserverServiceClient(conn)
|
||||
|
||||
return handler, client, func() {
|
||||
srv.Close()
|
||||
conn.Close()
|
||||
server.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("gRPC", func(t *testing.T) {
|
||||
readAll := func(execClient proto.GitserverService_P4ExecClient) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
for {
|
||||
resp, err := execClient.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
_, err = buf.Write(resp.GetData())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write data: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("users", func(t *testing.T) {
|
||||
executil.UpdateRunCommandMock(defaultMockRunCommand)
|
||||
|
||||
_, client, closeFunc := startServer(t)
|
||||
t.Cleanup(closeFunc)
|
||||
|
||||
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{
|
||||
Args: [][]byte{[]byte("users")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to call P4Exec: %v", err)
|
||||
}
|
||||
|
||||
data, err := readAll(stream)
|
||||
s, ok := status.FromError(err)
|
||||
if !ok {
|
||||
t.Fatal("received non-status error from p4exec call")
|
||||
}
|
||||
|
||||
if diff := cmp.Diff("the answer to life the universe and everything", s.Message()); diff != "" {
|
||||
t.Fatalf("unexpected error in stream (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
expectedData := []byte("admin <admin@joe-perforce-server> (admin) accessed 2021/01/31")
|
||||
|
||||
if diff := cmp.Diff(expectedData, data); diff != "" {
|
||||
t.Fatalf("unexpected data (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty request", func(t *testing.T) {
|
||||
executil.UpdateRunCommandMock(defaultMockRunCommand)
|
||||
|
||||
_, client, closeFunc := startServer(t)
|
||||
t.Cleanup(closeFunc)
|
||||
|
||||
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to call P4Exec: %v", err)
|
||||
}
|
||||
|
||||
_, err = readAll(stream)
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("expected InvalidArgument error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("disallowed command", func(t *testing.T) {
|
||||
|
||||
executil.UpdateRunCommandMock(defaultMockRunCommand)
|
||||
|
||||
_, client, closeFunc := startServer(t)
|
||||
t.Cleanup(closeFunc)
|
||||
|
||||
stream, err := client.P4Exec(context.Background(), &proto.P4ExecRequest{
|
||||
Args: [][]byte{[]byte("bad_command")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to call P4Exec: %v", err)
|
||||
}
|
||||
|
||||
_, err = readAll(stream)
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("expected InvalidArgument error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("context cancelled", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
executil.UpdateRunCommandMock(func(ctx context.Context, _ *exec.Cmd) (int, error) {
|
||||
// fake a context cancellation that occurs while the process is running
|
||||
|
||||
cancel()
|
||||
return 0, ctx.Err()
|
||||
})
|
||||
|
||||
_, client, closeFunc := startServer(t)
|
||||
t.Cleanup(closeFunc)
|
||||
|
||||
stream, err := client.P4Exec(ctx, &proto.P4ExecRequest{
|
||||
Args: [][]byte{[]byte("users")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to call P4Exec: %v", err)
|
||||
}
|
||||
|
||||
_, err = readAll(stream)
|
||||
if !(errors.Is(err, context.Canceled) || status.Code(err) == codes.Canceled) {
|
||||
t.Fatalf("expected context cancelation error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
t.Run("HTTP", func(t *testing.T) {
|
||||
|
||||
tests := []Test{
|
||||
{
|
||||
Name: "Command",
|
||||
Request: newRequest("POST", "/p4-exec", strings.NewReader(`{"args": ["users"]}`)),
|
||||
ExpectedCode: http.StatusOK,
|
||||
ExpectedBody: "admin <admin@joe-perforce-server> (admin) accessed 2021/01/31",
|
||||
ExpectedTrailers: http.Header{
|
||||
"X-Exec-Error": {"the answer to life the universe and everything"},
|
||||
"X-Exec-Exit-Status": {"42"},
|
||||
"X-Exec-Stderr": {"teststderr"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Error",
|
||||
Request: newRequest("POST", "/p4-exec", strings.NewReader(`{"args": ["bad_command"]}`)),
|
||||
ExpectedCode: http.StatusBadRequest,
|
||||
ExpectedBody: "subcommand \"bad_command\" is not allowed",
|
||||
},
|
||||
{
|
||||
Name: "EmptyBody",
|
||||
Request: newRequest("POST", "/p4-exec", nil),
|
||||
ExpectedCode: http.StatusBadRequest,
|
||||
ExpectedBody: `EOF`,
|
||||
},
|
||||
{
|
||||
Name: "EmptyInput",
|
||||
Request: newRequest("POST", "/p4-exec", strings.NewReader("{}")),
|
||||
ExpectedCode: http.StatusBadRequest,
|
||||
ExpectedBody: `args must be greater than or equal to 1`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
executil.UpdateRunCommandMock(defaultMockRunCommand)
|
||||
|
||||
handler, _, closeFunc := startServer(t)
|
||||
t.Cleanup(closeFunc)
|
||||
|
||||
w := httptest.ResponseRecorder{Body: new(bytes.Buffer)}
|
||||
handler.ServeHTTP(&w, test.Request)
|
||||
|
||||
res := w.Result()
|
||||
if res.StatusCode != test.ExpectedCode {
|
||||
t.Errorf("wrong status: expected %d, got %d", test.ExpectedCode, w.Code)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.TrimSpace(string(body)) != test.ExpectedBody {
|
||||
t.Errorf("wrong body: expected %q, got %q", test.ExpectedBody, string(body))
|
||||
}
|
||||
|
||||
for k, v := range test.ExpectedTrailers {
|
||||
if got := res.Trailer.Get(k); got != v[0] {
|
||||
t.Errorf("wrong trailer %q: expected %q, got %q", k, v[0], got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func staticGetRemoteURL(remote string) func(context.Context, api.RepoName) (string, error) {
|
||||
return func(context.Context, api.RepoName) (string, error) {
|
||||
return remote, nil
|
||||
|
||||
@ -60,3 +60,9 @@ and in its place the `gitserver client` exposes an API supporting specific Git o
|
||||
| `Stat` | returns a FileInfo describing the named file at commit | | maps output from `git ls-tree` into `FileInfo` structs, with the object hash in the `Sys_` member |
|
||||
| `IsPerforcePathCloneable` | check if a given depot/depot path is cloneable with the given credentials | calls gitserver's `/is-perforce-path-cloneable` endpoint | |
|
||||
| `CheckPerforceCredentials` | check if a given Perforce credential is valid | calls gitserver's `/check-perforce-credentials` endpoint | |
|
||||
| `PerforceUsers` | list all perforce users | calls gitserver's `/perforce-users` endpoint | |
|
||||
| `PerforceProtectsForUser` | list all protections that apply to a user | calls gitserver's `/perforce-protects-for-user` endpoint | |
|
||||
| `PerforceProtectsForDepot` | list all protections that apply to a depot | calls gitserver's `/perforce-protects-for-depot` endpoint | |
|
||||
| `PerforceGroupMembers` | list all members of a Perforce group | calls gitserver's `/perforce-group-members` endpoint | |
|
||||
| `IsPerforceSuperUser` | check if a given ticket is for a super level user | calls gitserver's `/is-perforce-super-user` endpoint | |
|
||||
| `PerforceGetChangelist` | get details about a perforce changelist | calls gitserver's `/perforce-get-changelist` endpoint | |
|
||||
|
||||
@ -5,6 +5,7 @@ go_library(
|
||||
name = "perforce",
|
||||
srcs = [
|
||||
"authz.go",
|
||||
"debug.go",
|
||||
"perforce.go",
|
||||
"protects.go",
|
||||
],
|
||||
@ -16,7 +17,9 @@ go_library(
|
||||
"//internal/extsvc",
|
||||
"//internal/extsvc/perforce",
|
||||
"//internal/gitserver",
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/licensing",
|
||||
"//internal/perforce",
|
||||
"//internal/trace",
|
||||
"//internal/types",
|
||||
"//lib/errors",
|
||||
@ -48,12 +51,15 @@ go_test(
|
||||
"//internal/extsvc",
|
||||
"//internal/extsvc/perforce",
|
||||
"//internal/gitserver",
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/perforce",
|
||||
"//internal/types",
|
||||
"//lib/errors",
|
||||
"//schema",
|
||||
"@com_github_google_go_cmp//cmp",
|
||||
"@com_github_inconshreveable_log15//:log15",
|
||||
"@com_github_json_iterator_go//:go",
|
||||
"@com_github_sourcegraph_log//:log",
|
||||
"@com_github_sourcegraph_log//logtest",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
|
||||
@ -24,9 +24,6 @@ func TestPerformDebugScan(t *testing.T) {
|
||||
run(logger, "//depot/main/", input, false)
|
||||
|
||||
logged := exporter()
|
||||
// For now we'll just check that the count as well as first and last lines are
|
||||
// what we expect
|
||||
assert.Len(t, logged, 444)
|
||||
assert.Equal(t, "Converted depot to glob", logged[0].Message) // fails without error
|
||||
assert.Equal(t, "Include rule", logged[len(logged)-1].Message)
|
||||
}
|
||||
|
||||
80
internal/authz/providers/perforce/debug.go
Normal file
80
internal/authz/providers/perforce/debug.go
Normal file
@ -0,0 +1,80 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/authz"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
)
|
||||
|
||||
// PerformDebugScan will scan protections rules from r and log detailed
|
||||
// information about how each line was parsed.
|
||||
func PerformDebugScan(logger log.Logger, r io.Reader, depot extsvc.RepoID, ignoreRulesWithHost bool) (*authz.ExternalUserPermissions, error) {
|
||||
perms := &authz.ExternalUserPermissions{
|
||||
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
|
||||
}
|
||||
|
||||
pr, err := parseP4ProtectsRaw(r)
|
||||
if err != nil {
|
||||
return perms, err
|
||||
}
|
||||
|
||||
scanner := fullRepoPermsScanner(logger, perms, []extsvc.RepoID{depot})
|
||||
err = scanProtects(logger, pr, scanner, ignoreRulesWithHost)
|
||||
return perms, err
|
||||
}
|
||||
|
||||
func parseP4ProtectsRaw(rc io.Reader) ([]*p4types.Protect, error) {
|
||||
protects := make([]*p4types.Protect, 0)
|
||||
|
||||
scanner := bufio.NewScanner(rc)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Trim whitespace
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Skip comments and blank lines
|
||||
if strings.HasPrefix(line, "##") || line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Trim trailing comments
|
||||
if i := strings.Index(line, "##"); i > -1 {
|
||||
line = line[:i]
|
||||
}
|
||||
|
||||
// Split into fields
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 {
|
||||
continue
|
||||
}
|
||||
|
||||
parsedLine := p4ProtectLine{
|
||||
level: fields[0],
|
||||
entityType: fields[1],
|
||||
name: fields[2],
|
||||
match: fields[4],
|
||||
}
|
||||
if strings.HasPrefix(parsedLine.match, "-") {
|
||||
parsedLine.isExclusion = true // is an exclusion
|
||||
parsedLine.match = strings.TrimPrefix(parsedLine.match, "-") // trim leading -
|
||||
}
|
||||
|
||||
protects = append(protects, &p4types.Protect{
|
||||
Level: parsedLine.level,
|
||||
EntityType: parsedLine.entityType,
|
||||
EntityName: parsedLine.name,
|
||||
Host: fields[3],
|
||||
Match: parsedLine.match,
|
||||
IsExclusion: parsedLine.isExclusion,
|
||||
})
|
||||
}
|
||||
|
||||
return protects, scanner.Err()
|
||||
}
|
||||
@ -1,12 +1,8 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -17,6 +13,8 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/authz"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
"github.com/sourcegraph/sourcegraph/internal/trace"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
@ -38,7 +36,7 @@ type Provider struct {
|
||||
user string
|
||||
password string
|
||||
|
||||
p4Execer p4Execer
|
||||
gitserverClient gitserver.Client
|
||||
|
||||
emailsCacheMutex sync.RWMutex
|
||||
cachedAllUserEmails map[string]string // username -> email
|
||||
@ -54,15 +52,11 @@ func cacheIsUpToDate(lastUpdate time.Time) bool {
|
||||
return time.Since(lastUpdate) < cacheTTL
|
||||
}
|
||||
|
||||
type p4Execer interface {
|
||||
P4Exec(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error)
|
||||
}
|
||||
|
||||
// NewProvider returns a new Perforce authorization provider that uses the given
|
||||
// host, user and password to talk to a Perforce Server that is the source of
|
||||
// truth for permissions. It assumes emails of Sourcegraph accounts match 1-1
|
||||
// with emails of Perforce Server users.
|
||||
func NewProvider(logger log.Logger, p4Execer p4Execer, urn, host, user, password string, depots []extsvc.RepoID, ignoreRulesWithHost bool) *Provider {
|
||||
func NewProvider(logger log.Logger, gitserverClient gitserver.Client, urn, host, user, password string, depots []extsvc.RepoID, ignoreRulesWithHost bool) *Provider {
|
||||
baseURL, _ := url.Parse(host)
|
||||
return &Provider{
|
||||
logger: logger,
|
||||
@ -72,7 +66,7 @@ func NewProvider(logger log.Logger, p4Execer p4Execer, urn, host, user, password
|
||||
host: host,
|
||||
user: user,
|
||||
password: password,
|
||||
p4Execer: p4Execer,
|
||||
gitserverClient: gitserverClient,
|
||||
cachedGroupMembers: make(map[string][]string),
|
||||
ignoreRulesWithHost: ignoreRulesWithHost,
|
||||
}
|
||||
@ -104,24 +98,25 @@ func (p *Provider) FetchAccount(ctx context.Context, user *types.User, _ []*exts
|
||||
emailSet[email] = struct{}{}
|
||||
}
|
||||
|
||||
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "users")
|
||||
users, err := p.gitserverClient.PerforceUsers(ctx, protocol.PerforceConnectionDetails{
|
||||
P4Port: p.host,
|
||||
P4User: p.user,
|
||||
P4Passwd: p.password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list users")
|
||||
}
|
||||
defer func() { _ = rc.Close() }()
|
||||
|
||||
scanner := bufio.NewScanner(rc)
|
||||
for scanner.Scan() {
|
||||
username, email, ok := scanEmail(scanner)
|
||||
if !ok {
|
||||
for _, p4User := range users {
|
||||
if p4User.Email == "" || p4User.Username == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := emailSet[email]; ok {
|
||||
if _, ok := emailSet[p4User.Email]; ok {
|
||||
accountData, err := jsoniter.Marshal(
|
||||
perforce.AccountData{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Username: p4User.Username,
|
||||
Email: p4User.Email,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@ -133,7 +128,7 @@ func (p *Provider) FetchAccount(ctx context.Context, user *types.User, _ []*exts
|
||||
AccountSpec: extsvc.AccountSpec{
|
||||
ServiceType: p.codeHost.ServiceType,
|
||||
ServiceID: p.codeHost.ServiceID,
|
||||
AccountID: email,
|
||||
AccountID: p4User.Email,
|
||||
},
|
||||
AccountData: extsvc.AccountData{
|
||||
Data: extsvc.NewUnencryptedData(accountData),
|
||||
@ -141,12 +136,7 @@ func (p *Provider) FetchAccount(ctx context.Context, user *types.User, _ []*exts
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, errors.Wrap(err, "scanner.Err")
|
||||
}
|
||||
|
||||
// Drain remaining body
|
||||
_, _ = io.Copy(io.Discard, rc)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -167,22 +157,23 @@ func (p *Provider) FetchUserPerms(ctx context.Context, account *extsvc.Account,
|
||||
return nil, errors.New("no user found in the external account data")
|
||||
}
|
||||
|
||||
// -u User : Displays protection lines that apply to the named user. This option
|
||||
// requires super access.
|
||||
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "protects", "-u", user.Username)
|
||||
protects, err := p.gitserverClient.PerforceProtectsForUser(ctx, protocol.PerforceConnectionDetails{
|
||||
P4Port: p.host,
|
||||
P4User: p.user,
|
||||
P4Passwd: p.password,
|
||||
}, user.Username)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list ACLs by user")
|
||||
}
|
||||
defer func() { _ = rc.Close() }()
|
||||
|
||||
// Pull permissions from protects file.
|
||||
perms := &authz.ExternalUserPermissions{}
|
||||
if len(p.depots) == 0 {
|
||||
err = errors.Wrap(scanProtects(p.logger, rc, repoIncludesExcludesScanner(perms), p.ignoreRulesWithHost), "repoIncludesExcludesScanner")
|
||||
err = errors.Wrap(scanProtects(p.logger, protects, repoIncludesExcludesScanner(perms), p.ignoreRulesWithHost), "repoIncludesExcludesScanner")
|
||||
} else {
|
||||
// SubRepoPermissions-enabled code path
|
||||
perms.SubRepoPermissions = make(map[extsvc.RepoID]*authz.SubRepoPermissions, len(p.depots))
|
||||
err = errors.Wrap(scanProtects(p.logger, rc, fullRepoPermsScanner(p.logger, perms, p.depots), p.ignoreRulesWithHost), "fullRepoPermsScanner")
|
||||
err = errors.Wrap(scanProtects(p.logger, protects, fullRepoPermsScanner(p.logger, perms, p.depots), p.ignoreRulesWithHost), "fullRepoPermsScanner")
|
||||
}
|
||||
|
||||
// As per interface definition for this method, implementation should return
|
||||
@ -197,22 +188,20 @@ func (p *Provider) getAllUserEmails(ctx context.Context) (map[string]string, err
|
||||
}
|
||||
|
||||
userEmails := make(map[string]string)
|
||||
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "users")
|
||||
users, err := p.gitserverClient.PerforceUsers(ctx, protocol.PerforceConnectionDetails{
|
||||
P4Port: p.host,
|
||||
P4User: p.user,
|
||||
P4Passwd: p.password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list users")
|
||||
}
|
||||
defer func() { _ = rc.Close() }()
|
||||
|
||||
scanner := bufio.NewScanner(rc)
|
||||
for scanner.Scan() {
|
||||
username, email, ok := scanEmail(scanner)
|
||||
if !ok {
|
||||
for _, p4User := range users {
|
||||
if p4User.Username == "" || p4User.Email == "" {
|
||||
continue
|
||||
}
|
||||
userEmails[username] = email
|
||||
}
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, errors.Wrap(err, "scanner.Err")
|
||||
userEmails[p4User.Username] = p4User.Email
|
||||
}
|
||||
|
||||
p.emailsCacheMutex.Lock()
|
||||
@ -248,39 +237,19 @@ func (p *Provider) getGroupMembers(ctx context.Context, group string) ([]string,
|
||||
|
||||
p.groupsCacheMutex.Lock()
|
||||
defer p.groupsCacheMutex.Unlock()
|
||||
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "group", "-o", group)
|
||||
|
||||
members, err := p.gitserverClient.PerforceGroupMembers(
|
||||
ctx,
|
||||
protocol.PerforceConnectionDetails{
|
||||
P4Port: p.host,
|
||||
P4User: p.user,
|
||||
P4Passwd: p.password,
|
||||
},
|
||||
group,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list group members")
|
||||
}
|
||||
defer func() { _ = rc.Close() }()
|
||||
|
||||
var members []string
|
||||
startScan := false
|
||||
scanner := bufio.NewScanner(rc)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Only start scan when we encounter the "Users:" line
|
||||
if !startScan {
|
||||
if strings.HasPrefix(line, "Users:") {
|
||||
startScan = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Lines for users always start with a tab "\t"
|
||||
if !strings.HasPrefix(line, "\t") {
|
||||
break
|
||||
}
|
||||
|
||||
members = append(members, strings.TrimSpace(line))
|
||||
}
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, errors.Wrap(err, "scanner.Err")
|
||||
}
|
||||
|
||||
// Drain remaining body
|
||||
_, _ = io.Copy(io.Discard, rc)
|
||||
|
||||
p.cachedGroupMembers[group] = members
|
||||
p.groupsCacheLastUpdate = time.Now()
|
||||
@ -334,16 +303,21 @@ func (p *Provider) FetchRepoPerms(ctx context.Context, repo *extsvc.Repository,
|
||||
return nil, &authz.ErrUnimplemented{Feature: "perforce.FetchRepoPerms for sub-repo permissions"}
|
||||
}
|
||||
|
||||
// -a : Displays protection lines for all users. This option requires super
|
||||
// access.
|
||||
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "protects", "-a", repo.ID)
|
||||
protects, err := p.gitserverClient.PerforceProtectsForDepot(
|
||||
ctx,
|
||||
protocol.PerforceConnectionDetails{
|
||||
P4Port: p.host,
|
||||
P4User: p.user,
|
||||
P4Passwd: p.password,
|
||||
},
|
||||
repo.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "list ACLs by depot")
|
||||
}
|
||||
defer func() { _ = rc.Close() }()
|
||||
|
||||
users := make(map[string]struct{})
|
||||
if err := scanProtects(p.logger, rc, allUsersScanner(ctx, p, users), p.ignoreRulesWithHost); err != nil {
|
||||
if err := scanProtects(p.logger, protects, allUsersScanner(ctx, p, users), p.ignoreRulesWithHost); err != nil {
|
||||
return nil, errors.Wrap(err, "scanning protects")
|
||||
}
|
||||
|
||||
@ -379,25 +353,9 @@ func (p *Provider) URN() string {
|
||||
}
|
||||
|
||||
func (p *Provider) ValidateConnection(ctx context.Context) error {
|
||||
// Validate the user has "super" access with "-u" option, see https://www.perforce.com/perforce/r12.1/manuals/cmdref/protects.html
|
||||
rc, _, err := p.p4Execer.P4Exec(ctx, p.host, p.user, p.password, "protects", "-u", p.user)
|
||||
if err == nil {
|
||||
_ = rc.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "You don't have permission for this operation.") {
|
||||
return errors.New("the user does not have super access")
|
||||
}
|
||||
return errors.Wrap(err, "invalid user access level")
|
||||
}
|
||||
|
||||
func scanEmail(s *bufio.Scanner) (string, string, bool) {
|
||||
fields := strings.Fields(s.Text())
|
||||
if len(fields) < 2 {
|
||||
return "", "", false
|
||||
}
|
||||
username := fields[0] // e.g. alice
|
||||
email := strings.Trim(fields[1], "<>") // e.g. alice@example.com
|
||||
return username, email, true
|
||||
return p.gitserverClient.IsPerforceSuperUser(ctx, protocol.PerforceConnectionDetails{
|
||||
P4Port: p.host,
|
||||
P4User: p.user,
|
||||
P4Passwd: p.password,
|
||||
})
|
||||
}
|
||||
|
||||
@ -3,14 +3,11 @@ package perforce
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/sourcegraph/log"
|
||||
"github.com/sourcegraph/log/logtest"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
@ -19,7 +16,10 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
func TestProvider_FetchAccount(t *testing.T) {
|
||||
@ -30,16 +30,14 @@ func TestProvider_FetchAccount(t *testing.T) {
|
||||
Username: "alice",
|
||||
}
|
||||
|
||||
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
|
||||
data := `
|
||||
alice <alice@example.com> (Alice) accessed 2020/12/04
|
||||
cindy <cindy@example.com> (Cindy) accessed 2020/12/04
|
||||
`
|
||||
return io.NopCloser(strings.NewReader(data)), nil, nil
|
||||
})
|
||||
gitserverClient := gitserver.NewStrictMockClient()
|
||||
gitserverClient.PerforceUsersFunc.SetDefaultReturn([]*p4types.User{
|
||||
{Username: "alice", Email: "alice@example.com"},
|
||||
{Username: "cindy", Email: "cindy@example.com"},
|
||||
}, nil)
|
||||
|
||||
t.Run("no matching account", func(t *testing.T) {
|
||||
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
|
||||
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
got, err := p.FetchAccount(ctx, user, nil, []string{"bob@example.com"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -51,7 +49,7 @@ cindy <cindy@example.com> (Cindy) accessed 2020/12/04
|
||||
})
|
||||
|
||||
t.Run("found matching account", func(t *testing.T) {
|
||||
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
|
||||
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
got, err := p.FetchAccount(ctx, user, nil, []string{"alice@example.com"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -89,7 +87,7 @@ func TestProvider_FetchUserPerms(t *testing.T) {
|
||||
|
||||
t.Run("nil account", func(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
p := NewProvider(logger, gitserver.NewClient(), "", "ssl:111.222.333.444:1666", "admin", "password", nil, false)
|
||||
p := NewProvider(logger, gitserver.NewClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
_, err := p.FetchUserPerms(ctx, nil, authz.FetchPermsOptions{})
|
||||
want := "no account provided"
|
||||
got := fmt.Sprintf("%v", err)
|
||||
@ -149,12 +147,12 @@ func TestProvider_FetchUserPerms(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
response string
|
||||
protects []*p4types.Protect
|
||||
wantPerms *authz.ExternalUserPermissions
|
||||
}{
|
||||
{
|
||||
name: "include only",
|
||||
response: `
|
||||
protects: testParseP4ProtectsRaw(t, strings.NewReader(`
|
||||
list user alice * //Sourcegraph/Security/... ## "list" can't grant read access
|
||||
read user alice * //Sourcegraph/Engineering/...
|
||||
owner user alice * //Sourcegraph/Engineering/Backend/...
|
||||
@ -162,7 +160,7 @@ open user alice * //Sourcegraph/Engineering/Frontend/...
|
||||
review user alice * //Sourcegraph/Handbook/...
|
||||
review user alice * //Sourcegraph/*/Handbook/...
|
||||
review user alice * //Sourcegraph/.../Handbook/...
|
||||
`,
|
||||
`)),
|
||||
wantPerms: &authz.ExternalUserPermissions{
|
||||
IncludeContains: []extsvc.RepoID{
|
||||
"//Sourcegraph/Engineering/%",
|
||||
@ -176,7 +174,7 @@ review user alice * //Sourcegraph/.../Handbook/...
|
||||
},
|
||||
{
|
||||
name: "exclude only",
|
||||
response: `
|
||||
protects: testParseP4ProtectsRaw(t, strings.NewReader(`
|
||||
list user alice * -//Sourcegraph/Security/...
|
||||
read user alice * -//Sourcegraph/Engineering/...
|
||||
owner user alice * -//Sourcegraph/Engineering/Backend/...
|
||||
@ -184,8 +182,7 @@ open user alice * -//Sourcegraph/Engineering/Frontend/...
|
||||
review user alice * -//Sourcegraph/Handbook/...
|
||||
review user alice * -//Sourcegraph/*/Handbook/...
|
||||
review user alice * -//Sourcegraph/.../Handbook/...
|
||||
`,
|
||||
wantPerms: &authz.ExternalUserPermissions{
|
||||
`)), wantPerms: &authz.ExternalUserPermissions{
|
||||
ExcludeContains: []extsvc.RepoID{
|
||||
"//Sourcegraph/[^/]+/Handbook/%",
|
||||
"//Sourcegraph/%/Handbook/%",
|
||||
@ -194,7 +191,7 @@ review user alice * -//Sourcegraph/.../Handbook/...
|
||||
},
|
||||
{
|
||||
name: "include and exclude",
|
||||
response: `
|
||||
protects: testParseP4ProtectsRaw(t, strings.NewReader(`
|
||||
read user alice * //Sourcegraph/Security/...
|
||||
read user alice * //Sourcegraph/Engineering/...
|
||||
owner user alice * //Sourcegraph/Engineering/Backend/...
|
||||
@ -208,7 +205,7 @@ list user alice * -//Sourcegraph/Security/... ## "list" c
|
||||
open user alice * -//Sourcegraph/Engineering/Backend/Credentials/... ## sub-match of a previous include
|
||||
open user alice * -//Sourcegraph/Engineering/*/Frontend/Folder/... ## sub-match of a previous include
|
||||
open user alice * -//Sourcegraph/*/Handbook/... ## sub-match of wildcard A include
|
||||
`,
|
||||
`)),
|
||||
wantPerms: &authz.ExternalUserPermissions{
|
||||
IncludeContains: []extsvc.RepoID{
|
||||
"//Sourcegraph/Engineering/%",
|
||||
@ -228,7 +225,7 @@ open user alice * -//Sourcegraph/*/Handbook/... ## sub-matc
|
||||
},
|
||||
{
|
||||
name: "include and exclude, then include again",
|
||||
response: `
|
||||
protects: testParseP4ProtectsRaw(t, strings.NewReader(`
|
||||
read user alice * //Sourcegraph/Security/...
|
||||
read user alice * //Sourcegraph/Engineering/...
|
||||
owner user alice * //Sourcegraph/Engineering/Backend/...
|
||||
@ -244,7 +241,7 @@ open user alice * -//Sourcegraph/Engineering/*/Frontend/Folder/... ## sub-matc
|
||||
open user alice * -//Sourcegraph/*/Handbook/... ## sub-match of wildcard A include
|
||||
|
||||
read user alice * //Sourcegraph/Security/... ## give access to alice again after revoking
|
||||
`,
|
||||
`)),
|
||||
wantPerms: &authz.ExternalUserPermissions{
|
||||
IncludeContains: []extsvc.RepoID{
|
||||
"//Sourcegraph/Engineering/%",
|
||||
@ -267,11 +264,10 @@ read user alice * //Sourcegraph/Security/... ## give access to alice agai
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
|
||||
return io.NopCloser(strings.NewReader(test.response)), nil, nil
|
||||
})
|
||||
gc := gitserver.NewStrictMockClient()
|
||||
gc.PerforceProtectsForUserFunc.SetDefaultReturn(test.protects, nil)
|
||||
|
||||
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
|
||||
p := NewProvider(logger, gc, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
got, err := p.FetchUserPerms(ctx,
|
||||
&extsvc.Account{
|
||||
AccountSpec: extsvc.AccountSpec{
|
||||
@ -297,13 +293,16 @@ read user alice * //Sourcegraph/Security/... ## give access to alice agai
|
||||
// Specific behaviour is tested in TestScanFullRepoPermissions
|
||||
t.Run("SubRepoPermissions", func(t *testing.T) {
|
||||
logger := logtest.Scoped(t)
|
||||
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
|
||||
return io.NopCloser(strings.NewReader(`
|
||||
read user alice * //Sourcegraph/Engineering/...
|
||||
read user alice * -//Sourcegraph/Security/...
|
||||
`)), nil, nil
|
||||
})
|
||||
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
|
||||
|
||||
gitserverClient := gitserver.NewStrictMockClient()
|
||||
ps := []*p4types.Protect{
|
||||
{Level: "read", EntityType: "user", EntityName: "alice", Host: "*", Match: "//Sourcegraph/Engineering/..."},
|
||||
{Level: "read", EntityType: "user", EntityName: "alice", Host: "*", Match: "//Sourcegraph/Security/...", IsExclusion: true},
|
||||
}
|
||||
gitserverClient.PerforceProtectsForDepotFunc.SetDefaultReturn(ps, nil)
|
||||
gitserverClient.PerforceProtectsForUserFunc.SetDefaultReturn(ps, nil)
|
||||
|
||||
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
p.depots = append(p.depots, "//Sourcegraph/")
|
||||
|
||||
got, err := p.FetchUserPerms(ctx,
|
||||
@ -370,54 +369,36 @@ func TestProvider_FetchRepoPerms(t *testing.T) {
|
||||
t.Fatalf("err: want %q but got %q", want, got)
|
||||
}
|
||||
})
|
||||
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
|
||||
var data string
|
||||
|
||||
switch args[0] {
|
||||
|
||||
case "protects":
|
||||
data = `
|
||||
## The actual depot prefix does not matter, the "-" sign does
|
||||
list user * * -//...
|
||||
write user alice * //Sourcegraph/...
|
||||
write user bob * //Sourcegraph/...
|
||||
admin group Backend * //Sourcegraph/... ## includes "alice" and "cindy"
|
||||
|
||||
admin group Frontend * -//Sourcegraph/... ## excludes "bob", "david" and "frank"
|
||||
read user cindy * -//Sourcegraph/...
|
||||
|
||||
list user david * //Sourcegraph/... ## "list" can't grant read access
|
||||
`
|
||||
case "users":
|
||||
data = `
|
||||
alice <alice@example.com> (Alice) accessed 2020/12/04
|
||||
bob <bob@example.com> (Bob) accessed 2020/12/04
|
||||
cindy <cindy@example.com> (Cindy) accessed 2020/12/04
|
||||
david <david@example.com> (David) accessed 2020/12/04
|
||||
frank <frank@example.com> (Frank) accessed 2020/12/04
|
||||
`
|
||||
case "group":
|
||||
switch args[2] {
|
||||
case "Backend":
|
||||
data = `
|
||||
Users:
|
||||
alice
|
||||
cindy
|
||||
`
|
||||
case "Frontend":
|
||||
data = `
|
||||
Users:
|
||||
bob
|
||||
david
|
||||
frank
|
||||
`
|
||||
}
|
||||
gitserverClient := gitserver.NewStrictMockClient()
|
||||
gitserverClient.PerforceUsersFunc.SetDefaultReturn([]*p4types.User{
|
||||
{Username: "alice", Email: "alice@example.com"},
|
||||
{Username: "bob", Email: "bob@example.com"},
|
||||
{Username: "cindy", Email: "cindy@example.com"},
|
||||
{Username: "david", Email: "david@example.com"},
|
||||
{Username: "frank", Email: "frank@example.com"},
|
||||
}, nil)
|
||||
gitserverClient.PerforceGroupMembersFunc.SetDefaultHook(func(ctx context.Context, conn protocol.PerforceConnectionDetails, group string) ([]string, error) {
|
||||
switch group {
|
||||
case "Backend":
|
||||
return []string{"alice", "cindy"}, nil
|
||||
case "Frontend":
|
||||
return []string{"bob", "david", "frank"}, nil
|
||||
default:
|
||||
return nil, errors.New("invalid group")
|
||||
}
|
||||
|
||||
return io.NopCloser(strings.NewReader(data)), nil, nil
|
||||
})
|
||||
gitserverClient.PerforceProtectsForDepotFunc.SetDefaultReturn([]*p4types.Protect{
|
||||
{Level: "list", EntityType: "user", EntityName: "*", Host: "*", Match: "//...", IsExclusion: true},
|
||||
{Level: "write", EntityType: "user", EntityName: "alice", Host: "*", Match: "//Sourcegraph/..."},
|
||||
{Level: "write", EntityType: "user", EntityName: "bob", Host: "*", Match: "//Sourcegraph/..."},
|
||||
{Level: "admin", EntityType: "group", EntityName: "Backend", Host: "*", Match: "//Sourcegraph/..."}, // includes "alice" and "cindy"
|
||||
{Level: "admin", EntityType: "group", EntityName: "Frontend", Host: "*", Match: "//Sourcegraph/...", IsExclusion: true}, // excludes "bob", "david" and "frank"
|
||||
{Level: "read", EntityType: "user", EntityName: "cindy", Host: "*", Match: "//Sourcegraph/...", IsExclusion: true},
|
||||
{Level: "list", EntityType: "user", EntityName: "david", Host: "*", Match: "//Sourcegraph/..."}, // "list" can't grant read access
|
||||
}, nil)
|
||||
|
||||
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
|
||||
p := NewProvider(logger, gitserverClient, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
got, err := p.FetchRepoPerms(ctx,
|
||||
&extsvc.Repository{
|
||||
URI: "gitlab.com/user/repo",
|
||||
@ -437,15 +418,3 @@ Users:
|
||||
t.Fatalf("Mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func NewTestProvider(logger log.Logger, urn, host, user, password string, execer p4Execer) *Provider {
|
||||
p := NewProvider(logger, gitserver.NewClient(), urn, host, user, password, []extsvc.RepoID{}, false)
|
||||
p.p4Execer = execer
|
||||
return p
|
||||
}
|
||||
|
||||
type p4ExecFunc func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error)
|
||||
|
||||
func (p p4ExecFunc) P4Exec(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
|
||||
return p(ctx, host, user, password, args...)
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
@ -12,6 +10,7 @@ import (
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/authz"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
@ -183,17 +182,6 @@ func matchesAgainstDepot(match globMatch, depot string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// PerformDebugScan will scan protections rules from r and log detailed
|
||||
// information about how each line was parsed.
|
||||
func PerformDebugScan(logger log.Logger, r io.Reader, depot extsvc.RepoID, ignoreRulesWithHost bool) (*authz.ExternalUserPermissions, error) {
|
||||
perms := &authz.ExternalUserPermissions{
|
||||
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
|
||||
}
|
||||
scanner := fullRepoPermsScanner(logger, perms, []extsvc.RepoID{depot})
|
||||
err := scanProtects(logger, r, scanner, ignoreRulesWithHost)
|
||||
return perms, err
|
||||
}
|
||||
|
||||
// protectsScanner provides callbacks for scanning the output of `p4 protects`.
|
||||
type protectsScanner struct {
|
||||
// Called on the parsed contents of each `p4 protects` line.
|
||||
@ -205,55 +193,26 @@ type protectsScanner struct {
|
||||
// scanProtects is a utility function for processing values from `p4 protects`.
|
||||
// It handles skipping comments, cleaning whitespace, parsing relevant fields, and
|
||||
// skipping entries that do not affect read access.
|
||||
func scanProtects(logger log.Logger, rc io.Reader, s *protectsScanner, ignoreRulesWithHost bool) error {
|
||||
func scanProtects(logger log.Logger, protects []*perforce.Protect, s *protectsScanner, ignoreRulesWithHost bool) error {
|
||||
logger = logger.Scoped("scanProtects", "")
|
||||
scanner := bufio.NewScanner(rc)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Trim whitespace
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Skip comments and blank lines
|
||||
if strings.HasPrefix(line, "##") || line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Trim trailing comments
|
||||
if i := strings.Index(line, "##"); i > -1 {
|
||||
line = line[:i]
|
||||
}
|
||||
|
||||
logger.Debug("Scanning protects line", log.String("line", line))
|
||||
|
||||
// Split into fields
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 {
|
||||
logger.Debug("Line has less than 5 fields, discarding")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, protect := range protects {
|
||||
// skip any rule that relies on particular client IP addresses or hostnames
|
||||
// this is the initial approach to address wrong behaviors
|
||||
// that are causing clients to need to disable sub-repo permissions
|
||||
// GitHub issue: https://github.com/sourcegraph/sourcegraph/issues/53374
|
||||
// Subsequent approaches will need to add more sophisticated handling of hosts
|
||||
// perhaps even capturing the browser IP address and comparing it to the host field.
|
||||
if ignoreRulesWithHost && fields[3] != "*" {
|
||||
logger.Debug("Skipping host-specific rule", log.String("line", line))
|
||||
if ignoreRulesWithHost && protect.Host != "*" {
|
||||
logger.Debug("Skipping host-specific rule", log.String("protect", fmt.Sprintf("%#v", protect)))
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse line
|
||||
parsedLine := p4ProtectLine{
|
||||
level: fields[0],
|
||||
entityType: fields[1],
|
||||
name: fields[2],
|
||||
match: fields[4],
|
||||
}
|
||||
if strings.HasPrefix(parsedLine.match, "-") {
|
||||
parsedLine.isExclusion = true // is an exclusion
|
||||
parsedLine.match = strings.TrimPrefix(parsedLine.match, "-") // trim leading -
|
||||
level: protect.Level,
|
||||
entityType: protect.EntityType,
|
||||
name: protect.EntityName,
|
||||
match: protect.Match,
|
||||
isExclusion: protect.IsExclusion,
|
||||
}
|
||||
|
||||
// We only care about read access. If the permission doesn't change read access,
|
||||
@ -269,12 +228,12 @@ func scanProtects(logger log.Logger, rc io.Reader, s *protectsScanner, ignoreRul
|
||||
return err
|
||||
}
|
||||
}
|
||||
var finalizeErr error
|
||||
|
||||
if s.finalize != nil {
|
||||
finalizeErr = s.finalize()
|
||||
return s.finalize()
|
||||
}
|
||||
scanErr := scanner.Err()
|
||||
return errors.CombineErrors(scanErr, finalizeErr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanRepoIncludesExcludes converts `p4 protects` to Postgres SIMILAR TO-compatible
|
||||
|
||||
@ -5,13 +5,13 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/sourcegraph/log/logtest"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
@ -19,6 +19,8 @@ import (
|
||||
srp "github.com/sourcegraph/sourcegraph/internal/authz/subrepoperms"
|
||||
"github.com/sourcegraph/sourcegraph/internal/conf"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
p4types "github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
@ -229,11 +231,7 @@ func TestScanFullRepoPermissions(t *testing.T) {
|
||||
|
||||
rc := io.NopCloser(bytes.NewReader(data))
|
||||
|
||||
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
|
||||
return rc, nil, nil
|
||||
})
|
||||
|
||||
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
|
||||
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
p.depots = []extsvc.RepoID{
|
||||
"//depot/main/",
|
||||
"//depot/training/",
|
||||
@ -244,7 +242,7 @@ func TestScanFullRepoPermissions(t *testing.T) {
|
||||
perms := &authz.ExternalUserPermissions{
|
||||
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
|
||||
}
|
||||
if err := scanProtects(logger, rc, fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
|
||||
if err := scanProtects(logger, testParseP4ProtectsRaw(t, rc), fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -314,18 +312,14 @@ func TestScanFullRepoPermissionsWithWildcardMatchingDepot(t *testing.T) {
|
||||
|
||||
rc := io.NopCloser(bytes.NewReader(data))
|
||||
|
||||
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
|
||||
return rc, nil, nil
|
||||
})
|
||||
|
||||
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
|
||||
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
p.depots = []extsvc.RepoID{
|
||||
"//depot/main/base/",
|
||||
}
|
||||
perms := &authz.ExternalUserPermissions{
|
||||
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
|
||||
}
|
||||
if err := scanProtects(logger, rc, fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
|
||||
if err := scanProtects(logger, testParseP4ProtectsRaw(t, rc), fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -614,17 +608,15 @@ read group Dev1 * //depot/main/.../*.go
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
|
||||
return rc, nil, nil
|
||||
})
|
||||
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
|
||||
|
||||
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
p.depots = []extsvc.RepoID{
|
||||
extsvc.RepoID(tc.depot),
|
||||
}
|
||||
perms := &authz.ExternalUserPermissions{
|
||||
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
|
||||
}
|
||||
if err := scanProtects(logger, rc, fullRepoPermsScanner(logger, perms, p.depots), true); err != nil {
|
||||
if err := scanProtects(logger, testParseP4ProtectsRaw(t, rc), fullRepoPermsScanner(logger, perms, p.depots), true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rules, ok := perms.SubRepoPermissions[extsvc.RepoID(tc.depot)]
|
||||
@ -680,18 +672,14 @@ func TestFullScanWildcardDepotMatching(t *testing.T) {
|
||||
|
||||
rc := io.NopCloser(bytes.NewReader(data))
|
||||
|
||||
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
|
||||
return rc, nil, nil
|
||||
})
|
||||
|
||||
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
|
||||
p := NewProvider(logger, gitserver.NewStrictMockClient(), "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
p.depots = []extsvc.RepoID{
|
||||
"//depot/654/deploy/base/",
|
||||
}
|
||||
perms := &authz.ExternalUserPermissions{
|
||||
SubRepoPermissions: make(map[extsvc.RepoID]*authz.SubRepoPermissions),
|
||||
}
|
||||
if err := scanProtects(logger, rc, fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
|
||||
if err := scanProtects(logger, testParseP4ProtectsRaw(t, rc), fullRepoPermsScanner(logger, perms, p.depots), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -829,12 +817,9 @@ func TestScanAllUsers(t *testing.T) {
|
||||
}
|
||||
|
||||
rc := io.NopCloser(bytes.NewReader(data))
|
||||
|
||||
execer := p4ExecFunc(func(ctx context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error) {
|
||||
return rc, nil, nil
|
||||
})
|
||||
|
||||
p := NewTestProvider(logger, "", "ssl:111.222.333.444:1666", "admin", "password", execer)
|
||||
gc := gitserver.NewStrictMockClient()
|
||||
gc.PerforceGroupMembersFunc.SetDefaultReturn(nil, nil)
|
||||
p := NewProvider(logger, gc, "", "ssl:111.222.333.444:1666", "admin", "password", []extsvc.RepoID{}, false)
|
||||
p.cachedGroupMembers = map[string][]string{
|
||||
"dev": {"user1", "user2"},
|
||||
}
|
||||
@ -844,7 +829,7 @@ func TestScanAllUsers(t *testing.T) {
|
||||
}
|
||||
|
||||
users := make(map[string]struct{})
|
||||
if err := scanProtects(logger, rc, allUsersScanner(ctx, p, users), false); err != nil {
|
||||
if err := scanProtects(logger, testParseP4ProtectsRaw(t, rc), allUsersScanner(ctx, p, users), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]struct{}{
|
||||
@ -855,3 +840,9 @@ func TestScanAllUsers(t *testing.T) {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func testParseP4ProtectsRaw(t *testing.T, rc io.Reader) []*p4types.Protect {
|
||||
protects, err := parseP4ProtectsRaw(rc)
|
||||
require.NoError(t, err)
|
||||
return protects
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ go_library(
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/httpcli",
|
||||
"//internal/jsonc",
|
||||
"//internal/perforce",
|
||||
"//internal/types",
|
||||
"//internal/vcs",
|
||||
"//lib/errors",
|
||||
@ -104,6 +105,7 @@ go_test(
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/httpcli",
|
||||
"//internal/httptestutil",
|
||||
"//internal/perforce",
|
||||
"//internal/ratelimit",
|
||||
"//internal/rcache",
|
||||
"//internal/testutil",
|
||||
|
||||
992
internal/batches/sources/mocks_test.go
generated
992
internal/batches/sources/mocks_test.go
generated
File diff suppressed because it is too large
Load Diff
@ -11,15 +11,16 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
"github.com/sourcegraph/sourcegraph/internal/httpcli"
|
||||
"github.com/sourcegraph/sourcegraph/internal/jsonc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
)
|
||||
|
||||
type PerforceSource struct {
|
||||
server schema.PerforceConnection
|
||||
conn schema.PerforceConnection
|
||||
gitServerClient gitserver.Client
|
||||
perforceCreds *gitserver.PerforceCredentials
|
||||
perforceCreds *protocol.PerforceConnectionDetails
|
||||
}
|
||||
|
||||
func NewPerforceSource(ctx context.Context, gitserverClient gitserver.Client, svc *types.ExternalService, _ *httpcli.Factory) (*PerforceSource, error) {
|
||||
@ -33,13 +34,17 @@ func NewPerforceSource(ctx context.Context, gitserverClient gitserver.Client, sv
|
||||
}
|
||||
|
||||
return &PerforceSource{
|
||||
server: c,
|
||||
conn: c,
|
||||
gitServerClient: gitserverClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GitserverPushConfig returns an authenticated push config used for pushing commits to the code host.
|
||||
func (s PerforceSource) GitserverPushConfig(repo *types.Repo) (*protocol.PushConfig, error) {
|
||||
if s.perforceCreds == nil {
|
||||
return nil, errors.New("no credentials set for Perforce Source")
|
||||
}
|
||||
|
||||
// Return a PushConfig with a crafted URL that includes the Perforce scheme and the credentials
|
||||
// The perforce scheme will tell `createCommitFromPatch` that this repo is a Perforce repo
|
||||
// so it can handle it differently from Git repos.
|
||||
@ -49,7 +54,7 @@ func (s PerforceSource) GitserverPushConfig(repo *types.Repo) (*protocol.PushCon
|
||||
if err == nil {
|
||||
depot = "//" + u.Path + "/"
|
||||
}
|
||||
remoteURL := fmt.Sprintf("perforce://%s:%s@%s%s", s.perforceCreds.Username, s.perforceCreds.Password, s.server.P4Port, depot)
|
||||
remoteURL := fmt.Sprintf("perforce://%s:%s@%s%s", s.perforceCreds.P4User, s.perforceCreds.P4Passwd, s.conn.P4Port, depot)
|
||||
return &protocol.PushConfig{
|
||||
RemoteURL: remoteURL,
|
||||
}, nil
|
||||
@ -61,16 +66,16 @@ func (s PerforceSource) GitserverPushConfig(repo *types.Repo) (*protocol.PushCon
|
||||
func (s PerforceSource) WithAuthenticator(a auth.Authenticator) (ChangesetSource, error) {
|
||||
switch av := a.(type) {
|
||||
case *auth.BasicAuthWithSSH:
|
||||
s.perforceCreds = &gitserver.PerforceCredentials{
|
||||
Username: av.Username,
|
||||
Password: av.Password,
|
||||
Host: s.server.P4Port,
|
||||
s.perforceCreds = &protocol.PerforceConnectionDetails{
|
||||
P4Port: s.conn.P4Port,
|
||||
P4User: av.Username,
|
||||
P4Passwd: av.Password,
|
||||
}
|
||||
case *auth.BasicAuth:
|
||||
s.perforceCreds = &gitserver.PerforceCredentials{
|
||||
Username: av.Username,
|
||||
Password: av.Password,
|
||||
Host: s.server.P4Port,
|
||||
s.perforceCreds = &protocol.PerforceConnectionDetails{
|
||||
P4Port: s.conn.P4Port,
|
||||
P4User: av.Username,
|
||||
P4Passwd: av.Password,
|
||||
}
|
||||
default:
|
||||
return s, errors.New("unexpected auther type for Perforce Source")
|
||||
@ -85,7 +90,7 @@ func (s PerforceSource) ValidateAuthenticator(ctx context.Context) error {
|
||||
if s.perforceCreds == nil {
|
||||
return errors.New("no credentials set for Perforce Source")
|
||||
}
|
||||
return s.gitServerClient.CheckPerforceCredentials(ctx, s.perforceCreds.Host, s.perforceCreds.Username, s.perforceCreds.Password)
|
||||
return s.gitServerClient.CheckPerforceCredentials(ctx, *s.perforceCreds)
|
||||
}
|
||||
|
||||
// LoadChangeset loads the given Changeset from the source and updates it. If
|
||||
@ -95,10 +100,11 @@ func (s PerforceSource) LoadChangeset(ctx context.Context, cs *Changeset) error
|
||||
if s.perforceCreds == nil {
|
||||
return errors.New("no credentials set for Perforce Source")
|
||||
}
|
||||
cl, err := s.gitServerClient.P4GetChangelist(ctx, cs.ExternalID, *s.perforceCreds)
|
||||
cl, err := s.gitServerClient.PerforceGetChangelist(ctx, *s.perforceCreds, cs.ExternalID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting changelist")
|
||||
}
|
||||
|
||||
return errors.Wrap(s.setChangesetMetadata(cl, cs), "setting perforce changeset metadata")
|
||||
}
|
||||
|
||||
@ -114,7 +120,7 @@ func (s PerforceSource) CreateDraftChangeset(_ context.Context, _ *Changeset) (b
|
||||
return false, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (s PerforceSource) setChangesetMetadata(cl *protocol.PerforceChangelist, cs *Changeset) error {
|
||||
func (s PerforceSource) setChangesetMetadata(cl *perforce.Changelist, cs *Changeset) error {
|
||||
if err := cs.SetMetadata(cl); err != nil {
|
||||
return errors.Wrap(err, "setting changeset metadata")
|
||||
}
|
||||
|
||||
@ -2,13 +2,12 @@ package sources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
"github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/schema"
|
||||
|
||||
btypes "github.com/sourcegraph/sourcegraph/internal/batches/types"
|
||||
@ -18,7 +17,7 @@ import (
|
||||
|
||||
var (
|
||||
testPerforceChangeID = "111"
|
||||
testPerforceCredentials = gitserver.PerforceCredentials{Username: "user", Password: "pass", Host: "https://perforce.sgdev.org:1666"}
|
||||
testPerforceCredentials = protocol.PerforceConnectionDetails{P4User: "user", P4Passwd: "pass", P4Port: "perforce.sgdev.org:1666"}
|
||||
)
|
||||
|
||||
func TestPerforceSource_ValidateAuthenticator(t *testing.T) {
|
||||
@ -43,10 +42,10 @@ func TestPerforceSource_LoadChangeset(t *testing.T) {
|
||||
cs, _ := mockPerforceChangeset()
|
||||
s, client := mockPerforceSource()
|
||||
want := errors.New("error")
|
||||
client.P4GetChangelistFunc.SetDefaultHook(func(ctx context.Context, changeID string, credentials gitserver.PerforceCredentials) (*protocol.PerforceChangelist, error) {
|
||||
client.PerforceGetChangelistFunc.SetDefaultHook(func(ctx context.Context, credentials protocol.PerforceConnectionDetails, changeID string) (*perforce.Changelist, error) {
|
||||
assert.Equal(t, changeID, testPerforceChangeID)
|
||||
assert.Equal(t, testPerforceCredentials, credentials)
|
||||
return new(protocol.PerforceChangelist), want
|
||||
return new(perforce.Changelist), want
|
||||
})
|
||||
|
||||
err := s.LoadChangeset(ctx, cs)
|
||||
@ -59,7 +58,7 @@ func TestPerforceSource_LoadChangeset(t *testing.T) {
|
||||
s, client := mockPerforceSource()
|
||||
|
||||
change := mockPerforceChange()
|
||||
client.P4GetChangelistFunc.SetDefaultHook(func(ctx context.Context, changeID string, credentials gitserver.PerforceCredentials) (*protocol.PerforceChangelist, error) {
|
||||
client.PerforceGetChangelistFunc.SetDefaultHook(func(ctx context.Context, credentials protocol.PerforceConnectionDetails, changeID string) (*perforce.Changelist, error) {
|
||||
assert.Equal(t, changeID, testPerforceChangeID)
|
||||
assert.Equal(t, testPerforceCredentials, credentials)
|
||||
return change, nil
|
||||
@ -77,10 +76,10 @@ func TestPerforceSource_CreateChangeset(t *testing.T) {
|
||||
cs, _ := mockPerforceChangeset()
|
||||
s, client := mockPerforceSource()
|
||||
want := errors.New("error")
|
||||
client.P4GetChangelistFunc.SetDefaultHook(func(ctx context.Context, changeID string, credentials gitserver.PerforceCredentials) (*protocol.PerforceChangelist, error) {
|
||||
assert.Equal(t, changeID, testPerforceChangeID)
|
||||
assert.Equal(t, testPerforceCredentials, credentials)
|
||||
return new(protocol.PerforceChangelist), want
|
||||
client.PerforceGetChangelistFunc.SetDefaultHook(func(ctx context.Context, conn protocol.PerforceConnectionDetails, changelistID string) (*perforce.Changelist, error) {
|
||||
assert.Equal(t, changelistID, testPerforceChangeID)
|
||||
assert.Equal(t, testPerforceCredentials, conn)
|
||||
return new(perforce.Changelist), want
|
||||
})
|
||||
|
||||
b, err := s.CreateChangeset(ctx, cs)
|
||||
@ -94,9 +93,9 @@ func TestPerforceSource_CreateChangeset(t *testing.T) {
|
||||
s, client := mockPerforceSource()
|
||||
|
||||
change := mockPerforceChange()
|
||||
client.P4GetChangelistFunc.SetDefaultHook(func(ctx context.Context, changeID string, credentials gitserver.PerforceCredentials) (*protocol.PerforceChangelist, error) {
|
||||
assert.Equal(t, changeID, testPerforceChangeID)
|
||||
assert.Equal(t, testPerforceCredentials, credentials)
|
||||
client.PerforceGetChangelistFunc.SetDefaultHook(func(ctx context.Context, conn protocol.PerforceConnectionDetails, changelistID string) (*perforce.Changelist, error) {
|
||||
assert.Equal(t, changelistID, testPerforceChangeID)
|
||||
assert.Equal(t, testPerforceCredentials, conn)
|
||||
return change, nil
|
||||
})
|
||||
|
||||
@ -126,11 +125,11 @@ func mockPerforceChangeset() (*Changeset, *types.Repo) {
|
||||
|
||||
// mockPerforceChange returns a plausible changelist that would be
|
||||
// returned from Perforce.
|
||||
func mockPerforceChange() *protocol.PerforceChangelist {
|
||||
return &protocol.PerforceChangelist{
|
||||
func mockPerforceChange() *perforce.Changelist {
|
||||
return &perforce.Changelist{
|
||||
ID: testPerforceChangeID,
|
||||
Author: "Peter Guy",
|
||||
State: protocol.PerforceChangelistStatePending,
|
||||
State: perforce.ChangelistStatePending,
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,12 +137,6 @@ func mockPerforceSource() (*PerforceSource, *MockGitserverClient) {
|
||||
client := NewStrictMockGitserverClient()
|
||||
// Cred checks should pass by default.
|
||||
client.CheckPerforceCredentialsFunc.SetDefaultReturn(nil)
|
||||
s := &PerforceSource{gitServerClient: client, perforceCreds: &testPerforceCredentials, server: schema.PerforceConnection{P4Port: "https://perforce.sgdev.org:1666"}}
|
||||
s := &PerforceSource{gitServerClient: client, perforceCreds: &testPerforceCredentials, conn: schema.PerforceConnection{P4Port: "perforce.sgdev.org:1666"}}
|
||||
return s, client
|
||||
}
|
||||
|
||||
type fakeCloser struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (fakeCloser) Close() error { return nil }
|
||||
|
||||
@ -28,7 +28,7 @@ go_library(
|
||||
"//internal/extsvc/github",
|
||||
"//internal/extsvc/gitlab",
|
||||
"//internal/gitserver",
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/perforce",
|
||||
"//internal/types",
|
||||
"//lib/errors",
|
||||
"@com_github_inconshreveable_log15//:log15",
|
||||
@ -54,7 +54,7 @@ go_test(
|
||||
"//internal/extsvc/bitbucketserver",
|
||||
"//internal/extsvc/github",
|
||||
"//internal/extsvc/gitlab",
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/perforce",
|
||||
"//internal/timeutil",
|
||||
"//internal/types",
|
||||
"//lib/errors",
|
||||
|
||||
@ -13,7 +13,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
adobatches "github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
"github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
|
||||
"github.com/sourcegraph/go-diff/diff"
|
||||
|
||||
@ -124,7 +124,7 @@ func computeCheckState(c *btypes.Changeset, events ChangesetEvents) btypes.Chang
|
||||
return computeAzureDevOpsBuildState(m)
|
||||
case *gerritbatches.AnnotatedChange:
|
||||
return computeGerritBuildState(m)
|
||||
case *protocol.PerforceChangelistState:
|
||||
case *perforce.ChangelistState:
|
||||
// Perforce doesn't have builds built-in, its better to be explicit by still
|
||||
// including this case for clarity.
|
||||
return btypes.ChangesetCheckStateUnknown
|
||||
@ -639,13 +639,13 @@ func computeSingleChangesetExternalState(c *btypes.Changeset) (s btypes.Changese
|
||||
default:
|
||||
return "", errors.Errorf("unknown Gerrit Change state: %s", m.Change.Status)
|
||||
}
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
switch m.State {
|
||||
case protocol.PerforceChangelistStateClosed:
|
||||
case perforce.ChangelistStateClosed:
|
||||
s = btypes.ChangesetExternalStateClosed
|
||||
case protocol.PerforceChangelistStateSubmitted:
|
||||
case perforce.ChangelistStateSubmitted:
|
||||
s = btypes.ChangesetExternalStateMerged
|
||||
case protocol.PerforceChangelistStatePending, protocol.PerforceChangelistStateShelved:
|
||||
case perforce.ChangelistStatePending, perforce.ChangelistStateShelved:
|
||||
s = btypes.ChangesetExternalStateOpen
|
||||
default:
|
||||
return "", errors.Errorf("unknown Perforce Change state: %s", m.State)
|
||||
@ -758,7 +758,7 @@ func computeSingleChangesetReviewState(c *btypes.Changeset) (s btypes.ChangesetR
|
||||
}
|
||||
|
||||
}
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
states[btypes.ChangesetReviewStatePending] = true
|
||||
default:
|
||||
return "", errors.New("unknown changeset type")
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
|
||||
azuredevops2 "github.com/sourcegraph/sourcegraph/internal/batches/sources/azuredevops"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
"github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
|
||||
btypes "github.com/sourcegraph/sourcegraph/internal/batches/types"
|
||||
@ -866,25 +866,25 @@ func TestComputeExternalState(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "perforce closed - no events",
|
||||
changeset: perforceChangeset(daysAgo(10), protocol.PerforceChangelistStateClosed),
|
||||
changeset: perforceChangeset(daysAgo(10), perforce.ChangelistStateClosed),
|
||||
history: []changesetStatesAtTime{},
|
||||
want: btypes.ChangesetExternalStateClosed,
|
||||
},
|
||||
{
|
||||
name: "perforce submitted - no events",
|
||||
changeset: perforceChangeset(daysAgo(10), protocol.PerforceChangelistStateSubmitted),
|
||||
changeset: perforceChangeset(daysAgo(10), perforce.ChangelistStateSubmitted),
|
||||
history: []changesetStatesAtTime{},
|
||||
want: btypes.ChangesetExternalStateMerged,
|
||||
},
|
||||
{
|
||||
name: "perforce pending - no events",
|
||||
changeset: perforceChangeset(daysAgo(10), protocol.PerforceChangelistStatePending),
|
||||
changeset: perforceChangeset(daysAgo(10), perforce.ChangelistStatePending),
|
||||
history: []changesetStatesAtTime{},
|
||||
want: btypes.ChangesetExternalStateOpen,
|
||||
},
|
||||
{
|
||||
name: "perforce shelved - no events",
|
||||
changeset: perforceChangeset(daysAgo(10), protocol.PerforceChangelistStateShelved),
|
||||
changeset: perforceChangeset(daysAgo(10), perforce.ChangelistStateShelved),
|
||||
history: []changesetStatesAtTime{},
|
||||
want: btypes.ChangesetExternalStateOpen,
|
||||
},
|
||||
@ -1051,11 +1051,11 @@ func gitLabChangeset(updatedAt time.Time, state gitlab.MergeRequestState, notes
|
||||
}
|
||||
}
|
||||
|
||||
func perforceChangeset(updatedAt time.Time, state protocol.PerforceChangelistState) *btypes.Changeset {
|
||||
func perforceChangeset(updatedAt time.Time, state perforce.ChangelistState) *btypes.Changeset {
|
||||
return &btypes.Changeset{
|
||||
ExternalServiceType: extsvc.TypePerforce,
|
||||
UpdatedAt: updatedAt,
|
||||
Metadata: &protocol.PerforceChangelist{
|
||||
Metadata: &perforce.Changelist{
|
||||
State: state,
|
||||
},
|
||||
}
|
||||
|
||||
@ -53,9 +53,9 @@ go_library(
|
||||
"//internal/extsvc/gitlab",
|
||||
"//internal/featureflag",
|
||||
"//internal/github_apps/store",
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/metrics",
|
||||
"//internal/observation",
|
||||
"//internal/perforce",
|
||||
"//internal/timeutil",
|
||||
"//internal/workerutil/dbworker/store",
|
||||
"//lib/batches",
|
||||
|
||||
@ -16,7 +16,7 @@ import (
|
||||
gerritbatches "github.com/sourcegraph/sourcegraph/internal/batches/sources/gerrit"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
"github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
|
||||
"github.com/keegancsmith/sqlf"
|
||||
"github.com/lib/pq"
|
||||
@ -1527,7 +1527,7 @@ func ScanChangeset(t *btypes.Changeset, s dbutil.Scanner) error {
|
||||
m.Change = &gerrit.Change{}
|
||||
t.Metadata = m
|
||||
case extsvc.TypePerforce:
|
||||
t.Metadata = new(protocol.PerforceChangelist)
|
||||
t.Metadata = new(perforce.Changelist)
|
||||
case extsvc.TypeGerrit:
|
||||
t.Metadata = new(gerrit.Change)
|
||||
default:
|
||||
|
||||
@ -44,7 +44,7 @@ go_library(
|
||||
"//internal/extsvc/gitlab",
|
||||
"//internal/extsvc/gitlab/webhooks",
|
||||
"//internal/gitserver/gitdomain",
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/perforce",
|
||||
"//internal/timeutil",
|
||||
"//internal/types",
|
||||
"//lib/batches",
|
||||
|
||||
@ -11,21 +11,19 @@ import (
|
||||
"github.com/inconshreveable/log15"
|
||||
"github.com/sourcegraph/go-diff/diff"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
|
||||
gerritbatches "github.com/sourcegraph/sourcegraph/internal/batches/sources/gerrit"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
adobatches "github.com/sourcegraph/sourcegraph/internal/batches/sources/azuredevops"
|
||||
bbcs "github.com/sourcegraph/sourcegraph/internal/batches/sources/bitbucketcloud"
|
||||
gerritbatches "github.com/sourcegraph/sourcegraph/internal/batches/sources/gerrit"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/azuredevops"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketcloud"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/bitbucketserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/gerrit"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/github"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/gitlab"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/timeutil"
|
||||
"github.com/sourcegraph/sourcegraph/lib/batches"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
@ -470,7 +468,7 @@ func (c *Changeset) SetMetadata(meta any) error {
|
||||
c.ExternalServiceType = extsvc.TypeGerrit
|
||||
c.ExternalBranch = gitdomain.EnsureRefPrefix(pr.Change.Branch)
|
||||
c.ExternalUpdatedAt = pr.Change.Updated
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
c.Metadata = pr
|
||||
c.ExternalID = pr.ID
|
||||
c.ExternalServiceType = extsvc.TypePerforce
|
||||
@ -510,7 +508,7 @@ func (c *Changeset) Title() (string, error) {
|
||||
// Remove extra quotes added by the commit message
|
||||
title = strings.TrimPrefix(strings.TrimSuffix(title, "\""), "\"")
|
||||
return title, nil
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
return m.Title, nil
|
||||
default:
|
||||
return "", errors.New("title unknown changeset type")
|
||||
@ -537,7 +535,7 @@ func (c *Changeset) AuthorName() (string, error) {
|
||||
return m.CreatedBy.UniqueName, nil
|
||||
case *gerritbatches.AnnotatedChange:
|
||||
return m.Change.Owner.Name, nil
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
return m.Author, nil
|
||||
default:
|
||||
return "", errors.New("authorname unknown changeset type")
|
||||
@ -572,7 +570,7 @@ func (c *Changeset) AuthorEmail() (string, error) {
|
||||
return m.CreatedBy.UniqueName, nil
|
||||
case *gerritbatches.AnnotatedChange:
|
||||
return m.Change.Owner.Email, nil
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
return "", nil
|
||||
default:
|
||||
return "", errors.New("author email unknown changeset type")
|
||||
@ -596,7 +594,7 @@ func (c *Changeset) ExternalCreatedAt() time.Time {
|
||||
return m.CreationDate
|
||||
case *gerritbatches.AnnotatedChange:
|
||||
return m.Change.Created
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
return m.CreationDate
|
||||
default:
|
||||
return time.Time{}
|
||||
@ -619,7 +617,7 @@ func (c *Changeset) Body() (string, error) {
|
||||
case *gerritbatches.AnnotatedChange:
|
||||
// Gerrit doesn't really differentiate between title/description.
|
||||
return m.Change.Subject, nil
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
return "", nil
|
||||
default:
|
||||
return "", errors.New("body unknown changeset type")
|
||||
@ -687,7 +685,7 @@ func (c *Changeset) URL() (s string, err error) {
|
||||
return returnURL.String(), nil
|
||||
case *gerritbatches.AnnotatedChange:
|
||||
return m.CodeHostURL.JoinPath("c", url.PathEscape(m.Change.Project), "+", url.PathEscape(strconv.Itoa(m.Change.ChangeNumber))).String(), nil
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
return "", nil
|
||||
default:
|
||||
return "", errors.New("url unknown changeset type")
|
||||
@ -907,7 +905,7 @@ func (c *Changeset) Events() (events []*ChangesetEvent, err error) {
|
||||
Metadata: reviewer,
|
||||
})
|
||||
}
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
// We don't have any events we care about right now
|
||||
break
|
||||
}
|
||||
@ -932,7 +930,7 @@ func (c *Changeset) HeadRefOid() (string, error) {
|
||||
return "", nil
|
||||
case *gerritbatches.AnnotatedChange:
|
||||
return "", nil
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
return "", nil
|
||||
default:
|
||||
return "", errors.New("head ref oid unknown changeset type")
|
||||
@ -955,7 +953,7 @@ func (c *Changeset) HeadRef() (string, error) {
|
||||
return m.SourceRefName, nil
|
||||
case *gerritbatches.AnnotatedChange:
|
||||
return "", nil
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
return "", nil
|
||||
default:
|
||||
return "", errors.New("headref unknown changeset type")
|
||||
@ -979,7 +977,7 @@ func (c *Changeset) BaseRefOid() (string, error) {
|
||||
return "", nil
|
||||
case *gerritbatches.AnnotatedChange:
|
||||
return "", nil
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
return "", nil
|
||||
default:
|
||||
return "", errors.New("base ref oid unknown changeset type")
|
||||
@ -1002,7 +1000,7 @@ func (c *Changeset) BaseRef() (string, error) {
|
||||
return m.TargetRefName, nil
|
||||
case *gerritbatches.AnnotatedChange:
|
||||
return "refs/heads/" + m.Change.Branch, nil
|
||||
case *protocol.PerforceChangelist:
|
||||
case *perforce.Changelist:
|
||||
// TODO: @peterguy we may need to change this to something.
|
||||
return "", nil
|
||||
default:
|
||||
|
||||
@ -97,8 +97,6 @@ go_test(
|
||||
"@com_github_stretchr_testify//assert",
|
||||
"@com_github_stretchr_testify//require",
|
||||
"@org_golang_google_grpc//:go_default_library",
|
||||
"@org_golang_google_grpc//codes",
|
||||
"@org_golang_google_grpc//status",
|
||||
"@org_golang_google_protobuf//encoding/protojson",
|
||||
],
|
||||
)
|
||||
|
||||
@ -40,7 +40,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/lazyregexp"
|
||||
"github.com/sourcegraph/sourcegraph/internal/limiter"
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
p4tools "github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
@ -279,12 +279,6 @@ type Client interface {
|
||||
// MergeBase returns the merge base commit for the specified commits.
|
||||
MergeBase(ctx context.Context, repo api.RepoName, a, b api.CommitID) (api.CommitID, error)
|
||||
|
||||
// P4Exec sends a p4 command with given arguments and returns an io.ReadCloser for the output.
|
||||
P4Exec(_ context.Context, host, user, password string, args ...string) (io.ReadCloser, http.Header, error)
|
||||
|
||||
// P4GetChangelist gets the changelist specified by changelistID.
|
||||
P4GetChangelist(_ context.Context, changelistID string, creds PerforceCredentials) (*protocol.PerforceChangelist, error)
|
||||
|
||||
// Remove removes the repository clone from gitserver.
|
||||
Remove(context.Context, api.RepoName) error
|
||||
|
||||
@ -440,39 +434,50 @@ type Client interface {
|
||||
Addrs() []string
|
||||
|
||||
// SystemsInfo returns information about all gitserver instances associated with a Sourcegraph instance.
|
||||
SystemsInfo(ctx context.Context) ([]SystemInfo, error)
|
||||
SystemsInfo(ctx context.Context) ([]protocol.SystemInfo, error)
|
||||
|
||||
// SystemInfo returns information about the gitserver instance at the given address.
|
||||
SystemInfo(ctx context.Context, addr string) (SystemInfo, error)
|
||||
SystemInfo(ctx context.Context, addr string) (protocol.SystemInfo, error)
|
||||
|
||||
// IsPerforcePathCloneable checks if the given Perforce depot path is cloneable by
|
||||
// checking if it is a valid depot and the given user has permission to access it.
|
||||
IsPerforcePathCloneable(ctx context.Context, p4port, p4user, p4passwd, depotPath string) error
|
||||
IsPerforcePathCloneable(ctx context.Context, conn protocol.PerforceConnectionDetails, depotPath string) error
|
||||
|
||||
// CheckPerforceCredentials checks if the given Perforce credentials are valid
|
||||
CheckPerforceCredentials(ctx context.Context, p4port, p4user, p4passwd string) error
|
||||
CheckPerforceCredentials(ctx context.Context, conn protocol.PerforceConnectionDetails) error
|
||||
|
||||
// PerforceUsers lists all the users known to the given Perforce server.
|
||||
PerforceUsers(ctx context.Context, conn protocol.PerforceConnectionDetails) ([]*perforce.User, error)
|
||||
|
||||
// PerforceProtectsForUser returns all protects that apply to the given Perforce user.
|
||||
PerforceProtectsForUser(ctx context.Context, conn protocol.PerforceConnectionDetails, username string) ([]*perforce.Protect, error)
|
||||
|
||||
// PerforceProtectsForDepot returns all protects that apply to the given Perforce depot.
|
||||
PerforceProtectsForDepot(ctx context.Context, conn protocol.PerforceConnectionDetails, depot string) ([]*perforce.Protect, error)
|
||||
|
||||
// PerforceGroupMembers returns the members of the given Perforce group.
|
||||
PerforceGroupMembers(ctx context.Context, conn protocol.PerforceConnectionDetails, group string) ([]string, error)
|
||||
|
||||
// IsPerforceSuperUser checks if the given Perforce user is a super user, and otherwise returns an error.
|
||||
IsPerforceSuperUser(ctx context.Context, conn protocol.PerforceConnectionDetails) error
|
||||
|
||||
// PerforceGetChangelist gets the perforce changelist details for the given changelist ID.
|
||||
PerforceGetChangelist(ctx context.Context, conn protocol.PerforceConnectionDetails, changelist string) (*perforce.Changelist, error)
|
||||
}
|
||||
|
||||
type SystemInfo struct {
|
||||
Address string
|
||||
FreeSpace uint64
|
||||
TotalSpace uint64
|
||||
PercentUsed float32
|
||||
}
|
||||
|
||||
func (c *clientImplementor) SystemsInfo(ctx context.Context) ([]SystemInfo, error) {
|
||||
func (c *clientImplementor) SystemsInfo(ctx context.Context) ([]protocol.SystemInfo, error) {
|
||||
addresses := c.clientSource.Addresses()
|
||||
|
||||
wg := pool.NewWithResults[SystemInfo]().WithErrors().WithContext(ctx)
|
||||
wg := pool.NewWithResults[protocol.SystemInfo]().WithErrors().WithContext(ctx)
|
||||
|
||||
for _, addr := range addresses {
|
||||
addr := addr // capture addr
|
||||
wg.Go(func(ctx context.Context) (SystemInfo, error) {
|
||||
wg.Go(func(ctx context.Context) (protocol.SystemInfo, error) {
|
||||
response, err := c.getDiskInfo(ctx, addr)
|
||||
if err != nil {
|
||||
return SystemInfo{}, err
|
||||
return protocol.SystemInfo{}, err
|
||||
}
|
||||
return SystemInfo{
|
||||
return protocol.SystemInfo{
|
||||
Address: addr.Address(),
|
||||
FreeSpace: response.GetFreeSpace(),
|
||||
TotalSpace: response.GetTotalSpace(),
|
||||
@ -484,18 +489,18 @@ func (c *clientImplementor) SystemsInfo(ctx context.Context) ([]SystemInfo, erro
|
||||
return wg.Wait()
|
||||
}
|
||||
|
||||
func (c *clientImplementor) SystemInfo(ctx context.Context, addr string) (SystemInfo, error) {
|
||||
func (c *clientImplementor) SystemInfo(ctx context.Context, addr string) (protocol.SystemInfo, error) {
|
||||
ac := c.clientSource.GetAddressWithClient(addr)
|
||||
if ac == nil {
|
||||
return SystemInfo{}, errors.Newf("no client for address: %s", addr)
|
||||
return protocol.SystemInfo{}, errors.Newf("no client for address: %s", addr)
|
||||
}
|
||||
|
||||
response, err := c.getDiskInfo(ctx, ac)
|
||||
if err != nil {
|
||||
return SystemInfo{}, nil
|
||||
return protocol.SystemInfo{}, err
|
||||
}
|
||||
|
||||
return SystemInfo{
|
||||
return protocol.SystemInfo{
|
||||
Address: ac.Address(),
|
||||
FreeSpace: response.FreeSpace,
|
||||
TotalSpace: response.TotalSpace,
|
||||
@ -951,140 +956,11 @@ func convertGitserverError(err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *clientImplementor) P4Exec(ctx context.Context, host, user, password string, args ...string) (_ io.ReadCloser, _ http.Header, err error) {
|
||||
ctx, _, endObservation := c.operations.p4Exec.With(ctx, &err, observation.Args{Attrs: []attribute.KeyValue{
|
||||
attribute.String("host", host),
|
||||
attribute.StringSlice("args", args),
|
||||
}})
|
||||
defer endObservation(1, observation.Args{})
|
||||
// Check that ctx is not expired.
|
||||
if err := ctx.Err(); err != nil {
|
||||
deadlineExceededCounter.Inc()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req := &protocol.P4ExecRequest{
|
||||
P4Port: host,
|
||||
P4User: user,
|
||||
P4Passwd: password,
|
||||
Args: args,
|
||||
}
|
||||
if conf.IsGRPCEnabled(ctx) {
|
||||
client, err := c.ClientForRepo(ctx, "")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
stream, err := client.P4Exec(ctx, req.ToProto())
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// We need to check the first message from the gRPC errors to see if we get an argument or permisison related
|
||||
// error before continuing to read the rest of the stream. If the first message is an error, we cancel the stream and
|
||||
// forward the error.
|
||||
//
|
||||
// This is necessary to provide parity between the REST and gRPC implementations of
|
||||
// P4Exec. Users of cli.P4Exec may assume error handling occurs immediately,
|
||||
// as is the case with the HTTP implementation where these kinds of errors are returned as soon as the
|
||||
// function returns. gRPC is asynchronous, so we have to start consuming messages from
|
||||
// the stream to see any errors from the server. Reading the first message ensures we
|
||||
// handle any errors synchronously, similar to the HTTP implementation.
|
||||
|
||||
firstMessage, firstError := stream.Recv()
|
||||
switch status.Code(firstError) {
|
||||
case codes.InvalidArgument, codes.PermissionDenied:
|
||||
cancel()
|
||||
return nil, nil, convertGitserverError(firstError)
|
||||
}
|
||||
|
||||
firstMessageRead := false
|
||||
r := streamio.NewReader(func() ([]byte, error) {
|
||||
// Check if we've read the first message yet. If not, read it and return.
|
||||
if !firstMessageRead {
|
||||
firstMessageRead = true
|
||||
|
||||
if firstError != nil {
|
||||
return nil, firstError
|
||||
}
|
||||
|
||||
return firstMessage.GetData(), nil
|
||||
}
|
||||
|
||||
msg, err := stream.Recv()
|
||||
if err != nil {
|
||||
if status.Code(err) == codes.Canceled {
|
||||
return nil, context.Canceled
|
||||
}
|
||||
|
||||
if status.Code(err) == codes.DeadlineExceeded {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
return msg.GetData(), nil
|
||||
})
|
||||
|
||||
return &readCloseWrapper{r: r, closeFn: cancel}, nil, nil
|
||||
} else {
|
||||
resp, err := c.httpPost(ctx, "", "p4-exec", req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
defer resp.Body.Close()
|
||||
return nil, nil, errors.Errorf("unexpected status code: %d - %s", resp.StatusCode, readResponseBody(resp.Body))
|
||||
}
|
||||
|
||||
return resp.Body, resp.Trailer, nil
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var deadlineExceededCounter = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "src_gitserver_client_deadline_exceeded",
|
||||
Help: "Times that Client.sendExec() returned context.DeadlineExceeded",
|
||||
})
|
||||
|
||||
func (c *clientImplementor) P4GetChangelist(ctx context.Context, changelistID string, creds PerforceCredentials) (*protocol.PerforceChangelist, error) {
|
||||
reader, _, err := c.P4Exec(ctx, creds.Host, creds.Username, creds.Password,
|
||||
"changes",
|
||||
"-r", // list in reverse order, which means that the given changelist id will be the first one listed
|
||||
"-m", "1", // limit output to one record, so that the given changelist is the only one listed
|
||||
"-l", // use a long listing, which includes the whole commit message
|
||||
"-e", changelistID, // start from this changelist and go up
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read the output of p4 changes")
|
||||
}
|
||||
output := strings.TrimSpace(string(body))
|
||||
if output == "" {
|
||||
return nil, errors.New("invalid changelist " + changelistID)
|
||||
}
|
||||
|
||||
pcl, err := p4tools.ParseChangelistOutput(output)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to parse change output")
|
||||
}
|
||||
return pcl, nil
|
||||
}
|
||||
|
||||
type PerforceCredentials struct {
|
||||
Host string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// BatchLog invokes the given callback with the `git log` output for a batch of repository
|
||||
// and commit pairs. If the invoked callback returns a non-nil error, the operation will begin
|
||||
// to abort processing further results.
|
||||
@ -1613,7 +1489,7 @@ func (c *clientImplementor) removeFrom(ctx context.Context, repo api.RepoName, f
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientImplementor) IsPerforcePathCloneable(ctx context.Context, p4port, p4user, p4passwd, depotPath string) error {
|
||||
func (c *clientImplementor) IsPerforcePathCloneable(ctx context.Context, conn protocol.PerforceConnectionDetails, depotPath string) error {
|
||||
if conf.IsGRPCEnabled(ctx) {
|
||||
// depotPath is not actually a repo name, but it will spread the load of isPerforcePathCloneable
|
||||
// a bit over the different gitserver instances. It's really just used as a consistent hashing
|
||||
@ -1623,10 +1499,8 @@ func (c *clientImplementor) IsPerforcePathCloneable(ctx context.Context, p4port,
|
||||
return err
|
||||
}
|
||||
_, err = client.IsPerforcePathCloneable(ctx, &proto.IsPerforcePathCloneableRequest{
|
||||
P4Port: p4port,
|
||||
P4User: p4user,
|
||||
P4Passwd: p4passwd,
|
||||
DepotPath: depotPath,
|
||||
ConnectionDetails: conn.ToProto(),
|
||||
DepotPath: depotPath,
|
||||
})
|
||||
if err != nil {
|
||||
// Unwrap proto errors for nicer error messages.
|
||||
@ -1641,9 +1515,9 @@ func (c *clientImplementor) IsPerforcePathCloneable(ctx context.Context, p4port,
|
||||
|
||||
addr := c.AddrForRepo(ctx, api.RepoName(depotPath))
|
||||
b, err := json.Marshal(&protocol.IsPerforcePathCloneableRequest{
|
||||
P4Port: p4port,
|
||||
P4User: p4user,
|
||||
P4Passwd: p4passwd,
|
||||
P4Port: conn.P4Port,
|
||||
P4User: conn.P4User,
|
||||
P4Passwd: conn.P4Passwd,
|
||||
DepotPath: depotPath,
|
||||
})
|
||||
if err != nil {
|
||||
@ -1668,19 +1542,17 @@ func (c *clientImplementor) IsPerforcePathCloneable(ctx context.Context, p4port,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientImplementor) CheckPerforceCredentials(ctx context.Context, p4port, p4user, p4passwd string) error {
|
||||
func (c *clientImplementor) CheckPerforceCredentials(ctx context.Context, conn protocol.PerforceConnectionDetails) error {
|
||||
if conf.IsGRPCEnabled(ctx) {
|
||||
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
|
||||
// a bit over the different gitserver instances. It's really just used as a consistent hashing
|
||||
// key here.
|
||||
client, err := c.ClientForRepo(ctx, api.RepoName(p4port))
|
||||
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.CheckPerforceCredentials(ctx, &proto.CheckPerforceCredentialsRequest{
|
||||
P4Port: p4port,
|
||||
P4User: p4user,
|
||||
P4Passwd: p4passwd,
|
||||
ConnectionDetails: conn.ToProto(),
|
||||
})
|
||||
if err != nil {
|
||||
// Unwrap proto errors for nicer error messages.
|
||||
@ -1693,11 +1565,11 @@ func (c *clientImplementor) CheckPerforceCredentials(ctx context.Context, p4port
|
||||
return nil
|
||||
}
|
||||
|
||||
addr := c.AddrForRepo(ctx, api.RepoName(p4port))
|
||||
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
b, err := json.Marshal(&protocol.CheckPerforceCredentialsRequest{
|
||||
P4Port: p4port,
|
||||
P4User: p4user,
|
||||
P4Passwd: p4passwd,
|
||||
P4Port: conn.P4Port,
|
||||
P4User: conn.P4User,
|
||||
P4Passwd: conn.P4Passwd,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -1721,6 +1593,370 @@ func (c *clientImplementor) CheckPerforceCredentials(ctx context.Context, p4port
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientImplementor) PerforceUsers(ctx context.Context, conn protocol.PerforceConnectionDetails) ([]*perforce.User, error) {
|
||||
if conf.IsGRPCEnabled(ctx) {
|
||||
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
|
||||
// a bit over the different gitserver instances. It's really just used as a consistent hashing
|
||||
// key here.
|
||||
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.PerforceUsers(ctx, &proto.PerforceUsersRequest{
|
||||
ConnectionDetails: conn.ToProto(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*perforce.User, len(resp.GetUsers()))
|
||||
for i, u := range resp.GetUsers() {
|
||||
users[i] = perforce.UserFromProto(u)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
b, err := json.Marshal(&protocol.PerforceUsersRequest{
|
||||
P4Port: conn.P4Port,
|
||||
P4User: conn.P4User,
|
||||
P4Passwd: conn.P4Passwd,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := "http://" + addr + "/perforce-users"
|
||||
resp, err := c.do(ctx, "perforce-users", uri, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, &url.Error{
|
||||
URL: resp.Request.URL.String(),
|
||||
Op: "PerforceUsers",
|
||||
Err: errors.Errorf("PerforceUsers: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
|
||||
}
|
||||
}
|
||||
|
||||
var payload protocol.PerforceUsersResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*perforce.User, len(payload.Users))
|
||||
for i, u := range payload.Users {
|
||||
users[i] = &perforce.User{
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (c *clientImplementor) PerforceProtectsForUser(ctx context.Context, conn protocol.PerforceConnectionDetails, username string) ([]*perforce.Protect, error) {
|
||||
if conf.IsGRPCEnabled(ctx) {
|
||||
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
|
||||
// a bit over the different gitserver instances. It's really just used as a consistent hashing
|
||||
// key here.
|
||||
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.PerforceProtectsForUser(ctx, &proto.PerforceProtectsForUserRequest{
|
||||
ConnectionDetails: conn.ToProto(),
|
||||
Username: username,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
protects := make([]*perforce.Protect, len(resp.GetProtects()))
|
||||
for i, p := range resp.GetProtects() {
|
||||
protects[i] = perforce.ProtectFromProto(p)
|
||||
}
|
||||
return protects, nil
|
||||
}
|
||||
|
||||
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
b, err := json.Marshal(&protocol.PerforceProtectsForUserRequest{
|
||||
P4Port: conn.P4Port,
|
||||
P4User: conn.P4User,
|
||||
P4Passwd: conn.P4Passwd,
|
||||
Username: username,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := "http://" + addr + "/perforce-protects-for-user"
|
||||
resp, err := c.do(ctx, "perforce-protects-for-user", uri, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, &url.Error{
|
||||
URL: resp.Request.URL.String(),
|
||||
Op: "PerforceProtectsForUser",
|
||||
Err: errors.Errorf("PerforceProtectsForUser: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
|
||||
}
|
||||
}
|
||||
|
||||
var payload protocol.PerforceProtectsForUserResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
protects := make([]*perforce.Protect, len(payload.Protects))
|
||||
for i, p := range payload.Protects {
|
||||
protects[i] = &perforce.Protect{
|
||||
Level: p.Level,
|
||||
EntityType: p.EntityType,
|
||||
EntityName: p.EntityName,
|
||||
Match: p.Match,
|
||||
IsExclusion: p.IsExclusion,
|
||||
Host: p.Host,
|
||||
}
|
||||
}
|
||||
|
||||
return protects, nil
|
||||
}
|
||||
|
||||
func (c *clientImplementor) PerforceProtectsForDepot(ctx context.Context, conn protocol.PerforceConnectionDetails, depot string) ([]*perforce.Protect, error) {
|
||||
if conf.IsGRPCEnabled(ctx) {
|
||||
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
|
||||
// a bit over the different gitserver instances. It's really just used as a consistent hashing
|
||||
// key here.
|
||||
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.PerforceProtectsForDepot(ctx, &proto.PerforceProtectsForDepotRequest{
|
||||
ConnectionDetails: conn.ToProto(),
|
||||
Depot: depot,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
protects := make([]*perforce.Protect, len(resp.GetProtects()))
|
||||
for i, p := range resp.GetProtects() {
|
||||
protects[i] = perforce.ProtectFromProto(p)
|
||||
}
|
||||
return protects, nil
|
||||
}
|
||||
|
||||
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
b, err := json.Marshal(&protocol.PerforceProtectsForDepotRequest{
|
||||
P4Port: conn.P4Port,
|
||||
P4User: conn.P4User,
|
||||
P4Passwd: conn.P4Passwd,
|
||||
Depot: depot,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := "http://" + addr + "/perforce-protects-for-depot"
|
||||
resp, err := c.do(ctx, "perforce-protects-for-depot", uri, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, &url.Error{
|
||||
URL: resp.Request.URL.String(),
|
||||
Op: "PerforceProtectsForDepot",
|
||||
Err: errors.Errorf("PerforceProtectsForDepot: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
|
||||
}
|
||||
}
|
||||
|
||||
var payload protocol.PerforceProtectsForDepotResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
protects := make([]*perforce.Protect, len(payload.Protects))
|
||||
for i, p := range payload.Protects {
|
||||
protects[i] = &perforce.Protect{
|
||||
Level: p.Level,
|
||||
EntityType: p.EntityType,
|
||||
EntityName: p.EntityName,
|
||||
Match: p.Match,
|
||||
IsExclusion: p.IsExclusion,
|
||||
Host: p.Host,
|
||||
}
|
||||
}
|
||||
|
||||
return protects, nil
|
||||
}
|
||||
|
||||
func (c *clientImplementor) PerforceGroupMembers(ctx context.Context, conn protocol.PerforceConnectionDetails, group string) ([]string, error) {
|
||||
if conf.IsGRPCEnabled(ctx) {
|
||||
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
|
||||
// a bit over the different gitserver instances. It's really just used as a consistent hashing
|
||||
// key here.
|
||||
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.PerforceGroupMembers(ctx, &proto.PerforceGroupMembersRequest{
|
||||
ConnectionDetails: conn.ToProto(),
|
||||
Group: group,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.GetUsernames(), nil
|
||||
}
|
||||
|
||||
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
b, err := json.Marshal(&protocol.PerforceGroupMembersRequest{
|
||||
P4Port: conn.P4Port,
|
||||
P4User: conn.P4User,
|
||||
P4Passwd: conn.P4Passwd,
|
||||
Group: group,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := "http://" + addr + "/perforce-group-members"
|
||||
resp, err := c.do(ctx, "perforce-group-members", uri, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, &url.Error{
|
||||
URL: resp.Request.URL.String(),
|
||||
Op: "PerforceGroupMembers",
|
||||
Err: errors.Errorf("PerforceGroupMembers: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
|
||||
}
|
||||
}
|
||||
|
||||
var payload protocol.PerforceGroupMembersResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return payload.Usernames, nil
|
||||
}
|
||||
|
||||
func (c *clientImplementor) IsPerforceSuperUser(ctx context.Context, conn protocol.PerforceConnectionDetails) error {
|
||||
if conf.IsGRPCEnabled(ctx) {
|
||||
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
|
||||
// a bit over the different gitserver instances. It's really just used as a consistent hashing
|
||||
// key here.
|
||||
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.IsPerforceSuperUser(ctx, &proto.IsPerforceSuperUserRequest{
|
||||
ConnectionDetails: conn.ToProto(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
b, err := json.Marshal(&protocol.IsPerforceSuperUserRequest{
|
||||
P4Port: conn.P4Port,
|
||||
P4User: conn.P4User,
|
||||
P4Passwd: conn.P4Passwd,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := "http://" + addr + "/is-perforce-super-user"
|
||||
resp, err := c.do(ctx, "is-perforce-super-user", uri, b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return &url.Error{
|
||||
URL: resp.Request.URL.String(),
|
||||
Op: "IsPerforceSuperUser",
|
||||
Err: errors.Errorf("IsPerforceSuperUser: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientImplementor) PerforceGetChangelist(ctx context.Context, conn protocol.PerforceConnectionDetails, changelist string) (*perforce.Changelist, error) {
|
||||
if conf.IsGRPCEnabled(ctx) {
|
||||
// p4port is not actually a repo name, but it will spread the load of CheckPerforceCredentials
|
||||
// a bit over the different gitserver instances. It's really just used as a consistent hashing
|
||||
// key here.
|
||||
client, err := c.ClientForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.PerforceGetChangelist(ctx, &proto.PerforceGetChangelistRequest{
|
||||
ConnectionDetails: conn.ToProto(),
|
||||
ChangelistId: changelist,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return perforce.ChangelistFromProto(resp.GetChangelist()), nil
|
||||
}
|
||||
|
||||
addr := c.AddrForRepo(ctx, api.RepoName(conn.P4Port))
|
||||
b, err := json.Marshal(&protocol.PerforceGetChangelistRequest{
|
||||
P4Port: conn.P4Port,
|
||||
P4User: conn.P4User,
|
||||
P4Passwd: conn.P4Passwd,
|
||||
ChangelistID: changelist,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := "http://" + addr + "/perforce-get-changelist"
|
||||
resp, err := c.do(ctx, "perforce-get-changelist", uri, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, &url.Error{
|
||||
URL: resp.Request.URL.String(),
|
||||
Op: "PerforceGetChangelist",
|
||||
Err: errors.Errorf("PerforceGetChangelist: http status %d: %s", resp.StatusCode, readResponseBody(io.LimitReader(resp.Body, 200))),
|
||||
}
|
||||
}
|
||||
|
||||
var payload protocol.PerforceGetChangelistResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cl := &perforce.Changelist{
|
||||
ID: payload.Changelist.ID,
|
||||
CreationDate: payload.Changelist.CreationDate,
|
||||
State: perforce.ChangelistState(payload.Changelist.State),
|
||||
Author: payload.Changelist.Author,
|
||||
Title: payload.Changelist.Title,
|
||||
Message: payload.Changelist.Message,
|
||||
}
|
||||
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
// httpPost will apply the MD5 hashing scheme on the repo name to determine the gitserver instance
|
||||
// to which the HTTP POST request is sent.
|
||||
func (c *clientImplementor) httpPost(ctx context.Context, repo api.RepoName, op string, payload any) (resp *http.Response, err error) {
|
||||
|
||||
@ -8,8 +8,6 @@ import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@ -23,8 +21,6 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
@ -256,25 +252,6 @@ func TestClient_RepoCloneProgress_ProtoRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_P4ExecRequest_ProtoRoundTrip(t *testing.T) {
|
||||
var diff string
|
||||
|
||||
fn := func(original protocol.P4ExecRequest) bool {
|
||||
var converted protocol.P4ExecRequest
|
||||
converted.FromProto(original.ToProto())
|
||||
|
||||
if diff = cmp.Diff(original, converted); diff != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if err := quick.Check(fn, nil); err != nil {
|
||||
t.Errorf("P4ExecRequest proto roundtrip failed (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_RepoClone_ProtoRoundTrip(t *testing.T) {
|
||||
var diff string
|
||||
|
||||
@ -326,9 +303,10 @@ func TestClient_Remove(t *testing.T) {
|
||||
*called = true
|
||||
return nil, nil
|
||||
}
|
||||
return &gitserver.MockGRPCClient{
|
||||
MockRepoDelete: mockRepoDelete,
|
||||
}
|
||||
|
||||
cli := gitserver.NewStrictMockGitserverServiceClient()
|
||||
cli.RepoDeleteFunc.SetDefaultHook(mockRepoDelete)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
cli := gitserver.NewTestClient(
|
||||
@ -393,277 +371,6 @@ func TestClient_Remove(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
type mockP4ExecClient struct {
|
||||
isEndOfStream bool
|
||||
Err error
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
func (m *mockP4ExecClient) Recv() (*proto.P4ExecResponse, error) {
|
||||
if m.isEndOfStream {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if m.Err != nil {
|
||||
s, _ := status.FromError(m.Err)
|
||||
return nil, s.Err()
|
||||
|
||||
}
|
||||
|
||||
response := &proto.P4ExecResponse{
|
||||
Data: []byte("example output"),
|
||||
}
|
||||
|
||||
// Set the end-of-stream condition
|
||||
m.isEndOfStream = true
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func TestClient_P4ExecGRPC(t *testing.T) {
|
||||
_ = gitserver.CreateRepoDir(t)
|
||||
type test struct {
|
||||
name string
|
||||
|
||||
host string
|
||||
user string
|
||||
password string
|
||||
args []string
|
||||
|
||||
mockErr error
|
||||
|
||||
wantBody string
|
||||
wantReaderConstructionError string
|
||||
wantReaderError string
|
||||
}
|
||||
tests := []test{
|
||||
{
|
||||
name: "check request body",
|
||||
|
||||
host: "ssl:111.222.333.444:1666",
|
||||
user: "admin",
|
||||
password: "pa$$word",
|
||||
args: []string{"protects"},
|
||||
|
||||
wantBody: "example output",
|
||||
wantReaderConstructionError: "<nil>",
|
||||
wantReaderError: "<nil>",
|
||||
},
|
||||
{
|
||||
name: "error response",
|
||||
|
||||
mockErr: errors.New("example error"),
|
||||
wantReaderConstructionError: "<nil>",
|
||||
wantReaderError: "rpc error: code = Unknown desc = example error",
|
||||
},
|
||||
{
|
||||
name: "context cancellation",
|
||||
|
||||
mockErr: status.New(codes.Canceled, context.Canceled.Error()).Err(),
|
||||
wantReaderConstructionError: "<nil>",
|
||||
wantReaderError: context.Canceled.Error(),
|
||||
},
|
||||
{
|
||||
name: "context expiration",
|
||||
|
||||
mockErr: status.New(codes.DeadlineExceeded, context.DeadlineExceeded.Error()).Err(),
|
||||
wantReaderConstructionError: "<nil>",
|
||||
wantReaderError: context.DeadlineExceeded.Error(),
|
||||
},
|
||||
{
|
||||
name: "invalid credentials - reported on reader instantiation",
|
||||
|
||||
mockErr: status.New(codes.InvalidArgument, "that is totally wrong").Err(),
|
||||
wantReaderConstructionError: status.New(codes.InvalidArgument, "that is totally wrong").Err().Error(),
|
||||
wantReaderError: status.New(codes.InvalidArgument, "that is totally wrong").Err().Error(),
|
||||
},
|
||||
{
|
||||
name: "permission denied - reported on reader instantiation",
|
||||
|
||||
mockErr: status.New(codes.PermissionDenied, "you can't do this").Err(),
|
||||
wantReaderConstructionError: status.New(codes.PermissionDenied, "you can't do this").Err().Error(),
|
||||
wantReaderError: status.New(codes.PermissionDenied, "you can't do this").Err().Error(),
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
conf.Mock(&conf.Unified{
|
||||
SiteConfiguration: schema.SiteConfiguration{
|
||||
ExperimentalFeatures: &schema.ExperimentalFeatures{
|
||||
EnableGRPC: boolPointer(true),
|
||||
},
|
||||
},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
conf.Mock(nil)
|
||||
})
|
||||
|
||||
const gitserverAddr = "172.16.8.1:8080"
|
||||
addrs := []string{gitserverAddr}
|
||||
called := false
|
||||
|
||||
source := gitserver.NewTestClientSource(t, addrs, func(o *gitserver.TestClientSourceOptions) {
|
||||
o.ClientFunc = func(cc *grpc.ClientConn) proto.GitserverServiceClient {
|
||||
mockP4Exec := func(ctx context.Context, in *proto.P4ExecRequest, opts ...grpc.CallOption) (proto.GitserverService_P4ExecClient, error) {
|
||||
called = true
|
||||
return &mockP4ExecClient{
|
||||
Err: test.mockErr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &gitserver.MockGRPCClient{MockP4Exec: mockP4Exec}
|
||||
}
|
||||
})
|
||||
|
||||
cli := gitserver.NewTestClient(&http.Client{}, source)
|
||||
rc, _, err := cli.P4Exec(ctx, test.host, test.user, test.password, test.args...)
|
||||
if diff := cmp.Diff(test.wantReaderConstructionError, fmt.Sprintf("%v", err)); diff != "" {
|
||||
t.Errorf("error when creating reader mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
var body []byte
|
||||
if rc != nil {
|
||||
t.Cleanup(func() {
|
||||
_ = rc.Close()
|
||||
})
|
||||
|
||||
body, err = io.ReadAll(rc)
|
||||
if err != nil {
|
||||
if diff := cmp.Diff(test.wantReaderError, fmt.Sprintf("%v", err)); diff != "" {
|
||||
t.Errorf("Mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(test.wantBody, string(body)); diff != "" {
|
||||
t.Fatalf("Mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
if !called {
|
||||
t.Fatal("GRPC should be called")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_P4Exec(t *testing.T) {
|
||||
_ = gitserver.CreateRepoDir(t)
|
||||
type test struct {
|
||||
name string
|
||||
host string
|
||||
user string
|
||||
password string
|
||||
args []string
|
||||
handler http.HandlerFunc
|
||||
wantBody string
|
||||
wantErr string
|
||||
}
|
||||
tests := []test{
|
||||
{
|
||||
name: "check request body",
|
||||
host: "ssl:111.222.333.444:1666",
|
||||
user: "admin",
|
||||
password: "pa$$word",
|
||||
args: []string{"protects"},
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ProtoMajor == 2 {
|
||||
// Ignore attempted gRPC connections
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantBody := `{"p4port":"ssl:111.222.333.444:1666","p4user":"admin","p4passwd":"pa$$word","args":["protects"]}`
|
||||
if diff := cmp.Diff(wantBody, string(body)); diff != "" {
|
||||
t.Fatalf("Mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("example output"))
|
||||
},
|
||||
wantBody: "example output",
|
||||
wantErr: "<nil>",
|
||||
},
|
||||
{
|
||||
name: "error response",
|
||||
handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ProtoMajor == 2 {
|
||||
// Ignore attempted gRPC connections
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("example error"))
|
||||
},
|
||||
wantErr: "unexpected status code: 400 - example error",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
runTest := func(t *testing.T, test test, cli gitserver.Client, called bool) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Log(test.name)
|
||||
|
||||
rc, _, err := cli.P4Exec(ctx, test.host, test.user, test.password, test.args...)
|
||||
if diff := cmp.Diff(test.wantErr, fmt.Sprintf("%v", err)); diff != "" {
|
||||
t.Fatalf("Mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
var body []byte
|
||||
if rc != nil {
|
||||
defer func() { _ = rc.Close() }()
|
||||
|
||||
body, err = io.ReadAll(rc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(test.wantBody, string(body)); diff != "" {
|
||||
t.Fatalf("Mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
t.Run("HTTP", func(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
conf.Mock(&conf.Unified{
|
||||
SiteConfiguration: schema.SiteConfiguration{
|
||||
ExperimentalFeatures: &schema.ExperimentalFeatures{
|
||||
EnableGRPC: boolPointer(false),
|
||||
},
|
||||
},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
conf.Mock(nil)
|
||||
})
|
||||
|
||||
testServer := httptest.NewServer(test.handler)
|
||||
defer testServer.Close()
|
||||
|
||||
u, _ := url.Parse(testServer.URL)
|
||||
addrs := []string{u.Host}
|
||||
source := gitserver.NewTestClientSource(t, addrs)
|
||||
called := false
|
||||
|
||||
cli := gitserver.NewTestClient(&http.Client{}, source)
|
||||
runTest(t, test, cli, called)
|
||||
|
||||
if called {
|
||||
t.Fatal("handler shoulde be called")
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_BatchLogGRPC(t *testing.T) {
|
||||
conf.Mock(&conf.Unified{
|
||||
SiteConfiguration: schema.SiteConfiguration{
|
||||
@ -703,7 +410,9 @@ func TestClient_BatchLogGRPC(t *testing.T) {
|
||||
return resp.ToProto(), nil
|
||||
}
|
||||
|
||||
return &gitserver.MockGRPCClient{MockBatchLog: mockBatchLog}
|
||||
cli := gitserver.NewStrictMockGitserverServiceClient()
|
||||
cli.BatchLogFunc.SetDefaultHook(mockBatchLog)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
|
||||
@ -784,9 +493,9 @@ func TestClient_BatchLog(t *testing.T) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &gitserver.MockGRPCClient{
|
||||
MockBatchLog: mockBatchLog,
|
||||
}
|
||||
cli := gitserver.NewStrictMockGitserverServiceClient()
|
||||
cli.BatchLogFunc.SetDefaultHook(mockBatchLog)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
|
||||
@ -984,7 +693,9 @@ func TestClient_IsRepoCloneableGRPC(t *testing.T) {
|
||||
}
|
||||
return tc.mockResponse.ToProto(), nil
|
||||
}
|
||||
return &gitserver.MockGRPCClient{MockIsRepoCloneable: mockIsRepoCloneable}
|
||||
cli := gitserver.NewStrictMockGitserverServiceClient()
|
||||
cli.IsRepoCloneableFunc.SetDefaultHook(mockIsRepoCloneable)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
|
||||
@ -1021,7 +732,9 @@ func TestClient_IsRepoCloneableGRPC(t *testing.T) {
|
||||
}
|
||||
return tc.mockResponse.ToProto(), nil
|
||||
}
|
||||
return &gitserver.MockGRPCClient{MockIsRepoCloneable: mockIsRepoCloneable}
|
||||
cli := gitserver.NewStrictMockGitserverServiceClient()
|
||||
cli.IsRepoCloneableFunc.SetDefaultHook(mockIsRepoCloneable)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
|
||||
@ -1056,7 +769,7 @@ func TestClient_SystemsInfo(t *testing.T) {
|
||||
gitserverAddr2 = "172.16.8.2:8080"
|
||||
)
|
||||
|
||||
expectedResponses := []gitserver.SystemInfo{
|
||||
expectedResponses := []protocol.SystemInfo{
|
||||
{
|
||||
Address: gitserverAddr1,
|
||||
FreeSpace: 102400,
|
||||
@ -1123,7 +836,9 @@ func TestClient_SystemsInfo(t *testing.T) {
|
||||
called = true
|
||||
return response, nil
|
||||
}
|
||||
return &gitserver.MockGRPCClient{MockDiskInfo: mockDiskInfo}
|
||||
cli := gitserver.NewStrictMockGitserverServiceClient()
|
||||
cli.DiskInfoFunc.SetDefaultHook(mockDiskInfo)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
|
||||
@ -1154,7 +869,9 @@ func TestClient_SystemsInfo(t *testing.T) {
|
||||
called = true
|
||||
return nil, nil
|
||||
}
|
||||
return &gitserver.MockGRPCClient{MockDiskInfo: mockDiskInfo}
|
||||
cli := gitserver.NewStrictMockGitserverServiceClient()
|
||||
cli.DiskInfoFunc.SetDefaultHook(mockDiskInfo)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
|
||||
@ -1227,7 +944,9 @@ func TestClient_SystemInfo(t *testing.T) {
|
||||
called = true
|
||||
return mockResponse, nil
|
||||
}
|
||||
return &gitserver.MockGRPCClient{MockDiskInfo: mockDiskInfo}
|
||||
cli := gitserver.NewStrictMockGitserverServiceClient()
|
||||
cli.DiskInfoFunc.SetDefaultHook(mockDiskInfo)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
|
||||
@ -1259,7 +978,9 @@ func TestClient_SystemInfo(t *testing.T) {
|
||||
called = true
|
||||
return mockResponse, nil
|
||||
}
|
||||
return &gitserver.MockGRPCClient{MockDiskInfo: mockDiskInfo}
|
||||
cli := gitserver.NewStrictMockGitserverServiceClient()
|
||||
cli.DiskInfoFunc.SetDefaultHook(mockDiskInfo)
|
||||
return cli
|
||||
}
|
||||
})
|
||||
|
||||
@ -1287,8 +1008,6 @@ func TestClient_SystemInfo(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
var _ proto.GitserverService_P4ExecClient = &mockP4ExecClient{}
|
||||
|
||||
type fuzzTime time.Time
|
||||
|
||||
func (fuzzTime) Generate(rand *rand.Rand, _ int) reflect.Value {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -5,11 +5,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sourcegraph/log"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/gitdomain"
|
||||
"github.com/sourcegraph/sourcegraph/internal/metrics"
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
type operations struct {
|
||||
@ -32,7 +32,6 @@ type operations struct {
|
||||
lstat *observation.Operation
|
||||
mergeBase *observation.Operation
|
||||
newFileReader *observation.Operation
|
||||
p4Exec *observation.Operation
|
||||
readDir *observation.Operation
|
||||
readFile *observation.Operation
|
||||
resolveRevision *observation.Operation
|
||||
@ -107,7 +106,6 @@ func newOperations(observationCtx *observation.Context) *operations {
|
||||
lstat: subOp("lStat"),
|
||||
mergeBase: op("MergeBase"),
|
||||
newFileReader: op("NewFileReader"),
|
||||
p4Exec: op("P4Exec"),
|
||||
readDir: op("ReadDir"),
|
||||
readFile: op("ReadFile"),
|
||||
resolveRevision: resolveRevisionOperation,
|
||||
|
||||
@ -2,7 +2,6 @@ package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@ -364,36 +363,6 @@ func (bl *BatchLogResult) FromProto(p *proto.BatchLogResult) {
|
||||
}
|
||||
}
|
||||
|
||||
// P4ExecRequest is a request to execute a p4 command with given arguments.
|
||||
//
|
||||
// Note that this request is deserialized by both gitserver and the frontend's
|
||||
// internal proxy route and any major change to this structure will need to be
|
||||
// reconciled in both places.
|
||||
type P4ExecRequest struct {
|
||||
P4Port string `json:"p4port"`
|
||||
P4User string `json:"p4user"`
|
||||
P4Passwd string `json:"p4passwd"`
|
||||
Args []string `json:"args"`
|
||||
}
|
||||
|
||||
func (r *P4ExecRequest) ToProto() *proto.P4ExecRequest {
|
||||
return &proto.P4ExecRequest{
|
||||
P4Port: r.P4Port,
|
||||
P4User: r.P4User,
|
||||
P4Passwd: r.P4Passwd,
|
||||
Args: stringsToByteSlices(r.Args),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *P4ExecRequest) FromProto(p *proto.P4ExecRequest) {
|
||||
*r = P4ExecRequest{
|
||||
P4Port: p.GetP4Port(),
|
||||
P4User: p.GetP4User(),
|
||||
P4Passwd: p.GetP4Passwd(),
|
||||
Args: byteSlicesToStrings(p.GetArgs()),
|
||||
}
|
||||
}
|
||||
|
||||
// RepoUpdateRequest is a request to update the contents of a given repo, or clone it if it doesn't exist.
|
||||
type RepoUpdateRequest struct {
|
||||
// Repo identifies URL for repo.
|
||||
@ -863,56 +832,6 @@ func (r *GetObjectResponse) FromProto(p *proto.GetObjectResponse) {
|
||||
|
||||
}
|
||||
|
||||
type PerforceChangelist struct {
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
State PerforceChangelistState
|
||||
Author string
|
||||
Title string
|
||||
Message string
|
||||
}
|
||||
|
||||
type PerforceChangelistState string
|
||||
|
||||
const (
|
||||
PerforceChangelistStateSubmitted PerforceChangelistState = "submitted"
|
||||
PerforceChangelistStatePending PerforceChangelistState = "pending"
|
||||
PerforceChangelistStateShelved PerforceChangelistState = "shelved"
|
||||
// Perforce doesn't actually return a state for closed changelists, so this is one we use to indicate the changelist is closed.
|
||||
PerforceChangelistStateClosed PerforceChangelistState = "closed"
|
||||
)
|
||||
|
||||
func ParsePerforceChangelistState(state string) (PerforceChangelistState, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(state)) {
|
||||
case "submitted":
|
||||
return PerforceChangelistStateSubmitted, nil
|
||||
case "pending":
|
||||
return PerforceChangelistStatePending, nil
|
||||
case "shelved":
|
||||
return PerforceChangelistStateShelved, nil
|
||||
case "closed":
|
||||
return PerforceChangelistStateClosed, nil
|
||||
default:
|
||||
return "", errors.Newf("invalid Perforce changelist state: %s", state)
|
||||
}
|
||||
}
|
||||
|
||||
func stringsToByteSlices(in []string) [][]byte {
|
||||
res := make([][]byte, len(in))
|
||||
for i, s := range in {
|
||||
res[i] = []byte(s)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func byteSlicesToStrings(in [][]byte) []string {
|
||||
res := make([]string, len(in))
|
||||
for i, s := range in {
|
||||
res[i] = string(s)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// IsPerforcePathCloneableRequest is the request to check if a Perforce path is cloneable.
|
||||
type IsPerforcePathCloneableRequest struct {
|
||||
P4Port string `json:"p4port"`
|
||||
@ -933,3 +852,112 @@ type CheckPerforceCredentialsRequest struct {
|
||||
|
||||
// IsPerforcePathCloneableResponse is the response from checking if given Perforce credentials are valid.
|
||||
type CheckPerforceCredentialsResponse struct{}
|
||||
|
||||
// PerforceConnectionDetails holds all the details required to talk to a Perforce server.
|
||||
type PerforceConnectionDetails struct {
|
||||
P4Port string
|
||||
P4User string
|
||||
P4Passwd string
|
||||
}
|
||||
|
||||
func (c PerforceConnectionDetails) ToProto() *proto.PerforceConnectionDetails {
|
||||
return &proto.PerforceConnectionDetails{
|
||||
P4Port: c.P4Port,
|
||||
P4User: c.P4User,
|
||||
P4Passwd: c.P4Passwd,
|
||||
}
|
||||
}
|
||||
|
||||
// SystemInfo holds info on a Gitserver instance.
|
||||
type SystemInfo struct {
|
||||
Address string
|
||||
FreeSpace uint64
|
||||
TotalSpace uint64
|
||||
PercentUsed float32
|
||||
}
|
||||
|
||||
type PerforceUsersRequest struct {
|
||||
P4Port string `json:"p4port"`
|
||||
P4User string `json:"p4user"`
|
||||
P4Passwd string `json:"p4passwd"`
|
||||
}
|
||||
|
||||
type PerforceUsersResponse struct {
|
||||
Users []PerforceUser `json:"users"`
|
||||
}
|
||||
|
||||
type PerforceUser struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type PerforceProtectsForUserRequest struct {
|
||||
P4Port string `json:"p4port"`
|
||||
P4User string `json:"p4user"`
|
||||
P4Passwd string `json:"p4passwd"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type PerforceProtectsForUserResponse struct {
|
||||
Protects []PerforceProtect `json:"protects"`
|
||||
}
|
||||
|
||||
type PerforceProtect struct {
|
||||
Level string `json:"level"`
|
||||
EntityType string `json:"entityType"`
|
||||
EntityName string `json:"entityName"`
|
||||
Match string `json:"match"`
|
||||
IsExclusion bool `json:"isExclusion"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
type PerforceProtectsForDepotRequest struct {
|
||||
P4Port string `json:"p4port"`
|
||||
P4User string `json:"p4user"`
|
||||
P4Passwd string `json:"p4passwd"`
|
||||
Depot string `json:"depot"`
|
||||
}
|
||||
|
||||
type PerforceProtectsForDepotResponse struct {
|
||||
Protects []PerforceProtect `json:"protects"`
|
||||
}
|
||||
|
||||
type PerforceGroupMembersRequest struct {
|
||||
P4Port string `json:"p4port"`
|
||||
P4User string `json:"p4user"`
|
||||
P4Passwd string `json:"p4passwd"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
|
||||
type PerforceGroupMembersResponse struct {
|
||||
Usernames []string `json:"usernames"`
|
||||
}
|
||||
|
||||
type IsPerforceSuperUserRequest struct {
|
||||
P4Port string `json:"p4port"`
|
||||
P4User string `json:"p4user"`
|
||||
P4Passwd string `json:"p4passwd"`
|
||||
}
|
||||
|
||||
type IsPerforceSuperUserResponse struct {
|
||||
}
|
||||
|
||||
type PerforceGetChangelistRequest struct {
|
||||
P4Port string `json:"p4port"`
|
||||
P4User string `json:"p4user"`
|
||||
P4Passwd string `json:"p4passwd"`
|
||||
ChangelistID string `json:"changelistID"`
|
||||
}
|
||||
|
||||
type PerforceGetChangelistResponse struct {
|
||||
Changelist PerforceChangelist `json:"changelist"`
|
||||
}
|
||||
|
||||
type PerforceChangelist struct {
|
||||
ID string `json:"id"`
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
State string `json:"state"`
|
||||
Author string `json:"author"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
@ -39,6 +39,7 @@ go_library(
|
||||
"//internal/gitserver:__pkg__",
|
||||
"//internal/gitserver/gitdomain:__pkg__",
|
||||
"//internal/gitserver/protocol:__pkg__",
|
||||
"//internal/perforce:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//lib/errors",
|
||||
|
||||
2084
internal/gitserver/v1/gitserver.pb.go
generated
2084
internal/gitserver/v1/gitserver.pb.go
generated
File diff suppressed because it is too large
Load Diff
@ -17,13 +17,21 @@ service GitserverService {
|
||||
rpc ListGitolite(ListGitoliteRequest) returns (ListGitoliteResponse) {}
|
||||
rpc Search(SearchRequest) returns (stream SearchResponse) {}
|
||||
rpc Archive(ArchiveRequest) returns (stream ArchiveResponse) {}
|
||||
rpc P4Exec(P4ExecRequest) returns (stream P4ExecResponse) {}
|
||||
rpc P4Exec(P4ExecRequest) returns (stream P4ExecResponse) {
|
||||
option deprecated = true;
|
||||
}
|
||||
rpc RepoClone(RepoCloneRequest) returns (RepoCloneResponse) {}
|
||||
rpc RepoCloneProgress(RepoCloneProgressRequest) returns (RepoCloneProgressResponse) {}
|
||||
rpc RepoDelete(RepoDeleteRequest) returns (RepoDeleteResponse) {}
|
||||
rpc RepoUpdate(RepoUpdateRequest) returns (RepoUpdateResponse) {}
|
||||
rpc IsPerforcePathCloneable(IsPerforcePathCloneableRequest) returns (IsPerforcePathCloneableResponse) {}
|
||||
rpc CheckPerforceCredentials(CheckPerforceCredentialsRequest) returns (CheckPerforceCredentialsResponse) {}
|
||||
rpc PerforceUsers(PerforceUsersRequest) returns (PerforceUsersResponse) {}
|
||||
rpc PerforceProtectsForUser(PerforceProtectsForUserRequest) returns (PerforceProtectsForUserResponse) {}
|
||||
rpc PerforceProtectsForDepot(PerforceProtectsForDepotRequest) returns (PerforceProtectsForDepotResponse) {}
|
||||
rpc PerforceGroupMembers(PerforceGroupMembersRequest) returns (PerforceGroupMembersResponse) {}
|
||||
rpc IsPerforceSuperUser(IsPerforceSuperUserRequest) returns (IsPerforceSuperUserResponse) {}
|
||||
rpc PerforceGetChangelist(PerforceGetChangelistRequest) returns (PerforceGetChangelistResponse) {}
|
||||
}
|
||||
|
||||
// DiskInfoRequest is a empty request for the DiskInfo RPC.
|
||||
@ -447,15 +455,17 @@ message RepoUpdateResponse {
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
// Do not use: The P4Exec method has been deprecated and will disappear soon!
|
||||
message P4ExecRequest {
|
||||
string p4port = 1;
|
||||
string p4user = 2;
|
||||
string p4passwd = 3;
|
||||
repeated bytes args = 4;
|
||||
string p4port = 1 [deprecated = true];
|
||||
string p4user = 2 [deprecated = true];
|
||||
string p4passwd = 3 [deprecated = true];
|
||||
repeated bytes args = 4 [deprecated = true];
|
||||
}
|
||||
|
||||
// Do not use: The P4Exec method has been deprecated and will disappear soon!
|
||||
message P4ExecResponse {
|
||||
bytes data = 1;
|
||||
bytes data = 1 [deprecated = true];
|
||||
}
|
||||
|
||||
// ListGitoliteRequest is a request to list all repositories in gitolite.
|
||||
@ -509,10 +519,8 @@ message GitObject {
|
||||
|
||||
// IsPerforcePathCloneableRequest is the request to check if a Perforce path is cloneable.
|
||||
message IsPerforcePathCloneableRequest {
|
||||
string p4port = 1;
|
||||
string p4user = 2;
|
||||
string p4passwd = 3;
|
||||
string depot_path = 4;
|
||||
PerforceConnectionDetails connection_details = 1;
|
||||
string depot_path = 2;
|
||||
}
|
||||
|
||||
// IsPerforcePathCloneableResponse is the response from checking if a Perforce path is cloneable.
|
||||
@ -520,10 +528,121 @@ message IsPerforcePathCloneableResponse {}
|
||||
|
||||
// CheckPerforceCredentialsRequest is the request to check if given Perforce credentials are valid.
|
||||
message CheckPerforceCredentialsRequest {
|
||||
PerforceConnectionDetails connection_details = 1;
|
||||
}
|
||||
|
||||
// IsPerforcePathCloneableResponse is the response from checking if given Perforce credentials are valid.
|
||||
message CheckPerforceCredentialsResponse {}
|
||||
|
||||
// PerforceConnectionDetails holds all the details required to talk to a Perforce server.
|
||||
message PerforceConnectionDetails {
|
||||
string p4port = 1;
|
||||
string p4user = 2;
|
||||
string p4passwd = 3;
|
||||
}
|
||||
|
||||
// IsPerforcePathCloneableResponse is the response from checking if given Perforce credentials are valid.
|
||||
message CheckPerforceCredentialsResponse {}
|
||||
// PerforceGetChangelistRequest is used to retrieve information about a specific
|
||||
// Perforce changelist.
|
||||
message PerforceGetChangelistRequest {
|
||||
PerforceConnectionDetails connection_details = 1;
|
||||
string changelist_id = 2;
|
||||
}
|
||||
|
||||
// PerforceGetChangelistResponse returns information about the requested changelist.
|
||||
message PerforceGetChangelistResponse {
|
||||
PerforceChangelist changelist = 1;
|
||||
}
|
||||
|
||||
// PerforceChangelist represents a changelist in Perforce.
|
||||
message PerforceChangelist {
|
||||
// PerforceChangelistState is the valid state values of a Perforce changelist.
|
||||
enum PerforceChangelistState {
|
||||
PERFORCE_CHANGELIST_STATE_UNSPECIFIED = 0;
|
||||
PERFORCE_CHANGELIST_STATE_SUBMITTED = 1;
|
||||
PERFORCE_CHANGELIST_STATE_PENDING = 2;
|
||||
PERFORCE_CHANGELIST_STATE_SHELVED = 3;
|
||||
// Perforce doesn't actually return a state for closed changelists, so this is
|
||||
// one we use to indicate the changelist is closed.
|
||||
PERFORCE_CHANGELIST_STATE_CLOSED = 4;
|
||||
}
|
||||
string id = 1;
|
||||
google.protobuf.Timestamp creation_date = 2;
|
||||
PerforceChangelistState state = 3;
|
||||
string author = 4;
|
||||
string title = 5;
|
||||
string message = 6;
|
||||
}
|
||||
|
||||
// IsPerforceSuperUserRequest can be used to check if a given Perforce user is a
|
||||
// super user.
|
||||
message IsPerforceSuperUserRequest {
|
||||
PerforceConnectionDetails connection_details = 1;
|
||||
}
|
||||
|
||||
// IsPerforceSuperUserResponse is the response from checking if a given Perforce
|
||||
// user is a super user.
|
||||
// No fields here, returning an error means "no".
|
||||
message IsPerforceSuperUserResponse {}
|
||||
|
||||
// PerforceProtectsForDepotRequest requests all the protections that apply to the
|
||||
// given depot.
|
||||
message PerforceProtectsForDepotRequest {
|
||||
PerforceConnectionDetails connection_details = 1;
|
||||
string depot = 2;
|
||||
}
|
||||
|
||||
// PerforceProtectsForDepotResponse returns all the protections that apply to the
|
||||
// given depot.
|
||||
message PerforceProtectsForDepotResponse {
|
||||
repeated PerforceProtect protects = 1;
|
||||
}
|
||||
|
||||
// PerforceProtectsForUserRequest requests all the protections that apply to the
|
||||
// given user.
|
||||
message PerforceProtectsForUserRequest {
|
||||
PerforceConnectionDetails connection_details = 1;
|
||||
string username = 2;
|
||||
}
|
||||
|
||||
// PerforceProtectsForUserResponse returns all the protections that apply to the
|
||||
// given user.
|
||||
message PerforceProtectsForUserResponse {
|
||||
repeated PerforceProtect protects = 1;
|
||||
}
|
||||
|
||||
// PerforceProtect is a single line definition of a protection in Perforce.
|
||||
message PerforceProtect {
|
||||
string level = 1;
|
||||
string entity_type = 2;
|
||||
string entity_name = 3;
|
||||
string match = 4;
|
||||
bool is_exclusion = 5;
|
||||
string host = 6;
|
||||
}
|
||||
|
||||
// PerforceGroupMembersRequest requests the members of the given Perforce group.
|
||||
message PerforceGroupMembersRequest {
|
||||
PerforceConnectionDetails connection_details = 1;
|
||||
string group = 2;
|
||||
}
|
||||
|
||||
// PerforceGroupMembersResponse returns the members of the requested Perforce group.
|
||||
message PerforceGroupMembersResponse {
|
||||
repeated string usernames = 1;
|
||||
}
|
||||
|
||||
// PerforceUsersRequest lists all the users known to the Perforce server.
|
||||
message PerforceUsersRequest {
|
||||
PerforceConnectionDetails connection_details = 1;
|
||||
}
|
||||
|
||||
// PerforceUsersResponse contains the list of users known by the server.
|
||||
message PerforceUsersResponse {
|
||||
repeated PerforceUser users = 1;
|
||||
}
|
||||
|
||||
// PerforceUser is a representation of a user account in Perforce.
|
||||
message PerforceUser {
|
||||
string username = 1;
|
||||
string email = 2;
|
||||
}
|
||||
|
||||
225
internal/gitserver/v1/gitserver_grpc.pb.go
generated
225
internal/gitserver/v1/gitserver_grpc.pb.go
generated
@ -35,6 +35,12 @@ const (
|
||||
GitserverService_RepoUpdate_FullMethodName = "/gitserver.v1.GitserverService/RepoUpdate"
|
||||
GitserverService_IsPerforcePathCloneable_FullMethodName = "/gitserver.v1.GitserverService/IsPerforcePathCloneable"
|
||||
GitserverService_CheckPerforceCredentials_FullMethodName = "/gitserver.v1.GitserverService/CheckPerforceCredentials"
|
||||
GitserverService_PerforceUsers_FullMethodName = "/gitserver.v1.GitserverService/PerforceUsers"
|
||||
GitserverService_PerforceProtectsForUser_FullMethodName = "/gitserver.v1.GitserverService/PerforceProtectsForUser"
|
||||
GitserverService_PerforceProtectsForDepot_FullMethodName = "/gitserver.v1.GitserverService/PerforceProtectsForDepot"
|
||||
GitserverService_PerforceGroupMembers_FullMethodName = "/gitserver.v1.GitserverService/PerforceGroupMembers"
|
||||
GitserverService_IsPerforceSuperUser_FullMethodName = "/gitserver.v1.GitserverService/IsPerforceSuperUser"
|
||||
GitserverService_PerforceGetChangelist_FullMethodName = "/gitserver.v1.GitserverService/PerforceGetChangelist"
|
||||
)
|
||||
|
||||
// GitserverServiceClient is the client API for GitserverService service.
|
||||
@ -50,6 +56,7 @@ type GitserverServiceClient interface {
|
||||
ListGitolite(ctx context.Context, in *ListGitoliteRequest, opts ...grpc.CallOption) (*ListGitoliteResponse, error)
|
||||
Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (GitserverService_SearchClient, error)
|
||||
Archive(ctx context.Context, in *ArchiveRequest, opts ...grpc.CallOption) (GitserverService_ArchiveClient, error)
|
||||
// Deprecated: Do not use.
|
||||
P4Exec(ctx context.Context, in *P4ExecRequest, opts ...grpc.CallOption) (GitserverService_P4ExecClient, error)
|
||||
RepoClone(ctx context.Context, in *RepoCloneRequest, opts ...grpc.CallOption) (*RepoCloneResponse, error)
|
||||
RepoCloneProgress(ctx context.Context, in *RepoCloneProgressRequest, opts ...grpc.CallOption) (*RepoCloneProgressResponse, error)
|
||||
@ -57,6 +64,12 @@ type GitserverServiceClient interface {
|
||||
RepoUpdate(ctx context.Context, in *RepoUpdateRequest, opts ...grpc.CallOption) (*RepoUpdateResponse, error)
|
||||
IsPerforcePathCloneable(ctx context.Context, in *IsPerforcePathCloneableRequest, opts ...grpc.CallOption) (*IsPerforcePathCloneableResponse, error)
|
||||
CheckPerforceCredentials(ctx context.Context, in *CheckPerforceCredentialsRequest, opts ...grpc.CallOption) (*CheckPerforceCredentialsResponse, error)
|
||||
PerforceUsers(ctx context.Context, in *PerforceUsersRequest, opts ...grpc.CallOption) (*PerforceUsersResponse, error)
|
||||
PerforceProtectsForUser(ctx context.Context, in *PerforceProtectsForUserRequest, opts ...grpc.CallOption) (*PerforceProtectsForUserResponse, error)
|
||||
PerforceProtectsForDepot(ctx context.Context, in *PerforceProtectsForDepotRequest, opts ...grpc.CallOption) (*PerforceProtectsForDepotResponse, error)
|
||||
PerforceGroupMembers(ctx context.Context, in *PerforceGroupMembersRequest, opts ...grpc.CallOption) (*PerforceGroupMembersResponse, error)
|
||||
IsPerforceSuperUser(ctx context.Context, in *IsPerforceSuperUserRequest, opts ...grpc.CallOption) (*IsPerforceSuperUserResponse, error)
|
||||
PerforceGetChangelist(ctx context.Context, in *PerforceGetChangelistRequest, opts ...grpc.CallOption) (*PerforceGetChangelistResponse, error)
|
||||
}
|
||||
|
||||
type gitserverServiceClient struct {
|
||||
@ -242,6 +255,7 @@ func (x *gitserverServiceArchiveClient) Recv() (*ArchiveResponse, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Deprecated: Do not use.
|
||||
func (c *gitserverServiceClient) P4Exec(ctx context.Context, in *P4ExecRequest, opts ...grpc.CallOption) (GitserverService_P4ExecClient, error) {
|
||||
stream, err := c.cc.NewStream(ctx, &GitserverService_ServiceDesc.Streams[4], GitserverService_P4Exec_FullMethodName, opts...)
|
||||
if err != nil {
|
||||
@ -328,6 +342,60 @@ func (c *gitserverServiceClient) CheckPerforceCredentials(ctx context.Context, i
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *gitserverServiceClient) PerforceUsers(ctx context.Context, in *PerforceUsersRequest, opts ...grpc.CallOption) (*PerforceUsersResponse, error) {
|
||||
out := new(PerforceUsersResponse)
|
||||
err := c.cc.Invoke(ctx, GitserverService_PerforceUsers_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *gitserverServiceClient) PerforceProtectsForUser(ctx context.Context, in *PerforceProtectsForUserRequest, opts ...grpc.CallOption) (*PerforceProtectsForUserResponse, error) {
|
||||
out := new(PerforceProtectsForUserResponse)
|
||||
err := c.cc.Invoke(ctx, GitserverService_PerforceProtectsForUser_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *gitserverServiceClient) PerforceProtectsForDepot(ctx context.Context, in *PerforceProtectsForDepotRequest, opts ...grpc.CallOption) (*PerforceProtectsForDepotResponse, error) {
|
||||
out := new(PerforceProtectsForDepotResponse)
|
||||
err := c.cc.Invoke(ctx, GitserverService_PerforceProtectsForDepot_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *gitserverServiceClient) PerforceGroupMembers(ctx context.Context, in *PerforceGroupMembersRequest, opts ...grpc.CallOption) (*PerforceGroupMembersResponse, error) {
|
||||
out := new(PerforceGroupMembersResponse)
|
||||
err := c.cc.Invoke(ctx, GitserverService_PerforceGroupMembers_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *gitserverServiceClient) IsPerforceSuperUser(ctx context.Context, in *IsPerforceSuperUserRequest, opts ...grpc.CallOption) (*IsPerforceSuperUserResponse, error) {
|
||||
out := new(IsPerforceSuperUserResponse)
|
||||
err := c.cc.Invoke(ctx, GitserverService_IsPerforceSuperUser_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *gitserverServiceClient) PerforceGetChangelist(ctx context.Context, in *PerforceGetChangelistRequest, opts ...grpc.CallOption) (*PerforceGetChangelistResponse, error) {
|
||||
out := new(PerforceGetChangelistResponse)
|
||||
err := c.cc.Invoke(ctx, GitserverService_PerforceGetChangelist_FullMethodName, in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GitserverServiceServer is the server API for GitserverService service.
|
||||
// All implementations must embed UnimplementedGitserverServiceServer
|
||||
// for forward compatibility
|
||||
@ -341,6 +409,7 @@ type GitserverServiceServer interface {
|
||||
ListGitolite(context.Context, *ListGitoliteRequest) (*ListGitoliteResponse, error)
|
||||
Search(*SearchRequest, GitserverService_SearchServer) error
|
||||
Archive(*ArchiveRequest, GitserverService_ArchiveServer) error
|
||||
// Deprecated: Do not use.
|
||||
P4Exec(*P4ExecRequest, GitserverService_P4ExecServer) error
|
||||
RepoClone(context.Context, *RepoCloneRequest) (*RepoCloneResponse, error)
|
||||
RepoCloneProgress(context.Context, *RepoCloneProgressRequest) (*RepoCloneProgressResponse, error)
|
||||
@ -348,6 +417,12 @@ type GitserverServiceServer interface {
|
||||
RepoUpdate(context.Context, *RepoUpdateRequest) (*RepoUpdateResponse, error)
|
||||
IsPerforcePathCloneable(context.Context, *IsPerforcePathCloneableRequest) (*IsPerforcePathCloneableResponse, error)
|
||||
CheckPerforceCredentials(context.Context, *CheckPerforceCredentialsRequest) (*CheckPerforceCredentialsResponse, error)
|
||||
PerforceUsers(context.Context, *PerforceUsersRequest) (*PerforceUsersResponse, error)
|
||||
PerforceProtectsForUser(context.Context, *PerforceProtectsForUserRequest) (*PerforceProtectsForUserResponse, error)
|
||||
PerforceProtectsForDepot(context.Context, *PerforceProtectsForDepotRequest) (*PerforceProtectsForDepotResponse, error)
|
||||
PerforceGroupMembers(context.Context, *PerforceGroupMembersRequest) (*PerforceGroupMembersResponse, error)
|
||||
IsPerforceSuperUser(context.Context, *IsPerforceSuperUserRequest) (*IsPerforceSuperUserResponse, error)
|
||||
PerforceGetChangelist(context.Context, *PerforceGetChangelistRequest) (*PerforceGetChangelistResponse, error)
|
||||
mustEmbedUnimplementedGitserverServiceServer()
|
||||
}
|
||||
|
||||
@ -403,6 +478,24 @@ func (UnimplementedGitserverServiceServer) IsPerforcePathCloneable(context.Conte
|
||||
func (UnimplementedGitserverServiceServer) CheckPerforceCredentials(context.Context, *CheckPerforceCredentialsRequest) (*CheckPerforceCredentialsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckPerforceCredentials not implemented")
|
||||
}
|
||||
func (UnimplementedGitserverServiceServer) PerforceUsers(context.Context, *PerforceUsersRequest) (*PerforceUsersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PerforceUsers not implemented")
|
||||
}
|
||||
func (UnimplementedGitserverServiceServer) PerforceProtectsForUser(context.Context, *PerforceProtectsForUserRequest) (*PerforceProtectsForUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PerforceProtectsForUser not implemented")
|
||||
}
|
||||
func (UnimplementedGitserverServiceServer) PerforceProtectsForDepot(context.Context, *PerforceProtectsForDepotRequest) (*PerforceProtectsForDepotResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PerforceProtectsForDepot not implemented")
|
||||
}
|
||||
func (UnimplementedGitserverServiceServer) PerforceGroupMembers(context.Context, *PerforceGroupMembersRequest) (*PerforceGroupMembersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PerforceGroupMembers not implemented")
|
||||
}
|
||||
func (UnimplementedGitserverServiceServer) IsPerforceSuperUser(context.Context, *IsPerforceSuperUserRequest) (*IsPerforceSuperUserResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method IsPerforceSuperUser not implemented")
|
||||
}
|
||||
func (UnimplementedGitserverServiceServer) PerforceGetChangelist(context.Context, *PerforceGetChangelistRequest) (*PerforceGetChangelistResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PerforceGetChangelist not implemented")
|
||||
}
|
||||
func (UnimplementedGitserverServiceServer) mustEmbedUnimplementedGitserverServiceServer() {}
|
||||
|
||||
// UnsafeGitserverServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
@ -724,6 +817,114 @@ func _GitserverService_CheckPerforceCredentials_Handler(srv interface{}, ctx con
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GitserverService_PerforceUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PerforceUsersRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GitserverServiceServer).PerforceUsers(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GitserverService_PerforceUsers_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GitserverServiceServer).PerforceUsers(ctx, req.(*PerforceUsersRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GitserverService_PerforceProtectsForUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PerforceProtectsForUserRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GitserverServiceServer).PerforceProtectsForUser(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GitserverService_PerforceProtectsForUser_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GitserverServiceServer).PerforceProtectsForUser(ctx, req.(*PerforceProtectsForUserRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GitserverService_PerforceProtectsForDepot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PerforceProtectsForDepotRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GitserverServiceServer).PerforceProtectsForDepot(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GitserverService_PerforceProtectsForDepot_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GitserverServiceServer).PerforceProtectsForDepot(ctx, req.(*PerforceProtectsForDepotRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GitserverService_PerforceGroupMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PerforceGroupMembersRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GitserverServiceServer).PerforceGroupMembers(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GitserverService_PerforceGroupMembers_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GitserverServiceServer).PerforceGroupMembers(ctx, req.(*PerforceGroupMembersRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GitserverService_IsPerforceSuperUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(IsPerforceSuperUserRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GitserverServiceServer).IsPerforceSuperUser(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GitserverService_IsPerforceSuperUser_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GitserverServiceServer).IsPerforceSuperUser(ctx, req.(*IsPerforceSuperUserRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GitserverService_PerforceGetChangelist_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PerforceGetChangelistRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GitserverServiceServer).PerforceGetChangelist(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GitserverService_PerforceGetChangelist_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GitserverServiceServer).PerforceGetChangelist(ctx, req.(*PerforceGetChangelistRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// GitserverService_ServiceDesc is the grpc.ServiceDesc for GitserverService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -775,6 +976,30 @@ var GitserverService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "CheckPerforceCredentials",
|
||||
Handler: _GitserverService_CheckPerforceCredentials_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PerforceUsers",
|
||||
Handler: _GitserverService_PerforceUsers_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PerforceProtectsForUser",
|
||||
Handler: _GitserverService_PerforceProtectsForUser_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PerforceProtectsForDepot",
|
||||
Handler: _GitserverService_PerforceProtectsForDepot_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PerforceGroupMembers",
|
||||
Handler: _GitserverService_PerforceGroupMembers_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "IsPerforceSuperUser",
|
||||
Handler: _GitserverService_IsPerforceSuperUser_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PerforceGetChangelist",
|
||||
Handler: _GitserverService_PerforceGetChangelist_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
load("//dev:go_defs.bzl", "go_test")
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
load("//dev:go_defs.bzl", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "perforce",
|
||||
srcs = ["changelist.go"],
|
||||
srcs = [
|
||||
"changelist.go",
|
||||
"types.go",
|
||||
],
|
||||
importpath = "github.com/sourcegraph/sourcegraph/internal/perforce",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/api",
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/gitserver/v1:gitserver",
|
||||
"//internal/lazyregexp",
|
||||
"//lib/errors",
|
||||
"@org_golang_google_protobuf//types/known/timestamppb",
|
||||
],
|
||||
)
|
||||
|
||||
@ -18,9 +22,5 @@ go_test(
|
||||
name = "perforce_test",
|
||||
srcs = ["changelist_test.go"],
|
||||
embed = [":perforce"],
|
||||
deps = [
|
||||
"//internal/gitserver/protocol",
|
||||
"@com_github_google_go_cmp//cmp",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
deps = ["@com_github_stretchr_testify//require"],
|
||||
)
|
||||
|
||||
@ -3,17 +3,73 @@ package perforce
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
v1 "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
|
||||
"github.com/sourcegraph/sourcegraph/internal/lazyregexp"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
)
|
||||
|
||||
type Changelist struct {
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
State ChangelistState
|
||||
Author string
|
||||
Title string
|
||||
Message string
|
||||
}
|
||||
|
||||
func ChangelistFromProto(proto *v1.PerforceChangelist) *Changelist {
|
||||
return &Changelist{
|
||||
ID: proto.GetId(),
|
||||
CreationDate: proto.GetCreationDate().AsTime(),
|
||||
State: "help",
|
||||
Author: proto.GetAuthor(),
|
||||
Title: proto.GetTitle(),
|
||||
Message: proto.GetMessage(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Changelist) ToProto() *v1.PerforceChangelist {
|
||||
return &v1.PerforceChangelist{
|
||||
Id: c.ID,
|
||||
CreationDate: timestamppb.New(c.CreationDate),
|
||||
State: c.State.ToProto(),
|
||||
Author: c.Author,
|
||||
Title: c.Title,
|
||||
Message: c.Message,
|
||||
}
|
||||
}
|
||||
|
||||
type ChangelistState string
|
||||
|
||||
func (s ChangelistState) ToProto() v1.PerforceChangelist_PerforceChangelistState {
|
||||
switch s {
|
||||
case ChangelistStateSubmitted:
|
||||
return v1.PerforceChangelist_PERFORCE_CHANGELIST_STATE_SUBMITTED
|
||||
case ChangelistStatePending:
|
||||
return v1.PerforceChangelist_PERFORCE_CHANGELIST_STATE_PENDING
|
||||
case ChangelistStateShelved:
|
||||
return v1.PerforceChangelist_PERFORCE_CHANGELIST_STATE_SHELVED
|
||||
case ChangelistStateClosed:
|
||||
return v1.PerforceChangelist_PERFORCE_CHANGELIST_STATE_CLOSED
|
||||
default:
|
||||
return v1.PerforceChangelist_PERFORCE_CHANGELIST_STATE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ChangelistStateSubmitted ChangelistState = "submitted"
|
||||
ChangelistStatePending ChangelistState = "pending"
|
||||
ChangelistStateShelved ChangelistState = "shelved"
|
||||
// Perforce doesn't actually return a state for closed changelists, so this is one we use to indicate the changelist is closed.
|
||||
ChangelistStateClosed ChangelistState = "closed"
|
||||
)
|
||||
|
||||
// Either git-p4 or p4-fusion could have been used to convert a perforce depot to a git repo. In
|
||||
// which case the which case the commit message would look like:
|
||||
//
|
||||
@ -45,89 +101,3 @@ func (e *ChangelistNotFoundError) NotFound() bool { return true }
|
||||
func (e *ChangelistNotFoundError) Error() string {
|
||||
return fmt.Sprintf("changelist ID not found. repo=%d, changelist id=%d", e.RepoID, e.ID)
|
||||
}
|
||||
|
||||
type BadChangelistError struct {
|
||||
CID string
|
||||
Repo api.RepoName
|
||||
}
|
||||
|
||||
func (e *BadChangelistError) Error() string {
|
||||
return fmt.Sprintf("invalid changelist ID %q for repo %q", e.Repo, e.CID)
|
||||
}
|
||||
|
||||
// Example changelist info output in "long" format
|
||||
// (from `p4 changes -l ...`)
|
||||
// Change 1188 on 2023/06/09 by admin@yet-moar-lines *pending*
|
||||
//
|
||||
// Append still another line to all SECOND.md files
|
||||
//
|
||||
// "admin@yet-moar-lines" is the username @ the client spec name, which in this case is the branch name from the batch change
|
||||
// the final field - "*pending*" in this example - is optional and not present when the changelist has been submitted ("merged", in Git parlance)
|
||||
// Example changelist info in json format
|
||||
// (from `p4 -ztags -Mj changes -l ...`)
|
||||
// {"data":"Change 1178 on 2023/06/01 by admin@hello-third-world *pending*\n\n\tAppend Hello World to all THIRD.md files\n","level":0}
|
||||
var changelistInfoPattern = lazyregexp.New(`^Change (\d+) on (\d{4}/\d{2}/\d{2}) by ([^ ]+)@([^ ]+)(?: [*](pending|submitted|shelved)[*])?(?: '(.+)')?$`)
|
||||
|
||||
type changelistJson struct {
|
||||
Data string `json:"data"`
|
||||
Level int `json:"level"`
|
||||
}
|
||||
|
||||
// Parses the output of `p4 changes`
|
||||
// Handles one changelist only
|
||||
// Accepts any format: standard, long, json standard, json long
|
||||
func ParseChangelistOutput(output string) (*protocol.PerforceChangelist, error) {
|
||||
// output will be whitespace-trimmed and not empty
|
||||
|
||||
// if the given text is json format, extract the Data portion
|
||||
// so that it will have the same format as the standard output
|
||||
cidj := new(changelistJson)
|
||||
err := json.Unmarshal([]byte(output), cidj)
|
||||
if err == nil {
|
||||
output = strings.TrimSpace(cidj.Data)
|
||||
}
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
|
||||
// the first line contains the changelist information
|
||||
matches := changelistInfoPattern.FindStringSubmatch(lines[0])
|
||||
|
||||
if matches == nil || len(matches) < 5 {
|
||||
return nil, errors.New("invalid changelist output")
|
||||
}
|
||||
|
||||
pcl := new(protocol.PerforceChangelist)
|
||||
pcl.ID = matches[1]
|
||||
time, err := time.Parse("2006/01/02", matches[2])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid date: "+matches[2])
|
||||
}
|
||||
pcl.CreationDate = time
|
||||
pcl.Author = matches[3]
|
||||
pcl.Title = matches[4]
|
||||
status := "submitted"
|
||||
if len(matches) > 5 && matches[5] != "" {
|
||||
status = matches[5]
|
||||
}
|
||||
cls, err := protocol.ParsePerforceChangelistState(status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pcl.State = cls
|
||||
|
||||
if len(matches) > 6 && matches[6] != "" {
|
||||
// the commit message is inline with the info
|
||||
pcl.Message = strings.TrimSpace(matches[6])
|
||||
} else {
|
||||
// the commit message is in subsequent lines of the output
|
||||
var builder strings.Builder
|
||||
for i := 2; i < len(lines); i++ {
|
||||
if i > 2 {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString(strings.TrimSpace(lines[i]))
|
||||
}
|
||||
pcl.Message = builder.String()
|
||||
}
|
||||
return pcl, nil
|
||||
}
|
||||
|
||||
@ -3,12 +3,8 @@ package perforce
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
)
|
||||
|
||||
func TestGetP4ChangelistID(t *testing.T) {
|
||||
@ -55,131 +51,3 @@ func TestGetP4ChangelistID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseChangelistOutput(t *testing.T) {
|
||||
testCases := []struct {
|
||||
output string
|
||||
expectedChangelist *protocol.PerforceChangelist
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
output: `Change 1188 on 2023/06/09 by admin@test-first-one *pending*
|
||||
|
||||
Append still another line to all SECOND.md files
|
||||
Here's a second line of message`,
|
||||
expectedChangelist: &protocol.PerforceChangelist{
|
||||
ID: "1188",
|
||||
CreationDate: time.Date(2023, 6, 9, 0, 0, 0, 0, time.UTC),
|
||||
Author: "admin",
|
||||
Title: "test-first-one",
|
||||
State: protocol.PerforceChangelistStatePending,
|
||||
Message: "Append still another line to all SECOND.md files\nHere's a second line of message",
|
||||
},
|
||||
},
|
||||
{
|
||||
output: `Change 1234567 on 2023/04/09 by someone@dsobsdfoibsdv
|
||||
|
||||
Append still another line to all SECOND.md files
|
||||
Here's a second line of message`,
|
||||
expectedChangelist: &protocol.PerforceChangelist{
|
||||
ID: "1234567",
|
||||
CreationDate: time.Date(2023, 4, 9, 0, 0, 0, 0, time.UTC),
|
||||
Author: "someone",
|
||||
Title: "dsobsdfoibsdv",
|
||||
State: protocol.PerforceChangelistStateSubmitted,
|
||||
Message: "Append still another line to all SECOND.md files\nHere's a second line of message",
|
||||
},
|
||||
},
|
||||
{
|
||||
output: `{"data":"Change 1188 on 2023/06/09 by admin@json-with-status *pending*\n\n\tAppend still another line to all SECOND.md files\n\tand another line here\n","level":0}`,
|
||||
expectedChangelist: &protocol.PerforceChangelist{
|
||||
ID: "1188",
|
||||
CreationDate: time.Date(2023, 6, 9, 0, 0, 0, 0, time.UTC),
|
||||
Author: "admin",
|
||||
Title: "json-with-status",
|
||||
State: protocol.PerforceChangelistStatePending,
|
||||
Message: "Append still another line to all SECOND.md files\nand another line here",
|
||||
},
|
||||
},
|
||||
{
|
||||
output: `{"data":"Change 1188 on 2023/06/09 by admin@json-no-status\n\n\tAppend still another line to all SECOND.md files\n\tand another line here\n","level":0}`,
|
||||
expectedChangelist: &protocol.PerforceChangelist{
|
||||
ID: "1188",
|
||||
CreationDate: time.Date(2023, 6, 9, 0, 0, 0, 0, time.UTC),
|
||||
Author: "admin",
|
||||
Title: "json-no-status",
|
||||
State: protocol.PerforceChangelistStateSubmitted,
|
||||
Message: "Append still another line to all SECOND.md files\nand another line here",
|
||||
},
|
||||
},
|
||||
{
|
||||
output: `{"data":"Change 27 on 2023/05/03 by admin@buttercup\n\n\t generated change at 2023-05-02 17:44:59.012487 -0700 PDT m=+7.371337167\n","level":0}`,
|
||||
expectedChangelist: &protocol.PerforceChangelist{
|
||||
ID: "27",
|
||||
CreationDate: time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC),
|
||||
Author: "admin",
|
||||
Title: "buttercup",
|
||||
State: protocol.PerforceChangelistStateSubmitted,
|
||||
Message: `generated change at 2023-05-02 17:44:59.012487 -0700 PDT m=+7.371337167`,
|
||||
},
|
||||
},
|
||||
{
|
||||
output: `Change 27 on 2023/05/03 by admin@buttercup`,
|
||||
expectedChangelist: &protocol.PerforceChangelist{
|
||||
ID: "27",
|
||||
CreationDate: time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC),
|
||||
Author: "admin",
|
||||
Title: "buttercup",
|
||||
State: protocol.PerforceChangelistStateSubmitted,
|
||||
Message: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
output: `{"data":"Change 55 on 2023/05/03 by admin@test5 ' generated change at 2023-05-'","level":0}`,
|
||||
expectedChangelist: &protocol.PerforceChangelist{
|
||||
ID: "55",
|
||||
CreationDate: time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC),
|
||||
Author: "admin",
|
||||
Title: "test5",
|
||||
State: protocol.PerforceChangelistStateSubmitted,
|
||||
Message: `generated change at 2023-05-`,
|
||||
},
|
||||
},
|
||||
{
|
||||
output: `{"data":"Change 55 on 2023/56/42 by admin@buttercup ' generated change at 2023-05-'","level":0}`,
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
output: `Change 55 by admin@buttercup 'generated change at 2023-05-'`,
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
output: `{"data":"Change 1185 on 2023/06/09 by admin@yet-moar-lines *INVALID* 'Append still another line to al'","level":0}`,
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
output: `Change INVALID on 2023/06/09 by admin@yet-moar-lines *pending*
|
||||
|
||||
Append still another line to all SECOND.md files
|
||||
|
||||
`,
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
changelist, err := ParseChangelistOutput(testCase.output)
|
||||
if testCase.shouldError {
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got nil")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(testCase.expectedChangelist, changelist); diff != "" {
|
||||
t.Errorf("parsed changelist did not match expected (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
internal/perforce/types.go
Normal file
55
internal/perforce/types.go
Normal file
@ -0,0 +1,55 @@
|
||||
package perforce
|
||||
|
||||
import (
|
||||
v1 "github.com/sourcegraph/sourcegraph/internal/gitserver/v1"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Email string
|
||||
}
|
||||
|
||||
func UserFromProto(proto *v1.PerforceUser) *User {
|
||||
return &User{
|
||||
Username: proto.GetUsername(),
|
||||
Email: proto.GetEmail(),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) ToProto() *v1.PerforceUser {
|
||||
return &v1.PerforceUser{
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
}
|
||||
}
|
||||
|
||||
type Protect struct {
|
||||
Level string
|
||||
EntityType string
|
||||
EntityName string
|
||||
Match string
|
||||
IsExclusion bool
|
||||
Host string
|
||||
}
|
||||
|
||||
func ProtectFromProto(proto *v1.PerforceProtect) *Protect {
|
||||
return &Protect{
|
||||
Level: proto.GetLevel(),
|
||||
EntityType: proto.GetEntityType(),
|
||||
EntityName: proto.GetEntityName(),
|
||||
Match: proto.GetMatch(),
|
||||
IsExclusion: proto.GetIsExclusion(),
|
||||
Host: proto.GetHost(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Protect) ToProto() *v1.PerforceProtect {
|
||||
return &v1.PerforceProtect{
|
||||
Level: p.Level,
|
||||
EntityType: p.EntityType,
|
||||
EntityName: p.EntityName,
|
||||
Match: p.Match,
|
||||
IsExclusion: p.IsExclusion,
|
||||
Host: p.Host,
|
||||
}
|
||||
}
|
||||
@ -77,6 +77,7 @@ go_library(
|
||||
"//internal/extsvc/pypi",
|
||||
"//internal/extsvc/rubygems",
|
||||
"//internal/gitserver",
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/goroutine",
|
||||
"//internal/httpcli",
|
||||
"//internal/httptestutil",
|
||||
@ -175,6 +176,7 @@ go_test(
|
||||
"//internal/extsvc/phabricator",
|
||||
"//internal/github_apps/types",
|
||||
"//internal/gitserver",
|
||||
"//internal/gitserver/protocol",
|
||||
"//internal/goroutine",
|
||||
"//internal/httpcli",
|
||||
"//internal/httptestutil",
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc/perforce"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
"github.com/sourcegraph/sourcegraph/internal/jsonc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
@ -51,7 +52,12 @@ func newPerforceSource(gitserverClient gitserver.Client, svc *types.ExternalServ
|
||||
// from the code host configuration.
|
||||
func (s PerforceSource) CheckConnection(ctx context.Context) error {
|
||||
gclient := gitserver.NewClient()
|
||||
err := gclient.CheckPerforceCredentials(ctx, s.config.P4Port, s.config.P4User, s.config.P4Passwd)
|
||||
conn := protocol.PerforceConnectionDetails{
|
||||
P4Port: s.config.P4Port,
|
||||
P4User: s.config.P4User,
|
||||
P4Passwd: s.config.P4Passwd,
|
||||
}
|
||||
err := gclient.CheckPerforceCredentials(ctx, conn)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to connect to the Perforce server")
|
||||
}
|
||||
@ -69,7 +75,12 @@ func (s PerforceSource) ListRepos(ctx context.Context, results chan SourceResult
|
||||
return
|
||||
}
|
||||
|
||||
err := s.gitserverClient.IsPerforcePathCloneable(ctx, s.config.P4Port, s.config.P4User, s.config.P4Passwd, depot)
|
||||
conn := protocol.PerforceConnectionDetails{
|
||||
P4Port: s.config.P4Port,
|
||||
P4User: s.config.P4User,
|
||||
P4Passwd: s.config.P4Passwd,
|
||||
}
|
||||
err := s.gitserverClient.IsPerforcePathCloneable(ctx, conn, depot)
|
||||
if err != nil {
|
||||
results <- SourceResult{Source: s, Err: errors.Wrap(err, "checking if perforce path is cloneable")}
|
||||
continue
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
"github.com/sourcegraph/sourcegraph/internal/testutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types/typestest"
|
||||
@ -83,7 +84,7 @@ func TestPerforceSource_ListRepos(t *testing.T) {
|
||||
}
|
||||
|
||||
gc := gitserver.NewMockClient()
|
||||
gc.IsPerforcePathCloneableFunc.SetDefaultHook(func(ctx context.Context, _, _, _, depotPath string) error {
|
||||
gc.IsPerforcePathCloneableFunc.SetDefaultHook(func(ctx context.Context, _ protocol.PerforceConnectionDetails, depotPath string) error {
|
||||
if depotPath == "//Sourcegraph" || depotPath == "//Engineering/Cloud" {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
|
||||
"github.com/sourcegraph/sourcegraph/internal/extsvc"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver"
|
||||
"github.com/sourcegraph/sourcegraph/internal/gitserver/protocol"
|
||||
"github.com/sourcegraph/sourcegraph/internal/observation"
|
||||
"github.com/sourcegraph/sourcegraph/internal/timeutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
@ -235,7 +236,7 @@ func TestStatusMessages(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
mockGitserverClient.SystemsInfoFunc.SetDefaultReturn([]gitserver.SystemInfo{
|
||||
mockGitserverClient.SystemsInfoFunc.SetDefaultReturn([]protocol.SystemInfo{
|
||||
{
|
||||
Address: "gitserver-0",
|
||||
PercentUsed: 75.10345,
|
||||
@ -257,7 +258,7 @@ func TestStatusMessages(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testSetup: func() {
|
||||
mockGitserverClient.SystemsInfoFunc.SetDefaultReturn([]gitserver.SystemInfo{
|
||||
mockGitserverClient.SystemsInfoFunc.SetDefaultReturn([]protocol.SystemInfo{
|
||||
{
|
||||
Address: "gitserver-0",
|
||||
PercentUsed: 95.10345,
|
||||
|
||||
@ -130,3 +130,7 @@
|
||||
path: github.com/sourcegraph/sourcegraph/internal/embeddings/background/repo
|
||||
interfaces:
|
||||
- RepoEmbeddingJobsStore
|
||||
- filename: internal/gitserver/mock.go
|
||||
path: github.com/sourcegraph/sourcegraph/internal/gitserver/v1
|
||||
interfaces:
|
||||
- GitserverServiceClient
|
||||
|
||||
@ -10,7 +10,8 @@
|
||||
"docker-images/*": "ignore all code under docker-images",
|
||||
"main.go": "ignore main",
|
||||
".*_test\\.go$": "ignore test code",
|
||||
"internal/batches/testing/*": "ignore batches testing code"
|
||||
"internal/batches/testing/*": "ignore batches testing code",
|
||||
"internal/gitserver/mock.go": "ignore deprecation warning of P4Exec"
|
||||
}
|
||||
},
|
||||
"bodyclose": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user