gerrit.Watcher is a class which watches the Gerrit stream-events SSH connection and produces events. There's a basic CLBot binary as well, to demonstrate driving it to produce messages on the logging output. It doesn't really do anything else. Change-Id: I274fe0a77c8329f79456425405e2fbdc3ca2edf0 Reviewed-on: https://cl.tvl.fyi/c/depot/+/245 Reviewed-by: tazjin <mail@tazj.in>
		
			
				
	
	
		
			190 lines
		
	
	
	
		
			6.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			190 lines
		
	
	
	
		
			6.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package gerrit
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/ecdsa"
 | |
| 	"crypto/elliptic"
 | |
| 	"crypto/rand"
 | |
| 	"crypto/subtle"
 | |
| 	"fmt"
 | |
| 	"net"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"code.tvl.fyi/fun/clbot/gerrit/gerritevents"
 | |
| 	log "github.com/golang/glog"
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| 	"golang.org/x/crypto/ssh"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	sshServerSigner, sshServerPublicKey = mustNewKey()
 | |
| 	sshClientSigner, sshClientPublicKey = mustNewKey()
 | |
| )
 | |
| 
 | |
| func mustNewKey() (ssh.Signer, ssh.PublicKey) {
 | |
| 	key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 	signer, err := ssh.NewSignerFromKey(key)
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 	publicKey, err := ssh.NewPublicKey(key.Public())
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 	return signer, publicKey
 | |
| }
 | |
| 
 | |
| func newSSHServer(lines string) (addr string, cleanup func(), err error) {
 | |
| 	config := &ssh.ServerConfig{
 | |
| 		PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
 | |
| 			pkBytes := pubKey.Marshal()
 | |
| 			wantPKBytes := sshClientPublicKey.Marshal()
 | |
| 			if subtle.ConstantTimeCompare(pkBytes, wantPKBytes) == 0 {
 | |
| 				return nil, fmt.Errorf("unauthorized")
 | |
| 			}
 | |
| 			return &ssh.Permissions{}, nil
 | |
| 		},
 | |
| 	}
 | |
| 	config.AddHostKey(sshServerSigner)
 | |
| 
 | |
| 	ln, err := net.Listen("tcp", ":0")
 | |
| 	if err != nil {
 | |
| 		log.Fatalf("Listen on tcp/:0: %v", err)
 | |
| 	}
 | |
