Merge pull request #167 from ionutbalutoiu/refactor-e2e-tests

Refactor integration E2E tests
This commit is contained in:
Gabriel 2023-08-24 15:56:12 +03:00 committed by GitHub
commit 385a00ef9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1473 additions and 1547 deletions

View file

@ -61,13 +61,9 @@ jobs:
run: |
set -o pipefail
set -o errexit
go run ./test/integration/e2e.go 2>&1 | tee /artifacts-logs/e2e.log
go run ./test/integration/main.go 2>&1 | tee /artifacts-logs/e2e.log
env:
GARM_BASE_URL: ${{ steps.ngrok.outputs.tunnel-url }}
GARM_USERNAME: admin
GARM_FULLNAME: Local GARM Admin
GARM_EMAIL: admin@example.com
GARM_NAME: local_garm
ORG_NAME: gsamfira
REPO_NAME: garm-testing
CREDENTIALS_NAME: test-garm-creds
@ -86,3 +82,18 @@ jobs:
with:
name: garm-logs
path: /artifacts-logs
- name: Cleanup orphan GARM resources via GitHub API
if: always()
run: |
set -o pipefail
set -o errexit
sudo systemctl stop garm
go run ./test/integration/gh_cleanup/main.go
env:
GARM_BASE_URL: ${{ steps.ngrok.outputs.tunnel-url }}
ORG_NAME: gsamfira
REPO_NAME: garm-testing
GH_TOKEN: ${{ secrets.GH_OAUTH_TOKEN }}

1
.gitignore vendored
View file

@ -4,6 +4,7 @@
*.dll
*.so
*.dylib
*.DS_Store
# Test binary, built with `go test -c`
*.test

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,60 @@
package e2e
import (
"log"
"net/url"
"github.com/cloudbase/garm/client"
"github.com/cloudbase/garm/params"
"github.com/go-openapi/runtime"
openapiRuntimeClient "github.com/go-openapi/runtime/client"
)
var (
cli *client.GarmAPI
authToken runtime.ClientAuthInfoWriter
)
func InitClient(baseURL string) {
garmUrl, err := url.Parse(baseURL)
if err != nil {
panic(err)
}
apiPath, err := url.JoinPath(garmUrl.Path, client.DefaultBasePath)
if err != nil {
panic(err)
}
transportCfg := client.DefaultTransportConfig().
WithHost(garmUrl.Host).
WithBasePath(apiPath).
WithSchemes([]string{garmUrl.Scheme})
cli = client.NewHTTPClientWithConfig(nil, transportCfg)
}
func FirstRun(adminUsername, adminPassword, adminFullName, adminEmail string) *params.User {
log.Println("First run")
newUser := params.NewUserParams{
Username: adminUsername,
Password: adminPassword,
FullName: adminFullName,
Email: adminEmail,
}
user, err := firstRun(cli, newUser)
if err != nil {
panic(err)
}
return &user
}
func Login(username, password string) {
log.Println("Login")
loginParams := params.PasswordLoginParams{
Username: username,
Password: password,
}
token, err := login(cli, loginParams)
if err != nil {
panic(err)
}
authToken = openapiRuntimeClient.BearerToken(token)
}

View file

