diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index e91133386c7..6940b126031 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -82,7 +82,7 @@ const docTemplate = `{ "required": true }, { - "description": "the agent's data (only 'name' and 'no_schedule' are read)", + "description": "the agent's data (only 'name', 'no_schedule' and 'filters' are read)", "name": "agent", "in": "body", "required": true, @@ -1141,7 +1141,7 @@ const docTemplate = `{ "required": true }, { - "description": "the agent's data (only 'name' and 'no_schedule' are read)", + "description": "the agent's data (only 'name', 'no_schedule' and 'filters' are read)", "name": "agent", "in": "body", "required": true, @@ -4589,6 +4589,13 @@ const docTemplate = `{ "created": { "type": "integer" }, + "filters": { + "description": "Server side enforced agent filters", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "id": { "type": "integer" }, diff --git a/server/api/agent.go b/server/api/agent.go index bf473c1d4e8..56e2d0b2307 100644 --- a/server/api/agent.go +++ b/server/api/agent.go @@ -140,6 +140,7 @@ func PatchAgent(c *gin.Context) { // Update allowed fields agent.Name = in.Name + agent.Filters = in.Filters agent.NoSchedule = in.NoSchedule if agent.NoSchedule { server.Config.Services.Queue.KickAgentWorkers(agent.ID) @@ -163,7 +164,7 @@ func PatchAgent(c *gin.Context) { // @Success 200 {object} Agent // @Tags Agents // @Param Authorization header string true "Insert your personal access token" default(Bearer ) -// @Param agent body Agent true "the agent's data (only 'name' and 'no_schedule' are read)" +// @Param agent body Agent true "the agent's data (only 'name', 'no_schedule' and 'filters' are read)" func PostAgent(c *gin.Context) { in := &model.Agent{} err := c.Bind(in) @@ -175,11 +176,12 @@ func PostAgent(c *gin.Context) { user := session.User(c) agent := &model.Agent{ - Name: in.Name, - OwnerID: user.ID, - OrgID: model.IDNotSet, - NoSchedule: in.NoSchedule, - Token: model.GenerateNewAgentToken(), + Name: in.Name, + OwnerID: user.ID, + OrgID: model.IDNotSet, + NoSchedule: in.NoSchedule, + Token: model.GenerateNewAgentToken(), + agent.Filters: in.Filters, } if err = store.FromContext(c).AgentCreate(agent); err != nil { c.String(http.StatusInternalServerError, err.Error()) @@ -245,7 +247,7 @@ func DeleteAgent(c *gin.Context) { // @Tags Agents // @Param Authorization header string true "Insert your personal access token" default(Bearer ) // @Param org_id path int true "the organization's id" -// @Param agent body Agent true "the agent's data (only 'name' and 'no_schedule' are read)" +// @Param agent body Agent true "the agent's data (only 'name', 'no_schedule' and 'filters' are read)" func PostOrgAgent(c *gin.Context) { _store := store.FromContext(c) user := session.User(c) @@ -264,11 +266,12 @@ func PostOrgAgent(c *gin.Context) { } agent := &model.Agent{ - Name: in.Name, - OwnerID: user.ID, - OrgID: orgID, - NoSchedule: in.NoSchedule, - Token: model.GenerateNewAgentToken(), + Name: in.Name, + OwnerID: user.ID, + OrgID: orgID, + NoSchedule: in.NoSchedule, + Token: model.GenerateNewAgentToken(), + agent.Filters: in.Filters, } if err = _store.AgentCreate(agent); err != nil { @@ -353,6 +356,7 @@ func PatchOrgAgent(c *gin.Context) { // Update allowed fields agent.Name = in.Name + agent.Filters = in.Filters agent.NoSchedule = in.NoSchedule if agent.NoSchedule { server.Config.Services.Queue.KickAgentWorkers(agent.ID) diff --git a/server/model/agent.go b/server/model/agent.go index 9a360cec48a..6980d033bdb 100644 --- a/server/model/agent.go +++ b/server/model/agent.go @@ -37,6 +37,8 @@ type Agent struct { NoSchedule bool `json:"no_schedule" xorm:"no_schedule"` // OrgID is counted as unset if set to -1, this is done to ensure a new(Agent) still enforce the OrgID check by default OrgID int64 `json:"org_id" xorm:"INDEX 'org_id'"` + // Server side enforced agent filters + Filters map[string]string `json:"filters" xorm:"'filters' json"` } // @name Agent const ( @@ -58,7 +60,10 @@ func GenerateNewAgentToken() string { } func (a *Agent) GetServerLabels() (map[string]string, error) { - filters := make(map[string]string) + filters := a.Filters + if filters == nil { + filters = make(map[string]string) + } // enforce filters for user and organization agents if a.OrgID != IDNotSet { diff --git a/server/store/datastore/migration/016_add_server-side-enforced-agent-labels.go b/server/store/datastore/migration/016_add_server-side-enforced-agent-labels.go new file mode 100644 index 00000000000..66727aca300 --- /dev/null +++ b/server/store/datastore/migration/016_add_server-side-enforced-agent-labels.go @@ -0,0 +1,43 @@ +// Copyright 2024 Woodpecker Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package migration + +import ( + "fmt" + + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" + + "go.woodpecker-ci.org/woodpecker/v2/server/model" +) + +type agentV016 struct { + ID int64 `xorm:"pk autoincr 'id'"` + Filters map[string]string `xorm:"'filters' json"` +} + +func (agentV016) TableName() string { + return "agents" +} + +var addServerSideEnforcedAgentLabels = xormigrate.Migration{ + ID: "add-server-side-enforced-agent-labels", + MigrateSession: func(sess *xorm.Session) (err error) { + if err := sess.Sync(new(agentV016)); err != nil { + return fmt.Errorf("sync models failed: %w", err) + } + return nil + }, +} diff --git a/server/store/datastore/migration/migration.go b/server/store/datastore/migration/migration.go index 9a885561740..80b09a54584 100644 --- a/server/store/datastore/migration/migration.go +++ b/server/store/datastore/migration/migration.go @@ -44,6 +44,7 @@ var migrationTasks = []*xormigrate.Migration{ &fixV31Registries, &removeOldMigrationsOfV1, &addOrgAgents, + &addServerSideEnforcedAgentLabels, } var allBeans = []any{ diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 26c04e5afec..4f4c5ac007a 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -324,7 +324,15 @@ "never": "Never", "delete_confirm": "Do you really want to delete this agent? It will not be able to connect to the server anymore.", "edit_agent": "Edit agent", - "delete_agent": "Delete agent" + "delete_agent": "Delete agent", + "filters": { + "name": "Filters", + "desc": "Server side enforced labels of agents to filter workflows", + "key": "Label name ...", + "value": "Filter value ...", + "add": "Add filter", + "delete": "Delete filter" + } }, "queue": { "queue": "Queue", diff --git a/web/src/lib/api/types/agent.ts b/web/src/lib/api/types/agent.ts index 62868a609ce..4610e71a399 100644 --- a/web/src/lib/api/types/agent.ts +++ b/web/src/lib/api/types/agent.ts @@ -12,4 +12,5 @@ export interface Agent { capacity: number; version: string; no_schedule: boolean; + filters?: Record; }