Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement repo agents #4150

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions cmd/server/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2341,6 +2341,193 @@ const docTemplate = `{
}
}
},
"/repos/{repo_id}/agents": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "List agents for a repository",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the repository's id",
"name": "repo_id",
"in": "path",
"required": true
},
{
"type": "integer",
"default": 1,
"description": "for response pagination, page offset number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"default": 50,
"description": "for response pagination, max items per page",
"name": "perPage",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Agent"
}
}
}
}
},
"post": {
"description": "Creates a new agent with a random token, scoped to the specified repository",
"produces": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "Create a new repository-scoped agent",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the repository's id",
"name": "repo_id",
"in": "path",
"required": true
},
{
"description": "the agent's data (only 'name' and 'no_schedule' are read)",
"name": "agent",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Agent"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Agent"
}
}
}
}
},
"/repos/{repo_id}/agents/{agent_id}": {
"delete": {
"produces": [
"text/plain"
],
"tags": [
"Agents"
],
"summary": "Delete a repository-scoped agent",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the repository's id",
"name": "repo_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the agent's id",
"name": "agent_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
},
"patch": {
"produces": [
"application/json"
],
"tags": [
"Agents"
],
"summary": "Update a repository-scoped agent",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the repository's id",
"name": "repo_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the agent's id",
"name": "agent_id",
"in": "path",
"required": true
},
{
"description": "the agent's updated data",
"name": "agent",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Agent"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Agent"
}
}
}
}
},
"/repos/{repo_id}/branches": {
"get": {
"produces": [
Expand Down Expand Up @@ -4621,6 +4808,10 @@ const docTemplate = `{
"platform": {
"type": "string"
},
"repo_id": {
"description": "RepoID is counted as unset if set to -1, this is done to ensure a new(Agent) still enforce the OrgID check by default",
"type": "integer"
},
"token": {
"type": "string"
},
Expand Down
173 changes: 173 additions & 0 deletions server/api/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
Name: in.Name,
OwnerID: user.ID,
OrgID: model.IDNotSet,
RepoID: model.IDNotSet,

Check warning on line 181 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L181

Added line #L181 was not covered by tests
NoSchedule: in.NoSchedule,
Token: model.GenerateNewAgentToken(),
}
Expand Down Expand Up @@ -267,6 +268,7 @@
Name: in.Name,
OwnerID: user.ID,
OrgID: orgID,
RepoID: model.IDNotSet,

Check warning on line 271 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L271

Added line #L271 was not covered by tests
NoSchedule: in.NoSchedule,
Token: model.GenerateNewAgentToken(),
}
Expand Down Expand Up @@ -421,3 +423,174 @@

c.Status(http.StatusNoContent)
}

//
// Repo Agents.
//

// PostRepoAgent
//
// @Summary Create a new repository-scoped agent
// @Description Creates a new agent with a random token, scoped to the specified repository
// @Router /repos/{repo_id}/agents [post]
// @Produce json
// @Success 200 {object} Agent
// @Tags Agents
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param repo_id path int true "the repository's id"
// @Param agent body Agent true "the agent's data (only 'name' and 'no_schedule' are read)"
func PostRepoAgent(c *gin.Context) {
_store := store.FromContext(c)
user := session.User(c)
repo := session.Repo(c)

in := new(model.Agent)
if err := c.Bind(in); err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}

Check warning on line 451 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L442-L451

Added lines #L442 - L451 were not covered by tests

agent := &model.Agent{
Name: in.Name,
NoSchedule: in.NoSchedule,
OwnerID: user.ID,
OrgID: repo.OrgID,
RepoID: repo.ID,
Token: model.GenerateNewAgentToken(),
}

if err := _store.AgentCreate(agent); err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}

Check warning on line 465 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L453-L465

Added lines #L453 - L465 were not covered by tests

c.JSON(http.StatusOK, agent)

Check warning on line 467 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L467

Added line #L467 was not covered by tests
}

