Add github package
This commit is contained in:
		
							parent
							
								
									a91df5c564
								
							
						
					
					
						commit
						5f608058cd
					
				|  | @ -0,0 +1,86 @@ | |||
| package fake | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	RegistrationToken = "fake-registration-token" | ||||
| 
 | ||||
| 	RunnersListBody = ` | ||||
| { | ||||
|   "total_count": 2, | ||||
|   "runners": [ | ||||
|     {"id": 1, "name": "test1", "os": "linux", "status": "online"}, | ||||
|     {"id": 2, "name": "test2", "os": "linux", "status": "offline"} | ||||
|   ] | ||||
| } | ||||
| ` | ||||
| ) | ||||
| 
 | ||||
| type handler struct { | ||||
| 	Status int | ||||
| 	Body   string | ||||
| } | ||||
| 
 | ||||
| func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	w.WriteHeader(h.Status) | ||||
| 	fmt.Fprintf(w, h.Body) | ||||
| } | ||||
| 
 | ||||
| func NewServer() *httptest.Server { | ||||
| 	routes := map[string]handler{ | ||||
| 		// For CreateRegistrationToken
 | ||||
| 		"/repos/test/valid/actions/runners/registration-token": handler{ | ||||
| 			Status: http.StatusCreated, | ||||
| 			Body:   fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)), | ||||
| 		}, | ||||
| 		"/repos/test/invalid/actions/runners/registration-token": handler{ | ||||
| 			Status: http.StatusOK, | ||||
| 			Body:   fmt.Sprintf("{\"token\": \"%s\", \"expires_at\": \"%s\"}", RegistrationToken, time.Now().Add(time.Hour*1).Format(time.RFC3339)), | ||||
| 		}, | ||||
| 		"/repos/test/error/actions/runners/registration-token": handler{ | ||||
| 			Status: http.StatusBadRequest, | ||||
| 			Body:   "", | ||||
| 		}, | ||||
| 
 | ||||
| 		// For ListRunners
 | ||||
| 		"/repos/test/valid/actions/runners": handler{ | ||||
| 			Status: http.StatusOK, | ||||
| 			Body:   RunnersListBody, | ||||
| 		}, | ||||
| 		"/repos/test/invalid/actions/runners": handler{ | ||||
| 			Status: http.StatusNoContent, | ||||
| 			Body:   "", | ||||
| 		}, | ||||
| 		"/repos/test/error/actions/runners": handler{ | ||||
| 			Status: http.StatusBadRequest, | ||||
| 			Body:   "", | ||||
| 		}, | ||||
| 
 | ||||
| 		// For RemoveRunner
 | ||||
| 		"/repos/test/valid/actions/runners/1": handler{ | ||||
| 			Status: http.StatusNoContent, | ||||
| 			Body:   "", | ||||
| 		}, | ||||
| 		"/repos/test/invalid/actions/runners/1": handler{ | ||||
| 			Status: http.StatusOK, | ||||
| 			Body:   "", | ||||
| 		}, | ||||
| 		"/repos/test/error/actions/runners/1": handler{ | ||||
| 			Status: http.StatusBadRequest, | ||||
| 			Body:   "", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	mux := http.NewServeMux() | ||||
| 	for path, handler := range routes { | ||||
| 		h := handler | ||||
| 		mux.Handle(path, &h) | ||||
| 	} | ||||
| 
 | ||||
| 	return httptest.NewServer(mux) | ||||
| } | ||||
|  | @ -0,0 +1,147 @@ | |||
| package github | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/bradleyfalzon/ghinstallation" | ||||
| 	"github.com/google/go-github/v31/github" | ||||
| 	"golang.org/x/oauth2" | ||||
| ) | ||||
| 
 | ||||
| type Client struct { | ||||
| 	*github.Client | ||||
| 	regTokens map[string]*github.RegistrationToken | ||||
| 	mu        sync.Mutex | ||||
| } | ||||
| 
 | ||||
| // NewClient returns a client authenticated as a GitHub App.
 | ||||
