From 9b30c744bfa474140241f182d123b6efc9cbafc5 Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 19 Feb 2026 16:44:33 +0530 Subject: [PATCH 1/6] feat: add leader_runner_id to runner group model and migration --- services/ctl-api/internal/app/install.go | 10 +++---- services/ctl-api/internal/app/runner.go | 11 ++++++++ services/ctl-api/internal/app/runner_group.go | 28 ++++++++++++++++++- .../ctl-api/internal/pkg/db/psql/models.go | 6 ++-- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/services/ctl-api/internal/app/install.go b/services/ctl-api/internal/app/install.go index c5816bd888..6c32c7a142 100644 --- a/services/ctl-api/internal/app/install.go +++ b/services/ctl-api/internal/app/install.go @@ -134,12 +134,12 @@ func (i *Install) BeforeCreate(tx *gorm.DB) error { func (i *Install) AfterQuery(tx *gorm.DB) error { i.Links = links.InstallLinks(tx.Statement.Context, i.ID) - // get the runner status + // get the runner status, preferring the leader runner if one is elected i.RunnerStatus = RunnerStatusDeprovisioned - if len(i.RunnerGroup.Runners) > 0 { - i.RunnerStatus = i.RunnerGroup.Runners[0].Status - i.RunnerStatusDescription = i.RunnerGroup.Runners[0].StatusDescription - i.RunnerID = i.RunnerGroup.Runners[0].ID + if runner := i.RunnerGroup.ActiveRunner(); runner != nil { + i.RunnerStatus = runner.Status + i.RunnerStatusDescription = runner.StatusDescription + i.RunnerID = runner.ID } if len(i.InstallInputs) > 0 { diff --git a/services/ctl-api/internal/app/runner.go b/services/ctl-api/internal/app/runner.go index c0a78f18b8..c557191271 100644 --- a/services/ctl-api/internal/app/runner.go +++ b/services/ctl-api/internal/app/runner.go @@ -85,6 +85,10 @@ type Runner struct { Name string `json:"name,omitzero" gorm:"index:idx_runner_name,unique" temporaljson:"name,omitzero,omitempty"` DisplayName string `json:"display_name,omitzero" gorm:"not null;default null" temporaljson:"display_name,omitzero,omitempty"` + Platform AppRunnerType `json:"platform,omitzero" gorm:"not null;default:'unknown'" swaggertype:"string" temporaljson:"platform,omitzero,omitempty"` + Tainted bool `json:"tainted,omitzero" gorm:"not null;default:false" temporaljson:"tainted,omitzero,omitempty"` + Leader bool `json:"leader,omitzero" gorm:"not null;default:false" temporaljson:"leader,omitzero,omitempty"` + Jobs []RunnerJob `json:"jobs,omitzero" gorm:"constraint:OnDelete:CASCADE;" temporaljson:"jobs,omitzero,omitempty"` Operations []RunnerOperation `json:"operations,omitzero" gorm:"constraint:OnDelete:CASCADE;" temporaljson:"operations,omitzero,omitempty"` @@ -99,6 +103,13 @@ func (r *Runner) Indexes(db *gorm.DB) []migrations.Index { "org_id", }, }, + { + Name: indexes.Name(db, &Runner{}, "runner_group_id_leader"), + Columns: []string{ + "runner_group_id", + "leader", + }, + }, } } diff --git a/services/ctl-api/internal/app/runner_group.go b/services/ctl-api/internal/app/runner_group.go index 9e6fb42d78..faa859b446 100644 --- a/services/ctl-api/internal/app/runner_group.go +++ b/services/ctl-api/internal/app/runner_group.go @@ -37,7 +37,8 @@ type RunnerGroup struct { Runners []Runner `json:"runners,omitzero" gorm:"constraint:OnDelete:CASCADE;" temporaljson:"runners,omitzero,omitempty"` Settings RunnerGroupSettings `json:"settings,omitzero" gorm:"constraint:OnDelete:CASCADE;" temporaljson:"settings,omitzero,omitempty"` Type RunnerGroupType `json:"type,omitzero" gorm:"notnull;defaultnull" temporaljson:"type,omitzero,omitempty"` - Platform AppRunnerType `json:"platform,omitzero" gorm:"notnull;defaultnull" temporaljson:"platform,omitzero,omitempty"` + // Deprecated: Platform is being phased out in favor of per-runner Runner.Platform field. + Platform AppRunnerType `json:"platform,omitzero" gorm:"notnull;defaultnull" swaggertype:"string" temporaljson:"platform,omitzero,omitempty"` } func (r *RunnerGroup) Indexes(db *gorm.DB) []migrations.Index { @@ -65,6 +66,31 @@ func (r *RunnerGroup) BeforeCreate(tx *gorm.DB) error { return nil } +// ActiveRunner returns the elected leader runner (Leader==true) if one exists +// in the group, otherwise falls back to the first runner. +// Returns nil if the group has no runners. +func (r *RunnerGroup) ActiveRunner() *Runner { + if len(r.Runners) == 0 { + return nil + } + for idx := range r.Runners { + if r.Runners[idx].Leader { + return &r.Runners[idx] + } + } + return &r.Runners[0] +} + +// HasLeader returns true if any runner in the group has Leader==true. +func (r *RunnerGroup) HasLeader() bool { + for idx := range r.Runners { + if r.Runners[idx].Leader { + return true + } + } + return false +} + func (r *RunnerGroup) EventLoops() []bulk.EventLoop { evs := make([]bulk.EventLoop, 0) for _, runner := range r.Runners { diff --git a/services/ctl-api/internal/pkg/db/psql/models.go b/services/ctl-api/internal/pkg/db/psql/models.go index 57620c00a4..96c71ebcab 100644 --- a/services/ctl-api/internal/pkg/db/psql/models.go +++ b/services/ctl-api/internal/pkg/db/psql/models.go @@ -101,11 +101,13 @@ func AllModels() []any { // log streams &app.LogStream{}, - // runner jobs and groups + // runner groups and runners &app.RunnerGroup{}, - &app.RunnerOperation{}, &app.RunnerGroupSettings{}, &app.Runner{}, + &app.RunnerOperation{}, + + // runner jobs &app.RunnerJob{}, &app.RunnerJobPlan{}, &app.RunnerJobExecution{}, From c98f5f0e565af61cc0bc5c96ce8be52ae6c58e39 Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 19 Feb 2026 16:45:04 +0530 Subject: [PATCH 2/6] feat: add runner leader election API endpoints and helpers --- .../app/installs/helpers/get_install_state.go | 4 +- .../service/admin_get_install_runner.go | 8 +- .../generate_terraform_installer_config.go | 5 +- .../app/orgs/service/admin_get_runner.go | 9 +- .../runners/helpers/create_runner_group.go | 13 +- .../app/runners/helpers/elect_leader.go | 82 +++++++++ .../app/runners/helpers/set_leader.go | 82 +++++++++ .../service/admin_create_runner_in_group.go | 170 ++++++++++++++++++ .../app/runners/service/admin_taint_runner.go | 45 +++++ .../service/get_runner_group_leader.go | 48 +++++ .../app/runners/service/get_runner_jobs.go | 23 +++ .../internal/app/runners/service/service.go | 55 ++++-- .../app/runners/service/taint_runner.go | 112 ++++++++++++ .../service/update_runner_group_leader.go | 84 +++++++++ 14 files changed, 710 insertions(+), 30 deletions(-) create mode 100644 services/ctl-api/internal/app/runners/helpers/elect_leader.go create mode 100644 services/ctl-api/internal/app/runners/helpers/set_leader.go create mode 100644 services/ctl-api/internal/app/runners/service/admin_create_runner_in_group.go create mode 100644 services/ctl-api/internal/app/runners/service/admin_taint_runner.go create mode 100644 services/ctl-api/internal/app/runners/service/get_runner_group_leader.go create mode 100644 services/ctl-api/internal/app/runners/service/taint_runner.go create mode 100644 services/ctl-api/internal/app/runners/service/update_runner_group_leader.go diff --git a/services/ctl-api/internal/app/installs/helpers/get_install_state.go b/services/ctl-api/internal/app/installs/helpers/get_install_state.go index 9d29b1b9c5..07192594fa 100644 --- a/services/ctl-api/internal/app/installs/helpers/get_install_state.go +++ b/services/ctl-api/internal/app/installs/helpers/get_install_state.go @@ -142,8 +142,8 @@ func (h *Helpers) GetInstallState(ctx context.Context, installID string, redacte is.App = h.toAppState(install.App) is.Org = h.toOrgState(install.Org) - if len(install.RunnerGroup.Runners) > 0 { - is.Runner = h.toRunnerState(install.RunnerGroup.Runners[0]) + if runner := install.RunnerGroup.ActiveRunner(); runner != nil { + is.Runner = h.toRunnerState(*runner) } is.Sandbox = h.toSandboxesState(sandboxRuns) diff --git a/services/ctl-api/internal/app/installs/service/admin_get_install_runner.go b/services/ctl-api/internal/app/installs/service/admin_get_install_runner.go index 992a807501..3b666a64ae 100644 --- a/services/ctl-api/internal/app/installs/service/admin_get_install_runner.go +++ b/services/ctl-api/internal/app/installs/service/admin_get_install_runner.go @@ -26,5 +26,11 @@ func (s *service) AdminGetInstallRunner(ctx *gin.Context) { return } - ctx.JSON(http.StatusOK, install.RunnerGroup.Runners[0]) + runner := install.RunnerGroup.ActiveRunner() + if runner == nil { + ctx.Error(fmt.Errorf("no runners in install %s runner group", installID)) + return + } + + ctx.JSON(http.StatusOK, runner) } diff --git a/services/ctl-api/internal/app/installs/service/generate_terraform_installer_config.go b/services/ctl-api/internal/app/installs/service/generate_terraform_installer_config.go index 0ac08e6178..4d619cd399 100644 --- a/services/ctl-api/internal/app/installs/service/generate_terraform_installer_config.go +++ b/services/ctl-api/internal/app/installs/service/generate_terraform_installer_config.go @@ -46,11 +46,12 @@ func (s *service) genTerraformInstallerConfig(ctx context.Context, installID str if err != nil { return "", err } - if len(runnerGroup.Runners) == 0 { + runner := runnerGroup.ActiveRunner() + if runner == nil { return "", fmt.Errorf("no runners in install runner group") } - token, err := s.runnersHelpers.CreateToken(ctx, runnerGroup.Runners[0].ID) + token, err := s.runnersHelpers.CreateToken(ctx, runner.ID) if err != nil { return "", err } diff --git a/services/ctl-api/internal/app/orgs/service/admin_get_runner.go b/services/ctl-api/internal/app/orgs/service/admin_get_runner.go index 73759d5b3d..dece21a5ed 100644 --- a/services/ctl-api/internal/app/orgs/service/admin_get_runner.go +++ b/services/ctl-api/internal/app/orgs/service/admin_get_runner.go @@ -1,6 +1,7 @@ package service import ( + "fmt" "net/http" "github.com/gin-gonic/gin" @@ -25,5 +26,11 @@ func (s *service) AdminGetOrgRunner(ctx *gin.Context) { return } - ctx.JSON(http.StatusOK, org.RunnerGroup.Runners[0]) + runner := org.RunnerGroup.ActiveRunner() + if runner == nil { + ctx.Error(fmt.Errorf("no runners in org %s runner group", nameOrID)) + return + } + + ctx.JSON(http.StatusOK, runner) } diff --git a/services/ctl-api/internal/app/runners/helpers/create_runner_group.go b/services/ctl-api/internal/app/runners/helpers/create_runner_group.go index c0431e8d77..8c3d150035 100644 --- a/services/ctl-api/internal/app/runners/helpers/create_runner_group.go +++ b/services/ctl-api/internal/app/runners/helpers/create_runner_group.go @@ -23,10 +23,10 @@ func (h *Helpers) CreateInstallRunnerGroup(ctx context.Context, install *app.Ins ctx = cctx.SetOrgIDContext(ctx, install.OrgID) ctx = cctx.SetAccountIDContext(ctx, install.CreatedByID) + // The default runner always gets the cloud platform from the app config. + // Local runners are created separately via the CreateRunnerInGroup endpoint + // so they get their own record and don't take over the default cloud runner. platform := install.AppRunnerConfig.Type - if install.Org.OrgType != app.OrgTypeDefault || h.cfg.UseLocalRunners { - platform = app.AppRunnerTypeLocal - } groups := append(app.CommonRunnerGroupSettingsGroups[:], app.DefaultInstallRunnerGroupSettingsGroups[:]...) runnerGroup := app.RunnerGroup{ @@ -40,6 +40,7 @@ func (h *Helpers) CreateInstallRunnerGroup(ctx context.Context, install *app.Ins DisplayName: "Default runner", Status: app.RunnerStatusPending, StatusDescription: string(app.RunnerStatusPending), + Platform: platform, }, }, Settings: app.RunnerGroupSettings{ @@ -86,10 +87,9 @@ func (h *Helpers) CreateOrgRunnerGroup(ctx context.Context, org *app.Org) (*app. ctx = cctx.SetOrgIDContext(ctx, org.ID) ctx = cctx.SetAccountIDContext(ctx, org.CreatedByID) + // The default runner always gets the cloud platform. + // Local runners are created separately via the CreateRunnerInGroup endpoint. platform := app.AppRunnerTypeAWSEKS - if org.OrgType != app.OrgTypeDefault || h.cfg.UseLocalRunners { - platform = app.AppRunnerTypeLocal - } groups := append(app.CommonRunnerGroupSettingsGroups[:], app.DefaultOrgRunnerGroupSettingsGroups[:]...) runnerGroup := app.RunnerGroup{ @@ -103,6 +103,7 @@ func (h *Helpers) CreateOrgRunnerGroup(ctx context.Context, org *app.Org) (*app. DisplayName: "Default runner", Status: app.RunnerStatusPending, StatusDescription: string(app.RunnerStatusPending), + Platform: platform, }, }, Settings: app.RunnerGroupSettings{ diff --git a/services/ctl-api/internal/app/runners/helpers/elect_leader.go b/services/ctl-api/internal/app/runners/helpers/elect_leader.go new file mode 100644 index 0000000000..c5fcf5f8fa --- /dev/null +++ b/services/ctl-api/internal/app/runners/helpers/elect_leader.go @@ -0,0 +1,82 @@ +package helpers + +import ( + "context" + "errors" + "fmt" + + "github.com/nuonco/nuon/services/ctl-api/internal/app" + "gorm.io/gorm" +) + +type ElectLeaderResult struct { + OldLeaderID string + NewLeaderID string +} + +func (s *Helpers) ElectLeader(ctx context.Context, groupID string) (*ElectLeaderResult, error) { + tx := s.db.WithContext(ctx).Begin() + if tx.Error != nil { + return nil, fmt.Errorf("unable to begin transaction: %w", tx.Error) + } + defer tx.Rollback() + + var group app.RunnerGroup + if err := tx.Raw("SELECT * FROM runner_groups WHERE id = ? AND deleted_at = 0 FOR UPDATE", groupID).Scan(&group).Error; err != nil { + tx.Rollback() + return nil, fmt.Errorf("unable to lock runner group: %w", err) + } + if group.ID == "" { + tx.Rollback() + return nil, fmt.Errorf("runner group not found: %s", groupID) + } + + // Find the current leader in this group. + var oldLeader app.Runner + var oldLeaderID string + if err := tx.Where("runner_group_id = ? AND leader = true AND deleted_at = 0", groupID).First(&oldLeader).Error; err == nil { + oldLeaderID = oldLeader.ID + } + + // Clear all leader flags in the group. + if err := tx.Model(&app.Runner{}). + Where("runner_group_id = ? AND deleted_at = 0", groupID). + Update("leader", false).Error; err != nil { + tx.Rollback() + return nil, fmt.Errorf("unable to clear leader flags: %w", err) + } + + // Elect the oldest active, untainted runner as leader. + var leader app.Runner + err := tx. + Where("runner_group_id = ? AND status = ? AND tainted = false AND deleted_at = 0", groupID, app.RunnerStatusActive). + Order("created_at ASC"). + First(&leader).Error + + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("unable to query active runners: %w", err) + } + // No active runners — leader stays cleared. + } else { + if err := tx.Model(&app.Runner{}). + Where("id = ? AND deleted_at = 0", leader.ID). + Update("leader", true).Error; err != nil { + tx.Rollback() + return nil, fmt.Errorf("unable to set leader: %w", err) + } + } + + if err := tx.Commit().Error; err != nil { + return nil, fmt.Errorf("unable to commit leader election: %w", err) + } + + result := &ElectLeaderResult{ + OldLeaderID: oldLeaderID, + } + if err == nil { + result.NewLeaderID = leader.ID + } + + return result, nil +} diff --git a/services/ctl-api/internal/app/runners/helpers/set_leader.go b/services/ctl-api/internal/app/runners/helpers/set_leader.go new file mode 100644 index 0000000000..d4c80b996d --- /dev/null +++ b/services/ctl-api/internal/app/runners/helpers/set_leader.go @@ -0,0 +1,82 @@ +package helpers + +import ( + "context" + "errors" + "fmt" + + "github.com/nuonco/nuon/services/ctl-api/internal/app" + "gorm.io/gorm" +) + +type SetLeaderResult struct { + OldLeaderID string + NewLeaderID string +} + +func (s *Helpers) SetLeader(ctx context.Context, groupID, runnerID string) (*SetLeaderResult, error) { + tx := s.db.WithContext(ctx).Begin() + if tx.Error != nil { + return nil, fmt.Errorf("unable to begin transaction: %w", tx.Error) + } + defer tx.Rollback() + + var group app.RunnerGroup + if err := tx.Raw("SELECT * FROM runner_groups WHERE id = ? AND deleted_at = 0 FOR UPDATE", groupID).Scan(&group).Error; err != nil { + return nil, fmt.Errorf("unable to lock runner group: %w", err) + } + if group.ID == "" { + return nil, fmt.Errorf("runner group not found: %s", groupID) + } + + // Verify the requested runner exists, is active, and untainted. + var runner app.Runner + err := tx.Where("id = ? AND runner_group_id = ? AND deleted_at = 0", runnerID, groupID).First(&runner).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("runner %s not found in group %s", runnerID, groupID) + } + return nil, fmt.Errorf("unable to query runner: %w", err) + } + if runner.Status != app.RunnerStatusActive { + return nil, fmt.Errorf("runner %s is not active (status: %s)", runnerID, runner.Status) + } + + // Find the current leader. + var oldLeader app.Runner + var oldLeaderID string + if err := tx.Where("runner_group_id = ? AND leader = true AND deleted_at = 0", groupID).First(&oldLeader).Error; err == nil { + oldLeaderID = oldLeader.ID + } + + // Already the leader — no-op. + if oldLeaderID == runnerID { + return &SetLeaderResult{ + OldLeaderID: oldLeaderID, + NewLeaderID: runnerID, + }, nil + } + + // Clear all leader flags in the group. + if err := tx.Model(&app.Runner{}). + Where("runner_group_id = ? AND deleted_at = 0", groupID). + Update("leader", false).Error; err != nil { + return nil, fmt.Errorf("unable to clear leader flags: %w", err) + } + + // Set the requested runner as leader. + if err := tx.Model(&app.Runner{}). + Where("id = ? AND deleted_at = 0", runnerID). + Update("leader", true).Error; err != nil { + return nil, fmt.Errorf("unable to set leader: %w", err) + } + + if err := tx.Commit().Error; err != nil { + return nil, fmt.Errorf("unable to commit set leader: %w", err) + } + + return &SetLeaderResult{ + OldLeaderID: oldLeaderID, + NewLeaderID: runnerID, + }, nil +} diff --git a/services/ctl-api/internal/app/runners/service/admin_create_runner_in_group.go b/services/ctl-api/internal/app/runners/service/admin_create_runner_in_group.go new file mode 100644 index 0000000000..6d4f60743a --- /dev/null +++ b/services/ctl-api/internal/app/runners/service/admin_create_runner_in_group.go @@ -0,0 +1,170 @@ +package service + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + "github.com/nuonco/nuon/services/ctl-api/internal/app" + runnergroupssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/signals" + "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/signals" + "github.com/nuonco/nuon/services/ctl-api/internal/middlewares/stderr" + "github.com/nuonco/nuon/services/ctl-api/internal/pkg/account" +) + +type createRunnerInGroupRequest struct { + Platform app.AppRunnerType `json:"platform" validate:"required"` +} + +type createRunnerInGroupResponse struct { + Runner *app.Runner `json:"runner"` + Token string `json:"token"` +} + +// ensureRunnerServiceAccount finds or creates a service account for the runner +// and assigns it the runner org role. Failures are logged but not fatal. +func (s *service) ensureRunnerServiceAccount(ctx *gin.Context, runnerID, orgID string) { + acct, err := s.acctClient.FindAccount(ctx, account.ServiceAccountEmail(runnerID)) + if err != nil { + // No existing account — create one. + acct, err = s.acctClient.CreateServiceAccount(ctx, runnerID) + if err != nil { + s.l.Warn("unable to create service account for runner", + zap.String("runner_id", runnerID), + zap.Error(err), + ) + return + } + } + + if err := s.authzClient.AddAccountOrgRole(ctx, app.RoleTypeRunner, orgID, acct.ID); err != nil { + s.l.Warn("unable to add org role for runner", + zap.String("runner_id", runnerID), + zap.Error(err), + ) + } +} + +// @ID AdminCreateRunnerInGroup +// @Summary find or create a runner in a runner group +// @Description Find an existing runner with matching platform or create a new one with service account and token +// @Tags runners/admin +// @Security AdminEmail +// @Accept json +// @Produce json +// @Param runner_group_id path string true "runner group ID" +// @Param request body createRunnerInGroupRequest true "runner creation request" +// @Success 200 {object} createRunnerInGroupResponse +// @Failure 400 {object} stderr.ErrResponse +// @Failure 404 {object} stderr.ErrResponse +// @Failure 500 {object} stderr.ErrResponse +// @Router /v1/runner-groups/{runner_group_id}/runners [post] +func (s *service) AdminCreateRunnerInGroup(ctx *gin.Context) { + groupID := ctx.Param("runner_group_id") + + var req createRunnerInGroupRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.Error(stderr.ErrUser{ + Err: fmt.Errorf("invalid request: %w", err), + Description: "invalid request body", + }) + return + } + + if req.Platform == "" { + ctx.Error(stderr.ErrUser{ + Err: fmt.Errorf("platform is required"), + Description: "platform field is required", + }) + return + } + + // Look up the runner group + var group app.RunnerGroup + res := s.db.WithContext(ctx).First(&group, "id = ? AND deleted_at = 0", groupID) + if res.Error != nil { + ctx.Error(stderr.ErrNotFound{ + Err: fmt.Errorf("runner group %s not found: %w", groupID, res.Error), + Description: "runner group not found", + }) + return + } + + // Find existing runner with matching platform in this group + var existing app.Runner + res = s.db.WithContext(ctx). + Where("runner_group_id = ? AND platform = ? AND deleted_at = 0", groupID, req.Platform). + First(&existing) + if res.Error == nil { + s.ensureRunnerServiceAccount(ctx, existing.ID, group.OrgID) + + // Ensure the runner's event loop is running (idempotent restart) + s.evClient.Send(ctx, existing.ID, &signals.Signal{ + Type: signals.OperationRestart, + }) + + // Found existing runner, return it with a fresh token + token, err := s.helpers.CreateToken(ctx, existing.ID) + if err != nil { + ctx.Error(fmt.Errorf("unable to create token for existing runner: %w", err)) + return + } + + ctx.JSON(http.StatusOK, createRunnerInGroupResponse{ + Runner: &existing, + Token: token.Token, + }) + return + } + + // Create new runner — local runners are immediately active since they + // don't go through the cloud provisioning workflow. + status := app.RunnerStatusPending + statusDesc := string(app.RunnerStatusPending) + if req.Platform == app.AppRunnerTypeLocal { + status = app.RunnerStatusActive + statusDesc = "local runner is active" + } + + runner := app.Runner{ + RunnerGroupID: groupID, + OrgID: group.OrgID, + Name: string(req.Platform), + DisplayName: fmt.Sprintf("%s runner", req.Platform), + Status: status, + StatusDescription: statusDesc, + Platform: req.Platform, + } + + res = s.db.WithContext(ctx).Create(&runner) + if res.Error != nil { + ctx.Error(fmt.Errorf("unable to create runner: %w", res.Error)) + return + } + + s.ensureRunnerServiceAccount(ctx, runner.ID, group.OrgID) + + // Start the runner's event loop so it can receive job signals + s.evClient.Send(ctx, runner.ID, &signals.Signal{ + Type: signals.OperationCreated, + }) + + // Trigger leader election for the group asynchronously. + s.evClient.Send(ctx, groupID, &runnergroupssignals.Signal{ + Type: runnergroupssignals.OperationElectLeader, + }) + + // Create token + token, err := s.helpers.CreateToken(ctx, runner.ID) + if err != nil { + ctx.Error(fmt.Errorf("unable to create token for runner: %w", err)) + return + } + + ctx.JSON(http.StatusCreated, createRunnerInGroupResponse{ + Runner: &runner, + Token: token.Token, + }) +} diff --git a/services/ctl-api/internal/app/runners/service/admin_taint_runner.go b/services/ctl-api/internal/app/runners/service/admin_taint_runner.go new file mode 100644 index 0000000000..58058e6e2f --- /dev/null +++ b/services/ctl-api/internal/app/runners/service/admin_taint_runner.go @@ -0,0 +1,45 @@ +package service + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// @ID AdminTaintRunner +// @Summary taint a runner to exclude it from leader election +// @Tags runners/admin +// @Security AdminEmail +// @Accept json +// @Produce json +// @Param runner_id path string true "runner ID" +// @Success 200 {object} app.Runner +// @Router /v1/runners/{runner_id}/taint [post] +func (s *service) AdminTaintRunner(ctx *gin.Context) { + runner, err := s.setRunnerTainted(ctx, ctx.Param("runner_id"), true, "") + if err != nil { + ctx.Error(err) + return + } + + ctx.JSON(http.StatusOK, runner) +} + +// @ID AdminUntaintRunner +// @Summary untaint a runner to include it in leader election +// @Tags runners/admin +// @Security AdminEmail +// @Accept json +// @Produce json +// @Param runner_id path string true "runner ID" +// @Success 200 {object} app.Runner +// @Router /v1/runners/{runner_id}/untaint [post] +func (s *service) AdminUntaintRunner(ctx *gin.Context) { + runner, err := s.setRunnerTainted(ctx, ctx.Param("runner_id"), false, "") + if err != nil { + ctx.Error(err) + return + } + + ctx.JSON(http.StatusOK, runner) +} diff --git a/services/ctl-api/internal/app/runners/service/get_runner_group_leader.go b/services/ctl-api/internal/app/runners/service/get_runner_group_leader.go new file mode 100644 index 0000000000..6305531947 --- /dev/null +++ b/services/ctl-api/internal/app/runners/service/get_runner_group_leader.go @@ -0,0 +1,48 @@ +package service + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/nuonco/nuon/services/ctl-api/internal/app" + "github.com/nuonco/nuon/services/ctl-api/internal/middlewares/stderr" + "github.com/nuonco/nuon/services/ctl-api/internal/pkg/cctx" +) + +// @ID GetRunnerGroupLeader +// @Summary get the leader runner for a runner group +// @Tags runners +// @Security APIKey +// @Security OrgID +// @Accept json +// @Produce json +// @Param runner_group_id path string true "runner group ID" +// @Success 200 {object} app.Runner +// @Failure 404 {object} stderr.ErrResponse +// @Failure 500 {object} stderr.ErrResponse +// @Router /v1/runner-groups/{runner_group_id}/leader [get] +func (s *service) GetRunnerGroupLeader(ctx *gin.Context) { + org, err := cctx.OrgFromContext(ctx) + if err != nil { + ctx.Error(err) + return + } + + groupID := ctx.Param("runner_group_id") + + var leader app.Runner + res := s.db.WithContext(ctx). + Where("runner_group_id = ? AND org_id = ? AND leader = true AND deleted_at = 0", groupID, org.ID). + First(&leader) + if res.Error != nil { + ctx.Error(stderr.ErrNotFound{ + Err: fmt.Errorf("no leader elected for runner group %s", groupID), + Description: "no leader elected", + }) + return + } + + ctx.JSON(http.StatusOK, &leader) +} diff --git a/services/ctl-api/internal/app/runners/service/get_runner_jobs.go b/services/ctl-api/internal/app/runners/service/get_runner_jobs.go index 0db39fc3ab..d00fb0eca3 100644 --- a/services/ctl-api/internal/app/runners/service/get_runner_jobs.go +++ b/services/ctl-api/internal/app/runners/service/get_runner_jobs.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/gin-gonic/gin" + "go.uber.org/zap" "github.com/nuonco/nuon/services/ctl-api/internal/app" "github.com/nuonco/nuon/services/ctl-api/internal/middlewares/stderr" @@ -63,6 +64,28 @@ func (s *service) GetRunnerJobs(ctx *gin.Context) { } func (s *service) getRunnerJobs(ctx *gin.Context, runnerID string, status app.RunnerJobStatus, grp app.RunnerJobGroup, limit int) ([]*app.RunnerJob, error) { + var runner app.Runner + if res := s.db.WithContext(ctx).First(&runner, "id = ?", runnerID); res.Error != nil { + s.l.Warn("failed to load runner for leader check, returning empty jobs", + zap.Error(res.Error), + zap.String("runner_id", runnerID), + ) + return []*app.RunnerJob{}, nil + } + // If a leader exists in the group and this runner is not it, return no jobs. + if !runner.Leader { + var hasLeader bool + s.db.WithContext(ctx). + Model(&app.Runner{}). + Select("1"). + Where("runner_group_id = ? AND leader = true AND deleted_at = 0", runner.RunnerGroupID). + Limit(1). + Find(&hasLeader) + if hasLeader { + return []*app.RunnerJob{}, nil + } + } + runnerJobs := []*app.RunnerJob{} where := app.RunnerJob{ diff --git a/services/ctl-api/internal/app/runners/service/service.go b/services/ctl-api/internal/app/runners/service/service.go index 7f316356aa..7e2ecfebfe 100644 --- a/services/ctl-api/internal/app/runners/service/service.go +++ b/services/ctl-api/internal/app/runners/service/service.go @@ -12,6 +12,7 @@ import ( "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/helpers" "github.com/nuonco/nuon/services/ctl-api/internal/pkg/account" "github.com/nuonco/nuon/services/ctl-api/internal/pkg/api" + "github.com/nuonco/nuon/services/ctl-api/internal/pkg/authz" "github.com/nuonco/nuon/services/ctl-api/internal/pkg/eventloop" ) @@ -26,19 +27,21 @@ type Params struct { L *zap.Logger EvClient eventloop.Client AccountClient *account.Client + AuthzClient *authz.Client Helpers *helpers.Helpers } type service struct { - v *validator.Validate - l *zap.Logger - db *gorm.DB - chDB *gorm.DB - mw metrics.Writer - cfg *internal.Config - evClient eventloop.Client - acctClient *account.Client - helpers *helpers.Helpers + v *validator.Validate + l *zap.Logger + db *gorm.DB + chDB *gorm.DB + mw metrics.Writer + cfg *internal.Config + evClient eventloop.Client + acctClient *account.Client + authzClient *authz.Client + helpers *helpers.Helpers } var _ api.Service = (*service)(nil) @@ -66,6 +69,10 @@ func (s *service) RegisterPublicRoutes(api *gin.Engine) error { api.POST("/v1/runners/:runner_id/mng/fetch-token", s.MngFetchToken) api.POST("/v1/runners/:runner_id/prune-tokens", s.PruneTokens) + // taint/untaint + api.POST("/v1/runners/:runner_id/taint", s.TaintRunner) + api.POST("/v1/runners/:runner_id/untaint", s.UntaintRunner) + // settings api.GET("/v1/runners/:runner_id/settings", s.GetRunnerSettingsPublic) api.PATCH("/v1/runners/:runner_id/settings", s.UpdateRunnerSettings) @@ -102,6 +109,10 @@ func (s *service) RegisterPublicRoutes(api *gin.Engine) error { api.GET("/v1/log-streams/:log_stream_id/logs", s.LogStreamReadLogs) api.GET("/v1/log-streams/:log_stream_id", s.GetLogStream) + // runner group leader + api.GET("/v1/runner-groups/:runner_group_id/leader", s.GetRunnerGroupLeader) + api.PUT("/v1/runner-groups/:runner_group_id/leader", s.UpdateRunnerGroupLeader) + return nil } func (s *service) RegisterInternalRoutes(api *gin.Engine) error { @@ -139,6 +150,10 @@ func (s *service) RegisterInternalRoutes(api *gin.Engine) error { runner.POST("/flush-orphaned-jobs", s.AdminFlushOrphanedJobs) runner.GET("/jobs/queue", s.AdminGetRunnerJobsQueue) + // taint/untaint + runner.POST("/taint", s.AdminTaintRunner) + runner.POST("/untaint", s.AdminUntaintRunner) + // trigger specific jobs runner.POST("/graceful-shutdown", s.AdminGracefulShutDown) runner.POST("/force-shutdown", s.AdminForceShutDown) @@ -151,6 +166,9 @@ func (s *service) RegisterInternalRoutes(api *gin.Engine) error { runnerGroups := api.Group("/v1/runner-groups/:runner_group_id") { runnerGroups.GET("", s.AdminGetRunnerGroup) + runnerGroups.GET("/leader", s.GetRunnerGroupLeader) + runnerGroups.PUT("/leader", s.UpdateRunnerGroupLeader) + runnerGroups.POST("/runners", s.AdminCreateRunnerInGroup) } // runner job management @@ -256,14 +274,15 @@ func (s *service) RegisterAdminDashboardRoutes(api *gin.Engine) error { func New(params Params) *service { return &service{ - cfg: params.Cfg, - l: params.L, - v: params.V, - db: params.DB, - chDB: params.CHDB, - mw: params.MW, - evClient: params.EvClient, - acctClient: params.AccountClient, - helpers: params.Helpers, + cfg: params.Cfg, + l: params.L, + v: params.V, + db: params.DB, + chDB: params.CHDB, + mw: params.MW, + evClient: params.EvClient, + acctClient: params.AccountClient, + authzClient: params.AuthzClient, + helpers: params.Helpers, } } diff --git a/services/ctl-api/internal/app/runners/service/taint_runner.go b/services/ctl-api/internal/app/runners/service/taint_runner.go new file mode 100644 index 0000000000..f976c66704 --- /dev/null +++ b/services/ctl-api/internal/app/runners/service/taint_runner.go @@ -0,0 +1,112 @@ +package service + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/nuonco/nuon/services/ctl-api/internal/app" + runnergroupssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/signals" + "github.com/nuonco/nuon/services/ctl-api/internal/middlewares/stderr" + "github.com/nuonco/nuon/services/ctl-api/internal/pkg/cctx" +) + +// setRunnerTainted fetches a runner by ID, optionally scoped to an org, and sets its tainted field. +// When orgID is non-empty, the lookup includes an org_id filter and returns stderr.ErrNotFound on miss. +// When orgID is empty (admin path), the lookup omits the org filter and returns a plain error on miss. +func (s *service) setRunnerTainted(ctx *gin.Context, runnerID string, tainted bool, orgID string) (*app.Runner, error) { + var runner app.Runner + + query := s.db.WithContext(ctx) + if orgID != "" { + query = query.Where("id = ? AND org_id = ?", runnerID, orgID) + } else { + query = query.Where("id = ?", runnerID) + } + + if res := query.First(&runner); res.Error != nil { + if orgID != "" { + return nil, stderr.ErrNotFound{ + Err: fmt.Errorf("runner %s not found: %w", runnerID, res.Error), + Description: "runner not found", + } + } + return nil, fmt.Errorf("runner %s not found: %w", runnerID, res.Error) + } + + if res := s.db.WithContext(ctx).Model(&runner).Update("tainted", tainted); res.Error != nil { + action := "taint" + if !tainted { + action = "untaint" + } + return nil, fmt.Errorf("unable to %s runner: %w", action, res.Error) + } + + runner.Tainted = tainted + + // Trigger leader election so the group picks a new leader after taint changes. + if runner.RunnerGroupID != "" { + s.evClient.Send(ctx, runner.RunnerGroupID, &runnergroupssignals.Signal{ + Type: runnergroupssignals.OperationElectLeader, + }) + } + + return &runner, nil +} + +// @ID TaintRunner +// @Summary taint a runner to exclude it from leader election +// @Tags runners +// @Security APIKey +// @Security OrgID +// @Accept json +// @Produce json +// @Param runner_id path string true "runner ID" +// @Success 200 {object} app.Runner +// @Failure 404 {object} stderr.ErrResponse +// @Failure 500 {object} stderr.ErrResponse +// @Router /v1/runners/{runner_id}/taint [post] +func (s *service) TaintRunner(ctx *gin.Context) { + org, err := cctx.OrgFromContext(ctx) + if err != nil { + ctx.Error(err) + return + } + + runner, err := s.setRunnerTainted(ctx, ctx.Param("runner_id"), true, org.ID) + if err != nil { + ctx.Error(err) + return + } + + ctx.JSON(http.StatusOK, runner) +} + +// @ID UntaintRunner +// @Summary untaint a runner to include it in leader election +// @Tags runners +// @Security APIKey +// @Security OrgID +// @Accept json +// @Produce json +// @Param runner_id path string true "runner ID" +// @Success 200 {object} app.Runner +// @Failure 404 {object} stderr.ErrResponse +// @Failure 500 {object} stderr.ErrResponse +// @Router /v1/runners/{runner_id}/untaint [post] +func (s *service) UntaintRunner(ctx *gin.Context) { + org, err := cctx.OrgFromContext(ctx) + if err != nil { + ctx.Error(err) + return + } + + runner, err := s.setRunnerTainted(ctx, ctx.Param("runner_id"), false, org.ID) + if err != nil { + ctx.Error(err) + return + } + + ctx.JSON(http.StatusOK, runner) +} diff --git a/services/ctl-api/internal/app/runners/service/update_runner_group_leader.go b/services/ctl-api/internal/app/runners/service/update_runner_group_leader.go new file mode 100644 index 0000000000..c91d48f91a --- /dev/null +++ b/services/ctl-api/internal/app/runners/service/update_runner_group_leader.go @@ -0,0 +1,84 @@ +package service + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/nuonco/nuon/services/ctl-api/internal/app" + runnergroupssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/signals" + "github.com/nuonco/nuon/services/ctl-api/internal/middlewares/stderr" + "github.com/nuonco/nuon/services/ctl-api/internal/pkg/cctx" +) + +type updateRunnerGroupLeaderRequest struct { + RunnerID *string `json:"runner_id"` +} + +// @ID UpdateRunnerGroupLeader +// @Summary set or auto-elect the leader runner for a runner group +// @Tags runners +// @Security APIKey +// @Security OrgID +// @Accept json +// @Produce json +// @Param runner_group_id path string true "runner group ID" +// @Param request body updateRunnerGroupLeaderRequest true "leader update request" +// @Success 202 {object} object +// @Failure 400 {object} stderr.ErrResponse +// @Failure 404 {object} stderr.ErrResponse +// @Failure 500 {object} stderr.ErrResponse +// @Router /v1/runner-groups/{runner_group_id}/leader [put] +func (s *service) UpdateRunnerGroupLeader(ctx *gin.Context) { + org, err := cctx.OrgFromContext(ctx) + if err != nil { + ctx.Error(err) + return + } + + groupID := ctx.Param("runner_group_id") + + var req updateRunnerGroupLeaderRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.Error(stderr.ErrUser{ + Err: fmt.Errorf("invalid request: %w", err), + Description: "invalid request body", + }) + return + } + + if req.RunnerID == nil { + // Trigger async leader election via the runner-groups event loop. + s.evClient.Send(ctx, groupID, &runnergroupssignals.Signal{ + Type: runnergroupssignals.OperationElectLeader, + }) + } else { + var runner app.Runner + res := s.db.WithContext(ctx).First(&runner, "id = ? AND runner_group_id = ? AND org_id = ?", *req.RunnerID, groupID, org.ID) + if res.Error != nil { + ctx.Error(stderr.ErrNotFound{ + Err: fmt.Errorf("runner %s not found in group %s: %w", *req.RunnerID, groupID, res.Error), + Description: "runner not found in this group", + }) + return + } + + if runner.Status != app.RunnerStatusActive { + ctx.Error(stderr.ErrUser{ + Err: fmt.Errorf("runner %s is not active (status: %s)", *req.RunnerID, runner.Status), + Description: "runner must be active to be elected leader", + }) + return + } + + // Dispatch to the runner-groups event loop so the set + reschedule + // happen atomically inside the workflow, avoiding races with ElectLeader. + s.evClient.Send(ctx, groupID, &runnergroupssignals.Signal{ + Type: runnergroupssignals.OperationSetLeader, + RequestedLeaderRunnerID: *req.RunnerID, + }) + } + + ctx.JSON(http.StatusAccepted, gin.H{"status": "accepted"}) +} From 7538be34053bd246e2ad850bd48cfbf26bade92c Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 19 Feb 2026 16:45:16 +0530 Subject: [PATCH 3/6] feat: add Temporal activities and workflows for leader election --- .../app/runner_groups/signals/signals.go | 113 ++++++++++++++++++ .../worker/cron_ensure_leader.go | 82 +++++++++++++ .../worker/event_loop_workflow.go | 106 ++++++++++++++++ .../app/runner_groups/worker/workflows.go | 55 +++++++++ .../runners/worker/activities/elect_leader.go | 29 +++++ .../worker/activities/get_group_leader.go | 36 ++++++ .../activities/reschedule_jobs_to_leader.go | 50 ++++++++ .../activities/retarget_job_to_leader.go | 64 ++++++++++ .../runners/worker/activities/set_leader.go | 30 +++++ .../app/runners/worker/cron_health_check.go | 25 ++++ .../app/runners/worker/offline_check.go | 11 ++ .../app/runners/worker/process_job.go | 24 ++++ .../pkg/workflows/signals/activities/send.go | 7 ++ 13 files changed, 632 insertions(+) create mode 100644 services/ctl-api/internal/app/runner_groups/signals/signals.go create mode 100644 services/ctl-api/internal/app/runner_groups/worker/cron_ensure_leader.go create mode 100644 services/ctl-api/internal/app/runner_groups/worker/event_loop_workflow.go create mode 100644 services/ctl-api/internal/app/runner_groups/worker/workflows.go create mode 100644 services/ctl-api/internal/app/runners/worker/activities/elect_leader.go create mode 100644 services/ctl-api/internal/app/runners/worker/activities/get_group_leader.go create mode 100644 services/ctl-api/internal/app/runners/worker/activities/reschedule_jobs_to_leader.go create mode 100644 services/ctl-api/internal/app/runners/worker/activities/retarget_job_to_leader.go create mode 100644 services/ctl-api/internal/app/runners/worker/activities/set_leader.go diff --git a/services/ctl-api/internal/app/runner_groups/signals/signals.go b/services/ctl-api/internal/app/runner_groups/signals/signals.go new file mode 100644 index 0000000000..33b815b951 --- /dev/null +++ b/services/ctl-api/internal/app/runner_groups/signals/signals.go @@ -0,0 +1,113 @@ +package signals + +import ( + "context" + "fmt" + + "github.com/go-playground/validator/v10" + "gorm.io/gorm" + + "github.com/nuonco/nuon/services/ctl-api/internal/app" + "github.com/nuonco/nuon/services/ctl-api/internal/pkg/eventloop" +) + +const ( + TemporalNamespace string = "runners" + EventLoop string = "runner-groups" + + OperationCreated eventloop.SignalType = "created" + OperationRestart eventloop.SignalType = "restart" + OperationElectLeader eventloop.SignalType = "elect_leader" + OperationSetLeader eventloop.SignalType = "set_leader" +) + +type RequestSignal struct { + *Signal + eventloop.EventLoopRequest +} + +func NewRequestSignal(ev eventloop.EventLoopRequest, signal *Signal) RequestSignal { + return RequestSignal{ + Signal: signal, + EventLoopRequest: ev, + } +} + +type Signal struct { + Type eventloop.SignalType `validate:"required"` + + // RequestedLeaderRunnerID is used with OperationSetLeader to explicitly select a leader runner. + RequestedLeaderRunnerID string `json:"requested_leader_runner_id,omitempty"` + + eventloop.BaseSignal +} + +var _ eventloop.Signal = (*Signal)(nil) + +func (s *Signal) Validate(v *validator.Validate) error { + if err := v.Struct(s); err != nil { + return fmt.Errorf("invalid request: %w", err) + } + + return nil +} + +func (s *Signal) SignalType() eventloop.SignalType { + return s.Type +} + +func (s *Signal) Namespace() string { + return TemporalNamespace +} + +func (s *Signal) Name() string { + return string(s.Type) +} + +func (s *Signal) WorkflowName() string { + return "RunnerGroupEventLoop" +} + +func (s *Signal) WorkflowID(id string) string { + return "runner-group-event-loop-" + id +} + +func (s *Signal) Restart() bool { + switch s.Type { + case OperationRestart: + return true + default: + } + + return false +} + +func (s *Signal) Stop() bool { + return false +} + +func (s *Signal) Start() bool { + switch s.Type { + case OperationCreated, OperationElectLeader, OperationSetLeader: + return true + default: + } + + return false +} + +func (s *Signal) GetOrg(ctx context.Context, id string, db *gorm.DB) (*app.Org, error) { + var group app.RunnerGroup + res := db.WithContext(ctx).Select("org_id").First(&group, "id = ? AND deleted_at = 0", id) + if res.Error != nil { + return nil, fmt.Errorf("unable to get runner group: %w", res.Error) + } + + var org app.Org + res = db.WithContext(ctx).First(&org, "id = ?", group.OrgID) + if res.Error != nil { + return nil, fmt.Errorf("unable to get org: %w", res.Error) + } + + return &org, nil +} diff --git a/services/ctl-api/internal/app/runner_groups/worker/cron_ensure_leader.go b/services/ctl-api/internal/app/runner_groups/worker/cron_ensure_leader.go new file mode 100644 index 0000000000..104a29fa4a --- /dev/null +++ b/services/ctl-api/internal/app/runner_groups/worker/cron_ensure_leader.go @@ -0,0 +1,82 @@ +package worker + +import ( + "fmt" + + enumsv1 "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/workflow" + "go.uber.org/zap" + + runnersactivities "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/worker/activities" + "github.com/nuonco/nuon/services/ctl-api/internal/pkg/log" +) + +const ensureLeaderCronTab = "* * * * *" + +func ensureLeaderWorkflowID(groupID string) string { + return fmt.Sprintf("ensure-leader-%s", groupID) +} + +func (w *Workflows) startEnsureLeaderWorkflow(ctx workflow.Context, groupID string) { + cwo := workflow.ChildWorkflowOptions{ + WorkflowID: ensureLeaderWorkflowID(groupID), + CronSchedule: ensureLeaderCronTab, + WorkflowIDReusePolicy: enumsv1.WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING, + ParentClosePolicy: enumsv1.PARENT_CLOSE_POLICY_TERMINATE, + } + ctx = workflow.WithChildOptions(ctx, cwo) + + workflow.ExecuteChildWorkflow(ctx, w.EnsureLeader, &EnsureLeaderRequest{ + RunnerGroupID: groupID, + }) +} + +type EnsureLeaderRequest struct { + RunnerGroupID string `validate:"required" json:"runner_group_id"` +} + +func (w *Workflows) EnsureLeader(ctx workflow.Context, req *EnsureLeaderRequest) error { + l, err := log.WorkflowLogger(ctx) + if err != nil { + return err + } + + resp, err := runnersactivities.AwaitGetGroupLeader(ctx, runnersactivities.GetGroupLeaderRequest{ + RunnerGroupID: req.RunnerGroupID, + }) + if err != nil { + l.Warn("unable to check group leader", + zap.String("runner_group_id", req.RunnerGroupID), + zap.Error(err), + ) + return nil + } + + if resp.LeaderRunnerID != nil { + return nil + } + + l.Info("no leader found for runner group, triggering election", + zap.String("runner_group_id", req.RunnerGroupID), + ) + + result, err := runnersactivities.AwaitElectLeader(ctx, runnersactivities.ElectLeaderRequest{ + RunnerGroupID: req.RunnerGroupID, + }) + if err != nil { + l.Error("ensure-leader election failed", + zap.String("runner_group_id", req.RunnerGroupID), + zap.Error(err), + ) + return nil + } + + if result.NewLeaderID != "" { + l.Info("ensure-leader elected new leader", + zap.String("runner_group_id", req.RunnerGroupID), + zap.String("new_leader_id", result.NewLeaderID), + ) + } + + return nil +} diff --git a/services/ctl-api/internal/app/runner_groups/worker/event_loop_workflow.go b/services/ctl-api/internal/app/runner_groups/worker/event_loop_workflow.go new file mode 100644 index 0000000000..7929f8903c --- /dev/null +++ b/services/ctl-api/internal/app/runner_groups/worker/event_loop_workflow.go @@ -0,0 +1,106 @@ +package worker + +import ( + "go.temporal.io/sdk/workflow" + "go.uber.org/zap" + + "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/signals" + runnersactivities "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/worker/activities" + "github.com/nuonco/nuon/services/ctl-api/internal/pkg/eventloop" + "github.com/nuonco/nuon/services/ctl-api/internal/pkg/eventloop/loop" + "github.com/nuonco/nuon/services/ctl-api/internal/pkg/log" +) + +func (w *Workflows) RunnerGroupEventLoop(ctx workflow.Context, req eventloop.EventLoopRequest, pendingSignals []*signals.Signal) error { + handlers := map[eventloop.SignalType]func(workflow.Context, signals.RequestSignal) error{ + signals.OperationElectLeader: w.handleElectLeader, + signals.OperationSetLeader: w.handleSetLeader, + } + + l := loop.Loop[*signals.Signal, signals.RequestSignal]{ + Cfg: w.cfg, + V: w.v, + MW: w.mw, + Handlers: handlers, + NewRequestSignal: signals.NewRequestSignal, + ExistsHook: func(ctx workflow.Context, req eventloop.EventLoopRequest) (bool, error) { + return true, nil + }, + StartupHook: func(ctx workflow.Context, req eventloop.EventLoopRequest) error { + w.startEnsureLeaderWorkflow(ctx, req.ID) + return nil + }, + } + + return l.Run(ctx, req, pendingSignals) +} + +func (w *Workflows) handleSetLeader(ctx workflow.Context, req signals.RequestSignal) error { + l, err := log.WorkflowLogger(ctx) + if err != nil { + return err + } + + result, err := runnersactivities.AwaitSetLeader(ctx, runnersactivities.SetLeaderRequest{ + RunnerGroupID: req.ID, + RunnerID: req.RequestedLeaderRunnerID, + }) + if err != nil { + l.Error("set leader failed", + zap.String("runner_group_id", req.ID), + zap.String("requested_runner_id", req.RequestedLeaderRunnerID), + zap.Error(err), + ) + return err + } + + // If leadership changed, reschedule queued jobs from old to new leader. + if result.NewLeaderID != "" && result.OldLeaderID != "" && result.OldLeaderID != result.NewLeaderID { + if _, err := runnersactivities.AwaitRescheduleJobsToLeader(ctx, runnersactivities.RescheduleJobsToLeaderRequest{ + OldLeaderRunnerID: result.OldLeaderID, + NewLeaderRunnerID: result.NewLeaderID, + }); err != nil { + l.Error("unable to reschedule jobs to new leader", + zap.String("old_leader", result.OldLeaderID), + zap.String("new_leader", result.NewLeaderID), + zap.Error(err), + ) + } + } + + return nil +} + +func (w *Workflows) handleElectLeader(ctx workflow.Context, req signals.RequestSignal) error { + l, err := log.WorkflowLogger(ctx) + if err != nil { + return err + } + + result, err := runnersactivities.AwaitElectLeader(ctx, runnersactivities.ElectLeaderRequest{ + RunnerGroupID: req.ID, + }) + if err != nil { + l.Error("leader election failed", + zap.String("runner_group_id", req.ID), + zap.Error(err), + ) + return err + } + + // If leadership changed, reschedule queued jobs from old to new leader. + if result.NewLeaderID != "" && result.OldLeaderID != "" && result.OldLeaderID != result.NewLeaderID { + if _, err := runnersactivities.AwaitRescheduleJobsToLeader(ctx, runnersactivities.RescheduleJobsToLeaderRequest{ + OldLeaderRunnerID: result.OldLeaderID, + NewLeaderRunnerID: result.NewLeaderID, + }); err != nil { + l.Error("unable to reschedule jobs to new leader", + zap.String("old_leader", result.OldLeaderID), + zap.String("new_leader", result.NewLeaderID), + zap.Error(err), + ) + } + } + + return nil +} diff --git a/services/ctl-api/internal/app/runner_groups/worker/workflows.go b/services/ctl-api/internal/app/runner_groups/worker/workflows.go new file mode 100644 index 0000000000..35a0e39b91 --- /dev/null +++ b/services/ctl-api/internal/app/runner_groups/worker/workflows.go @@ -0,0 +1,55 @@ +package worker + +import ( + "fmt" + + "github.com/go-playground/validator/v10" + "go.uber.org/fx" + + "github.com/nuonco/nuon/pkg/metrics" + tmetrics "github.com/nuonco/nuon/pkg/temporal/metrics" + "github.com/nuonco/nuon/services/ctl-api/internal" + "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/signals" + teventloop "github.com/nuonco/nuon/services/ctl-api/internal/pkg/eventloop/temporal" +) + +type Workflows struct { + cfg *internal.Config + v *validator.Validate + mw tmetrics.Writer + evClient teventloop.Client +} + +type WorkflowParams struct { + fx.In + + V *validator.Validate + Cfg *internal.Config + MetricsWriter metrics.Writer + EvClient teventloop.Client +} + +func (w *Workflows) All() []any { + return []any{ + w.RunnerGroupEventLoop, + w.EnsureLeader, + } +} + +func NewWorkflows(params WorkflowParams) (*Workflows, error) { + tmw, err := tmetrics.New(params.V, + tmetrics.WithMetricsWriter(params.MetricsWriter), + tmetrics.WithTags(map[string]string{ + "namespace": signals.TemporalNamespace, + "context": "worker", + })) + if err != nil { + return nil, fmt.Errorf("unable to create temporal metrics writer: %w", err) + } + return &Workflows{ + cfg: params.Cfg, + v: params.V, + mw: tmw, + evClient: params.EvClient, + }, nil +} diff --git a/services/ctl-api/internal/app/runners/worker/activities/elect_leader.go b/services/ctl-api/internal/app/runners/worker/activities/elect_leader.go new file mode 100644 index 0000000000..461fbea31c --- /dev/null +++ b/services/ctl-api/internal/app/runners/worker/activities/elect_leader.go @@ -0,0 +1,29 @@ +package activities + +import ( + "context" + "fmt" +) + +type ElectLeaderRequest struct { + RunnerGroupID string `validate:"required"` +} + +type ElectLeaderResponse struct { + OldLeaderID string + NewLeaderID string +} + +// @temporal-gen activity +// @by-id RunnerGroupID +func (a *Activities) ElectLeader(ctx context.Context, req ElectLeaderRequest) (*ElectLeaderResponse, error) { + result, err := a.helpers.ElectLeader(ctx, req.RunnerGroupID) + if err != nil { + return nil, fmt.Errorf("unable to elect leader: %w", err) + } + + return &ElectLeaderResponse{ + OldLeaderID: result.OldLeaderID, + NewLeaderID: result.NewLeaderID, + }, nil +} diff --git a/services/ctl-api/internal/app/runners/worker/activities/get_group_leader.go b/services/ctl-api/internal/app/runners/worker/activities/get_group_leader.go new file mode 100644 index 0000000000..426f16fa80 --- /dev/null +++ b/services/ctl-api/internal/app/runners/worker/activities/get_group_leader.go @@ -0,0 +1,36 @@ +package activities + +import ( + "context" + "fmt" + + "github.com/nuonco/nuon/services/ctl-api/internal/app" + "gorm.io/gorm" +) + +type GetGroupLeaderRequest struct { + RunnerGroupID string `validate:"required"` +} + +type GetGroupLeaderResponse struct { + LeaderRunnerID *string +} + +// @temporal-gen activity +// @by-id RunnerGroupID +func (a *Activities) GetGroupLeader(ctx context.Context, req GetGroupLeaderRequest) (*GetGroupLeaderResponse, error) { + var leader app.Runner + err := a.db.WithContext(ctx). + Where("runner_group_id = ? AND leader = true AND deleted_at = 0", req.RunnerGroupID). + First(&leader).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return &GetGroupLeaderResponse{LeaderRunnerID: nil}, nil + } + return nil, fmt.Errorf("unable to get group leader: %w", err) + } + + return &GetGroupLeaderResponse{ + LeaderRunnerID: &leader.ID, + }, nil +} diff --git a/services/ctl-api/internal/app/runners/worker/activities/reschedule_jobs_to_leader.go b/services/ctl-api/internal/app/runners/worker/activities/reschedule_jobs_to_leader.go new file mode 100644 index 0000000000..5325f7a05c --- /dev/null +++ b/services/ctl-api/internal/app/runners/worker/activities/reschedule_jobs_to_leader.go @@ -0,0 +1,50 @@ +package activities + +import ( + "context" + "fmt" + + "github.com/nuonco/nuon/services/ctl-api/internal/app" + "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/signals" +) + +type RescheduleJobsToLeaderRequest struct { + OldLeaderRunnerID string `validate:"required"` + NewLeaderRunnerID string `validate:"required"` +} + +type RescheduleJobsToLeaderResponse struct { + RescheduledCount int +} + +// @temporal-gen activity +// @by-id NewLeaderRunnerID +func (a *Activities) RescheduleJobsToLeader(ctx context.Context, req RescheduleJobsToLeaderRequest) (*RescheduleJobsToLeaderResponse, error) { + // Batch-update all queued jobs from old leader to new leader in one query. + res := a.db.WithContext(ctx). + Model(&app.RunnerJob{}). + Where("runner_id = ? AND status = ? AND deleted_at = 0", req.OldLeaderRunnerID, app.RunnerJobStatusQueued). + Update("runner_id", req.NewLeaderRunnerID) + if res.Error != nil { + return nil, fmt.Errorf("unable to reschedule jobs to new leader: %w", res.Error) + } + + // Always signal queued jobs on the new leader to ensure idempotency on retry. + // Redundant signals are safe; missing signals leave jobs stuck. + var jobs []app.RunnerJob + if err := a.db.WithContext(ctx). + Select("id"). + Where("runner_id = ? AND status = ? AND deleted_at = 0", req.NewLeaderRunnerID, app.RunnerJobStatusQueued). + Find(&jobs).Error; err != nil { + return nil, fmt.Errorf("unable to fetch queued jobs on new leader: %w", err) + } + + for _, job := range jobs { + a.evClient.Send(ctx, req.NewLeaderRunnerID, &signals.Signal{ + Type: signals.OperationProcessJob, + JobID: job.ID, + }) + } + + return &RescheduleJobsToLeaderResponse{RescheduledCount: int(res.RowsAffected)}, nil +} diff --git a/services/ctl-api/internal/app/runners/worker/activities/retarget_job_to_leader.go b/services/ctl-api/internal/app/runners/worker/activities/retarget_job_to_leader.go new file mode 100644 index 0000000000..754748df20 --- /dev/null +++ b/services/ctl-api/internal/app/runners/worker/activities/retarget_job_to_leader.go @@ -0,0 +1,64 @@ +package activities + +import ( + "context" + "fmt" + + "github.com/nuonco/nuon/services/ctl-api/internal/app" + "gorm.io/gorm" +) + +type RetargetJobToLeaderRequest struct { + JobID string `validate:"required"` + RunnerID string `validate:"required"` +} + +type RetargetJobToLeaderResponse struct { + NoLeader bool + Retargeted bool + LeaderRunnerID string +} + +// @temporal-gen activity +// @by-id RunnerID +func (a *Activities) RetargetJobToLeader(ctx context.Context, req RetargetJobToLeaderRequest) (*RetargetJobToLeaderResponse, error) { + resp := &RetargetJobToLeaderResponse{} + + err := a.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // Look up the runner. + var runner app.Runner + if res := tx.First(&runner, "id = ? AND deleted_at = 0", req.RunnerID); res.Error != nil { + return fmt.Errorf("unable to get runner: %w", res.Error) + } + + // Find the leader directly instead of loading all runners in the group. + var leader app.Runner + err := tx.Where("runner_group_id = ? AND leader = true AND deleted_at = 0", runner.RunnerGroupID). + First(&leader).Error + if err != nil { + resp.NoLeader = true + return nil + } + + // This runner is already the leader. + if leader.ID == req.RunnerID { + return nil + } + + // Retarget the job to the leader runner. + if res := tx.Model(&app.RunnerJob{}). + Where("id = ? AND deleted_at = 0", req.JobID). + Update("runner_id", leader.ID); res.Error != nil { + return fmt.Errorf("unable to retarget job to leader: %w", res.Error) + } + + resp.Retargeted = true + resp.LeaderRunnerID = leader.ID + return nil + }) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/services/ctl-api/internal/app/runners/worker/activities/set_leader.go b/services/ctl-api/internal/app/runners/worker/activities/set_leader.go new file mode 100644 index 0000000000..737fd1cb5c --- /dev/null +++ b/services/ctl-api/internal/app/runners/worker/activities/set_leader.go @@ -0,0 +1,30 @@ +package activities + +import ( + "context" + "fmt" +) + +type SetLeaderRequest struct { + RunnerGroupID string `validate:"required"` + RunnerID string `validate:"required"` +} + +type SetLeaderResponse struct { + OldLeaderID string + NewLeaderID string +} + +// @temporal-gen activity +// @by-id RunnerGroupID +func (a *Activities) SetLeader(ctx context.Context, req SetLeaderRequest) (*SetLeaderResponse, error) { + result, err := a.helpers.SetLeader(ctx, req.RunnerGroupID, req.RunnerID) + if err != nil { + return nil, fmt.Errorf("unable to set leader: %w", err) + } + + return &SetLeaderResponse{ + OldLeaderID: result.OldLeaderID, + NewLeaderID: result.NewLeaderID, + }, nil +} diff --git a/services/ctl-api/internal/app/runners/worker/cron_health_check.go b/services/ctl-api/internal/app/runners/worker/cron_health_check.go index 6c03731cc5..bed796b045 100644 --- a/services/ctl-api/internal/app/runners/worker/cron_health_check.go +++ b/services/ctl-api/internal/app/runners/worker/cron_health_check.go @@ -15,6 +15,7 @@ import ( "github.com/nuonco/nuon/pkg/generics" "github.com/nuonco/nuon/pkg/metrics" "github.com/nuonco/nuon/services/ctl-api/internal/app" + runnergroupssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/signals" "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/worker/activities" "github.com/nuonco/nuon/services/ctl-api/internal/pkg/log" ) @@ -174,6 +175,30 @@ func (w *Workflows) executeHealthCheck(ctx workflow.Context, runnerID string) (a }); err != nil { return app.RunnerStatusUnknown, false, errors.Wrap(err, "unable to update runner status") } + + // Leader election: if runner became unhealthy and is the current leader, elect a new one. + // If runner became active and the group has no leader, elect it. + leaderBecameUnhealthy := newStatus != app.RunnerStatusActive && runner.Leader + groupLeader, _ := activities.AwaitGetGroupLeader(ctx, activities.GetGroupLeaderRequest{ + RunnerGroupID: runner.RunnerGroupID, + }) + groupHasLeader := groupLeader != nil && groupLeader.LeaderRunnerID != nil + runnerBecameActiveWithNoLeader := newStatus == app.RunnerStatusActive && !groupHasLeader + + if leaderBecameUnhealthy || runnerBecameActiveWithNoLeader { + if leaderBecameUnhealthy { + l.Info("current leader became unhealthy, triggering leader election", + zap.String("runner_group_id", runner.RunnerGroupID), + ) + } else { + l.Info("runner became active with no group leader, triggering leader election", + zap.String("runner_group_id", runner.RunnerGroupID), + ) + } + w.evClient.Send(ctx, runner.RunnerGroupID, &runnergroupssignals.Signal{ + Type: runnergroupssignals.OperationElectLeader, + }) + } } return newStatus, isChanged, nil diff --git a/services/ctl-api/internal/app/runners/worker/offline_check.go b/services/ctl-api/internal/app/runners/worker/offline_check.go index b872b19b8d..9ccc67256f 100644 --- a/services/ctl-api/internal/app/runners/worker/offline_check.go +++ b/services/ctl-api/internal/app/runners/worker/offline_check.go @@ -12,6 +12,7 @@ import ( "github.com/nuonco/nuon/pkg/generics" "github.com/nuonco/nuon/pkg/metrics" "github.com/nuonco/nuon/services/ctl-api/internal/app" + runnergroupssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/signals" "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/signals" "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/worker/activities" "github.com/nuonco/nuon/services/ctl-api/internal/pkg/log" @@ -107,6 +108,16 @@ func (w *Workflows) checkOffline(ctx workflow.Context, runnerID string) error { }); err != nil { return errors.Wrap(err, "unable to update runner status") } + + // If the runner going offline was the group leader, trigger election. + if runner.Leader { + l.Info("offline leader detected, triggering leader election", + zap.String("runner_group_id", runner.RunnerGroupID), + ) + w.evClient.Send(ctx, runner.RunnerGroupID, &runnergroupssignals.Signal{ + Type: runnergroupssignals.OperationElectLeader, + }) + } } return nil diff --git a/services/ctl-api/internal/app/runners/worker/process_job.go b/services/ctl-api/internal/app/runners/worker/process_job.go index 7dd218e6bf..424b3e3a08 100644 --- a/services/ctl-api/internal/app/runners/worker/process_job.go +++ b/services/ctl-api/internal/app/runners/worker/process_job.go @@ -39,6 +39,30 @@ func (w *Workflows) ProcessJob(ctx workflow.Context, sreq signals.RequestSignal) return errors.New("runner is not healthy") } + // Leader election check: if this runner is already the leader, skip the + // retarget activity entirely. The Get activity above preloads + // RunnerGroup.Runners, so we can check locally. + if !runner.Leader { + retargetResp, err := activities.AwaitRetargetJobToLeader(ctx, activities.RetargetJobToLeaderRequest{ + JobID: sreq.JobID, + RunnerID: sreq.ID, + }) + if err != nil { + return fmt.Errorf("unable to check leader status: %w", err) + } + if retargetResp.NoLeader { + l.Warn("no leader set for runner group, marking job as not attempted") + w.updateJobStatus(ctx, sreq.JobID, app.RunnerJobStatusNotAttempted, "no leader runner available for group") + return nil + } + if retargetResp.Retargeted { + l.Info("job retargeted to group leader", + zap.String("leader_runner_id", retargetResp.LeaderRunnerID), + ) + return nil + } + } + runnerJob, err := activities.AwaitGetJob(ctx, activities.GetJobRequest{ ID: sreq.JobID, }) diff --git a/services/ctl-api/internal/pkg/workflows/signals/activities/send.go b/services/ctl-api/internal/pkg/workflows/signals/activities/send.go index 5a5ffb799c..b82233345b 100644 --- a/services/ctl-api/internal/pkg/workflows/signals/activities/send.go +++ b/services/ctl-api/internal/pkg/workflows/signals/activities/send.go @@ -10,6 +10,7 @@ import ( installssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/installs/signals" orgssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/orgs/signals" releasessignals "github.com/nuonco/nuon/services/ctl-api/internal/app/releases/signals" + runnergroupssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/signals" runnersignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/signals" ) @@ -25,6 +26,12 @@ func (a *Activities) PkgSignalsSendRunnersSignal(ctx context.Context, req *SendS return nil } +// @temporal-gen activity +func (a *Activities) PkgSignalsSendRunnerGroupsSignal(ctx context.Context, req *SendSignalRequest[*runnergroupssignals.Signal]) error { + a.evClient.Send(ctx, req.ID, req.Signal) + return nil +} + // @temporal-gen activity func (a *Activities) PkgSignalsSendComponentsSignal(ctx context.Context, req *SendSignalRequest[*componentssignals.Signal]) error { a.evClient.Send(ctx, req.ID, req.Signal) From c11686388d5fa6da3cdb3a953866b9efd15b95f6 Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 19 Feb 2026 16:45:33 +0530 Subject: [PATCH 4/6] refactor: merge runner-groups into runners Temporal namespace --- .../app/runner_groups/worker/worker.go | 1 + .../internal/app/runners/worker/worker.go | 6 +++ .../internal/fxmodules/workers_namespaces.go | 3 ++ .../internal/pkg/eventloop/temporal/send.go | 45 +++++++++++-------- 4 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 services/ctl-api/internal/app/runner_groups/worker/worker.go diff --git a/services/ctl-api/internal/app/runner_groups/worker/worker.go b/services/ctl-api/internal/app/runner_groups/worker/worker.go new file mode 100644 index 0000000000..4df0094f40 --- /dev/null +++ b/services/ctl-api/internal/app/runner_groups/worker/worker.go @@ -0,0 +1 @@ +package worker diff --git a/services/ctl-api/internal/app/runners/worker/worker.go b/services/ctl-api/internal/app/runners/worker/worker.go index 361398d7ba..c0565cf171 100644 --- a/services/ctl-api/internal/app/runners/worker/worker.go +++ b/services/ctl-api/internal/app/runners/worker/worker.go @@ -14,6 +14,7 @@ import ( temporalclient "github.com/nuonco/nuon/pkg/temporal/client" pkgworkflows "github.com/nuonco/nuon/pkg/workflows" "github.com/nuonco/nuon/services/ctl-api/internal" + runnergroupsworker "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/worker" "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/signals" "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/worker/activities" runner "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/worker/kuberunner" @@ -35,6 +36,8 @@ type WorkerParams struct { L *zap.Logger Lc fx.Lifecycle + RunnerGroupsWkflows *runnergroupsworker.Workflows + SharedActivities *workflows.Activities SharedWorkflows *workflows.Workflows Interceptors []interceptor.WorkerInterceptor `group:"interceptors"` @@ -69,6 +72,9 @@ func New(params WorkerParams) (*Worker, error) { for _, wkflow := range params.Wkflows.All() { wkr.RegisterWorkflow(wkflow) } + for _, wkflow := range params.RunnerGroupsWkflows.All() { + wkr.RegisterWorkflow(wkflow) + } for _, wkflow := range params.SharedWorkflows.AllWorkflows() { wkr.RegisterWorkflow(wkflow) } diff --git a/services/ctl-api/internal/fxmodules/workers_namespaces.go b/services/ctl-api/internal/fxmodules/workers_namespaces.go index a930f54b31..3d562f4f8b 100644 --- a/services/ctl-api/internal/fxmodules/workers_namespaces.go +++ b/services/ctl-api/internal/fxmodules/workers_namespaces.go @@ -25,6 +25,7 @@ import ( orgsactivities "github.com/nuonco/nuon/services/ctl-api/internal/app/orgs/worker/activities" releasesworker "github.com/nuonco/nuon/services/ctl-api/internal/app/releases/worker" releasesactivities "github.com/nuonco/nuon/services/ctl-api/internal/app/releases/worker/activities" + runnergroupsworker "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/worker" runnersworker "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/worker" runnersactivities "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/worker/activities" ) @@ -84,9 +85,11 @@ var ReleasesWorkerModule = fx.Module("worker-releases", ) // RunnersWorkerModule provides the runners namespace worker. +// Runner-groups workflows are also registered here (they share the runners namespace). var RunnersWorkerModule = fx.Module("worker-runners", fx.Provide(runnersactivities.New), fx.Provide(runnersworker.NewWorkflows), + fx.Provide(runnergroupsworker.NewWorkflows), fx.Provide(worker.AsWorker(runnersworker.New)), ) diff --git a/services/ctl-api/internal/pkg/eventloop/temporal/send.go b/services/ctl-api/internal/pkg/eventloop/temporal/send.go index 76e06ad1b2..cae6a6f4a1 100644 --- a/services/ctl-api/internal/pkg/eventloop/temporal/send.go +++ b/services/ctl-api/internal/pkg/eventloop/temporal/send.go @@ -22,6 +22,7 @@ import ( installssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/installs/signals" orgssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/orgs/signals" releasessignals "github.com/nuonco/nuon/services/ctl-api/internal/app/releases/signals" + runnergroupssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runner_groups/signals" runnerssignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/signals" signalsactivities "github.com/nuonco/nuon/services/ctl-api/internal/pkg/workflows/signals/activities" ) @@ -37,49 +38,57 @@ func (e *evClient) Send(ctx workflow.Context, id string, signal eventloop.Signal return } - switch signal.Namespace() { - case actionssignals.TemporalNamespace: + // Dispatch by concrete signal type. We use type switches instead of + // namespace strings because runner-groups signals share the runners + // Temporal namespace. + switch s := signal.(type) { + case *actionssignals.Signal: signalsactivities.AwaitPkgSignalsSendActionsSignal(ctx, &signalsactivities.SendSignalRequest[*actionssignals.Signal]{ ID: id, - Signal: signal.(*actionssignals.Signal), + Signal: s, }) - case appssignals.TemporalNamespace: + case *appssignals.Signal: signalsactivities.AwaitPkgSignalsSendAppsSignal(ctx, &signalsactivities.SendSignalRequest[*appssignals.Signal]{ ID: id, - Signal: signal.(*appssignals.Signal), + Signal: s, }) - case installssignals.TemporalNamespace: + case *installssignals.Signal: signalsactivities.AwaitPkgSignalsSendInstallsSignal(ctx, &signalsactivities.SendSignalRequest[*installssignals.Signal]{ ID: id, - Signal: signal.(*installssignals.Signal), + Signal: s, }) - case componentssignals.TemporalNamespace: + case *componentssignals.Signal: signalsactivities.AwaitPkgSignalsSendComponentsSignal(ctx, &signalsactivities.SendSignalRequest[*componentssignals.Signal]{ ID: id, - Signal: signal.(*componentssignals.Signal), + Signal: s, }) - case orgssignals.TemporalNamespace: + case *orgssignals.Signal: signalsactivities.AwaitPkgSignalsSendOrgsSignal(ctx, &signalsactivities.SendSignalRequest[*orgssignals.Signal]{ ID: id, - Signal: signal.(*orgssignals.Signal), + Signal: s, }) - case releasessignals.TemporalNamespace: + case *releasessignals.Signal: signalsactivities.AwaitPkgSignalsSendReleasesSignal(ctx, &signalsactivities.SendSignalRequest[*releasessignals.Signal]{ ID: id, - Signal: signal.(*releasessignals.Signal), + Signal: s, }) - case runnerssignals.TemporalNamespace: + case *runnerssignals.Signal: signalsactivities.AwaitPkgSignalsSendRunnersSignal(ctx, &signalsactivities.SendSignalRequest[*runnerssignals.Signal]{ ID: id, - Signal: signal.(*runnerssignals.Signal), + Signal: s, }) - case generalsignals.TemporalNamespace: + case *runnergroupssignals.Signal: + signalsactivities.AwaitPkgSignalsSendRunnerGroupsSignal(ctx, &signalsactivities.SendSignalRequest[*runnergroupssignals.Signal]{ + ID: id, + Signal: s, + }) + case *generalsignals.Signal: signalsactivities.AwaitPkgSignalsSendGeneralSignal(ctx, &signalsactivities.SendSignalRequest[*generalsignals.Signal]{ ID: id, - Signal: signal.(*generalsignals.Signal), + Signal: s, }) default: - err = errors.New("unsupported namespace " + signal.Namespace()) + err = errors.New("unsupported signal type for namespace " + signal.Namespace()) } if err != nil { From 206d356e9fbc057193a919bca70b9b5128ca32cb Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 19 Feb 2026 16:45:47 +0530 Subject: [PATCH 5/6] feat: update install/org/component workers for leader-aware job routing --- .../app/components/worker/exec_build.go | 5 ++-- .../components/shared_execute_deploy.go | 8 +++++- .../app/installs/worker/deprovision_runner.go | 16 ++++++++---- .../app/installs/worker/provision_runner.go | 12 ++++++--- .../app/installs/worker/reprovision_runner.go | 12 ++++++--- .../stack/generate_install_stack_version.go | 17 +++++++++--- .../worker/stack/install_stack_version_run.go | 26 +++++++++++++------ .../internal/app/orgs/worker/delete.go | 8 +++++- .../internal/app/orgs/worker/deprovision.go | 5 ++-- .../internal/app/orgs/worker/reprovision.go | 10 +++++-- .../app/runners/worker/exec_provision.go | 2 +- .../app/runners/worker/update_version.go | 12 ++++++--- 12 files changed, 98 insertions(+), 35 deletions(-) diff --git a/services/ctl-api/internal/app/components/worker/exec_build.go b/services/ctl-api/internal/app/components/worker/exec_build.go index 24d6be8e1f..6bd797ddbb 100644 --- a/services/ctl-api/internal/app/components/worker/exec_build.go +++ b/services/ctl-api/internal/app/components/worker/exec_build.go @@ -25,11 +25,12 @@ func (w *Workflows) execBuild(ctx workflow.Context, compID, buildID string, curr return fmt.Errorf("unable to get component: %w", err) } - if len(comp.Org.RunnerGroup.Runners) == 0 { + leader := comp.Org.RunnerGroup.ActiveRunner() + if leader == nil { w.updateBuildStatus(ctx, buildID, app.ComponentBuildStatusError, "no runners available in runner group") return fmt.Errorf("no runners available in runner group for org %s", comp.Org.ID) } - runnerID := comp.Org.RunnerGroup.Runners[0].ID + runnerID := leader.ID logStreamID, err := cctx.GetLogStreamIDWorkflow(ctx) if err != nil { diff --git a/services/ctl-api/internal/app/installs/worker/components/shared_execute_deploy.go b/services/ctl-api/internal/app/installs/worker/components/shared_execute_deploy.go index 46fe4c7f9c..ec9dc154a0 100644 --- a/services/ctl-api/internal/app/installs/worker/components/shared_execute_deploy.go +++ b/services/ctl-api/internal/app/installs/worker/components/shared_execute_deploy.go @@ -49,8 +49,14 @@ func (w *Workflows) execPlan(ctx workflow.Context, install *app.Install, install jobTyp := build.ComponentConfigConnection.Type.DeployPlanJobType() + runner := install.RunnerGroup.ActiveRunner() + if runner == nil { + w.updateDeployStatusWithoutStatusSync(ctx, installDeploy.ID, app.InstallDeployStatusError, "no runners available in runner group") + return fmt.Errorf("no runners available in runner group for install %s", install.ID) + } + runnerJob, err := activities.AwaitCreateDeployJob(ctx, &activities.CreateDeployJobRequest{ - RunnerID: install.RunnerGroup.Runners[0].ID, + RunnerID: runner.ID, DeployID: installDeploy.ID, Op: op, Type: jobTyp, diff --git a/services/ctl-api/internal/app/installs/worker/deprovision_runner.go b/services/ctl-api/internal/app/installs/worker/deprovision_runner.go index 19778b1a72..530c8d1353 100644 --- a/services/ctl-api/internal/app/installs/worker/deprovision_runner.go +++ b/services/ctl-api/internal/app/installs/worker/deprovision_runner.go @@ -5,6 +5,7 @@ import ( "go.temporal.io/sdk/workflow" + "github.com/nuonco/nuon/services/ctl-api/internal/app" "github.com/nuonco/nuon/services/ctl-api/internal/app/installs/signals" "github.com/nuonco/nuon/services/ctl-api/internal/app/installs/worker/activities" runnersignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/signals" @@ -23,11 +24,16 @@ func (w *Workflows) DeprovisionRunner(ctx workflow.Context, sreq signals.Request return fmt.Errorf("unable to get install: %w", err) } - w.evClient.Send(ctx, install.RunnerGroup.Runners[0].ID, &runnersignals.Signal{ - Type: runnersignals.OperationDeprovision, - }) - if err := w.pollRunner(ctx, install.RunnerGroup.Runners[0].ID); err != nil { - return err + for _, runner := range install.RunnerGroup.Runners { + if runner.Platform == app.AppRunnerTypeLocal { + continue + } + w.evClient.Send(ctx, runner.ID, &runnersignals.Signal{ + Type: runnersignals.OperationDeprovision, + }) + if err := w.pollRunner(ctx, runner.ID); err != nil { + return err + } } return nil diff --git a/services/ctl-api/internal/app/installs/worker/provision_runner.go b/services/ctl-api/internal/app/installs/worker/provision_runner.go index 735079bd31..457e57a1bc 100644 --- a/services/ctl-api/internal/app/installs/worker/provision_runner.go +++ b/services/ctl-api/internal/app/installs/worker/provision_runner.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" + "github.com/nuonco/nuon/services/ctl-api/internal/app" "github.com/nuonco/nuon/services/ctl-api/internal/app/installs/signals" "github.com/nuonco/nuon/services/ctl-api/internal/app/installs/worker/activities" runnersignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/signals" @@ -25,9 +26,14 @@ func (w *Workflows) ProvisionRunner(ctx workflow.Context, sreq signals.RequestSi return errors.Wrap(err, "unable to get install") } - w.evClient.Send(ctx, install.RunnerID, &runnersignals.Signal{ - Type: runnersignals.OperationProvisionServiceAccount, - }) + for _, runner := range install.RunnerGroup.Runners { + if runner.Platform == app.AppRunnerTypeLocal { + continue + } + w.evClient.Send(ctx, runner.ID, &runnersignals.Signal{ + Type: runnersignals.OperationProvisionServiceAccount, + }) + } return nil } diff --git a/services/ctl-api/internal/app/installs/worker/reprovision_runner.go b/services/ctl-api/internal/app/installs/worker/reprovision_runner.go index ff544e974d..2c8b2426df 100644 --- a/services/ctl-api/internal/app/installs/worker/reprovision_runner.go +++ b/services/ctl-api/internal/app/installs/worker/reprovision_runner.go @@ -5,6 +5,7 @@ import ( "go.temporal.io/sdk/workflow" + "github.com/nuonco/nuon/services/ctl-api/internal/app" "github.com/nuonco/nuon/services/ctl-api/internal/app/installs/signals" "github.com/nuonco/nuon/services/ctl-api/internal/app/installs/worker/activities" runnersignals "github.com/nuonco/nuon/services/ctl-api/internal/app/runners/signals" @@ -23,9 +24,14 @@ func (w *Workflows) ReprovisionRunner(ctx workflow.Context, sreq signals.Request return fmt.Errorf("unable to get install: %w", err) } - w.evClient.Send(ctx, install.RunnerID, &runnersignals.Signal{ - Type: runnersignals.OperationReprovisionServiceAccount, - }) + for _, runner := range install.RunnerGroup.Runners { + if runner.Platform == app.AppRunnerTypeLocal { + continue + } + w.evClient.Send(ctx, runner.ID, &runnersignals.Signal{ + Type: runnersignals.OperationReprovisionServiceAccount, + }) + } return nil } diff --git a/services/ctl-api/internal/app/installs/worker/stack/generate_install_stack_version.go b/services/ctl-api/internal/app/installs/worker/stack/generate_install_stack_version.go index 5ab130737e..5fc8df4e87 100644 --- a/services/ctl-api/internal/app/installs/worker/stack/generate_install_stack_version.go +++ b/services/ctl-api/internal/app/installs/worker/stack/generate_install_stack_version.go @@ -85,9 +85,18 @@ func (w *Workflows) GenerateInstallStackVersion(ctx workflow.Context, sreq signa return errors.Wrap(err, "unable to render cloudformation stack config") } - runner, err := activities.AwaitGetRunnerByID(ctx, install.RunnerID) - if err != nil { - return errors.Wrap(err, "unable to get runner") + // Find the cloud runner matching the configured platform type. + // The stack template must use the cloud runner's ID (not the local leader's) + // so the provisioned VM heartbeats as the correct runner. + var runner *app.Runner + for i := range install.RunnerGroup.Runners { + if install.RunnerGroup.Runners[i].Platform == cfg.RunnerConfig.Type { + runner = &install.RunnerGroup.Runners[i] + break + } + } + if runner == nil { + return errors.Errorf("unable to find runner with platform %s in runner group", cfg.RunnerConfig.Type) } // need to generate a token @@ -117,7 +126,7 @@ func (w *Workflows) GenerateInstallStackVersion(ctx workflow.Context, sreq signa return errors.Wrap(err, "unable to update stack version") } - token, err := activities.AwaitCreateRunnerTokenRequestByRunnerID(ctx, install.RunnerID) + token, err := activities.AwaitCreateRunnerTokenRequestByRunnerID(ctx, runner.ID) if err != nil { return errors.Wrap(err, "unable to create runner token") } diff --git a/services/ctl-api/internal/app/installs/worker/stack/install_stack_version_run.go b/services/ctl-api/internal/app/installs/worker/stack/install_stack_version_run.go index 4e17185687..db97e9253d 100644 --- a/services/ctl-api/internal/app/installs/worker/stack/install_stack_version_run.go +++ b/services/ctl-api/internal/app/installs/worker/stack/install_stack_version_run.go @@ -95,10 +95,15 @@ func (w *Workflows) InstallStackVersionRun(ctx workflow.Context, sreq signals.Re if err != nil { return errors.Wrap(err, "unable to create sandbox version run") } - w.evClient.Send(ctx, install.RunnerID, &runnersignals.Signal{ - Type: runnersignals.OperationInstallStackVersionRun, - InstallStackVersionRunID: run.ID, - }) + for _, runner := range install.RunnerGroup.Runners { + if runner.Platform == app.AppRunnerTypeLocal { + continue + } + w.evClient.Send(ctx, runner.ID, &runnersignals.Signal{ + Type: runnersignals.OperationInstallStackVersionRun, + InstallStackVersionRunID: run.ID, + }) + } if err := statusactivities.AwaitPkgStatusUpdateInstallStackVersionStatus(ctx, statusactivities.UpdateStatusRequest{ ID: version.ID, @@ -156,10 +161,15 @@ func (w *Workflows) InstallStackVersionRun(ctx workflow.Context, sreq signals.Re return errors.Wrap(err, "unable to get install stack run in time") } - w.evClient.Send(ctx, install.RunnerID, &runnersignals.Signal{ - Type: runnersignals.OperationInstallStackVersionRun, - InstallStackVersionRunID: run.ID, - }) + for _, runner := range install.RunnerGroup.Runners { + if runner.Platform == app.AppRunnerTypeLocal { + continue + } + w.evClient.Send(ctx, runner.ID, &runnersignals.Signal{ + Type: runnersignals.OperationInstallStackVersionRun, + InstallStackVersionRunID: run.ID, + }) + } // successfully got a run l.Debug("successfully got run", zap.Any("data", run.Data)) diff --git a/services/ctl-api/internal/app/orgs/worker/delete.go b/services/ctl-api/internal/app/orgs/worker/delete.go index 314eaef7cc..5a9e690fd7 100644 --- a/services/ctl-api/internal/app/orgs/worker/delete.go +++ b/services/ctl-api/internal/app/orgs/worker/delete.go @@ -40,7 +40,13 @@ func (w *Workflows) Delete(ctx workflow.Context, sreq signals.RequestSignal) err l.Error("unable to deprovision org, continuing anyway", zap.Error(err)) } - w.ev.Send(ctx, org.RunnerGroup.Runners[0].ID, &runnersignals.Signal{ + runner := org.RunnerGroup.ActiveRunner() + if runner == nil { + w.updateStatus(ctx, sreq.ID, app.OrgStatusError, "no runners available in runner group") + return fmt.Errorf("no runners available in runner group") + } + + w.ev.Send(ctx, runner.ID, &runnersignals.Signal{ Type: runnersignals.OperationDelete, }) err = w.pollRunnerNotFound(ctx, sreq.ID) diff --git a/services/ctl-api/internal/app/orgs/worker/deprovision.go b/services/ctl-api/internal/app/orgs/worker/deprovision.go index e9866d1ed1..50ed0e2a5e 100644 --- a/services/ctl-api/internal/app/orgs/worker/deprovision.go +++ b/services/ctl-api/internal/app/orgs/worker/deprovision.go @@ -76,11 +76,12 @@ func (w *Workflows) deprovisionOrg(ctx workflow.Context, orgID string, sandboxMo zap.String("org_name", org.Name)) } - if len(org.RunnerGroup.Runners) < 1 { + runner := org.RunnerGroup.ActiveRunner() + if runner == nil { return nil } - w.ev.Send(ctx, org.RunnerGroup.Runners[0].ID, &runnersignals.Signal{ + w.ev.Send(ctx, runner.ID, &runnersignals.Signal{ Type: runnersignals.OperationDeprovision, }) return nil diff --git a/services/ctl-api/internal/app/orgs/worker/reprovision.go b/services/ctl-api/internal/app/orgs/worker/reprovision.go index 37038a7ae7..009c62e832 100644 --- a/services/ctl-api/internal/app/orgs/worker/reprovision.go +++ b/services/ctl-api/internal/app/orgs/worker/reprovision.go @@ -62,10 +62,16 @@ func (w *Workflows) Reprovision(ctx workflow.Context, sreq signals.RequestSignal zap.String("org_name", org.Name)) } - w.ev.Send(ctx, org.RunnerGroup.Runners[0].ID, &runnersignals.Signal{ + runner := org.RunnerGroup.ActiveRunner() + if runner == nil { + w.updateStatus(ctx, sreq.ID, app.OrgStatusError, "no runners available in runner group") + return fmt.Errorf("no runners available in runner group") + } + + w.ev.Send(ctx, runner.ID, &runnersignals.Signal{ Type: runnersignals.OperationReprovision, }) - if err := w.pollRunner(ctx, org.RunnerGroup.Runners[0].ID); err != nil { + if err := w.pollRunner(ctx, runner.ID); err != nil { w.updateStatus(ctx, sreq.ID, app.OrgStatusError, "organization did not provision runner") return fmt.Errorf("runner did not reprovision correctly: %w", err) } diff --git a/services/ctl-api/internal/app/runners/worker/exec_provision.go b/services/ctl-api/internal/app/runners/worker/exec_provision.go index 50f5a83636..52eb230312 100644 --- a/services/ctl-api/internal/app/runners/worker/exec_provision.go +++ b/services/ctl-api/internal/app/runners/worker/exec_provision.go @@ -21,7 +21,7 @@ func (w *Workflows) executeProvisionOrgRunner(ctx workflow.Context, runnerID, ap return fmt.Errorf("unable to get runner: %w", err) } - if runner.RunnerGroup.Platform == app.AppRunnerTypeLocal { + if runner.Platform == app.AppRunnerTypeLocal { w.updateStatus(ctx, runnerID, app.RunnerStatusActive, "local runner must be run locally") return nil } diff --git a/services/ctl-api/internal/app/runners/worker/update_version.go b/services/ctl-api/internal/app/runners/worker/update_version.go index 0f0d5790d5..b00ff4616d 100644 --- a/services/ctl-api/internal/app/runners/worker/update_version.go +++ b/services/ctl-api/internal/app/runners/worker/update_version.go @@ -34,8 +34,14 @@ func (w *Workflows) UpdateVersion(ctx workflow.Context, sreq signals.RequestSign return errors.Wrap(err, "could not get logger") } + leader := runner.Org.RunnerGroup.ActiveRunner() + if leader == nil { + w.updateStatus(ctx, sreq.ID, app.RunnerStatusError, "no runners available in runner group") + return errors.New("no runners available in runner group") + } + runnerJob, err := activities.AwaitCreateUpdateVersionJob(ctx, &activities.CreateUpdateVersionJobRequest{ - RunnerID: runner.Org.RunnerGroup.Runners[0].ID, + RunnerID: leader.ID, OwnerID: sreq.HealthCheckID, LogStreamID: logStream.ID, }) @@ -54,7 +60,7 @@ func (w *Workflows) UpdateVersion(ctx workflow.Context, sreq signals.RequestSign } l.Info("dispatching job to update runner version", - zap.String("runner_id", runner.Org.RunnerGroup.Runners[0].ID), + zap.String("runner_id", leader.ID), zap.String("runner_type", string(runner.RunnerGroup.Type)), zap.String("expected_version", runner.RunnerGroup.Settings.ExpectedVersion), zap.String("api_version", w.cfg.Version), @@ -62,7 +68,7 @@ func (w *Workflows) UpdateVersion(ctx workflow.Context, sreq signals.RequestSign // We have to send the signal and then return to allow it to be processed. // Waiting for it to complete would deadlock. Not a big deal because // we wouldn't do anything differently even if it failed. - w.evClient.Send(ctx, runner.Org.RunnerGroup.Runners[0].ID, &signals.Signal{ + w.evClient.Send(ctx, leader.ID, &signals.Signal{ Type: signals.OperationProcessJob, JobID: runnerJob.ID, }) From 9477007c43340e14ef13bb6422920b70f66a52e1 Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 19 Feb 2026 16:45:58 +0530 Subject: [PATCH 6/6] feat: add runner leader election SDK and API client methods --- pkg/api/client.go | 4 + pkg/api/runner.go | 46 +++ sdks/nuon-go/client.go | 7 + .../get_runner_group_leader_parameters.go | 151 ++++++++ .../get_runner_group_leader_responses.go | 259 ++++++++++++++ .../client/operations/operations_client.go | 184 ++++++++++ .../operations/taint_runner_parameters.go | 151 ++++++++ .../operations/taint_runner_responses.go | 259 ++++++++++++++ .../operations/untaint_runner_parameters.go | 151 ++++++++ .../operations/untaint_runner_responses.go | 259 ++++++++++++++ .../update_runner_group_leader_parameters.go | 175 +++++++++ .../update_runner_group_leader_responses.go | 333 ++++++++++++++++++ sdks/nuon-go/models/app_runner.go | 9 + sdks/nuon-go/models/app_runner_group.go | 55 +-- ...vice_update_runner_group_leader_request.go | 50 +++ sdks/nuon-go/runner_groups.go | 43 +++ sdks/nuon-go/runners.go | 47 +++ sdks/nuon-runner-go/models/app_runner.go | 9 + .../nuon-runner-go/models/app_runner_group.go | 55 +-- 19 files changed, 2141 insertions(+), 106 deletions(-) create mode 100644 sdks/nuon-go/client/operations/get_runner_group_leader_parameters.go create mode 100644 sdks/nuon-go/client/operations/get_runner_group_leader_responses.go create mode 100644 sdks/nuon-go/client/operations/taint_runner_parameters.go create mode 100644 sdks/nuon-go/client/operations/taint_runner_responses.go create mode 100644 sdks/nuon-go/client/operations/untaint_runner_parameters.go create mode 100644 sdks/nuon-go/client/operations/untaint_runner_responses.go create mode 100644 sdks/nuon-go/client/operations/update_runner_group_leader_parameters.go create mode 100644 sdks/nuon-go/client/operations/update_runner_group_leader_responses.go create mode 100644 sdks/nuon-go/models/service_update_runner_group_leader_request.go create mode 100644 sdks/nuon-go/runner_groups.go create mode 100644 sdks/nuon-go/runners.go diff --git a/pkg/api/client.go b/pkg/api/client.go index 9674b4d08a..a843aa4c87 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -56,6 +56,10 @@ type Client interface { GetRunnerGroup(ctx context.Context, id string) (*RunnerGroup, error) GetRunnerServiceAccount(ctx context.Context, runnerID string) (*RunnerServiceAccount, error) GetRunnerServiceAccountToken(ctx context.Context, runnerID string, dur time.Duration, invalidate bool) (string, error) + + CreateRunnerInGroup(ctx context.Context, runnerGroupID string, platform string) (*CreateRunnerInGroupResponse, error) + TaintRunner(ctx context.Context, runnerID string) error + UntaintRunner(ctx context.Context, runnerID string) error } var _ Client = (*client)(nil) diff --git a/pkg/api/runner.go b/pkg/api/runner.go index 04a7a3ccd3..afc7f092f0 100644 --- a/pkg/api/runner.go +++ b/pkg/api/runner.go @@ -56,6 +56,8 @@ type Runner struct { RunnerGroupID string `json:"runner_group_id"` Name string `json:"name"` DisplayName string `json:"display_name"` + Platform string `json:"platform"` + Tainted bool `json:"tainted"` } func (c *client) ListRunners(ctx context.Context, typ string) ([]Runner, error) { @@ -122,6 +124,50 @@ func (c *client) GetRunnerGroup(ctx context.Context, id string) (*RunnerGroup, e return &resp, nil } +type CreateRunnerInGroupRequest struct { + Platform string `json:"platform"` +} + +type CreateRunnerInGroupResponse struct { + Runner Runner `json:"runner"` + Token string `json:"token"` +} + +func (c *client) CreateRunnerInGroup(ctx context.Context, runnerGroupID string, platform string) (*CreateRunnerInGroupResponse, error) { + endpoint := fmt.Sprintf("/v1/runner-groups/%s/runners", runnerGroupID) + byts, err := c.execPostRequest(ctx, endpoint, CreateRunnerInGroupRequest{ + Platform: platform, + }) + if err != nil { + return nil, fmt.Errorf("unable to create runner in group: %w", err) + } + + var resp CreateRunnerInGroupResponse + if err := json.Unmarshal(byts, &resp); err != nil { + return nil, fmt.Errorf("unable to parse response: %w", err) + } + + return &resp, nil +} + +func (c *client) TaintRunner(ctx context.Context, runnerID string) error { + endpoint := fmt.Sprintf("/v1/runners/%s/taint", runnerID) + _, err := c.execPostRequest(ctx, endpoint, map[string]interface{}{}) + if err != nil { + return fmt.Errorf("unable to taint runner: %w", err) + } + return nil +} + +func (c *client) UntaintRunner(ctx context.Context, runnerID string) error { + endpoint := fmt.Sprintf("/v1/runners/%s/untaint", runnerID) + _, err := c.execPostRequest(ctx, endpoint, map[string]interface{}{}) + if err != nil { + return fmt.Errorf("unable to untaint runner: %w", err) + } + return nil +} + func (c *client) RestartRunner(ctx context.Context, runnerID string) error { endpoint := fmt.Sprintf("/v1/runners/%s/restart", runnerID) byts, err := c.execPostRequest(ctx, endpoint, map[string]interface{}{}) diff --git a/sdks/nuon-go/client.go b/sdks/nuon-go/client.go index 91c2362731..743e1564d0 100644 --- a/sdks/nuon-go/client.go +++ b/sdks/nuon-go/client.go @@ -168,6 +168,13 @@ type Client interface { // runner job plan GetRunnerJobPlan(ctx context.Context, runnerJobID string) (string, error) + // install runner groups + GetInstallRunnerGroup(ctx context.Context, installID string) (*models.AppRunnerGroup, error) + GetRunnerGroupLeader(ctx context.Context, runnerGroupID string) (*models.AppRunner, error) + UpdateRunnerGroupLeader(ctx context.Context, runnerGroupID string, runnerID string) error + TaintRunner(ctx context.Context, runnerID string) (*models.AppRunner, error) + UntaintRunner(ctx context.Context, runnerID string) (*models.AppRunner, error) + // install stacks GetInstallStack(ctx context.Context, installID string) (*models.AppInstallStack, error) diff --git a/sdks/nuon-go/client/operations/get_runner_group_leader_parameters.go b/sdks/nuon-go/client/operations/get_runner_group_leader_parameters.go new file mode 100644 index 0000000000..cf55f4acb1 --- /dev/null +++ b/sdks/nuon-go/client/operations/get_runner_group_leader_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewGetRunnerGroupLeaderParams creates a new GetRunnerGroupLeaderParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewGetRunnerGroupLeaderParams() *GetRunnerGroupLeaderParams { + return &GetRunnerGroupLeaderParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewGetRunnerGroupLeaderParamsWithTimeout creates a new GetRunnerGroupLeaderParams object +// with the ability to set a timeout on a request. +func NewGetRunnerGroupLeaderParamsWithTimeout(timeout time.Duration) *GetRunnerGroupLeaderParams { + return &GetRunnerGroupLeaderParams{ + timeout: timeout, + } +} + +// NewGetRunnerGroupLeaderParamsWithContext creates a new GetRunnerGroupLeaderParams object +// with the ability to set a context for a request. +func NewGetRunnerGroupLeaderParamsWithContext(ctx context.Context) *GetRunnerGroupLeaderParams { + return &GetRunnerGroupLeaderParams{ + Context: ctx, + } +} + +// NewGetRunnerGroupLeaderParamsWithHTTPClient creates a new GetRunnerGroupLeaderParams object +// with the ability to set a custom HTTPClient for a request. +func NewGetRunnerGroupLeaderParamsWithHTTPClient(client *http.Client) *GetRunnerGroupLeaderParams { + return &GetRunnerGroupLeaderParams{ + HTTPClient: client, + } +} + +/* +GetRunnerGroupLeaderParams contains all the parameters to send to the API endpoint + + for the get runner group leader operation. + + Typically these are written to a http.Request. +*/ +type GetRunnerGroupLeaderParams struct { + + /* RunnerGroupID. + + runner group ID + */ + RunnerGroupID string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the get runner group leader params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetRunnerGroupLeaderParams) WithDefaults() *GetRunnerGroupLeaderParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the get runner group leader params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *GetRunnerGroupLeaderParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the get runner group leader params +func (o *GetRunnerGroupLeaderParams) WithTimeout(timeout time.Duration) *GetRunnerGroupLeaderParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the get runner group leader params +func (o *GetRunnerGroupLeaderParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the get runner group leader params +func (o *GetRunnerGroupLeaderParams) WithContext(ctx context.Context) *GetRunnerGroupLeaderParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the get runner group leader params +func (o *GetRunnerGroupLeaderParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the get runner group leader params +func (o *GetRunnerGroupLeaderParams) WithHTTPClient(client *http.Client) *GetRunnerGroupLeaderParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the get runner group leader params +func (o *GetRunnerGroupLeaderParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithRunnerGroupID adds the runnerGroupID to the get runner group leader params +func (o *GetRunnerGroupLeaderParams) WithRunnerGroupID(runnerGroupID string) *GetRunnerGroupLeaderParams { + o.SetRunnerGroupID(runnerGroupID) + return o +} + +// SetRunnerGroupID adds the runnerGroupId to the get runner group leader params +func (o *GetRunnerGroupLeaderParams) SetRunnerGroupID(runnerGroupID string) { + o.RunnerGroupID = runnerGroupID +} + +// WriteToRequest writes these params to a swagger request +func (o *GetRunnerGroupLeaderParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param runner_group_id + if err := r.SetPathParam("runner_group_id", o.RunnerGroupID); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/sdks/nuon-go/client/operations/get_runner_group_leader_responses.go b/sdks/nuon-go/client/operations/get_runner_group_leader_responses.go new file mode 100644 index 0000000000..87ed873a0c --- /dev/null +++ b/sdks/nuon-go/client/operations/get_runner_group_leader_responses.go @@ -0,0 +1,259 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + stderrors "errors" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/nuonco/nuon/sdks/nuon-go/models" +) + +// GetRunnerGroupLeaderReader is a Reader for the GetRunnerGroupLeader structure. +type GetRunnerGroupLeaderReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *GetRunnerGroupLeaderReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { + switch response.Code() { + case 200: + result := NewGetRunnerGroupLeaderOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 404: + result := NewGetRunnerGroupLeaderNotFound() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewGetRunnerGroupLeaderInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[GET /v1/runner-groups/{runner_group_id}/leader] GetRunnerGroupLeader", response, response.Code()) + } +} + +// NewGetRunnerGroupLeaderOK creates a GetRunnerGroupLeaderOK with default headers values +func NewGetRunnerGroupLeaderOK() *GetRunnerGroupLeaderOK { + return &GetRunnerGroupLeaderOK{} +} + +/* +GetRunnerGroupLeaderOK describes a response with status code 200, with default header values. + +OK +*/ +type GetRunnerGroupLeaderOK struct { + Payload *models.AppRunner +} + +// IsSuccess returns true when this get runner group leader o k response has a 2xx status code +func (o *GetRunnerGroupLeaderOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this get runner group leader o k response has a 3xx status code +func (o *GetRunnerGroupLeaderOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get runner group leader o k response has a 4xx status code +func (o *GetRunnerGroupLeaderOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this get runner group leader o k response has a 5xx status code +func (o *GetRunnerGroupLeaderOK) IsServerError() bool { + return false +} + +// IsCode returns true when this get runner group leader o k response a status code equal to that given +func (o *GetRunnerGroupLeaderOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the get runner group leader o k response +func (o *GetRunnerGroupLeaderOK) Code() int { + return 200 +} + +func (o *GetRunnerGroupLeaderOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /v1/runner-groups/{runner_group_id}/leader][%d] getRunnerGroupLeaderOK %s", 200, payload) +} + +func (o *GetRunnerGroupLeaderOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /v1/runner-groups/{runner_group_id}/leader][%d] getRunnerGroupLeaderOK %s", 200, payload) +} + +func (o *GetRunnerGroupLeaderOK) GetPayload() *models.AppRunner { + return o.Payload +} + +func (o *GetRunnerGroupLeaderOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.AppRunner) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} + +// NewGetRunnerGroupLeaderNotFound creates a GetRunnerGroupLeaderNotFound with default headers values +func NewGetRunnerGroupLeaderNotFound() *GetRunnerGroupLeaderNotFound { + return &GetRunnerGroupLeaderNotFound{} +} + +/* +GetRunnerGroupLeaderNotFound describes a response with status code 404, with default header values. + +Not Found +*/ +type GetRunnerGroupLeaderNotFound struct { + Payload *models.StderrErrResponse +} + +// IsSuccess returns true when this get runner group leader not found response has a 2xx status code +func (o *GetRunnerGroupLeaderNotFound) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this get runner group leader not found response has a 3xx status code +func (o *GetRunnerGroupLeaderNotFound) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get runner group leader not found response has a 4xx status code +func (o *GetRunnerGroupLeaderNotFound) IsClientError() bool { + return true +} + +// IsServerError returns true when this get runner group leader not found response has a 5xx status code +func (o *GetRunnerGroupLeaderNotFound) IsServerError() bool { + return false +} + +// IsCode returns true when this get runner group leader not found response a status code equal to that given +func (o *GetRunnerGroupLeaderNotFound) IsCode(code int) bool { + return code == 404 +} + +// Code gets the status code for the get runner group leader not found response +func (o *GetRunnerGroupLeaderNotFound) Code() int { + return 404 +} + +func (o *GetRunnerGroupLeaderNotFound) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /v1/runner-groups/{runner_group_id}/leader][%d] getRunnerGroupLeaderNotFound %s", 404, payload) +} + +func (o *GetRunnerGroupLeaderNotFound) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /v1/runner-groups/{runner_group_id}/leader][%d] getRunnerGroupLeaderNotFound %s", 404, payload) +} + +func (o *GetRunnerGroupLeaderNotFound) GetPayload() *models.StderrErrResponse { + return o.Payload +} + +func (o *GetRunnerGroupLeaderNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.StderrErrResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} + +// NewGetRunnerGroupLeaderInternalServerError creates a GetRunnerGroupLeaderInternalServerError with default headers values +func NewGetRunnerGroupLeaderInternalServerError() *GetRunnerGroupLeaderInternalServerError { + return &GetRunnerGroupLeaderInternalServerError{} +} + +/* +GetRunnerGroupLeaderInternalServerError describes a response with status code 500, with default header values. + +Internal Server Error +*/ +type GetRunnerGroupLeaderInternalServerError struct { + Payload *models.StderrErrResponse +} + +// IsSuccess returns true when this get runner group leader internal server error response has a 2xx status code +func (o *GetRunnerGroupLeaderInternalServerError) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this get runner group leader internal server error response has a 3xx status code +func (o *GetRunnerGroupLeaderInternalServerError) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get runner group leader internal server error response has a 4xx status code +func (o *GetRunnerGroupLeaderInternalServerError) IsClientError() bool { + return false +} + +// IsServerError returns true when this get runner group leader internal server error response has a 5xx status code +func (o *GetRunnerGroupLeaderInternalServerError) IsServerError() bool { + return true +} + +// IsCode returns true when this get runner group leader internal server error response a status code equal to that given +func (o *GetRunnerGroupLeaderInternalServerError) IsCode(code int) bool { + return code == 500 +} + +// Code gets the status code for the get runner group leader internal server error response +func (o *GetRunnerGroupLeaderInternalServerError) Code() int { + return 500 +} + +func (o *GetRunnerGroupLeaderInternalServerError) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /v1/runner-groups/{runner_group_id}/leader][%d] getRunnerGroupLeaderInternalServerError %s", 500, payload) +} + +func (o *GetRunnerGroupLeaderInternalServerError) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /v1/runner-groups/{runner_group_id}/leader][%d] getRunnerGroupLeaderInternalServerError %s", 500, payload) +} + +func (o *GetRunnerGroupLeaderInternalServerError) GetPayload() *models.StderrErrResponse { + return o.Payload +} + +func (o *GetRunnerGroupLeaderInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.StderrErrResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} diff --git a/sdks/nuon-go/client/operations/operations_client.go b/sdks/nuon-go/client/operations/operations_client.go index 905c13fc28..2ed035e8b8 100644 --- a/sdks/nuon-go/client/operations/operations_client.go +++ b/sdks/nuon-go/client/operations/operations_client.go @@ -508,6 +508,8 @@ type ClientService interface { GetRunnerConnectStatus(params *GetRunnerConnectStatusParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetRunnerConnectStatusOK, error) + GetRunnerGroupLeader(params *GetRunnerGroupLeaderParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetRunnerGroupLeaderOK, error) + GetRunnerJob(params *GetRunnerJobParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetRunnerJobOK, error) GetRunnerJobCompositePlan(params *GetRunnerJobCompositePlanParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetRunnerJobCompositePlanOK, error) @@ -602,12 +604,16 @@ type ClientService interface { SyncSecrets(params *SyncSecretsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*SyncSecretsCreated, error) + TaintRunner(params *TaintRunnerParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*TaintRunnerOK, error) + TeardownInstallComponent(params *TeardownInstallComponentParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*TeardownInstallComponentCreated, error) TeardownInstallComponents(params *TeardownInstallComponentsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*TeardownInstallComponentsCreated, error) UnlockTerraformWorkspace(params *UnlockTerraformWorkspaceParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UnlockTerraformWorkspaceOK, error) + UntaintRunner(params *UntaintRunnerParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UntaintRunnerOK, error) + UpdateApp(params *UpdateAppParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateAppOK, error) UpdateAppAction(params *UpdateAppActionParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateAppActionCreated, error) @@ -638,6 +644,8 @@ type ClientService interface { UpdateOrgFeatures(params *UpdateOrgFeaturesParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateOrgFeaturesOK, error) + UpdateRunnerGroupLeader(params *UpdateRunnerGroupLeaderParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateRunnerGroupLeaderAccepted, error) + UpdateRunnerMng(params *UpdateRunnerMngParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateRunnerMngCreated, error) UpdateRunnerSettings(params *UpdateRunnerSettingsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateRunnerSettingsOK, error) @@ -10534,6 +10542,50 @@ func (a *Client) GetRunnerConnectStatus(params *GetRunnerConnectStatusParams, au panic(msg) } +/* +GetRunnerGroupLeader gets the leader runner for a runner group +*/ +func (a *Client) GetRunnerGroupLeader(params *GetRunnerGroupLeaderParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetRunnerGroupLeaderOK, error) { + // NOTE: parameters are not validated before sending + if params == nil { + params = NewGetRunnerGroupLeaderParams() + } + op := &runtime.ClientOperation{ + ID: "GetRunnerGroupLeader", + Method: "GET", + PathPattern: "/v1/runner-groups/{runner_group_id}/leader", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"https"}, + Params: params, + Reader: &GetRunnerGroupLeaderReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + + // only one success response has to be checked + success, ok := result.(*GetRunnerGroupLeaderOK) + if ok { + return success, nil + } + + // unexpected success response. + + // no default response is defined. + // + // safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for GetRunnerGroupLeader: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* GetRunnerJob gets runner job @@ -12697,6 +12749,50 @@ func (a *Client) SyncSecrets(params *SyncSecretsParams, authInfo runtime.ClientA panic(msg) } +/* +TaintRunner taints a runner to exclude it from leader election +*/ +func (a *Client) TaintRunner(params *TaintRunnerParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*TaintRunnerOK, error) { + // NOTE: parameters are not validated before sending + if params == nil { + params = NewTaintRunnerParams() + } + op := &runtime.ClientOperation{ + ID: "TaintRunner", + Method: "POST", + PathPattern: "/v1/runners/{runner_id}/taint", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"https"}, + Params: params, + Reader: &TaintRunnerReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + + // only one success response has to be checked + success, ok := result.(*TaintRunnerOK) + if ok { + return success, nil + } + + // unexpected success response. + + // no default response is defined. + // + // safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for TaintRunner: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* TeardownInstallComponent teardowns an install component @@ -12835,6 +12931,50 @@ func (a *Client) UnlockTerraformWorkspace(params *UnlockTerraformWorkspaceParams panic(msg) } +/* +UntaintRunner untaints a runner to include it in leader election +*/ +func (a *Client) UntaintRunner(params *UntaintRunnerParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UntaintRunnerOK, error) { + // NOTE: parameters are not validated before sending + if params == nil { + params = NewUntaintRunnerParams() + } + op := &runtime.ClientOperation{ + ID: "UntaintRunner", + Method: "POST", + PathPattern: "/v1/runners/{runner_id}/untaint", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"https"}, + Params: params, + Reader: &UntaintRunnerReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + + // only one success response has to be checked + success, ok := result.(*UntaintRunnerOK) + if ok { + return success, nil + } + + // unexpected success response. + + // no default response is defined. + // + // safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for UntaintRunner: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* UpdateApp updates an app @@ -13537,6 +13677,50 @@ func (a *Client) UpdateOrgFeatures(params *UpdateOrgFeaturesParams, authInfo run panic(msg) } +/* +UpdateRunnerGroupLeader sets or auto elect the leader runner for a runner group +*/ +func (a *Client) UpdateRunnerGroupLeader(params *UpdateRunnerGroupLeaderParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*UpdateRunnerGroupLeaderAccepted, error) { + // NOTE: parameters are not validated before sending + if params == nil { + params = NewUpdateRunnerGroupLeaderParams() + } + op := &runtime.ClientOperation{ + ID: "UpdateRunnerGroupLeader", + Method: "PUT", + PathPattern: "/v1/runner-groups/{runner_group_id}/leader", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"https"}, + Params: params, + Reader: &UpdateRunnerGroupLeaderReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + + // only one success response has to be checked + success, ok := result.(*UpdateRunnerGroupLeaderAccepted) + if ok { + return success, nil + } + + // unexpected success response. + + // no default response is defined. + // + // safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for UpdateRunnerGroupLeader: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + /* UpdateRunnerMng updates an install runner via the mng process */ diff --git a/sdks/nuon-go/client/operations/taint_runner_parameters.go b/sdks/nuon-go/client/operations/taint_runner_parameters.go new file mode 100644 index 0000000000..d9424e609e --- /dev/null +++ b/sdks/nuon-go/client/operations/taint_runner_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewTaintRunnerParams creates a new TaintRunnerParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewTaintRunnerParams() *TaintRunnerParams { + return &TaintRunnerParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewTaintRunnerParamsWithTimeout creates a new TaintRunnerParams object +// with the ability to set a timeout on a request. +func NewTaintRunnerParamsWithTimeout(timeout time.Duration) *TaintRunnerParams { + return &TaintRunnerParams{ + timeout: timeout, + } +} + +// NewTaintRunnerParamsWithContext creates a new TaintRunnerParams object +// with the ability to set a context for a request. +func NewTaintRunnerParamsWithContext(ctx context.Context) *TaintRunnerParams { + return &TaintRunnerParams{ + Context: ctx, + } +} + +// NewTaintRunnerParamsWithHTTPClient creates a new TaintRunnerParams object +// with the ability to set a custom HTTPClient for a request. +func NewTaintRunnerParamsWithHTTPClient(client *http.Client) *TaintRunnerParams { + return &TaintRunnerParams{ + HTTPClient: client, + } +} + +/* +TaintRunnerParams contains all the parameters to send to the API endpoint + + for the taint runner operation. + + Typically these are written to a http.Request. +*/ +type TaintRunnerParams struct { + + /* RunnerID. + + runner ID + */ + RunnerID string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the taint runner params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *TaintRunnerParams) WithDefaults() *TaintRunnerParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the taint runner params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *TaintRunnerParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the taint runner params +func (o *TaintRunnerParams) WithTimeout(timeout time.Duration) *TaintRunnerParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the taint runner params +func (o *TaintRunnerParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the taint runner params +func (o *TaintRunnerParams) WithContext(ctx context.Context) *TaintRunnerParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the taint runner params +func (o *TaintRunnerParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the taint runner params +func (o *TaintRunnerParams) WithHTTPClient(client *http.Client) *TaintRunnerParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the taint runner params +func (o *TaintRunnerParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithRunnerID adds the runnerID to the taint runner params +func (o *TaintRunnerParams) WithRunnerID(runnerID string) *TaintRunnerParams { + o.SetRunnerID(runnerID) + return o +} + +// SetRunnerID adds the runnerId to the taint runner params +func (o *TaintRunnerParams) SetRunnerID(runnerID string) { + o.RunnerID = runnerID +} + +// WriteToRequest writes these params to a swagger request +func (o *TaintRunnerParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param runner_id + if err := r.SetPathParam("runner_id", o.RunnerID); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/sdks/nuon-go/client/operations/taint_runner_responses.go b/sdks/nuon-go/client/operations/taint_runner_responses.go new file mode 100644 index 0000000000..37d49a922b --- /dev/null +++ b/sdks/nuon-go/client/operations/taint_runner_responses.go @@ -0,0 +1,259 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + stderrors "errors" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/nuonco/nuon/sdks/nuon-go/models" +) + +// TaintRunnerReader is a Reader for the TaintRunner structure. +type TaintRunnerReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *TaintRunnerReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { + switch response.Code() { + case 200: + result := NewTaintRunnerOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 404: + result := NewTaintRunnerNotFound() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewTaintRunnerInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[POST /v1/runners/{runner_id}/taint] TaintRunner", response, response.Code()) + } +} + +// NewTaintRunnerOK creates a TaintRunnerOK with default headers values +func NewTaintRunnerOK() *TaintRunnerOK { + return &TaintRunnerOK{} +} + +/* +TaintRunnerOK describes a response with status code 200, with default header values. + +OK +*/ +type TaintRunnerOK struct { + Payload *models.AppRunner +} + +// IsSuccess returns true when this taint runner o k response has a 2xx status code +func (o *TaintRunnerOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this taint runner o k response has a 3xx status code +func (o *TaintRunnerOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this taint runner o k response has a 4xx status code +func (o *TaintRunnerOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this taint runner o k response has a 5xx status code +func (o *TaintRunnerOK) IsServerError() bool { + return false +} + +// IsCode returns true when this taint runner o k response a status code equal to that given +func (o *TaintRunnerOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the taint runner o k response +func (o *TaintRunnerOK) Code() int { + return 200 +} + +func (o *TaintRunnerOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/taint][%d] taintRunnerOK %s", 200, payload) +} + +func (o *TaintRunnerOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/taint][%d] taintRunnerOK %s", 200, payload) +} + +func (o *TaintRunnerOK) GetPayload() *models.AppRunner { + return o.Payload +} + +func (o *TaintRunnerOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.AppRunner) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} + +// NewTaintRunnerNotFound creates a TaintRunnerNotFound with default headers values +func NewTaintRunnerNotFound() *TaintRunnerNotFound { + return &TaintRunnerNotFound{} +} + +/* +TaintRunnerNotFound describes a response with status code 404, with default header values. + +Not Found +*/ +type TaintRunnerNotFound struct { + Payload *models.StderrErrResponse +} + +// IsSuccess returns true when this taint runner not found response has a 2xx status code +func (o *TaintRunnerNotFound) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this taint runner not found response has a 3xx status code +func (o *TaintRunnerNotFound) IsRedirect() bool { + return false +} + +// IsClientError returns true when this taint runner not found response has a 4xx status code +func (o *TaintRunnerNotFound) IsClientError() bool { + return true +} + +// IsServerError returns true when this taint runner not found response has a 5xx status code +func (o *TaintRunnerNotFound) IsServerError() bool { + return false +} + +// IsCode returns true when this taint runner not found response a status code equal to that given +func (o *TaintRunnerNotFound) IsCode(code int) bool { + return code == 404 +} + +// Code gets the status code for the taint runner not found response +func (o *TaintRunnerNotFound) Code() int { + return 404 +} + +func (o *TaintRunnerNotFound) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/taint][%d] taintRunnerNotFound %s", 404, payload) +} + +func (o *TaintRunnerNotFound) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/taint][%d] taintRunnerNotFound %s", 404, payload) +} + +func (o *TaintRunnerNotFound) GetPayload() *models.StderrErrResponse { + return o.Payload +} + +func (o *TaintRunnerNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.StderrErrResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} + +// NewTaintRunnerInternalServerError creates a TaintRunnerInternalServerError with default headers values +func NewTaintRunnerInternalServerError() *TaintRunnerInternalServerError { + return &TaintRunnerInternalServerError{} +} + +/* +TaintRunnerInternalServerError describes a response with status code 500, with default header values. + +Internal Server Error +*/ +type TaintRunnerInternalServerError struct { + Payload *models.StderrErrResponse +} + +// IsSuccess returns true when this taint runner internal server error response has a 2xx status code +func (o *TaintRunnerInternalServerError) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this taint runner internal server error response has a 3xx status code +func (o *TaintRunnerInternalServerError) IsRedirect() bool { + return false +} + +// IsClientError returns true when this taint runner internal server error response has a 4xx status code +func (o *TaintRunnerInternalServerError) IsClientError() bool { + return false +} + +// IsServerError returns true when this taint runner internal server error response has a 5xx status code +func (o *TaintRunnerInternalServerError) IsServerError() bool { + return true +} + +// IsCode returns true when this taint runner internal server error response a status code equal to that given +func (o *TaintRunnerInternalServerError) IsCode(code int) bool { + return code == 500 +} + +// Code gets the status code for the taint runner internal server error response +func (o *TaintRunnerInternalServerError) Code() int { + return 500 +} + +func (o *TaintRunnerInternalServerError) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/taint][%d] taintRunnerInternalServerError %s", 500, payload) +} + +func (o *TaintRunnerInternalServerError) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/taint][%d] taintRunnerInternalServerError %s", 500, payload) +} + +func (o *TaintRunnerInternalServerError) GetPayload() *models.StderrErrResponse { + return o.Payload +} + +func (o *TaintRunnerInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.StderrErrResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} diff --git a/sdks/nuon-go/client/operations/untaint_runner_parameters.go b/sdks/nuon-go/client/operations/untaint_runner_parameters.go new file mode 100644 index 0000000000..3be9d528eb --- /dev/null +++ b/sdks/nuon-go/client/operations/untaint_runner_parameters.go @@ -0,0 +1,151 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" +) + +// NewUntaintRunnerParams creates a new UntaintRunnerParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewUntaintRunnerParams() *UntaintRunnerParams { + return &UntaintRunnerParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewUntaintRunnerParamsWithTimeout creates a new UntaintRunnerParams object +// with the ability to set a timeout on a request. +func NewUntaintRunnerParamsWithTimeout(timeout time.Duration) *UntaintRunnerParams { + return &UntaintRunnerParams{ + timeout: timeout, + } +} + +// NewUntaintRunnerParamsWithContext creates a new UntaintRunnerParams object +// with the ability to set a context for a request. +func NewUntaintRunnerParamsWithContext(ctx context.Context) *UntaintRunnerParams { + return &UntaintRunnerParams{ + Context: ctx, + } +} + +// NewUntaintRunnerParamsWithHTTPClient creates a new UntaintRunnerParams object +// with the ability to set a custom HTTPClient for a request. +func NewUntaintRunnerParamsWithHTTPClient(client *http.Client) *UntaintRunnerParams { + return &UntaintRunnerParams{ + HTTPClient: client, + } +} + +/* +UntaintRunnerParams contains all the parameters to send to the API endpoint + + for the untaint runner operation. + + Typically these are written to a http.Request. +*/ +type UntaintRunnerParams struct { + + /* RunnerID. + + runner ID + */ + RunnerID string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the untaint runner params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UntaintRunnerParams) WithDefaults() *UntaintRunnerParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the untaint runner params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UntaintRunnerParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the untaint runner params +func (o *UntaintRunnerParams) WithTimeout(timeout time.Duration) *UntaintRunnerParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the untaint runner params +func (o *UntaintRunnerParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the untaint runner params +func (o *UntaintRunnerParams) WithContext(ctx context.Context) *UntaintRunnerParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the untaint runner params +func (o *UntaintRunnerParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the untaint runner params +func (o *UntaintRunnerParams) WithHTTPClient(client *http.Client) *UntaintRunnerParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the untaint runner params +func (o *UntaintRunnerParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithRunnerID adds the runnerID to the untaint runner params +func (o *UntaintRunnerParams) WithRunnerID(runnerID string) *UntaintRunnerParams { + o.SetRunnerID(runnerID) + return o +} + +// SetRunnerID adds the runnerId to the untaint runner params +func (o *UntaintRunnerParams) SetRunnerID(runnerID string) { + o.RunnerID = runnerID +} + +// WriteToRequest writes these params to a swagger request +func (o *UntaintRunnerParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param runner_id + if err := r.SetPathParam("runner_id", o.RunnerID); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/sdks/nuon-go/client/operations/untaint_runner_responses.go b/sdks/nuon-go/client/operations/untaint_runner_responses.go new file mode 100644 index 0000000000..9787cfc396 --- /dev/null +++ b/sdks/nuon-go/client/operations/untaint_runner_responses.go @@ -0,0 +1,259 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + stderrors "errors" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/nuonco/nuon/sdks/nuon-go/models" +) + +// UntaintRunnerReader is a Reader for the UntaintRunner structure. +type UntaintRunnerReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *UntaintRunnerReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { + switch response.Code() { + case 200: + result := NewUntaintRunnerOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 404: + result := NewUntaintRunnerNotFound() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewUntaintRunnerInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[POST /v1/runners/{runner_id}/untaint] UntaintRunner", response, response.Code()) + } +} + +// NewUntaintRunnerOK creates a UntaintRunnerOK with default headers values +func NewUntaintRunnerOK() *UntaintRunnerOK { + return &UntaintRunnerOK{} +} + +/* +UntaintRunnerOK describes a response with status code 200, with default header values. + +OK +*/ +type UntaintRunnerOK struct { + Payload *models.AppRunner +} + +// IsSuccess returns true when this untaint runner o k response has a 2xx status code +func (o *UntaintRunnerOK) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this untaint runner o k response has a 3xx status code +func (o *UntaintRunnerOK) IsRedirect() bool { + return false +} + +// IsClientError returns true when this untaint runner o k response has a 4xx status code +func (o *UntaintRunnerOK) IsClientError() bool { + return false +} + +// IsServerError returns true when this untaint runner o k response has a 5xx status code +func (o *UntaintRunnerOK) IsServerError() bool { + return false +} + +// IsCode returns true when this untaint runner o k response a status code equal to that given +func (o *UntaintRunnerOK) IsCode(code int) bool { + return code == 200 +} + +// Code gets the status code for the untaint runner o k response +func (o *UntaintRunnerOK) Code() int { + return 200 +} + +func (o *UntaintRunnerOK) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/untaint][%d] untaintRunnerOK %s", 200, payload) +} + +func (o *UntaintRunnerOK) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/untaint][%d] untaintRunnerOK %s", 200, payload) +} + +func (o *UntaintRunnerOK) GetPayload() *models.AppRunner { + return o.Payload +} + +func (o *UntaintRunnerOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.AppRunner) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} + +// NewUntaintRunnerNotFound creates a UntaintRunnerNotFound with default headers values +func NewUntaintRunnerNotFound() *UntaintRunnerNotFound { + return &UntaintRunnerNotFound{} +} + +/* +UntaintRunnerNotFound describes a response with status code 404, with default header values. + +Not Found +*/ +type UntaintRunnerNotFound struct { + Payload *models.StderrErrResponse +} + +// IsSuccess returns true when this untaint runner not found response has a 2xx status code +func (o *UntaintRunnerNotFound) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this untaint runner not found response has a 3xx status code +func (o *UntaintRunnerNotFound) IsRedirect() bool { + return false +} + +// IsClientError returns true when this untaint runner not found response has a 4xx status code +func (o *UntaintRunnerNotFound) IsClientError() bool { + return true +} + +// IsServerError returns true when this untaint runner not found response has a 5xx status code +func (o *UntaintRunnerNotFound) IsServerError() bool { + return false +} + +// IsCode returns true when this untaint runner not found response a status code equal to that given +func (o *UntaintRunnerNotFound) IsCode(code int) bool { + return code == 404 +} + +// Code gets the status code for the untaint runner not found response +func (o *UntaintRunnerNotFound) Code() int { + return 404 +} + +func (o *UntaintRunnerNotFound) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/untaint][%d] untaintRunnerNotFound %s", 404, payload) +} + +func (o *UntaintRunnerNotFound) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/untaint][%d] untaintRunnerNotFound %s", 404, payload) +} + +func (o *UntaintRunnerNotFound) GetPayload() *models.StderrErrResponse { + return o.Payload +} + +func (o *UntaintRunnerNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.StderrErrResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} + +// NewUntaintRunnerInternalServerError creates a UntaintRunnerInternalServerError with default headers values +func NewUntaintRunnerInternalServerError() *UntaintRunnerInternalServerError { + return &UntaintRunnerInternalServerError{} +} + +/* +UntaintRunnerInternalServerError describes a response with status code 500, with default header values. + +Internal Server Error +*/ +type UntaintRunnerInternalServerError struct { + Payload *models.StderrErrResponse +} + +// IsSuccess returns true when this untaint runner internal server error response has a 2xx status code +func (o *UntaintRunnerInternalServerError) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this untaint runner internal server error response has a 3xx status code +func (o *UntaintRunnerInternalServerError) IsRedirect() bool { + return false +} + +// IsClientError returns true when this untaint runner internal server error response has a 4xx status code +func (o *UntaintRunnerInternalServerError) IsClientError() bool { + return false +} + +// IsServerError returns true when this untaint runner internal server error response has a 5xx status code +func (o *UntaintRunnerInternalServerError) IsServerError() bool { + return true +} + +// IsCode returns true when this untaint runner internal server error response a status code equal to that given +func (o *UntaintRunnerInternalServerError) IsCode(code int) bool { + return code == 500 +} + +// Code gets the status code for the untaint runner internal server error response +func (o *UntaintRunnerInternalServerError) Code() int { + return 500 +} + +func (o *UntaintRunnerInternalServerError) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/untaint][%d] untaintRunnerInternalServerError %s", 500, payload) +} + +func (o *UntaintRunnerInternalServerError) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[POST /v1/runners/{runner_id}/untaint][%d] untaintRunnerInternalServerError %s", 500, payload) +} + +func (o *UntaintRunnerInternalServerError) GetPayload() *models.StderrErrResponse { + return o.Payload +} + +func (o *UntaintRunnerInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.StderrErrResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} diff --git a/sdks/nuon-go/client/operations/update_runner_group_leader_parameters.go b/sdks/nuon-go/client/operations/update_runner_group_leader_parameters.go new file mode 100644 index 0000000000..50a6151d7f --- /dev/null +++ b/sdks/nuon-go/client/operations/update_runner_group_leader_parameters.go @@ -0,0 +1,175 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + "github.com/nuonco/nuon/sdks/nuon-go/models" +) + +// NewUpdateRunnerGroupLeaderParams creates a new UpdateRunnerGroupLeaderParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewUpdateRunnerGroupLeaderParams() *UpdateRunnerGroupLeaderParams { + return &UpdateRunnerGroupLeaderParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewUpdateRunnerGroupLeaderParamsWithTimeout creates a new UpdateRunnerGroupLeaderParams object +// with the ability to set a timeout on a request. +func NewUpdateRunnerGroupLeaderParamsWithTimeout(timeout time.Duration) *UpdateRunnerGroupLeaderParams { + return &UpdateRunnerGroupLeaderParams{ + timeout: timeout, + } +} + +// NewUpdateRunnerGroupLeaderParamsWithContext creates a new UpdateRunnerGroupLeaderParams object +// with the ability to set a context for a request. +func NewUpdateRunnerGroupLeaderParamsWithContext(ctx context.Context) *UpdateRunnerGroupLeaderParams { + return &UpdateRunnerGroupLeaderParams{ + Context: ctx, + } +} + +// NewUpdateRunnerGroupLeaderParamsWithHTTPClient creates a new UpdateRunnerGroupLeaderParams object +// with the ability to set a custom HTTPClient for a request. +func NewUpdateRunnerGroupLeaderParamsWithHTTPClient(client *http.Client) *UpdateRunnerGroupLeaderParams { + return &UpdateRunnerGroupLeaderParams{ + HTTPClient: client, + } +} + +/* +UpdateRunnerGroupLeaderParams contains all the parameters to send to the API endpoint + + for the update runner group leader operation. + + Typically these are written to a http.Request. +*/ +type UpdateRunnerGroupLeaderParams struct { + + /* Request. + + leader update request + */ + Request *models.ServiceUpdateRunnerGroupLeaderRequest + + /* RunnerGroupID. + + runner group ID + */ + RunnerGroupID string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the update runner group leader params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateRunnerGroupLeaderParams) WithDefaults() *UpdateRunnerGroupLeaderParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the update runner group leader params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *UpdateRunnerGroupLeaderParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the update runner group leader params +func (o *UpdateRunnerGroupLeaderParams) WithTimeout(timeout time.Duration) *UpdateRunnerGroupLeaderParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the update runner group leader params +func (o *UpdateRunnerGroupLeaderParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the update runner group leader params +func (o *UpdateRunnerGroupLeaderParams) WithContext(ctx context.Context) *UpdateRunnerGroupLeaderParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the update runner group leader params +func (o *UpdateRunnerGroupLeaderParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the update runner group leader params +func (o *UpdateRunnerGroupLeaderParams) WithHTTPClient(client *http.Client) *UpdateRunnerGroupLeaderParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the update runner group leader params +func (o *UpdateRunnerGroupLeaderParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithRequest adds the request to the update runner group leader params +func (o *UpdateRunnerGroupLeaderParams) WithRequest(request *models.ServiceUpdateRunnerGroupLeaderRequest) *UpdateRunnerGroupLeaderParams { + o.SetRequest(request) + return o +} + +// SetRequest adds the request to the update runner group leader params +func (o *UpdateRunnerGroupLeaderParams) SetRequest(request *models.ServiceUpdateRunnerGroupLeaderRequest) { + o.Request = request +} + +// WithRunnerGroupID adds the runnerGroupID to the update runner group leader params +func (o *UpdateRunnerGroupLeaderParams) WithRunnerGroupID(runnerGroupID string) *UpdateRunnerGroupLeaderParams { + o.SetRunnerGroupID(runnerGroupID) + return o +} + +// SetRunnerGroupID adds the runnerGroupId to the update runner group leader params +func (o *UpdateRunnerGroupLeaderParams) SetRunnerGroupID(runnerGroupID string) { + o.RunnerGroupID = runnerGroupID +} + +// WriteToRequest writes these params to a swagger request +func (o *UpdateRunnerGroupLeaderParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if o.Request != nil { + if err := r.SetBodyParam(o.Request); err != nil { + return err + } + } + + // path param runner_group_id + if err := r.SetPathParam("runner_group_id", o.RunnerGroupID); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/sdks/nuon-go/client/operations/update_runner_group_leader_responses.go b/sdks/nuon-go/client/operations/update_runner_group_leader_responses.go new file mode 100644 index 0000000000..dd8af57afd --- /dev/null +++ b/sdks/nuon-go/client/operations/update_runner_group_leader_responses.go @@ -0,0 +1,333 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + stderrors "errors" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/nuonco/nuon/sdks/nuon-go/models" +) + +// UpdateRunnerGroupLeaderReader is a Reader for the UpdateRunnerGroupLeader structure. +type UpdateRunnerGroupLeaderReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *UpdateRunnerGroupLeaderReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) { + switch response.Code() { + case 202: + result := NewUpdateRunnerGroupLeaderAccepted() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewUpdateRunnerGroupLeaderBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 404: + result := NewUpdateRunnerGroupLeaderNotFound() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewUpdateRunnerGroupLeaderInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("[PUT /v1/runner-groups/{runner_group_id}/leader] UpdateRunnerGroupLeader", response, response.Code()) + } +} + +// NewUpdateRunnerGroupLeaderAccepted creates a UpdateRunnerGroupLeaderAccepted with default headers values +func NewUpdateRunnerGroupLeaderAccepted() *UpdateRunnerGroupLeaderAccepted { + return &UpdateRunnerGroupLeaderAccepted{} +} + +/* +UpdateRunnerGroupLeaderAccepted describes a response with status code 202, with default header values. + +Accepted +*/ +type UpdateRunnerGroupLeaderAccepted struct { + Payload any +} + +// IsSuccess returns true when this update runner group leader accepted response has a 2xx status code +func (o *UpdateRunnerGroupLeaderAccepted) IsSuccess() bool { + return true +} + +// IsRedirect returns true when this update runner group leader accepted response has a 3xx status code +func (o *UpdateRunnerGroupLeaderAccepted) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update runner group leader accepted response has a 4xx status code +func (o *UpdateRunnerGroupLeaderAccepted) IsClientError() bool { + return false +} + +// IsServerError returns true when this update runner group leader accepted response has a 5xx status code +func (o *UpdateRunnerGroupLeaderAccepted) IsServerError() bool { + return false +} + +// IsCode returns true when this update runner group leader accepted response a status code equal to that given +func (o *UpdateRunnerGroupLeaderAccepted) IsCode(code int) bool { + return code == 202 +} + +// Code gets the status code for the update runner group leader accepted response +func (o *UpdateRunnerGroupLeaderAccepted) Code() int { + return 202 +} + +func (o *UpdateRunnerGroupLeaderAccepted) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /v1/runner-groups/{runner_group_id}/leader][%d] updateRunnerGroupLeaderAccepted %s", 202, payload) +} + +func (o *UpdateRunnerGroupLeaderAccepted) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /v1/runner-groups/{runner_group_id}/leader][%d] updateRunnerGroupLeaderAccepted %s", 202, payload) +} + +func (o *UpdateRunnerGroupLeaderAccepted) GetPayload() any { + return o.Payload +} + +func (o *UpdateRunnerGroupLeaderAccepted) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} + +// NewUpdateRunnerGroupLeaderBadRequest creates a UpdateRunnerGroupLeaderBadRequest with default headers values +func NewUpdateRunnerGroupLeaderBadRequest() *UpdateRunnerGroupLeaderBadRequest { + return &UpdateRunnerGroupLeaderBadRequest{} +} + +/* +UpdateRunnerGroupLeaderBadRequest describes a response with status code 400, with default header values. + +Bad Request +*/ +type UpdateRunnerGroupLeaderBadRequest struct { + Payload *models.StderrErrResponse +} + +// IsSuccess returns true when this update runner group leader bad request response has a 2xx status code +func (o *UpdateRunnerGroupLeaderBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this update runner group leader bad request response has a 3xx status code +func (o *UpdateRunnerGroupLeaderBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update runner group leader bad request response has a 4xx status code +func (o *UpdateRunnerGroupLeaderBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this update runner group leader bad request response has a 5xx status code +func (o *UpdateRunnerGroupLeaderBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this update runner group leader bad request response a status code equal to that given +func (o *UpdateRunnerGroupLeaderBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the update runner group leader bad request response +func (o *UpdateRunnerGroupLeaderBadRequest) Code() int { + return 400 +} + +func (o *UpdateRunnerGroupLeaderBadRequest) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /v1/runner-groups/{runner_group_id}/leader][%d] updateRunnerGroupLeaderBadRequest %s", 400, payload) +} + +func (o *UpdateRunnerGroupLeaderBadRequest) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /v1/runner-groups/{runner_group_id}/leader][%d] updateRunnerGroupLeaderBadRequest %s", 400, payload) +} + +func (o *UpdateRunnerGroupLeaderBadRequest) GetPayload() *models.StderrErrResponse { + return o.Payload +} + +func (o *UpdateRunnerGroupLeaderBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.StderrErrResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} + +// NewUpdateRunnerGroupLeaderNotFound creates a UpdateRunnerGroupLeaderNotFound with default headers values +func NewUpdateRunnerGroupLeaderNotFound() *UpdateRunnerGroupLeaderNotFound { + return &UpdateRunnerGroupLeaderNotFound{} +} + +/* +UpdateRunnerGroupLeaderNotFound describes a response with status code 404, with default header values. + +Not Found +*/ +type UpdateRunnerGroupLeaderNotFound struct { + Payload *models.StderrErrResponse +} + +// IsSuccess returns true when this update runner group leader not found response has a 2xx status code +func (o *UpdateRunnerGroupLeaderNotFound) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this update runner group leader not found response has a 3xx status code +func (o *UpdateRunnerGroupLeaderNotFound) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update runner group leader not found response has a 4xx status code +func (o *UpdateRunnerGroupLeaderNotFound) IsClientError() bool { + return true +} + +// IsServerError returns true when this update runner group leader not found response has a 5xx status code +func (o *UpdateRunnerGroupLeaderNotFound) IsServerError() bool { + return false +} + +// IsCode returns true when this update runner group leader not found response a status code equal to that given +func (o *UpdateRunnerGroupLeaderNotFound) IsCode(code int) bool { + return code == 404 +} + +// Code gets the status code for the update runner group leader not found response +func (o *UpdateRunnerGroupLeaderNotFound) Code() int { + return 404 +} + +func (o *UpdateRunnerGroupLeaderNotFound) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /v1/runner-groups/{runner_group_id}/leader][%d] updateRunnerGroupLeaderNotFound %s", 404, payload) +} + +func (o *UpdateRunnerGroupLeaderNotFound) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /v1/runner-groups/{runner_group_id}/leader][%d] updateRunnerGroupLeaderNotFound %s", 404, payload) +} + +func (o *UpdateRunnerGroupLeaderNotFound) GetPayload() *models.StderrErrResponse { + return o.Payload +} + +func (o *UpdateRunnerGroupLeaderNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.StderrErrResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} + +// NewUpdateRunnerGroupLeaderInternalServerError creates a UpdateRunnerGroupLeaderInternalServerError with default headers values +func NewUpdateRunnerGroupLeaderInternalServerError() *UpdateRunnerGroupLeaderInternalServerError { + return &UpdateRunnerGroupLeaderInternalServerError{} +} + +/* +UpdateRunnerGroupLeaderInternalServerError describes a response with status code 500, with default header values. + +Internal Server Error +*/ +type UpdateRunnerGroupLeaderInternalServerError struct { + Payload *models.StderrErrResponse +} + +// IsSuccess returns true when this update runner group leader internal server error response has a 2xx status code +func (o *UpdateRunnerGroupLeaderInternalServerError) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this update runner group leader internal server error response has a 3xx status code +func (o *UpdateRunnerGroupLeaderInternalServerError) IsRedirect() bool { + return false +} + +// IsClientError returns true when this update runner group leader internal server error response has a 4xx status code +func (o *UpdateRunnerGroupLeaderInternalServerError) IsClientError() bool { + return false +} + +// IsServerError returns true when this update runner group leader internal server error response has a 5xx status code +func (o *UpdateRunnerGroupLeaderInternalServerError) IsServerError() bool { + return true +} + +// IsCode returns true when this update runner group leader internal server error response a status code equal to that given +func (o *UpdateRunnerGroupLeaderInternalServerError) IsCode(code int) bool { + return code == 500 +} + +// Code gets the status code for the update runner group leader internal server error response +func (o *UpdateRunnerGroupLeaderInternalServerError) Code() int { + return 500 +} + +func (o *UpdateRunnerGroupLeaderInternalServerError) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /v1/runner-groups/{runner_group_id}/leader][%d] updateRunnerGroupLeaderInternalServerError %s", 500, payload) +} + +func (o *UpdateRunnerGroupLeaderInternalServerError) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /v1/runner-groups/{runner_group_id}/leader][%d] updateRunnerGroupLeaderInternalServerError %s", 500, payload) +} + +func (o *UpdateRunnerGroupLeaderInternalServerError) GetPayload() *models.StderrErrResponse { + return o.Payload +} + +func (o *UpdateRunnerGroupLeaderInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.StderrErrResponse) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} diff --git a/sdks/nuon-go/models/app_runner.go b/sdks/nuon-go/models/app_runner.go index 0940f6dd32..a5398577e0 100644 --- a/sdks/nuon-go/models/app_runner.go +++ b/sdks/nuon-go/models/app_runner.go @@ -35,6 +35,9 @@ type AppRunner struct { // jobs Jobs []*AppRunnerJob `json:"jobs"` + // leader + Leader bool `json:"leader,omitempty"` + // name Name string `json:"name,omitempty"` @@ -44,6 +47,9 @@ type AppRunner struct { // org id OrgID string `json:"org_id,omitempty"` + // platform + Platform string `json:"platform,omitempty"` + // runner group RunnerGroup *AppRunnerGroup `json:"runner_group,omitempty"` @@ -59,6 +65,9 @@ type AppRunner struct { // status description StatusDescription string `json:"status_description,omitempty"` + // tainted + Tainted bool `json:"tainted,omitempty"` + // updated at UpdatedAt string `json:"updated_at,omitempty"` } diff --git a/sdks/nuon-go/models/app_runner_group.go b/sdks/nuon-go/models/app_runner_group.go index 1c063893d5..303c303e4e 100644 --- a/sdks/nuon-go/models/app_runner_group.go +++ b/sdks/nuon-go/models/app_runner_group.go @@ -38,8 +38,8 @@ type AppRunnerGroup struct { // owner type OwnerType string `json:"owner_type,omitempty"` - // platform - Platform AppAppRunnerType `json:"platform,omitempty"` + // Deprecated: Platform is being phased out in favor of per-runner Runner.Platform field. + Platform string `json:"platform,omitempty"` // runners Runners []*AppRunner `json:"runners"` @@ -58,10 +58,6 @@ type AppRunnerGroup struct { func (m *AppRunnerGroup) Validate(formats strfmt.Registry) error { var res []error - if err := m.validatePlatform(formats); err != nil { - res = append(res, err) - } - if err := m.validateRunners(formats); err != nil { res = append(res, err) } @@ -80,27 +76,6 @@ func (m *AppRunnerGroup) Validate(formats strfmt.Registry) error { return nil } -func (m *AppRunnerGroup) validatePlatform(formats strfmt.Registry) error { - if swag.IsZero(m.Platform) { // not required - return nil - } - - if err := m.Platform.Validate(formats); err != nil { - ve := new(errors.Validation) - if stderrors.As(err, &ve) { - return ve.ValidateName("platform") - } - ce := new(errors.CompositeError) - if stderrors.As(err, &ce) { - return ce.ValidateName("platform") - } - - return err - } - - return nil -} - func (m *AppRunnerGroup) validateRunners(formats strfmt.Registry) error { if swag.IsZero(m.Runners) { // not required return nil @@ -179,10 +154,6 @@ func (m *AppRunnerGroup) validateType(formats strfmt.Registry) error { func (m *AppRunnerGroup) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error - if err := m.contextValidatePlatform(ctx, formats); err != nil { - res = append(res, err) - } - if err := m.contextValidateRunners(ctx, formats); err != nil { res = append(res, err) } @@ -201,28 +172,6 @@ func (m *AppRunnerGroup) ContextValidate(ctx context.Context, formats strfmt.Reg return nil } -func (m *AppRunnerGroup) contextValidatePlatform(ctx context.Context, formats strfmt.Registry) error { - - if swag.IsZero(m.Platform) { // not required - return nil - } - - if err := m.Platform.ContextValidate(ctx, formats); err != nil { - ve := new(errors.Validation) - if stderrors.As(err, &ve) { - return ve.ValidateName("platform") - } - ce := new(errors.CompositeError) - if stderrors.As(err, &ce) { - return ce.ValidateName("platform") - } - - return err - } - - return nil -} - func (m *AppRunnerGroup) contextValidateRunners(ctx context.Context, formats strfmt.Registry) error { for i := 0; i < len(m.Runners); i++ { diff --git a/sdks/nuon-go/models/service_update_runner_group_leader_request.go b/sdks/nuon-go/models/service_update_runner_group_leader_request.go new file mode 100644 index 0000000000..fd024c5421 --- /dev/null +++ b/sdks/nuon-go/models/service_update_runner_group_leader_request.go @@ -0,0 +1,50 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// ServiceUpdateRunnerGroupLeaderRequest service update runner group leader request +// +// swagger:model service.updateRunnerGroupLeaderRequest +type ServiceUpdateRunnerGroupLeaderRequest struct { + + // runner id + RunnerID string `json:"runner_id,omitempty"` +} + +// Validate validates this service update runner group leader request +func (m *ServiceUpdateRunnerGroupLeaderRequest) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this service update runner group leader request based on context it is used +func (m *ServiceUpdateRunnerGroupLeaderRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *ServiceUpdateRunnerGroupLeaderRequest) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ServiceUpdateRunnerGroupLeaderRequest) UnmarshalBinary(b []byte) error { + var res ServiceUpdateRunnerGroupLeaderRequest + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/sdks/nuon-go/runner_groups.go b/sdks/nuon-go/runner_groups.go new file mode 100644 index 0000000000..089b116ae5 --- /dev/null +++ b/sdks/nuon-go/runner_groups.go @@ -0,0 +1,43 @@ +package nuon + +import ( + "context" + + "github.com/nuonco/nuon/sdks/nuon-go/client/operations" + "github.com/nuonco/nuon/sdks/nuon-go/models" +) + +func (c *client) GetInstallRunnerGroup(ctx context.Context, installID string) (*models.AppRunnerGroup, error) { + resp, err := c.genClient.Operations.GetInstallRunnerGroup(&operations.GetInstallRunnerGroupParams{ + InstallID: installID, + Context: ctx, + }, c.getOrgIDAuthInfo()) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} + +func (c *client) GetRunnerGroupLeader(ctx context.Context, runnerGroupID string) (*models.AppRunner, error) { + resp, err := c.genClient.Operations.GetRunnerGroupLeader(&operations.GetRunnerGroupLeaderParams{ + RunnerGroupID: runnerGroupID, + Context: ctx, + }, c.getOrgIDAuthInfo()) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} + +func (c *client) UpdateRunnerGroupLeader(ctx context.Context, runnerGroupID string, runnerID string) error { + _, err := c.genClient.Operations.UpdateRunnerGroupLeader(&operations.UpdateRunnerGroupLeaderParams{ + RunnerGroupID: runnerGroupID, + Request: &models.ServiceUpdateRunnerGroupLeaderRequest{ + RunnerID: runnerID, + }, + Context: ctx, + }, c.getOrgIDAuthInfo()) + return err +} diff --git a/sdks/nuon-go/runners.go b/sdks/nuon-go/runners.go new file mode 100644 index 0000000000..2d513f4412 --- /dev/null +++ b/sdks/nuon-go/runners.go @@ -0,0 +1,47 @@ +package nuon + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/nuonco/nuon/sdks/nuon-go/models" +) + +func (c *client) TaintRunner(ctx context.Context, runnerID string) (*models.AppRunner, error) { + return c.postRunnerAction(ctx, runnerID, "taint") +} + +func (c *client) UntaintRunner(ctx context.Context, runnerID string) (*models.AppRunner, error) { + return c.postRunnerAction(ctx, runnerID, "untaint") +} + +func (c *client) postRunnerAction(ctx context.Context, runnerID, action string) (*models.AppRunner, error) { + reqURL := fmt.Sprintf("%s/v1/runners/%s/%s", c.APIURL, runnerID, action) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + httpClient := &http.Client{Transport: c.appTransport} + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body)) + } + + var runner models.AppRunner + if err := json.NewDecoder(resp.Body).Decode(&runner); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &runner, nil +} diff --git a/sdks/nuon-runner-go/models/app_runner.go b/sdks/nuon-runner-go/models/app_runner.go index 0940f6dd32..a5398577e0 100644 --- a/sdks/nuon-runner-go/models/app_runner.go +++ b/sdks/nuon-runner-go/models/app_runner.go @@ -35,6 +35,9 @@ type AppRunner struct { // jobs Jobs []*AppRunnerJob `json:"jobs"` + // leader + Leader bool `json:"leader,omitempty"` + // name Name string `json:"name,omitempty"` @@ -44,6 +47,9 @@ type AppRunner struct { // org id OrgID string `json:"org_id,omitempty"` + // platform + Platform string `json:"platform,omitempty"` + // runner group RunnerGroup *AppRunnerGroup `json:"runner_group,omitempty"` @@ -59,6 +65,9 @@ type AppRunner struct { // status description StatusDescription string `json:"status_description,omitempty"` + // tainted + Tainted bool `json:"tainted,omitempty"` + // updated at UpdatedAt string `json:"updated_at,omitempty"` } diff --git a/sdks/nuon-runner-go/models/app_runner_group.go b/sdks/nuon-runner-go/models/app_runner_group.go index 1c063893d5..303c303e4e 100644 --- a/sdks/nuon-runner-go/models/app_runner_group.go +++ b/sdks/nuon-runner-go/models/app_runner_group.go @@ -38,8 +38,8 @@ type AppRunnerGroup struct { // owner type OwnerType string `json:"owner_type,omitempty"` - // platform - Platform AppAppRunnerType `json:"platform,omitempty"` + // Deprecated: Platform is being phased out in favor of per-runner Runner.Platform field. + Platform string `json:"platform,omitempty"` // runners Runners []*AppRunner `json:"runners"` @@ -58,10 +58,6 @@ type AppRunnerGroup struct { func (m *AppRunnerGroup) Validate(formats strfmt.Registry) error { var res []error - if err := m.validatePlatform(formats); err != nil { - res = append(res, err) - } - if err := m.validateRunners(formats); err != nil { res = append(res, err) } @@ -80,27 +76,6 @@ func (m *AppRunnerGroup) Validate(formats strfmt.Registry) error { return nil } -func (m *AppRunnerGroup) validatePlatform(formats strfmt.Registry) error { - if swag.IsZero(m.Platform) { // not required - return nil - } - - if err := m.Platform.Validate(formats); err != nil { - ve := new(errors.Validation) - if stderrors.As(err, &ve) { - return ve.ValidateName("platform") - } - ce := new(errors.CompositeError) - if stderrors.As(err, &ce) { - return ce.ValidateName("platform") - } - - return err - } - - return nil -} - func (m *AppRunnerGroup) validateRunners(formats strfmt.Registry) error { if swag.IsZero(m.Runners) { // not required return nil @@ -179,10 +154,6 @@ func (m *AppRunnerGroup) validateType(formats strfmt.Registry) error { func (m *AppRunnerGroup) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error - if err := m.contextValidatePlatform(ctx, formats); err != nil { - res = append(res, err) - } - if err := m.contextValidateRunners(ctx, formats); err != nil { res = append(res, err) } @@ -201,28 +172,6 @@ func (m *AppRunnerGroup) ContextValidate(ctx context.Context, formats strfmt.Reg return nil } -func (m *AppRunnerGroup) contextValidatePlatform(ctx context.Context, formats strfmt.Registry) error { - - if swag.IsZero(m.Platform) { // not required - return nil - } - - if err := m.Platform.ContextValidate(ctx, formats); err != nil { - ve := new(errors.Validation) - if stderrors.As(err, &ve) { - return ve.ValidateName("platform") - } - ce := new(errors.CompositeError) - if stderrors.As(err, &ce) { - return ce.ValidateName("platform") - } - - return err - } - - return nil -} - func (m *AppRunnerGroup) contextValidateRunners(ctx context.Context, formats strfmt.Registry) error { for i := 0; i < len(m.Runners); i++ {