// GetRepoAgents
//
// @Summary List agents for a repository
// @Router /repos/{repo_id}/agents [get]
// @Produce json
// @Success 200 {array} Agent
// @Tags Agents
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param repo_id path int true "the repository's id"
// @Param page query int false "for response pagination, page offset number" default(1)
// @Param perPage query int false "for response pagination, max items per page" default(50)
func GetRepoAgents(c *gin.Context) {
_store := store.FromContext(c)
repo := session.Repo(c)

agents, err := _store.AgentListForRepo(repo.ID, session.Pagination(c))
if err != nil {
c.String(http.StatusInternalServerError, "Error getting agent list. %s", err)
return
}

Check warning on line 489 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L481-L489

Added lines #L481 - L489 were not covered by tests

c.JSON(http.StatusOK, agents)

Check warning on line 491 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L491

Added line #L491 was not covered by tests
}

// PatchRepoAgent
//
// @Summary Update a repository-scoped agent
// @Router /repos/{repo_id}/agents/{agent_id} [patch]
// @Produce json
// @Success 200 {object} Agent
// @Tags Agents
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param repo_id path int true "the repository's id"
// @Param agent_id path int true "the agent's id"
// @Param agent body Agent true "the agent's updated data"
func PatchRepoAgent(c *gin.Context) {
_store := store.FromContext(c)
repo := session.Repo(c)

agentID, err := strconv.ParseInt(c.Param("agent_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Invalid agent ID")
return
}

Check warning on line 513 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L505-L513

Added lines #L505 - L513 were not covered by tests

agent, err := _store.AgentFind(agentID)
if err != nil {
c.String(http.StatusNotFound, "Agent not found")
return
}

Check warning on line 519 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L515-L519

Added lines #L515 - L519 were not covered by tests

if agent.RepoID != repo.ID {
c.String(http.StatusBadRequest, "Agent does not belong to this repository")
return
}

Check warning on line 524 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L521-L524

Added lines #L521 - L524 were not covered by tests

in := new(model.Agent)
if err := c.Bind(in); err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}

Check warning on line 530 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L526-L530

Added lines #L526 - L530 were not covered by tests

// Update allowed fields
agent.Name = in.Name
agent.NoSchedule = in.NoSchedule
if agent.NoSchedule {
server.Config.Services.Queue.KickAgentWorkers(agent.ID)
}

Check warning on line 537 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L533-L537

Added lines #L533 - L537 were not covered by tests

if err := _store.AgentUpdate(agent); err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}

Check warning on line 542 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L539-L542

Added lines #L539 - L542 were not covered by tests

c.JSON(http.StatusOK, agent)

Check warning on line 544 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L544

Added line #L544 was not covered by tests
}

// DeleteRepoAgent
//
// @Summary Delete a repository-scoped agent
// @Router /repos/{repo_id}/agents/{agent_id} [delete]
// @Produce plain
// @Success 204
// @Tags Agents
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param repo_id path int true "the repository's id"
// @Param agent_id path int true "the agent's id"
func DeleteRepoAgent(c *gin.Context) {
_store := store.FromContext(c)
repo := session.Repo(c)

agentID, err := strconv.ParseInt(c.Param("agent_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Invalid agent ID")
return
}

Check warning on line 565 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L557-L565

Added lines #L557 - L565 were not covered by tests

agent, err := _store.AgentFind(agentID)
if err != nil {
c.String(http.StatusNotFound, "Agent not found")
return
}

Check warning on line 571 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L567-L571

Added lines #L567 - L571 were not covered by tests

if agent.RepoID != repo.ID {
c.String(http.StatusBadRequest, "Agent does not belong to this repository")
return
}

Check warning on line 576 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L573-L576

Added lines #L573 - L576 were not covered by tests

// Check if the agent has any running tasks
info := server.Config.Services.Queue.Info(c)
for _, task := range info.Running {
if task.AgentID == agent.ID {
c.String(http.StatusConflict, "Agent has running tasks")
return
}

Check warning on line 584 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L579-L584

Added lines #L579 - L584 were not covered by tests
}

// Kick workers to remove the agent from the queue
server.Config.Services.Queue.KickAgentWorkers(agent.ID)

if err := _store.AgentDelete(agent); err != nil {
c.String(http.StatusInternalServerError, "Error deleting agent. %s", err)
return
}

Check warning on line 593 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L588-L593

Added lines #L588 - L593 were not covered by tests

c.Status(http.StatusNoContent)

Check warning on line 595 in server/api/agent.go

View check run for this annotation

Codecov / codecov/patch

server/api/agent.go#L595

Added line #L595 was not covered by tests
}
Loading