@ -0,0 +1,431 @@
package e2e
import (
"github.com/cloudbase/garm/client"
clientControllerInfo "github.com/cloudbase/garm/client/controller_info"
clientCredentials "github.com/cloudbase/garm/client/credentials"
clientFirstRun "github.com/cloudbase/garm/client/first_run"
clientInstances "github.com/cloudbase/garm/client/instances"
clientJobs "github.com/cloudbase/garm/client/jobs"
clientLogin "github.com/cloudbase/garm/client/login"
clientMetricsToken "github.com/cloudbase/garm/client/metrics_token"
clientOrganizations "github.com/cloudbase/garm/client/organizations"
clientPools "github.com/cloudbase/garm/client/pools"
clientProviders "github.com/cloudbase/garm/client/providers"
clientRepositories "github.com/cloudbase/garm/client/repositories"
"github.com/cloudbase/garm/params"
"github.com/go-openapi/runtime"
)
// ///////////
// Garm Init /
// ///////////
func firstRun(apiCli *client.GarmAPI, newUser params.NewUserParams) (params.User, error) {
firstRunResponse, err := apiCli.FirstRun.FirstRun(
clientFirstRun.NewFirstRunParams().WithBody(newUser),
nil)
if err != nil {
return params.User{}, err
}
return firstRunResponse.Payload, nil
}
func login(apiCli *client.GarmAPI, params params.PasswordLoginParams) (string, error) {
loginResponse, err := apiCli.Login.Login(
clientLogin.NewLoginParams().WithBody(params),
nil)
if err != nil {
return "", err
}
return loginResponse.Payload.Token, nil
}
// ////////////////////////////
// Credentials and Providers //
// ////////////////////////////
func listCredentials(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Credentials, error) {
listCredentialsResponse, err := apiCli.Credentials.ListCredentials(
clientCredentials.NewListCredentialsParams(),
apiAuthToken)
if err != nil {
return nil, err
}
return listCredentialsResponse.Payload, nil
}
func listProviders(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Providers, error) {
listProvidersResponse, err := apiCli.Providers.ListProviders(
clientProviders.NewListProvidersParams(),
apiAuthToken)
if err != nil {
return nil, err
}
return listProvidersResponse.Payload, nil
}
// ////////////////////////
// // Controller info ////
// ////////////////////////
func getControllerInfo(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.ControllerInfo, error) {
controllerInfoResponse, err := apiCli.ControllerInfo.ControllerInfo(
clientControllerInfo.NewControllerInfoParams(),
apiAuthToken)
if err != nil {
return params.ControllerInfo{}, err
}
return controllerInfoResponse.Payload, nil
}
// ////////
// Jobs //
// ////////
func listJobs(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Jobs, error) {
listJobsResponse, err := apiCli.Jobs.ListJobs(
clientJobs.NewListJobsParams(),
apiAuthToken)
if err != nil {
return nil, err
}
return listJobsResponse.Payload, nil
}
// //////////////////
// / Metrics Token //
// //////////////////
func getMetricsToken(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (string, error) {
getMetricsTokenResponse, err := apiCli.MetricsToken.GetMetricsToken(
clientMetricsToken.NewGetMetricsTokenParams(),
apiAuthToken)
if err != nil {
return "", err
}
return getMetricsTokenResponse.Payload.Token, nil
}
// ///////////////
// Repositories //
// ///////////////
func createRepo(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoParams params.CreateRepoParams) (*params.Repository, error) {
createRepoResponse, err := apiCli.Repositories.CreateRepo(
clientRepositories.NewCreateRepoParams().WithBody(repoParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &createRepoResponse.Payload, nil
}
func listRepos(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Repositories, error) {
listReposResponse, err := apiCli.Repositories.ListRepos(
clientRepositories.NewListReposParams(),
apiAuthToken)
if err != nil {
return nil, err
}
return listReposResponse.Payload, nil
}
func updateRepo(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID string, repoParams params.UpdateEntityParams) (*params.Repository, error) {
updateRepoResponse, err := apiCli.Repositories.UpdateRepo(
clientRepositories.NewUpdateRepoParams().WithRepoID(repoID).WithBody(repoParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &updateRepoResponse.Payload, nil
}
func getRepo(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID string) (*params.Repository, error) {
getRepoResponse, err := apiCli.Repositories.GetRepo(
clientRepositories.NewGetRepoParams().WithRepoID(repoID),
apiAuthToken)
if err != nil {
return nil, err
}
return &getRepoResponse.Payload, nil
}
func installRepoWebhook(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID string, webhookParams params.InstallWebhookParams) (*params.HookInfo, error) {
installRepoWebhookResponse, err := apiCli.Repositories.InstallRepoWebhook(
clientRepositories.NewInstallRepoWebhookParams().WithRepoID(repoID).WithBody(webhookParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &installRepoWebhookResponse.Payload, nil
}
func getRepoWebhook(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID string) (*params.HookInfo, error) {
getRepoWebhookResponse, err := apiCli.Repositories.GetRepoWebhookInfo(
clientRepositories.NewGetRepoWebhookInfoParams().WithRepoID(repoID),
apiAuthToken)
if err != nil {
return nil, err
}
return &getRepoWebhookResponse.Payload, nil
}
func createRepoPool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID string, poolParams params.CreatePoolParams) (*params.Pool, error) {
createRepoPoolResponse, err := apiCli.Repositories.CreateRepoPool(
clientRepositories.NewCreateRepoPoolParams().WithRepoID(repoID).WithBody(poolParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &createRepoPoolResponse.Payload, nil
}
func listRepoPools(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID string) (params.Pools, error) {
listRepoPoolsResponse, err := apiCli.Repositories.ListRepoPools(
clientRepositories.NewListRepoPoolsParams().WithRepoID(repoID),
apiAuthToken)
if err != nil {
return nil, err
}
return listRepoPoolsResponse.Payload, nil
}
func getRepoPool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID, poolID string) (*params.Pool, error) {
getRepoPoolResponse, err := apiCli.Repositories.GetRepoPool(
clientRepositories.NewGetRepoPoolParams().WithRepoID(repoID).WithPoolID(poolID),
apiAuthToken)
if err != nil {
return nil, err
}
return &getRepoPoolResponse.Payload, nil
}
func updateRepoPool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID, poolID string, poolParams params.UpdatePoolParams) (*params.Pool, error) {
updateRepoPoolResponse, err := apiCli.Repositories.UpdateRepoPool(
clientRepositories.NewUpdateRepoPoolParams().WithRepoID(repoID).WithPoolID(poolID).WithBody(poolParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &updateRepoPoolResponse.Payload, nil
}
func listRepoInstances(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID string) (params.Instances, error) {
listRepoInstancesResponse, err := apiCli.Repositories.ListRepoInstances(
clientRepositories.NewListRepoInstancesParams().WithRepoID(repoID),
apiAuthToken)
if err != nil {
return nil, err
}
return listRepoInstancesResponse.Payload, nil
}
func deleteRepo(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID string) error {
return apiCli.Repositories.DeleteRepo(
clientRepositories.NewDeleteRepoParams().WithRepoID(repoID),
apiAuthToken)
}
func deleteRepoPool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, repoID, poolID string) error {
return apiCli.Repositories.DeleteRepoPool(
clientRepositories.NewDeleteRepoPoolParams().WithRepoID(repoID).WithPoolID(poolID),
apiAuthToken)
}
// ////////////////
// Organizations //
// ////////////////
func createOrg(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgParams params.CreateOrgParams) (*params.Organization, error) {
createOrgResponse, err := apiCli.Organizations.CreateOrg(
clientOrganizations.NewCreateOrgParams().WithBody(orgParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &createOrgResponse.Payload, nil
}
func listOrgs(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Organizations, error) {
listOrgsResponse, err := apiCli.Organizations.ListOrgs(
clientOrganizations.NewListOrgsParams(),
apiAuthToken)
if err != nil {
return nil, err
}
return listOrgsResponse.Payload, nil
}
func updateOrg(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID string, orgParams params.UpdateEntityParams) (*params.Organization, error) {
updateOrgResponse, err := apiCli.Organizations.UpdateOrg(
clientOrganizations.NewUpdateOrgParams().WithOrgID(orgID).WithBody(orgParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &updateOrgResponse.Payload, nil
}
func getOrg(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID string) (*params.Organization, error) {
getOrgResponse, err := apiCli.Organizations.GetOrg(
clientOrganizations.NewGetOrgParams().WithOrgID(orgID),
apiAuthToken)
if err != nil {
return nil, err
}
return &getOrgResponse.Payload, nil
}
func installOrgWebhook(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID string, webhookParams params.InstallWebhookParams) (*params.HookInfo, error) {
installOrgWebhookResponse, err := apiCli.Organizations.InstallOrgWebhook(
clientOrganizations.NewInstallOrgWebhookParams().WithOrgID(orgID).WithBody(webhookParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &installOrgWebhookResponse.Payload, nil
}
func getOrgWebhook(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID string) (*params.HookInfo, error) {
getOrgWebhookResponse, err := apiCli.Organizations.GetOrgWebhookInfo(
clientOrganizations.NewGetOrgWebhookInfoParams().WithOrgID(orgID),
apiAuthToken)
if err != nil {
return nil, err
}
return &getOrgWebhookResponse.Payload, nil
}
func createOrgPool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID string, poolParams params.CreatePoolParams) (*params.Pool, error) {
createOrgPoolResponse, err := apiCli.Organizations.CreateOrgPool(
clientOrganizations.NewCreateOrgPoolParams().WithOrgID(orgID).WithBody(poolParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &createOrgPoolResponse.Payload, nil
}
func listOrgPools(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID string) (params.Pools, error) {
listOrgPoolsResponse, err := apiCli.Organizations.ListOrgPools(
clientOrganizations.NewListOrgPoolsParams().WithOrgID(orgID),
apiAuthToken)
if err != nil {
return nil, err
}
return listOrgPoolsResponse.Payload, nil
}
func getOrgPool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID, poolID string) (*params.Pool, error) {
getOrgPoolResponse, err := apiCli.Organizations.GetOrgPool(
clientOrganizations.NewGetOrgPoolParams().WithOrgID(orgID).WithPoolID(poolID),
apiAuthToken)
if err != nil {
return nil, err
}
return &getOrgPoolResponse.Payload, nil
}
func updateOrgPool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID, poolID string, poolParams params.UpdatePoolParams) (*params.Pool, error) {
updateOrgPoolResponse, err := apiCli.Organizations.UpdateOrgPool(
clientOrganizations.NewUpdateOrgPoolParams().WithOrgID(orgID).WithPoolID(poolID).WithBody(poolParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &updateOrgPoolResponse.Payload, nil
}
func listOrgInstances(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID string) (params.Instances, error) {
listOrgInstancesResponse, err := apiCli.Organizations.ListOrgInstances(
clientOrganizations.NewListOrgInstancesParams().WithOrgID(orgID),
apiAuthToken)
if err != nil {
return nil, err
}
return listOrgInstancesResponse.Payload, nil
}
func deleteOrg(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID string) error {
return apiCli.Organizations.DeleteOrg(
clientOrganizations.NewDeleteOrgParams().WithOrgID(orgID),
apiAuthToken)
}
func deleteOrgPool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, orgID, poolID string) error {
return apiCli.Organizations.DeleteOrgPool(
clientOrganizations.NewDeleteOrgPoolParams().WithOrgID(orgID).WithPoolID(poolID),
apiAuthToken)
}
// ////////////
// Instances //
// ////////////
func listInstances(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Instances, error) {
listInstancesResponse, err := apiCli.Instances.ListInstances(
clientInstances.NewListInstancesParams(),
apiAuthToken)
if err != nil {
return nil, err
}
return listInstancesResponse.Payload, nil
}
func getInstance(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, instanceID string) (*params.Instance, error) {
getInstancesResponse, err := apiCli.Instances.GetInstance(
clientInstances.NewGetInstanceParams().WithInstanceName(instanceID),
apiAuthToken)
if err != nil {
return nil, err
}
return &getInstancesResponse.Payload, nil
}
func deleteInstance(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, instanceID string) error {
return apiCli.Instances.DeleteInstance(
clientInstances.NewDeleteInstanceParams().WithInstanceName(instanceID),
apiAuthToken)
}
// ////////
// Pools //
// ////////
func listPools(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter) (params.Pools, error) {
listPoolsResponse, err := apiCli.Pools.ListPools(
clientPools.NewListPoolsParams(),
apiAuthToken)
if err != nil {
return nil, err
}
return listPoolsResponse.Payload, nil
}
func getPool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, poolID string) (*params.Pool, error) {
getPoolResponse, err := apiCli.Pools.GetPool(
clientPools.NewGetPoolParams().WithPoolID(poolID),
apiAuthToken)
if err != nil {
return nil, err
}
return &getPoolResponse.Payload, nil
}
func updatePool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, poolID string, poolParams params.UpdatePoolParams) (*params.Pool, error) {
updatePoolResponse, err := apiCli.Pools.UpdatePool(
clientPools.NewUpdatePoolParams().WithPoolID(poolID).WithBody(poolParams),
apiAuthToken)
if err != nil {
return nil, err
}
return &updatePoolResponse.Payload, nil
}
func listPoolInstances(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, poolID string) (params.Instances, error) {
listPoolInstancesResponse, err := apiCli.Instances.ListPoolInstances(
clientInstances.NewListPoolInstancesParams().WithPoolID(poolID),
apiAuthToken)
if err != nil {
return nil, err
}
return listPoolInstancesResponse.Payload, nil
}
func deletePool(apiCli *client.GarmAPI, apiAuthToken runtime.ClientAuthInfoWriter, poolID string) error {
return apiCli.Pools.DeletePool(
clientPools.NewDeletePoolParams().WithPoolID(poolID),
apiAuthToken)
}

137
test/integration/e2e/e2e.go Normal file
View file

@ -0,0 +1,137 @@
package e2e
import (
"fmt"
"log"
"os"
"time"
"github.com/cloudbase/garm/params"
)
func ListCredentials() params.Credentials {
log.Println("List credentials")
credentials, err := listCredentials(cli, authToken)
if err != nil {
panic(err)
}
return credentials
}
func ListProviders() params.Providers {
log.Println("List providers")
providers, err := listProviders(cli, authToken)
if err != nil {
panic(err)
}
return providers
}
func GetMetricsToken() {
log.Println("Get metrics token")
_, err := getMetricsToken(cli, authToken)
if err != nil {
panic(err)
}
}
func GetControllerInfo() *params.ControllerInfo {
log.Println("Get controller info")
controllerInfo, err := getControllerInfo(cli, authToken)
if err != nil {
panic(err)
}
if err := appendCtrlInfoToGitHubEnv(&controllerInfo); err != nil {
panic(err)
}
if err := printJsonResponse(controllerInfo); err != nil {
panic(err)
}
return &controllerInfo
}
func GracefulCleanup() {
// disable all the pools
pools, err := listPools(cli, authToken)
if err != nil {
panic(err)
}
enabled := false
poolParams := params.UpdatePoolParams{Enabled: &enabled}
for _, pool := range pools {
if _, err := updatePool(cli, authToken, pool.ID, poolParams); err != nil {
panic(err)
}
log.Printf("Pool %s disabled", pool.ID)
}
// delete all the instances
for _, pool := range pools {
poolInstances, err := listPoolInstances(cli, authToken, pool.ID)
if err != nil {
panic(err)
}
for _, instance := range poolInstances {
if err := deleteInstance(cli, authToken, instance.Name); err != nil {
panic(err)
}
log.Printf("Instance %s deletion initiated", instance.Name)
}
}
// wait for all instances to be deleted
for _, pool := range pools {
if err := waitPoolNoInstances(pool.ID, 3*time.Minute); err != nil {
panic(err)
}
}
// delete all the pools
for _, pool := range pools {
if err := deletePool(cli, authToken, pool.ID); err != nil {
panic(err)
}
log.Printf("Pool %s deleted", pool.ID)
}
// delete all the repositories
repos, err := listRepos(cli, authToken)
if err != nil {
panic(err)
}
for _, repo := range repos {
if err := deleteRepo(cli, authToken, repo.ID); err != nil {
panic(err)
}
log.Printf("Repo %s deleted", repo.ID)
}
// delete all the organizations
orgs, err := listOrgs(cli, authToken)
if err != nil {
panic(err)
}
for _, org := range orgs {
if err := deleteOrg(cli, authToken, org.ID); err != nil {
panic(err)
}
log.Printf("Org %s deleted", org.ID)
}
}
func appendCtrlInfoToGitHubEnv(controllerInfo *params.ControllerInfo) error {
envFile, found := os.LookupEnv("GITHUB_ENV")
if !found {
log.Printf("GITHUB_ENV not set, skipping appending controller info")
return nil
}
file, err := os.OpenFile(envFile, os.O_WRONLY|os.O_APPEND, os.ModeAppend)
if err != nil {
return err
}
defer file.Close()
if _, err := file.WriteString(fmt.Sprintf("GARM_CONTROLLER_ID=%s\n", controllerInfo.ControllerID)); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,90 @@
package e2e
import (
"context"
"fmt"
"log"
"github.com/google/go-github/v53/github"
"golang.org/x/oauth2"
)
func TriggerWorkflow(ghToken, orgName, repoName, workflowFileName, labelName string) {
log.Printf("Trigger workflow with label %s", labelName)
client := getGithubClient(ghToken)
eventReq := github.CreateWorkflowDispatchEventRequest{
Ref: "main",
Inputs: map[string]interface{}{
"sleep_time": "50",
"runner_label": labelName,
},
}
if _, err := client.Actions.CreateWorkflowDispatchEventByFileName(context.Background(), orgName, repoName, workflowFileName, eventReq); err != nil {
panic(err)
}
}
func GhOrgRunnersCleanup(ghToken, orgName, controllerID string) error {
log.Printf("Cleanup Github runners, labelled with controller ID %s, from org %s", controllerID, orgName)
client := getGithubClient(ghToken)
ghOrgRunners, _, err := client.Actions.ListOrganizationRunners(context.Background(), orgName, nil)
if err != nil {
return err
}
// Remove organization runners
controllerLabel := fmt.Sprintf("runner-controller-id:%s", controllerID)
for _, orgRunner := range ghOrgRunners.Runners {
for _, label := range orgRunner.Labels {
if label.GetName() == controllerLabel {
if _, err := client.Actions.RemoveOrganizationRunner(context.Background(), orgName, orgRunner.GetID()); err != nil {
// We don't fail if we can't remove a single runner. This
// is a best effort to try and remove all the orphan runners.
log.Printf("Failed to remove organization runner %s: %v", orgRunner.GetName(), err)
break
}
log.Printf("Removed organization runner %s", orgRunner.GetName())
break
}
}
}
return nil
}
func GhRepoRunnersCleanup(ghToken, orgName, repoName, controllerID string) error {
log.Printf("Cleanup Github runners, labelled with controller ID %s, from repo %s/%s", controllerID, orgName, repoName)
client := getGithubClient(ghToken)
ghRepoRunners, _, err := client.Actions.ListRunners(context.Background(), orgName, repoName, nil)
if err != nil {
return err
}
// Remove repository runners
controllerLabel := fmt.Sprintf("runner-controller-id:%s", controllerID)
for _, repoRunner := range ghRepoRunners.Runners {
for _, label := range repoRunner.Labels {
if label.GetName() == controllerLabel {
if _, err := client.Actions.RemoveRunner(context.Background(), orgName, repoName, repoRunner.GetID()); err != nil {
// We don't fail if we can't remove a single runner. This
// is a best effort to try and remove all the orphan runners.
log.Printf("Failed to remove repository runner %s: %v", repoRunner.GetName(), err)
break
}
log.Printf("Removed repository runner %s", repoRunner.GetName())
break
}
}
}
return nil
}
func getGithubClient(oauthToken string) *github.Client {
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: oauthToken})
tc := oauth2.NewClient(context.Background(), ts)
return github.NewClient(tc)
}

View file

@ -0,0 +1,110 @@
package e2e
import (
"fmt"
"log"
"time"
commonParams "github.com/cloudbase/garm-provider-common/params"
"github.com/cloudbase/garm/params"
)
func waitInstanceStatus(name string, status commonParams.InstanceStatus, runnerStatus params.RunnerStatus, timeout time.Duration) (*params.Instance, error) {
var timeWaited time.Duration = 0
var instance *params.Instance
log.Printf("Waiting for instance %s status to reach status %s and runner status %s", name, status, runnerStatus)
for timeWaited < timeout {
instance, err := getInstance(cli, authToken, name)
if err != nil {
return nil, err
}
log.Printf("Instance %s status %s and runner status %s", name, instance.Status, instance.RunnerStatus)
if instance.Status == status && instance.RunnerStatus == runnerStatus {
return instance, nil
}
time.Sleep(5 * time.Second)
timeWaited += 5 * time.Second
}
if err := printJsonResponse(*instance); err != nil {
return nil, err
}
return nil, fmt.Errorf("timeout waiting for instance %s status to reach status %s and runner status %s", name, status, runnerStatus)
}
func waitInstanceToBeRemoved(name string, timeout time.Duration) error {
var timeWaited time.Duration = 0
var instance *params.Instance
log.Printf("Waiting for instance %s to be removed", name)
for timeWaited < timeout {
instances, err := listInstances(cli, authToken)
if err != nil {
return err
}
instance = nil
for _, i := range instances {
if i.Name == name {
instance = &i
break
}
}
if instance == nil {
// The instance is not found in the list. We can safely assume
// that it is removed
return nil
}
time.Sleep(5 * time.Second)
timeWaited += 5 * time.Second
}
if err := printJsonResponse(*instance); err != nil {
return err
}
return fmt.Errorf("instance %s was not removed within the timeout", name)
}
func waitPoolRunningIdleInstances(poolID string, timeout time.Duration) error {
var timeWaited time.Duration = 0
var instances params.Instances
var poolInstances params.Instances
var err error
pool, err := getPool(cli, authToken, poolID)
if err != nil {
return err
}
log.Printf("Waiting for pool %s to have all instances as idle running", poolID)
for timeWaited < timeout {
instances, err = listInstances(cli, authToken)
if err != nil {
return err
}
poolInstances = make(params.Instances, 0)
runningIdleCount := 0
for _, instance := range instances {
if instance.PoolID == poolID {
poolInstances = append(poolInstances, instance)
}
if instance.Status == commonParams.InstanceRunning && instance.RunnerStatus == params.RunnerIdle {
runningIdleCount++
}
}
log.Printf("Pool min idle runners: %d, pool instances: %d, current pool running idle instances: %d", pool.MinIdleRunners, len(poolInstances), runningIdleCount)
if runningIdleCount == int(pool.MinIdleRunners) && runningIdleCount == len(poolInstances) {
return nil
}
time.Sleep(5 * time.Second)
timeWaited += 5 * time.Second
}
_ = dumpPoolInstancesDetails(pool.ID)
return fmt.Errorf("timeout waiting for pool %s to have all idle instances running", poolID)
}

View file

@ -0,0 +1,123 @@
package e2e
import (
"fmt"
"log"
"time"
commonParams "github.com/cloudbase/garm-provider-common/params"
"github.com/cloudbase/garm/params"
)
func ValidateJobLifecycle(label string) {
log.Printf("Validate GARM job lifecycle with label %s", label)
// wait for job list to be updated
job, err := waitLabelledJob(label, 4*time.Minute)
if err != nil {
panic(err)
}
// check expected job status
job, err = waitJobStatus(job.ID, params.JobStatusQueued, 4*time.Minute)
if err != nil {
panic(err)
}
job, err = waitJobStatus(job.ID, params.JobStatusInProgress, 4*time.Minute)
if err != nil {
panic(err)
}
// check expected instance status
instance, err := waitInstanceStatus(job.RunnerName, commonParams.InstanceRunning, params.RunnerActive, 5*time.Minute)
if err != nil {
panic(err)
}
// wait for job to be completed
_, err = waitJobStatus(job.ID, params.JobStatusCompleted, 4*time.Minute)
if err != nil {
panic(err)
}
// wait for instance to be removed
err = waitInstanceToBeRemoved(instance.Name, 5*time.Minute)
if err != nil {
panic(err)
}
// wait for GARM to rebuild the pool running idle instances
err = waitPoolRunningIdleInstances(instance.PoolID, 6*time.Minute)
if err != nil {
panic(err)
}
}
func waitLabelledJob(label string, timeout time.Duration) (*params.Job, error) {
var timeWaited time.Duration = 0
var jobs params.Jobs
var err error
log.Printf("Waiting for job with label %s", label)
for timeWaited < timeout {
jobs, err = listJobs(cli, authToken)
if err != nil {
return nil, err
}
for _, job := range jobs {
for _, jobLabel := range job.Labels {
if jobLabel == label {
return &job, err
}
}
}
time.Sleep(5 * time.Second)
timeWaited += 5 * time.Second
}
if err := printJsonResponse(jobs); err != nil {
return nil, err
}
return nil, fmt.Errorf("failed to wait job with label %s", label)
}
func waitJobStatus(id int64, status params.JobStatus, timeout time.Duration) (*params.Job, error) {
var timeWaited time.Duration = 0
var job *params.Job
log.Printf("Waiting for job %d to reach status %s", id, status)
for timeWaited < timeout {
jobs, err := listJobs(cli, authToken)
if err != nil {
return nil, err
}
job = nil
for _, j := range jobs {
if j.ID == id {
job = &j
break
}
}
if job == nil {
if status == params.JobStatusCompleted {
// The job is not found in the list. We can safely assume
// that it is completed
return nil, nil
}
// if the job is not found, and expected status is not "completed",
// we need to error out.
return nil, fmt.Errorf("job %d not found, expected to be found in status %s", id, status)
} else if job.Status == string(status) {
return job, nil
}
time.Sleep(5 * time.Second)
timeWaited += 5 * time.Second
}
if err := printJsonResponse(*job); err != nil {
return nil, err
}
return nil, fmt.Errorf("timeout waiting for job %d to reach status %s", id, status)
}

View file

@ -0,0 +1,132 @@
package e2e
import (
"log"
"time"
"github.com/cloudbase/garm/params"
)
func CreateOrg(orgName, credentialsName, orgWebhookSecret string) *params.Organization {
log.Printf("Create org %s", orgName)
orgParams := params.CreateOrgParams{
Name: orgName,
CredentialsName: credentialsName,
WebhookSecret: orgWebhookSecret,
}
org, err := createOrg(cli, authToken, orgParams)
if err != nil {
panic(err)
}
return org
}
func UpdateOrg(id, credentialsName string) *params.Organization {
log.Printf("Update org %s", id)
updateParams := params.UpdateEntityParams{
CredentialsName: credentialsName,
}
org, err := updateOrg(cli, authToken, id, updateParams)
if err != nil {
panic(err)
}
return org
}
func InstallOrgWebhook(id string) *params.HookInfo {
log.Printf("Install org %s webhook", id)
webhookParams := params.InstallWebhookParams{
WebhookEndpointType: params.WebhookEndpointDirect,
}
_, err := installOrgWebhook(cli, authToken, id, webhookParams)
if err != nil {
panic(err)
}
webhookInfo, err := getOrgWebhook(cli, authToken, id)
if err != nil {
panic(err)
}
return webhookInfo
}
func CreateOrgPool(orgID string, poolParams params.CreatePoolParams) *params.Pool {
log.Printf("Create org %s pool", orgID)
pool, err := createOrgPool(cli, authToken, orgID, poolParams)
if err != nil {
panic(err)
}
return pool
}
func GetOrgPool(orgID, orgPoolID string) *params.Pool {
log.Printf("Get org %s pool %s", orgID, orgPoolID)
pool, err := getOrgPool(cli, authToken, orgID, orgPoolID)
if err != nil {
panic(err)
}
return pool
}
func UpdateOrgPool(orgID, orgPoolID string, maxRunners, minIdleRunners uint) *params.Pool {
log.Printf("Update org %s pool %s", orgID, orgPoolID)
poolParams := params.UpdatePoolParams{
MinIdleRunners: &minIdleRunners,
MaxRunners: &maxRunners,
}
pool, err := updateOrgPool(cli, authToken, orgID, orgPoolID, poolParams)
if err != nil {
panic(err)
}
return pool
}
func DeleteOrgPool(orgID, orgPoolID string) {
log.Printf("Delete org %s pool %s", orgID, orgPoolID)
if err := deleteOrgPool(cli, authToken, orgID, orgPoolID); err != nil {
panic(err)
}
}
func WaitOrgRunningIdleInstances(orgID string, timeout time.Duration) {
orgPools, err := listOrgPools(cli, authToken, orgID)
if err != nil {
panic(err)
}
for _, pool := range orgPools {
err := waitPoolRunningIdleInstances(pool.ID, timeout)
if err != nil {
_ = dumpOrgInstancesDetails(orgID)
panic(err)
}
}
}
func dumpOrgInstancesDetails(orgID string) error {
log.Printf("Dumping org %s instances details", orgID)
// print org details
org, err := getOrg(cli, authToken, orgID)
if err != nil {
return err
}
if err := printJsonResponse(org); err != nil {
return err
}
// print org instances details
instances, err := listOrgInstances(cli, authToken, orgID)
if err != nil {
return err
}
for _, instance := range instances {
instance, err := getInstance(cli, authToken, instance.Name)
if err != nil {
return err
}
log.Printf("Instance %s info:", instance.Name)
if err := printJsonResponse(instance); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,54 @@
package e2e
import (
"fmt"
"log"
"time"
"github.com/cloudbase/garm/params"
)
func waitPoolNoInstances(id string, timeout time.Duration) error {
var timeWaited time.Duration = 0
var pool *params.Pool
var err error
log.Printf("Wait until pool %s has no instances", id)
for timeWaited < timeout {
pool, err = getPool(cli, authToken, id)
if err != nil {
return err
}
log.Printf("Current pool instances: %d", len(pool.Instances))
if len(pool.Instances) == 0 {
return nil
}
time.Sleep(5 * time.Second)
timeWaited += 5 * time.Second
}
_ = dumpPoolInstancesDetails(pool.ID)
return fmt.Errorf("failed to wait for pool %s to have no instances", pool.ID)
}
func dumpPoolInstancesDetails(poolID string) error {
pool, err := getPool(cli, authToken, poolID)
if err != nil {
return err
}
if err := printJsonResponse(pool); err != nil {
return err
}
for _, instance := range pool.Instances {
instanceDetails, err := getInstance(cli, authToken, instance.Name)
if err != nil {
return err
}
log.Printf("Instance %s details:", instance.Name)
if err := printJsonResponse(instanceDetails); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,133 @@
package e2e
import (
"log"
"time"
"github.com/cloudbase/garm/params"
)
func CreateRepo(orgName, repoName, credentialsName, repoWebhookSecret string) *params.Repository {
log.Printf("Create repository %s/%s", orgName, repoName)
createParams := params.CreateRepoParams{
Owner: orgName,
Name: repoName,
CredentialsName: credentialsName,
WebhookSecret: repoWebhookSecret,
}
repo, err := createRepo(cli, authToken, createParams)
if err != nil {
panic(err)
}
return repo
}
func UpdateRepo(id, credentialsName string) *params.Repository {
log.Printf("Update repo %s", id)
updateParams := params.UpdateEntityParams{
CredentialsName: credentialsName,
}
repo, err := updateRepo(cli, authToken, id, updateParams)
if err != nil {
panic(err)
}
return repo
}
func InstallRepoWebhook(id string) *params.HookInfo {
log.Printf("Install repo %s webhook", id)
webhookParams := params.InstallWebhookParams{
WebhookEndpointType: params.WebhookEndpointDirect,
}
_, err := installRepoWebhook(cli, authToken, id, webhookParams)
if err != nil {
panic(err)
}
webhookInfo, err := getRepoWebhook(cli, authToken, id)
if err != nil {
panic(err)
}
return webhookInfo
}
func CreateRepoPool(repoID string, poolParams params.CreatePoolParams) *params.Pool {
log.Printf("Create repo %s pool", repoID)
pool, err := createRepoPool(cli, authToken, repoID, poolParams)
if err != nil {
panic(err)
}
return pool
}
func GetRepoPool(repoID, repoPoolID string) *params.Pool {
log.Printf("Get repo %s pool %s", repoID, repoPoolID)
pool, err := getRepoPool(cli, authToken, repoID, repoPoolID)
if err != nil {
panic(err)
}
return pool
}
func UpdateRepoPool(repoID, repoPoolID string, maxRunners, minIdleRunners uint) *params.Pool {
log.Printf("Update repo %s pool %s", repoID, repoPoolID)
poolParams := params.UpdatePoolParams{
MinIdleRunners: &minIdleRunners,
MaxRunners: &maxRunners,
}
pool, err := updateRepoPool(cli, authToken, repoID, repoPoolID, poolParams)
if err != nil {
panic(err)
}
return pool
}
func DeleteRepoPool(repoID, repoPoolID string) {
log.Printf("Delete repo %s pool %s", repoID, repoPoolID)
if err := deleteRepoPool(cli, authToken, repoID, repoPoolID); err != nil {
panic(err)
}
}
func WaitRepoRunningIdleInstances(repoID string, timeout time.Duration) {
repoPools, err := listRepoPools(cli, authToken, repoID)
if err != nil {
panic(err)
}
for _, pool := range repoPools {
err := waitPoolRunningIdleInstances(pool.ID, timeout)
if err != nil {
_ = dumpRepoInstancesDetails(repoID)
panic(err)
}
}
}
func dumpRepoInstancesDetails(repoID string) error {
log.Printf("Dumping repo %s instances details", repoID)
// print repo details
repo, err := getRepo(cli, authToken, repoID)
if err != nil {
return err
}
if err := printJsonResponse(repo); err != nil {
return err
}
// print repo instances details
instances, err := listRepoInstances(cli, authToken, repoID)
if err != nil {
return err
}
for _, instance := range instances {
instance, err := getInstance(cli, authToken, instance.Name)
if err != nil {
return err
}
log.Printf("Instance %s info:", instance.Name)
if err := printJsonResponse(instance); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,15 @@
package e2e
import (
"encoding/json"
"log"
)
func printJsonResponse(resp interface{}) error {
b, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return err
}
log.Println(string(b))
return nil
}

View file

@ -0,0 +1,34 @@
package main
import (
"log"
"os"
"github.com/cloudbase/garm/test/integration/e2e"
)
var (
orgName = os.Getenv("ORG_NAME")
repoName = os.Getenv("REPO_NAME")
ghToken = os.Getenv("GH_TOKEN")
)
func main() {
controllerID, ctrlIdFound := os.LookupEnv("GARM_CONTROLLER_ID")
if ctrlIdFound {
_ = e2e.GhOrgRunnersCleanup(ghToken, orgName, controllerID)
_ = e2e.GhRepoRunnersCleanup(ghToken, orgName, repoName, controllerID)
} else {
log.Println("Env variable GARM_CONTROLLER_ID is not set, skipping GitHub runners cleanup")
}
baseURL, baseUrlFound := os.LookupEnv("GARM_BASE_URL")
if ctrlIdFound && baseUrlFound {
log.Printf("TODO: Cleanup org & repo webhooks staring with: %s/webhooks/%s", baseURL, controllerID)
// TODO: Cleanup org webhooks that start with "{baseURL}/webhooks/{controllerID}"
// TODO: Cleanup repo webhooks that start with "{baseURL}/webhooks/{controllerID}"
} else {
log.Println("Env variables GARM_CONTROLLER_ID & GARM_BASE_URL are not set, skipping webhooks cleanup")
}
}

137
test/integration/main.go Normal file
View file

@ -0,0 +1,137 @@
package main
import (
"fmt"
"os"
"time"
commonParams "github.com/cloudbase/garm-provider-common/params"
"github.com/cloudbase/garm/params"
"github.com/cloudbase/garm/test/integration/e2e"
)
const (
adminUsername = "admin"
adminFullName = "GARM Admin"
adminEmail = "admin@example.com"
)
var (
baseURL = os.Getenv("GARM_BASE_URL")
adminPassword = os.Getenv("GARM_PASSWORD")
credentialsName = os.Getenv("CREDENTIALS_NAME")
repoName = os.Getenv("REPO_NAME")
repoWebhookSecret = os.Getenv("REPO_WEBHOOK_SECRET")
repoPoolParams = params.CreatePoolParams{
MaxRunners: 2,
MinIdleRunners: 0,
Flavor: "default",
Image: "ubuntu:22.04",
OSType: commonParams.Linux,
OSArch: commonParams.Amd64,
ProviderName: "lxd_local",
Tags: []string{"repo-runner"},
Enabled: true,
}
orgName = os.Getenv("ORG_NAME")
orgWebhookSecret = os.Getenv("ORG_WEBHOOK_SECRET")
orgPoolParams = params.CreatePoolParams{
MaxRunners: 2,
MinIdleRunners: 0,
Flavor: "default",
Image: "ubuntu:22.04",
OSType: commonParams.Linux,
OSArch: commonParams.Amd64,
ProviderName: "lxd_local",
Tags: []string{"org-runner"},
Enabled: true,
}
ghToken = os.Getenv("GH_TOKEN")
workflowFileName = os.Getenv("WORKFLOW_FILE_NAME")
)
func main() {
/////////////
// Cleanup //
/////////////
defer e2e.GracefulCleanup()
///////////////
// garm init //
///////////////
e2e.InitClient(baseURL)
e2e.FirstRun(adminUsername, adminPassword, adminFullName, adminEmail)
e2e.Login(adminUsername, adminPassword)
// //////////////////
// controller info //
// //////////////////
e2e.GetControllerInfo()
// ////////////////////////////
// credentials and providers //
// ////////////////////////////
e2e.ListCredentials()
e2e.ListProviders()
////////////////////
/// metrics token //
////////////////////
e2e.GetMetricsToken()
//////////////////
// repositories //
//////////////////
repo := e2e.CreateRepo(orgName, repoName, credentialsName, repoWebhookSecret)
repo = e2e.UpdateRepo(repo.ID, fmt.Sprintf("%s-clone", credentialsName))
e2e.InstallRepoWebhook(repo.ID)
// TODO:
// * Check that the webhook is properly installed in GitHub.
// * Uninstall webhook
// * Check that webhook is properly removed from GitHub.
// * Install webhook again.
repoPool := e2e.CreateRepoPool(repo.ID, repoPoolParams)
repoPool = e2e.GetRepoPool(repo.ID, repoPool.ID)
e2e.DeleteRepoPool(repo.ID, repoPool.ID)
repoPool = e2e.CreateRepoPool(repo.ID, repoPoolParams)
_ = e2e.UpdateRepoPool(repo.ID, repoPool.ID, repoPoolParams.MaxRunners, 1)
///////////////////
// organizations //
///////////////////
org := e2e.CreateOrg(orgName, credentialsName, orgWebhookSecret)
org = e2e.UpdateOrg(org.ID, fmt.Sprintf("%s-clone", credentialsName))
e2e.InstallOrgWebhook(org.ID)
// TODO:
// * Check that the webhook is properly installed in GitHub.
// * Uninstall webhook
// * Check that webhook is properly removed from GitHub.
// * Install webhook again.
orgPool := e2e.CreateOrgPool(org.ID, orgPoolParams)
orgPool = e2e.GetOrgPool(org.ID, orgPool.ID)
e2e.DeleteOrgPool(org.ID, orgPool.ID)
orgPool = e2e.CreateOrgPool(org.ID, orgPoolParams)
_ = e2e.UpdateOrgPool(org.ID, orgPool.ID, orgPoolParams.MaxRunners, 1)
///////////////
// instances //
///////////////
e2e.WaitRepoRunningIdleInstances(repo.ID, 6*time.Minute)
e2e.WaitOrgRunningIdleInstances(org.ID, 6*time.Minute)
//////////
// jobs //
//////////
e2e.TriggerWorkflow(ghToken, orgName, repoName, workflowFileName, "org-runner")
e2e.ValidateJobLifecycle("org-runner")
e2e.TriggerWorkflow(ghToken, orgName, repoName, workflowFileName, "repo-runner")
e2e.ValidateJobLifecycle("repo-runner")
}