| func NewClient(appID, installationID int64, privateKeyPath string) (*Client, error) { | ||||
| 	tr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, appID, installationID, privateKeyPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("authentication failed: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return &Client{ | ||||
| 		Client:    github.NewClient(&http.Client{Transport: tr}), | ||||
| 		regTokens: map[string]*github.RegistrationToken{}, | ||||
| 		mu:        sync.Mutex{}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // NewClient returns a client authenticated with personal access token.
 | ||||
| func NewClientWithAccessToken(token string) (*Client, error) { | ||||
| 	tc := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( | ||||
| 		&oauth2.Token{AccessToken: token}, | ||||
| 	)) | ||||
| 
 | ||||
| 	return &Client{ | ||||
| 		Client:    github.NewClient(tc), | ||||
| 		regTokens: map[string]*github.RegistrationToken{}, | ||||
| 		mu:        sync.Mutex{}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // GetRegistrationToken returns a registration token tied with the name of repository and runner.
 | ||||
| func (c *Client) GetRegistrationToken(ctx context.Context, repository, name string) (*github.RegistrationToken, error) { | ||||
| 	c.mu.Lock() | ||||
| 	defer c.mu.Unlock() | ||||
| 
 | ||||
| 	owner, repo, err := splitOwnerAndRepo(repository) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	key := fmt.Sprintf("%s/%s", repo, name) | ||||
| 	rt, ok := c.regTokens[key] | ||||
| 	if ok && rt.GetExpiresAt().After(time.Now().Add(-10*time.Minute)) { | ||||
| 		return rt, nil | ||||
| 	} | ||||
| 
 | ||||
| 	rt, res, err := c.Client.Actions.CreateRegistrationToken(ctx, owner, repo) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to create registration token: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if res.StatusCode != 201 { | ||||
| 		return nil, fmt.Errorf("unexpected status: %d", res.StatusCode) | ||||
| 	} | ||||
| 
 | ||||
| 	c.regTokens[key] = rt | ||||
| 	go func() { | ||||
| 		c.cleanup() | ||||
| 	}() | ||||
| 
 | ||||
| 	return rt, nil | ||||
| } | ||||
| 
 | ||||
| // RemoveRunner removes a runner with specified runner ID from repocitory.
 | ||||
| func (c *Client) RemoveRunner(ctx context.Context, repository string, runnerID int64) error { | ||||
| 	owner, repo, err := splitOwnerAndRepo(repository) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	res, err := c.Client.Actions.RemoveRunner(ctx, owner, repo, runnerID) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to remove runner: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if res.StatusCode != 204 { | ||||
| 		return fmt.Errorf("unexpected status: %d", res.StatusCode) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // ListRunners returns a list of runners of specified repository name.
 | ||||
| func (c *Client) ListRunners(ctx context.Context, repository string) ([]*github.Runner, error) { | ||||
| 	var runners []*github.Runner | ||||
| 
 | ||||
| 	owner, repo, err := splitOwnerAndRepo(repository) | ||||
| 	if err != nil { | ||||
| 		return runners, err | ||||
| 	} | ||||
| 
 | ||||
| 	opts := github.ListOptions{PerPage: 10} | ||||
| 	for { | ||||
| 		list, res, err := c.Client.Actions.ListRunners(ctx, owner, repo, &opts) | ||||
| 		if err != nil { | ||||
| 			return runners, fmt.Errorf("failed to remove runner: %v", err) | ||||
| 		} | ||||
| 
 | ||||
| 		runners = append(runners, list.Runners...) | ||||
| 		if res.NextPage == 0 { | ||||
| 			break | ||||
| 		} | ||||
| 		opts.Page = res.NextPage | ||||
| 	} | ||||
| 
 | ||||
| 	return runners, nil | ||||
| } | ||||
| 
 | ||||
| // cleanup removes expired registration tokens.
 | ||||
| func (c *Client) cleanup() { | ||||
| 	c.mu.Lock() | ||||
| 	defer c.mu.Unlock() | ||||
| 
 | ||||
| 	for key, rt := range c.regTokens { | ||||
| 		if rt.GetExpiresAt().Before(time.Now()) { | ||||
| 			delete(c.regTokens, key) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // splitOwnerAndRepo splits specified repository name to the owner and repo name.
 | ||||
| func splitOwnerAndRepo(repo string) (string, string, error) { | ||||
| 	chunk := strings.Split(repo, "/") | ||||
| 	if len(chunk) != 2 { | ||||
| 		return "", "", errors.New("invalid repository name") | ||||
| 	} | ||||
| 	return chunk[0], chunk[1], nil | ||||
| } | ||||
|  | @ -0,0 +1,124 @@ | |||
| package github | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/google/go-github/v31/github" | ||||
| 	"github.com/summerwind/actions-runner-controller/github/fake" | ||||
| ) | ||||
| 
 | ||||
| var server *httptest.Server | ||||
| 
 | ||||
| func newTestClient() *Client { | ||||
| 	client, err := NewClientWithAccessToken("token") | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 
 | ||||
| 	baseURL, err := url.Parse(server.URL + "/") | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	client.Client.BaseURL = baseURL | ||||
| 
 | ||||
| 	return client | ||||
| } | ||||
| 
 | ||||
| func TestMain(m *testing.M) { | ||||
| 	server = fake.NewServer() | ||||
| 	defer server.Close() | ||||
| 	m.Run() | ||||
| } | ||||
| 
 | ||||
| func TestGetRegistrationToken(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		repo  string | ||||
| 		token string | ||||
| 		err   bool | ||||
| 	}{ | ||||
| 		{repo: "test/valid", token: fake.RegistrationToken, err: false}, | ||||
| 		{repo: "test/invalid", token: "", err: true}, | ||||
| 		{repo: "test/error", token: "", err: true}, | ||||
| 	} | ||||
| 
 | ||||
| 	client := newTestClient() | ||||
| 	for i, tt := range tests { | ||||
| 		rt, err := client.GetRegistrationToken(context.Background(), tt.repo, "test") | ||||
| 		if !tt.err && err != nil { | ||||
| 			t.Errorf("[%d] unexpected error: %v", i, err) | ||||
| 		} | ||||
| 		if tt.token != rt.GetToken() { | ||||
| 			t.Errorf("[%d] unexpected token: %v", i, rt.GetToken()) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestListRunners(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		repo   string | ||||
| 		length int | ||||
| 		err    bool | ||||
| 	}{ | ||||
| 		{repo: "test/valid", length: 2, err: false}, | ||||
| 		{repo: "test/invalid", length: 0, err: true}, | ||||
| 		{repo: "test/error", length: 0, err: true}, | ||||
| 	} | ||||
| 
 | ||||
| 	client := newTestClient() | ||||
| 	for i, tt := range tests { | ||||
| 		runners, err := client.ListRunners(context.Background(), tt.repo) | ||||
| 		if !tt.err && err != nil { | ||||
| 			t.Errorf("[%d] unexpected error: %v", i, err) | ||||
| 		} | ||||
| 		if tt.length != len(runners) { | ||||
| 			t.Errorf("[%d] unexpected runners list: %v", i, runners) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestRemoveRunner(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		repo string | ||||
| 		err  bool | ||||
| 	}{ | ||||
| 		{repo: "test/valid", err: false}, | ||||
| 		{repo: "test/invalid", err: true}, | ||||
| 		{repo: "test/error", err: true}, | ||||
| 	} | ||||
| 
 | ||||
| 	client := newTestClient() | ||||
| 	for i, tt := range tests { | ||||
| 		err := client.RemoveRunner(context.Background(), tt.repo, int64(1)) | ||||
| 		if !tt.err && err != nil { | ||||
| 			t.Errorf("[%d] unexpected error: %v", i, err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCleanup(t *testing.T) { | ||||
| 	token := "token" | ||||
| 
 | ||||
| 	client := newTestClient() | ||||
| 	client.regTokens = map[string]*github.RegistrationToken{ | ||||
| 		"active": &github.RegistrationToken{ | ||||
| 			Token:     &token, | ||||
| 			ExpiresAt: &github.Timestamp{Time: time.Now().Add(time.Hour * 1)}, | ||||
| 		}, | ||||
| 		"expired": &github.RegistrationToken{ | ||||
| 			Token:     &token, | ||||
| 			ExpiresAt: &github.Timestamp{Time: time.Now().Add(-time.Hour * 1)}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	client.cleanup() | ||||
| 	if _, ok := client.regTokens["active"]; !ok { | ||||
| 		t.Errorf("active token was accidentally removed") | ||||
| 	} | ||||
| 	if _, ok := client.regTokens["expired"]; ok { | ||||
| 		t.Errorf("expired token still exists") | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue