diff --git a/go.mod b/go.mod index d380313..c73fb21 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/ory/dockertest/v3 v3.8.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.11.0 + github.com/r3labs/diff/v2 v2.15.1 // indirect github.com/satori/go.uuid v1.2.0 goji.io v2.0.2+incompatible golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 diff --git a/go.sum b/go.sum index 3e9c556..4bee1c9 100644 --- a/go.sum +++ b/go.sum @@ -205,6 +205,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg= +github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -222,12 +224,15 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -323,6 +328,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/pkg/datastore/mysql/job.go b/pkg/datastore/mysql/job.go index 67bad35..ac784bf 100644 --- a/pkg/datastore/mysql/job.go +++ b/pkg/datastore/mysql/job.go @@ -23,7 +23,7 @@ func (m *MySQL) EnqueueJob(ctx context.Context, job datastore.Job) error { // ListJobs get all jobs func (m *MySQL) ListJobs(ctx context.Context) ([]datastore.Job, error) { var jobs []datastore.Job - query := `SELECT uuid, ghe_domain, repository, check_event, target_id FROM jobs` + query := `SELECT uuid, ghe_domain, repository, check_event, target_id, created_at, updated_at FROM jobs` if err := m.Conn.SelectContext(ctx, &jobs, query); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, datastore.ErrNotFound diff --git a/pkg/datastore/mysql/job_test.go b/pkg/datastore/mysql/job_test.go index 65790bc..654d183 100644 --- a/pkg/datastore/mysql/job_test.go +++ b/pkg/datastore/mysql/job_test.go @@ -129,9 +129,9 @@ func TestMySQL_ListJobs(t *testing.T) { if len(test.want) != len(got) { t.Fatalf("incorrect length jobs, want: %d but got: %d", len(test.want), len(got)) } - for _, g := range got { - g.CreatedAt = time.Time{} - g.UpdatedAt = time.Time{} + for i := range got { + got[i].CreatedAt = time.Time{} + got[i].UpdatedAt = time.Time{} } if diff := cmp.Diff(test.want, got); diff != "" { diff --git a/pkg/gh/github.go b/pkg/gh/github.go index c30ac49..81eb2a3 100644 --- a/pkg/gh/github.go +++ b/pkg/gh/github.go @@ -1,8 +1,6 @@ package gh import ( - "context" - "errors" "fmt" "net/http" "net/url" @@ -15,7 +13,6 @@ import ( "github.com/google/go-github/v35/github" "github.com/m4ns0ur/httpcache" "github.com/patrickmn/go-cache" - "github.com/whywaita/myshoes/pkg/logger" "golang.org/x/oauth2" ) @@ -138,6 +135,23 @@ func CheckSignature(installationID int64) error { return nil } +// ExistRunnerReleases check exist of runner file +func ExistRunnerReleases(runnerVersion string) error { + releasesURL := fmt.Sprintf("https://github.com/actions/runner/releases/tag/%s", runnerVersion) + resp, err := http.Get(releasesURL) + if err != nil { + return fmt.Errorf("failed to GET from %s: %w", releasesURL, ErrNotFound) + } + + if resp.StatusCode == http.StatusOK { + return nil + } else if resp.StatusCode == http.StatusNotFound { + return ErrNotFound + } + + return fmt.Errorf("invalid response code (%d)", resp.StatusCode) +} + // ExistGitHubRepository check exist of GitHub repository func ExistGitHubRepository(scope, gheDomain string, accessToken string) error { repoURL, err := getRepositoryURL(scope, gheDomain) @@ -160,86 +174,12 @@ func ExistGitHubRepository(scope, gheDomain string, accessToken string) error { if resp.StatusCode == http.StatusOK { return nil } else if resp.StatusCode == http.StatusNotFound { - return errors.New("not found") + return ErrNotFound } return fmt.Errorf("invalid response code (%d)", resp.StatusCode) } -// ExistGitHubRunner check exist registered of GitHub runner -func ExistGitHubRunner(ctx context.Context, client *github.Client, owner, repo, runnerName string) (*github.Runner, error) { - runners, err := ListRunners(ctx, client, owner, repo) - if err != nil { - return nil, fmt.Errorf("failed to get list of runners: %w", err) - } - - return ExistGitHubRunnerWithRunner(runners, runnerName) -} - -// ExistGitHubRunnerWithRunner check exist registered of GitHub runner from a list of runner -func ExistGitHubRunnerWithRunner(runners []*github.Runner, runnerName string) (*github.Runner, error) { - for _, r := range runners { - if strings.EqualFold(r.GetName(), runnerName) { - return r, nil - } - } - - return nil, ErrNotFound -} - -// ListRunners get runners that registered repository or org -func ListRunners(ctx context.Context, client *github.Client, owner, repo string) ([]*github.Runner, error) { - if cachedRs, found := responseCache.Get(getCacheKey(owner, repo)); found { - return cachedRs.([]*github.Runner), nil - } - - var opts = &github.ListOptions{ - Page: 0, - PerPage: 100, - } - - var rs []*github.Runner - for { - logger.Logf(true, "get runners from GitHub, page: %d, now all runners: %d", opts.Page, len(rs)) - runners, resp, err := listRunners(ctx, client, owner, repo, opts) - if err != nil { - return nil, fmt.Errorf("failed to list runners: %w", err) - } - storeRateLimit(getRateLimitKey(owner, repo), resp.Rate) - - rs = append(rs, runners.Runners...) - if resp.NextPage == 0 { - break - } - opts.Page = resp.NextPage - } - - responseCache.Set(getCacheKey(owner, repo), rs, 1*time.Second) - logger.Logf(true, "found %d runners in GitHub", len(rs)) - - return rs, nil -} - -func getCacheKey(owner, repo string) string { - return fmt.Sprintf("owner-%s-repo-%s", owner, repo) -} - -func listRunners(ctx context.Context, client *github.Client, owner, repo string, opts *github.ListOptions) (*github.Runners, *github.Response, error) { - if repo == "" { - runners, resp, err := client.Actions.ListOrganizationRunners(ctx, owner, opts) - if err != nil { - return nil, nil, fmt.Errorf("failed to list organization runners: %w", err) - } - return runners, resp, nil - } - - runners, resp, err := client.Actions.ListRunners(ctx, owner, repo, opts) - if err != nil { - return nil, nil, fmt.Errorf("failed to list repository runners: %w", err) - } - return runners, resp, nil -} - func getRepositoryURL(scope, gheDomain string) (string, error) { // github.com // => https://api.github.com/repos/:owner/:repo diff --git a/pkg/gh/runner.go b/pkg/gh/runner.go new file mode 100644 index 0000000..a11e355 --- /dev/null +++ b/pkg/gh/runner.go @@ -0,0 +1,85 @@ +package gh + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/google/go-github/v35/github" + "github.com/whywaita/myshoes/pkg/logger" +) + +// ExistGitHubRunner check exist registered of GitHub runner +func ExistGitHubRunner(ctx context.Context, client *github.Client, owner, repo, runnerName string) (*github.Runner, error) { + runners, err := ListRunners(ctx, client, owner, repo) + if err != nil { + return nil, fmt.Errorf("failed to get list of runners: %w", err) + } + + return ExistGitHubRunnerWithRunner(runners, runnerName) +} + +// ExistGitHubRunnerWithRunner check exist registered of GitHub runner from a list of runner +func ExistGitHubRunnerWithRunner(runners []*github.Runner, runnerName string) (*github.Runner, error) { + for _, r := range runners { + if strings.EqualFold(r.GetName(), runnerName) { + return r, nil + } + } + + return nil, ErrNotFound +} + +// ListRunners get runners that registered repository or org +func ListRunners(ctx context.Context, client *github.Client, owner, repo string) ([]*github.Runner, error) { + if cachedRs, found := responseCache.Get(getCacheKey(owner, repo)); found { + return cachedRs.([]*github.Runner), nil + } + + var opts = &github.ListOptions{ + Page: 0, + PerPage: 100, + } + + var rs []*github.Runner + for { + logger.Logf(true, "get runners from GitHub, page: %d, now all runners: %d", opts.Page, len(rs)) + runners, resp, err := listRunners(ctx, client, owner, repo, opts) + if err != nil { + return nil, fmt.Errorf("failed to list runners: %w", err) + } + storeRateLimit(getRateLimitKey(owner, repo), resp.Rate) + + rs = append(rs, runners.Runners...) + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + + responseCache.Set(getCacheKey(owner, repo), rs, 1*time.Second) + logger.Logf(true, "found %d runners in GitHub", len(rs)) + + return rs, nil +} + +func getCacheKey(owner, repo string) string { + return fmt.Sprintf("owner-%s-repo-%s", owner, repo) +} + +func listRunners(ctx context.Context, client *github.Client, owner, repo string, opts *github.ListOptions) (*github.Runners, *github.Response, error) { + if repo == "" { + runners, resp, err := client.Actions.ListOrganizationRunners(ctx, owner, opts) + if err != nil { + return nil, nil, fmt.Errorf("failed to list organization runners: %w", err) + } + return runners, resp, nil + } + + runners, resp, err := client.Actions.ListRunners(ctx, owner, repo, opts) + if err != nil { + return nil, nil, fmt.Errorf("failed to list repository runners: %w", err) + } + return runners, resp, nil +} diff --git a/pkg/metric/scrape_datastore.go b/pkg/metric/scrape_datastore.go index eb86a8c..a1d2d8e 100644 --- a/pkg/metric/scrape_datastore.go +++ b/pkg/metric/scrape_datastore.go @@ -3,6 +3,8 @@ package metric import ( "context" "fmt" + "sort" + "time" "github.com/prometheus/client_golang/prometheus" "github.com/whywaita/myshoes/pkg/datastore" @@ -21,6 +23,11 @@ var ( "Number of targets", []string{"resource_type"}, nil, ) + datastoreJobDurationOldest = prometheus.NewDesc( + prometheus.BuildFQName(namespace, datastoreName, "job_duration_oldest_seconds"), + "Duration time of oldest job", + []string{"job_id"}, nil, + ) ) // ScraperDatastore is scraper implement for datastore.Datastore @@ -61,6 +68,15 @@ func scrapeJobs(ctx context.Context, ds datastore.Datastore, ch chan<- prometheu return nil } + sort.SliceStable(jobs, func(i, j int) bool { + // oldest job is first + return jobs[i].CreatedAt.Before(jobs[j].CreatedAt) + }) + + oldestJob := jobs[0] + ch <- prometheus.MustNewConstMetric( + datastoreJobDurationOldest, prometheus.GaugeValue, time.Since(oldestJob.CreatedAt).Seconds(), oldestJob.UUID.String()) + result := map[string]float64{} // key: target_id, value: number for _, j := range jobs { result[j.TargetID.String()]++ diff --git a/pkg/web/target.go b/pkg/web/target.go index fff601a..13660ab 100644 --- a/pkg/web/target.go +++ b/pkg/web/target.go @@ -10,13 +10,13 @@ import ( "strings" "time" - "github.com/google/go-cmp/cmp" + "github.com/r3labs/diff/v2" + uuid "github.com/satori/go.uuid" "github.com/whywaita/myshoes/pkg/datastore" "github.com/whywaita/myshoes/pkg/gh" "github.com/whywaita/myshoes/pkg/logger" - uuid "github.com/satori/go.uuid" "goji.io/pat" ) @@ -64,71 +64,13 @@ func sortUserTarget(uts []UserTarget) []UserTarget { // function pointer (for testing) var ( GHExistGitHubRepositoryFunc = gh.ExistGitHubRepository + GHExistRunnerReleases = gh.ExistRunnerReleases GHListRunnersFunc = gh.ListRunners GHIsInstalledGitHubApp = gh.IsInstalledGitHubApp GHGenerateGitHubAppsToken = gh.GenerateGitHubAppsToken GHNewClientApps = gh.NewClientGitHubApps ) -func toNullString(input *string) sql.NullString { - if input == nil || strings.EqualFold(*input, "") { - return sql.NullString{ - Valid: false, - } - } - - return sql.NullString{ - Valid: true, - String: *input, - } -} - -func isValidTargetCreateParam(input TargetCreateParam) (bool, error) { - if input.Scope == "" || input.ResourceType == datastore.ResourceTypeUnknown { - return false, fmt.Errorf("scope, resource_type must be set") - } - - if input.GHEDomain != "" { - if _, err := url.Parse(input.GHEDomain); err != nil { - return false, fmt.Errorf("domain of GitHub Enterprise is not valid URL: %w", err) - } - } - - if input.RunnerVersion != nil { - // valid format: vX.X.X (X is [0-9]) - if !strings.HasPrefix(*input.RunnerVersion, "v") { - return false, fmt.Errorf("runner_version must has prefix 'v'") - } - - s := strings.Split(*input.RunnerVersion, ".") - if len(s) != 3 { - return false, fmt.Errorf("runner_version must has version of major, sem, patch") - } - } - - return true, nil -} - -// ToDS convert to datastore.Target -func (t *TargetCreateParam) ToDS(appToken string, tokenExpired time.Time) datastore.Target { - gheDomain := toNullString(&t.GHEDomain) - runnerUser := toNullString(t.RunnerUser) - runnerVersion := toNullString(t.RunnerVersion) - providerURL := toNullString(t.ProviderURL) - - return datastore.Target{ - UUID: t.UUID, - Scope: t.Scope, - GitHubToken: appToken, - TokenExpiredAt: tokenExpired, - GHEDomain: gheDomain, - ResourceType: t.ResourceType, - RunnerUser: runnerUser, - RunnerVersion: runnerVersion, - ProviderURL: providerURL, - } -} - func handleTargetList(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) { ctx := r.Context() @@ -220,7 +162,7 @@ func handleTargetUpdate(w http.ResponseWriter, r *http.Request, ds datastore.Dat } if err := validateUpdateTarget(*oldTarget, newTarget); err != nil { logger.Logf(false, "input error in validateUpdateTarget: %+v", err) - outputErrorMsg(w, http.StatusBadRequest, "request parameter has value of not updatable") + outputErrorMsg(w, http.StatusBadRequest, err.Error()) return } @@ -319,6 +261,13 @@ func validateUpdateTarget(old, new datastore.Target) error { oldv := old newv := new + if new.RunnerVersion.Valid { + if err := validRunnerVersion(new.RunnerVersion.String); err != nil { + logger.Logf(false, "invalid input runner_version (runner_version: %s): %+v", new.RunnerVersion.String, err) + return fmt.Errorf("invalid input runner_version (runner_version: %s): %w", new.RunnerVersion.String, err) + } + } + for _, t := range []*datastore.Target{&oldv, &newv} { t.UUID = uuid.UUID{} @@ -339,13 +288,105 @@ func validateUpdateTarget(old, new datastore.Target) error { t.GitHubToken = "" } - if diff := cmp.Diff(oldv, newv); diff != "" { - return fmt.Errorf("mismatch (-want +got):\n%s", diff) + changelog, err := diff.Diff(oldv, newv) + if err != nil { + logger.Logf(false, "failed to check diff: %+v", err) + return fmt.Errorf("failed to check diff: %w", err) + } + if len(changelog) != 0 { + logger.Logf(false, "invalid updatable parameter: %+v", changelog) + + var invalidFields []string + for _, cl := range changelog { + if len(cl.Path) == 2 && !strings.EqualFold(cl.Path[1], "String") { + continue + } + + fieldName := cl.Path[0] + invalidFields = append(invalidFields, fieldName) + } + + return fmt.Errorf("invalid input: can't updatable fields (%s)", strings.Join(invalidFields, ", ")) } return nil } +func isValidTargetCreateParam(input TargetCreateParam) error { + if input.Scope == "" || input.ResourceType == datastore.ResourceTypeUnknown { + return fmt.Errorf("scope, resource_type must be set") + } + + if input.GHEDomain != "" { + if strings.EqualFold(input.GHEDomain, "https://github.com") { + return fmt.Errorf("ghe_domain must not https://github.com, please set blank") + } + + if _, err := url.Parse(input.GHEDomain); err != nil { + return fmt.Errorf("domain of GitHub Enterprise is not valid URL: %w", err) + } + } + + if input.RunnerVersion != nil { + if err := validRunnerVersion(*input.RunnerVersion); err != nil { + logger.Logf(false, "invalid input runner_version (runner_version: %s): %+v", *input.RunnerVersion, err) + return fmt.Errorf("invalid input runner_version (runner_version: %s): %w", *input.RunnerVersion, err) + } + } + + return nil +} + +func validRunnerVersion(runnerVersion string) error { + if !strings.HasPrefix(runnerVersion, "v") { + return fmt.Errorf("runner_version must has prefix 'v'") + } + + s := strings.Split(runnerVersion, ".") + if len(s) != 3 { + return fmt.Errorf("runner_version must has version of major, sem, patch") + } + + if err := GHExistRunnerReleases(runnerVersion); err != nil { + return fmt.Errorf("runner_version is not found in GitHub Release: %w", err) + } + + return nil +} + +func toNullString(input *string) sql.NullString { + if input == nil || strings.EqualFold(*input, "") { + return sql.NullString{ + Valid: false, + } + } + + return sql.NullString{ + Valid: true, + String: *input, + } +} + +// ToDS convert to datastore.Target +func (t *TargetCreateParam) ToDS(appToken string, tokenExpired time.Time) datastore.Target { + gheDomain := toNullString(&t.GHEDomain) + runnerUser := toNullString(t.RunnerUser) + runnerVersion := toNullString(t.RunnerVersion) + providerURL := toNullString(t.ProviderURL) + + return datastore.Target{ + UUID: t.UUID, + Scope: t.Scope, + GitHubToken: appToken, + TokenExpiredAt: tokenExpired, + GHEDomain: gheDomain, + ResourceType: t.ResourceType, + RunnerUser: runnerUser, + RunnerVersion: runnerVersion, + ProviderURL: providerURL, + } +} + type getWillUpdateTargetVariableOld struct { resourceType datastore.ResourceType runnerVersion sql.NullString diff --git a/pkg/web/target_create.go b/pkg/web/target_create.go index 616887b..6e34bda 100644 --- a/pkg/web/target_create.go +++ b/pkg/web/target_create.go @@ -24,7 +24,7 @@ func handleTargetCreate(w http.ResponseWriter, r *http.Request, ds datastore.Dat return } - if ok, err := isValidTargetCreateParam(inputTarget); !ok { + if err := isValidTargetCreateParam(inputTarget); err != nil { logger.Logf(false, "failed to validate input: %+v", err) outputErrorMsg(w, http.StatusBadRequest, err.Error()) return diff --git a/pkg/web/target_test.go b/pkg/web/target_test.go index 47e2d0a..dbf006b 100644 --- a/pkg/web/target_test.go +++ b/pkg/web/target_test.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "strings" "testing" "time" @@ -39,6 +40,10 @@ func setStubFunctions() { return nil } + web.GHExistRunnerReleases = func(runnerVersion string) error { + return nil + } + web.GHListRunnersFunc = func(ctx context.Context, client *github.Client, owner, repo string) ([]*github.Runner, error) { return nil, nil } @@ -495,7 +500,7 @@ func Test_handleTargetUpdate(t *testing.T) { } content, code := parseResponse(resp) if code != http.StatusOK { - t.Fatalf("must be response statuscode is 201, but got %d: %+v", code, string(content)) + t.Fatalf("must be response statuscode is 200, but got %d: %+v", code, string(content)) } var got web.UserTarget @@ -515,6 +520,68 @@ func Test_handleTargetUpdate(t *testing.T) { } } +func Test_handleTargetUpdate_Error(t *testing.T) { + testURL := testutils.GetTestURL() + _, teardown := testutils.GetTestDatastore() + defer teardown() + + setStubFunctions() + + tests := []struct { + input string + wantCode int + want string + }{ + { // Invalid: must set scope + input: `{"ghe_domain": "https://github.example.com", "resource_type": "nano", "runner_user": "ubuntu"}`, + wantCode: http.StatusBadRequest, + want: `{"error":"invalid input: can't updatable fields (Scope)"}`, + }, + { // Invalid: must set ghe_domain + input: `{"scope": "repo", "resource_type": "nano", "runner_user": "ubuntu"}`, + wantCode: http.StatusBadRequest, + want: `{"error":"invalid input: can't updatable fields (GHEDomain)"}`, + }, + { // Invalid: runner_version is semver + input: `{"scope": "repo", "ghe_domain": "https://github.example.com", "resource_type": "nano", "runner_user": "ubuntu", "runner_version": "v2.100"}`, + wantCode: http.StatusBadRequest, + want: `{"error": "runner_version must has version of major, sem, patch"}`, + }, + } + + for _, test := range tests { + target := `{"scope": "repo", "ghe_domain": "https://github.example.com", "resource_type": "micro", "runner_user": "ubuntu", "provider_url": "https://example.com/default-shoes"}` + respCreate, err := http.Post(testURL+"/target", "application/json", bytes.NewBufferString(target)) + if err != nil { + t.Fatalf("failed to POST request: %+v", err) + } + contentCreate, statusCode := parseResponse(respCreate) + if statusCode != http.StatusCreated { + t.Fatalf("must be response statuscode is 201, but got %d: %+v", respCreate.StatusCode, string(contentCreate)) + } + var respTarget web.UserTarget + if err := json.Unmarshal(contentCreate, &respTarget); err != nil { + t.Fatalf("failed to unmarshal response JSON: %+v", err) + } + targetUUID := respTarget.UUID + + resp, err := http.Post(fmt.Sprintf("%s/target/%s", testURL, targetUUID.String()), "application/json", bytes.NewBufferString(test.input)) + if err != nil { + t.Fatalf("failed to POST request: %+v", err) + } + content, code := parseResponse(resp) + got := string(content) + if code != test.wantCode { + t.Fatalf("must be response statuscode is %d, but got %d: %+v", test.wantCode, code, got) + } + if strings.EqualFold(test.want, got) { + t.Fatalf("invalid error response: %+v", string(content)) + } + + teardown() + } +} + func Test_handleTargetDelete(t *testing.T) { testURL := testutils.GetTestURL() testDatastore, teardown := testutils.GetTestDatastore()