| 	handle := func(conn net.Conn) {
 | |
| 		defer conn.Close()
 | |
| 
 | |
| 		sc, newchch, newreqch, err := ssh.NewServerConn(conn, config)
 | |
| 		if err != nil {
 | |
| 			log.Fatalf("NewServerConn: %v", err)
 | |
| 		}
 | |
| 		go ssh.DiscardRequests(newreqch)
 | |
| 		for newCh := range newchch {
 | |
| 			if newCh.ChannelType() != "session" {
 | |
| 				newCh.Reject(ssh.UnknownChannelType, "unknown channel type")
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			channel, reqs, err := newCh.Accept()
 | |
| 			if err != nil {
 | |
| 				log.Fatalf("Could not accept channel: %v", err)
 | |
| 			}
 | |
| 			go func(in <-chan *ssh.Request) {
 | |
| 				for req := range in {
 | |
| 					req.Reply(req.Type == "exec", nil)
 | |
| 				}
 | |
| 			}(reqs)
 | |
| 			channel.Write([]byte(lines))
 | |
| 			sc.SendRequest("goaway", false, nil)
 | |
| 		}
 | |
| 	}
 | |
| 	go func() {
 | |
| 		for {
 | |
| 			conn, err := ln.Accept()
 | |
| 			if err != nil {
 | |
| 				return
 | |
| 			}
 | |
| 			go handle(conn)
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	cleanup = func() {
 | |
| 		ln.Close()
 | |
| 	}
 | |
| 	return ln.Addr().String(), cleanup, err
 | |
| }
 | |
| 
 | |
| func ts(s string) gerritevents.Time {
 | |
| 	t, err := time.Parse("2006-01-02 15:04:05 -0700 MST", s)
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 	return gerritevents.Time{t}
 | |
| }
 | |
| 
 | |
| func optStr(s string) *string { return &s }
 | |
| 
 | |
| func TestWatcher(t *testing.T) {
 | |
| 	tcs := []struct {
 | |
| 		name  string
 | |
| 		lines string
 | |
| 		want  []gerritevents.Event
 | |
| 	}{{
 | |
| 		name: "no events",
 | |
| 	}, {
 | |
| 		name: "single test event",
 | |
| 		lines: `{"author":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"approvals":[{"type":"Code-Review","description":"Code-Review","value":"2","oldValue":"0"}],"comment":"Patch Set 3: Code-Review+2","patchSet":{"number":3,"revision":"6fe272d3f82c6efdfe1167fab98bf918efc03fe5","parents":["d984b6018cf68c7e8b7169b475d90134fbcee767"],"ref":"refs/changes/44/244/3","uploader":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"createdOn":1592081910,"author":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"kind":"REWORK","sizeInsertions":83,"sizeDeletions":-156},"change":{"project":"depot","branch":"master","id":"I546c701145fa204b7ba7518a8a56a783588629e0","number":244,"subject":"refactor(ops/nixos): Move my NixOS configurations to //users/tazjin","owner":{"name":"tazjin","email":"mail@tazj.in","username":"tazjin"},"url":"https://cl.tvl.fyi/c/depot/+/244","commitMessage":"refactor(ops/nixos): Move my NixOS configurations to //users/tazjin\n\nNixOS modules move one level up because it\u0027s unlikely that //ops/nixos\nwill contain actual systems at this point (they\u0027re user-specific).\n\nThis is the first users folder, so it is also added to the root\nreadTree invocation for the repository.\n\nChange-Id: I546c701145fa204b7ba7518a8a56a783588629e0\n","createdOn":1592081577,"status":"NEW"},"project":"depot","refName":"refs/heads/master","changeKey":{"id":"I546c701145fa204b7ba7518a8a56a783588629e0"},"type":"comment-added","eventCreatedOn":1592081929}
 | |
| `,
 | |
| 		want: []gerritevents.Event{
 | |
| 			&gerritevents.CommentAdded{
 | |
| 				Type: "comment-added",
 | |
| 				Change: gerritevents.Change{
 | |
| 					Project:       "depot",
 | |
| 					Branch:        "master",
 | |
| 					ID:            "I546c701145fa204b7ba7518a8a56a783588629e0",
 | |
| 					Number:        244,
 | |
| 					Subject:       "refactor(ops/nixos): Move my NixOS configurations to //users/tazjin",
 | |
| 					Owner:         gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
 | |
| 					URL:           "https://cl.tvl.fyi/c/depot/+/244",
 | |
| 					CommitMessage: "refactor(ops/nixos): Move my NixOS configurations to //users/tazjin\n\nNixOS modules move one level up because it's unlikely that //ops/nixos\nwill contain actual systems at this point (they're user-specific).\n\nThis is the first users folder, so it is also added to the root\nreadTree invocation for the repository.\n\nChange-Id: I546c701145fa204b7ba7518a8a56a783588629e0\n",
 | |
| 					CreatedOn:     ts("2020-06-13 21:52:57 +0100 BST"),
 | |
| 					Status:        "NEW",
 | |
| 				},
 | |
| 				PatchSet: gerritevents.PatchSet{
 | |
| 					Number:         3,
 | |
| 					Revision:       "6fe272d3f82c6efdfe1167fab98bf918efc03fe5",
 | |
| 					Parents:        []string{"d984b6018cf68c7e8b7169b475d90134fbcee767"},
 | |
| 					Ref:            "refs/changes/44/244/3",
 | |
| 					Uploader:       gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
 | |
| 					Author:         gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
 | |
| 					CreatedOn:      ts("2020-06-13 21:58:30 +0100 BST"),
 | |
| 					Kind:           "REWORK",
 | |
| 					SizeInsertions: 83,
 | |
| 					SizeDeletions:  -156,
 | |
| 				},
 | |
| 				Author:         gerritevents.Account{Name: "tazjin", Email: "mail@tazj.in", Username: "tazjin"},
 | |
| 				Approvals:      []gerritevents.Approval{{Type: "Code-Review", Description: "Code-Review", Value: "2", OldValue: optStr("0")}},
 | |
| 				Comment:        "Patch Set 3: Code-Review+2",
 | |
| 				EventCreatedOn: ts("2020-06-13 21:58:49 +0100 BST"),
 | |
| 			},
 | |
| 		},
 | |
| 	}}
 | |
| 	for _, tc := range tcs {
 | |
| 		tc := tc
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			ctx, cancel := context.WithCancel(context.Background())
 | |
| 			defer cancel()
 | |
| 
 | |
| 			serverAddr, cleanup, err := newSSHServer(tc.lines)
 | |
| 			if err != nil {
 | |
| 				t.Fatalf("newSSHServer: %v", err)
 | |
| 			}
 | |
| 			t.Cleanup(cleanup)
 | |
| 
 | |
| 			config := &ssh.ClientConfig{
 | |
| 				User:            "bert",
 | |
| 				Auth:            []ssh.AuthMethod{ssh.PublicKeys(sshClientSigner)},
 | |
| 				HostKeyCallback: ssh.FixedHostKey(sshServerPublicKey),
 | |
| 				Timeout:         10 * time.Millisecond,
 | |
| 			}
 | |
| 			w, err := New(ctx, "tcp", serverAddr, config)
 | |
| 			if err != nil {
 | |
| 				t.Fatalf("New: %v", err)
 | |
| 			}
 | |
| 
 | |
| 			var gotEvents []gerritevents.Event
 | |
| 			for ev := range w.Events() {
 | |
| 				gotEvents = append(gotEvents, ev)
 | |
| 			}
 | |
| 			if diff := cmp.Diff(gotEvents, tc.want); diff != "" {
 | |
| 				t.Errorf("got events != want events: diff:\n%v", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |