//go:build integration // +build integration // Copyright 2025 Cloudbase Solutions SRL // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. package integration import ( "context" "fmt" "net/url" "os" "testing" "time" "github.com/go-openapi/runtime" openapiRuntimeClient "github.com/go-openapi/runtime/client" "github.com/stretchr/testify/suite" "github.com/cloudbase/garm/client" "github.com/cloudbase/garm/params" ) var ( orgName string repoName string orgWebhookSecret string workflowFileName string ) type GarmSuite struct { suite.Suite cli *client.GarmAPI authToken runtime.ClientAuthInfoWriter ghToken string credentialsName string repo *params.Repository } func (suite *GarmSuite) SetupSuite() { t := suite.T() suite.ghToken = os.Getenv("GH_TOKEN") orgWebhookSecret = os.Getenv("ORG_WEBHOOK_SECRET") workflowFileName = os.Getenv("WORKFLOW_FILE_NAME") baseURL := os.Getenv("GARM_BASE_URL") adminPassword := os.Getenv("GARM_PASSWORD") adminUsername := os.Getenv("GARM_ADMIN_USERNAME") adminFullName := "GARM Admin" adminEmail := "admin@example.com" garmURL, err := url.Parse(baseURL) suite.NoError(err, "error parsing GARM_BASE_URL") apiPath, err := url.JoinPath(garmURL.Path, client.DefaultBasePath) suite.NoError(err, "error joining path") transportCfg := client.DefaultTransportConfig(). WithHost(garmURL.Host). WithBasePath(apiPath). WithSchemes([]string{garmURL.Scheme}) suite.cli = client.NewHTTPClientWithConfig(nil, transportCfg) t.Log("First run") newUser := params.NewUserParams{ Username: adminUsername, Password: adminPassword, FullName: adminFullName, Email: adminEmail, } _, err = firstRun(suite.cli, newUser) suite.NoError(err, "error at first run") t.Log("Login") loginParams := params.PasswordLoginParams{ Username: adminUsername, Password: adminPassword, } token, err := login(suite.cli, loginParams) suite.NoError(err, "error at login") suite.authToken = openapiRuntimeClient.BearerToken(token) t.Log("Log in successful") suite.credentialsName = os.Getenv("CREDENTIALS_NAME") suite.EnsureTestCredentials(suite.credentialsName, suite.ghToken, "github.com") t.Log("Create repository") orgName = os.Getenv("ORG_NAME") repoName = os.Getenv("REPO_NAME") repoWebhookSecret := os.Getenv("REPO_WEBHOOK_SECRET") createParams := params.CreateRepoParams{ Owner: orgName, Name: repoName, CredentialsName: suite.credentialsName, WebhookSecret: repoWebhookSecret, } suite.repo, err = createRepo(suite.cli, suite.authToken, createParams) suite.NoError(err, "error creating repository") suite.Equal(orgName, suite.repo.Owner, "owner name mismatch") suite.Equal(repoName, suite.repo.Name, "repo name mismatch") suite.Equal(suite.credentialsName, suite.repo.CredentialsName, "credentials name mismatch") } func (suite *GarmSuite) TearDownSuite() { t := suite.T() t.Log("Graceful cleanup") // disable all the pools pools, err := listPools(suite.cli, suite.authToken) suite.NoError(err, "error listing pools") enabled := false poolParams := params.UpdatePoolParams{Enabled: &enabled} for _, pool := range pools { _, err := updatePool(suite.cli, suite.authToken, pool.ID, poolParams) suite.NoError(err, "error disabling pool") t.Logf("Pool %s disabled during stage graceful_cleanup", pool.ID) } // delete all the instances for _, pool := range pools { poolInstances, err := listPoolInstances(suite.cli, suite.authToken, pool.ID) suite.NoError(err, "error listing pool instances") for _, instance := range poolInstances { err := deleteInstance(suite.cli, suite.authToken, instance.Name, false, false) suite.NoError(err, "error deleting instance") t.Logf("Instance deletion initiated for instace %s during stage graceful_cleanup", instance.Name) } } // wait for all instances to be deleted for _, pool := range pools { err := suite.waitPoolNoInstances(pool.ID, 3*time.Minute) suite.NoError(err, "error waiting for pool to have no instances") } // delete all the pools for _, pool := range pools { err := deletePool(suite.cli, suite.authToken, pool.ID) suite.NoError(err, "error deleting pool") t.Logf("Pool %s deleted during stage graceful_cleanup", pool.ID) } // delete all the repositories repos, err := listRepos(suite.cli, suite.authToken) suite.NoError(err, "error listing repositories") for _, repo := range repos { err := deleteRepo(suite.cli, suite.authToken, repo.ID) suite.NoError(err, "error deleting repository") t.Logf("Repo %s deleted during stage graceful_cleanup", repo.ID) } // delete all the organizations orgs, err := listOrgs(suite.cli, suite.authToken) suite.NoError(err, "error listing organizations") for _, org := range orgs { err := deleteOrg(suite.cli, suite.authToken, org.ID) suite.NoError(err, "error deleting organization") t.Logf("Org %s deleted during stage graceful_cleanup", org.ID) } } func TestGarmTestSuite(t *testing.T) { suite.Run(t, new(GarmSuite)) } func (suite *GarmSuite) waitPoolNoInstances(id string, timeout time.Duration) error { t := suite.T() var timeWaited time.Duration // default is 0 var pool *params.Pool var err error t.Logf("Wait until pool with id %s has no instances", id) for timeWaited < timeout { pool, err = getPool(suite.cli, suite.authToken, id) suite.NoError(err, "error getting pool") t.Logf("Current pool has %d instances", len(pool.Instances)) if len(pool.Instances) == 0 { return nil } time.Sleep(5 * time.Second) timeWaited += 5 * time.Second } err = suite.dumpPoolInstancesDetails(pool.ID) suite.NoError(err, "error dumping pool instances details") return fmt.Errorf("failed to wait for pool %s to have no instances", pool.ID) } func (suite *GarmSuite) GhOrgRunnersCleanup(ghToken, orgName, controllerID string) error { t := suite.T() t.Logf("Cleanup Github runners for controller %s and 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. t.Logf("Failed to remove organization runner %s: %v", orgRunner.GetName(), err) break } t.Logf("Removed organization runner %s", orgRunner.GetName()) break } } } return nil }