mirror of https://github.com/coder/coder.git
feat: add port-sharing backend (#11939)
This commit is contained in:
parent
c939416702
commit
3ab3a62bef
|
@ -7188,6 +7188,125 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/workspaces/{workspace}/port-share": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"PortSharing"
|
||||||
|
],
|
||||||
|
"summary": "Get workspace agent port shares",
|
||||||
|
"operationId": "get-workspace-agent-port-shares",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Workspace ID",
|
||||||
|
"name": "workspace",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShares"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"PortSharing"
|
||||||
|
],
|
||||||
|
"summary": "Upsert workspace agent port share",
|
||||||
|
"operationId": "upsert-workspace-agent-port-share",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Workspace ID",
|
||||||
|
"name": "workspace",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Upsert port sharing level request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UpsertWorkspaceAgentPortShareRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShare"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"PortSharing"
|
||||||
|
],
|
||||||
|
"summary": "Get workspace agent port shares",
|
||||||
|
"operationId": "get-workspace-agent-port-shares",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Workspace ID",
|
||||||
|
"name": "workspace",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Delete port sharing level request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.DeleteWorkspaceAgentPortShareRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/workspaces/{workspace}/resolve-autostart": {
|
"/workspaces/{workspace}/resolve-autostart": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -9028,6 +9147,17 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.DeleteWorkspaceAgentPortShareRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"agent_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.DeploymentConfig": {
|
"codersdk.DeploymentConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -9337,13 +9467,15 @@ const docTemplate = `{
|
||||||
"codersdk.Experiment": {
|
"codersdk.Experiment": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"example"
|
"example",
|
||||||
|
"shared-ports"
|
||||||
],
|
],
|
||||||
"x-enum-comments": {
|
"x-enum-comments": {
|
||||||
"ExperimentExample": "This isn't used for anything."
|
"ExperimentExample": "This isn't used for anything."
|
||||||
},
|
},
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"ExperimentExample"
|
"ExperimentExample",
|
||||||
|
"ExperimentSharedPorts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.ExternalAuth": {
|
"codersdk.ExternalAuth": {
|
||||||
|
@ -11022,6 +11154,9 @@ const docTemplate = `{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uuid"
|
"format": "uuid"
|
||||||
},
|
},
|
||||||
|
"max_port_share_level": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
|
||||||
|
},
|
||||||
"max_ttl_ms": {
|
"max_ttl_ms": {
|
||||||
"description": "TODO(@dean): remove max_ttl once autostop_requirement is matured",
|
"description": "TODO(@dean): remove max_ttl once autostop_requirement is matured",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
@ -11839,6 +11974,20 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.UpsertWorkspaceAgentPortShareRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"agent_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"share_level": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.User": {
|
"codersdk.User": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -12533,6 +12682,48 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.WorkspaceAgentPortShare": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"agent_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"share_level": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.WorkspaceAgentPortShareLevel": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"owner",
|
||||||
|
"authenticated",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"WorkspaceAgentPortShareLevelOwner",
|
||||||
|
"WorkspaceAgentPortShareLevelAuthenticated",
|
||||||
|
"WorkspaceAgentPortShareLevelPublic"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"codersdk.WorkspaceAgentPortShares": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"shares": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShare"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.WorkspaceAgentScript": {
|
"codersdk.WorkspaceAgentScript": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -6350,6 +6350,111 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/workspaces/{workspace}/port-share": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["PortSharing"],
|
||||||
|
"summary": "Get workspace agent port shares",
|
||||||
|
"operationId": "get-workspace-agent-port-shares",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Workspace ID",
|
||||||
|
"name": "workspace",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShares"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": ["application/json"],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["PortSharing"],
|
||||||
|
"summary": "Upsert workspace agent port share",
|
||||||
|
"operationId": "upsert-workspace-agent-port-share",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Workspace ID",
|
||||||
|
"name": "workspace",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Upsert port sharing level request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UpsertWorkspaceAgentPortShareRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShare"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": ["application/json"],
|
||||||
|
"tags": ["PortSharing"],
|
||||||
|
"summary": "Get workspace agent port shares",
|
||||||
|
"operationId": "get-workspace-agent-port-shares",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "Workspace ID",
|
||||||
|
"name": "workspace",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Delete port sharing level request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.DeleteWorkspaceAgentPortShareRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/workspaces/{workspace}/resolve-autostart": {
|
"/workspaces/{workspace}/resolve-autostart": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -8064,6 +8169,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.DeleteWorkspaceAgentPortShareRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"agent_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.DeploymentConfig": {
|
"codersdk.DeploymentConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -8368,11 +8484,11 @@
|
||||||
},
|
},
|
||||||
"codersdk.Experiment": {
|
"codersdk.Experiment": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["example"],
|
"enum": ["example", "shared-ports"],
|
||||||
"x-enum-comments": {
|
"x-enum-comments": {
|
||||||
"ExperimentExample": "This isn't used for anything."
|
"ExperimentExample": "This isn't used for anything."
|
||||||
},
|
},
|
||||||
"x-enum-varnames": ["ExperimentExample"]
|
"x-enum-varnames": ["ExperimentExample", "ExperimentSharedPorts"]
|
||||||
},
|
},
|
||||||
"codersdk.ExternalAuth": {
|
"codersdk.ExternalAuth": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -9964,6 +10080,9 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uuid"
|
"format": "uuid"
|
||||||
},
|
},
|
||||||
|
"max_port_share_level": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
|
||||||
|
},
|
||||||
"max_ttl_ms": {
|
"max_ttl_ms": {
|
||||||
"description": "TODO(@dean): remove max_ttl once autostop_requirement is matured",
|
"description": "TODO(@dean): remove max_ttl once autostop_requirement is matured",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
@ -10730,6 +10849,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.UpsertWorkspaceAgentPortShareRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"agent_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"share_level": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.User": {
|
"codersdk.User": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["created_at", "email", "id", "username"],
|
"required": ["created_at", "email", "id", "username"],
|
||||||
|
@ -11403,6 +11536,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.WorkspaceAgentPortShare": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"agent_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"share_level": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.WorkspaceAgentPortShareLevel": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["owner", "authenticated", "public"],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"WorkspaceAgentPortShareLevelOwner",
|
||||||
|
"WorkspaceAgentPortShareLevelAuthenticated",
|
||||||
|
"WorkspaceAgentPortShareLevelPublic"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"codersdk.WorkspaceAgentPortShares": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"shares": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShare"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.WorkspaceAgentScript": {
|
"codersdk.WorkspaceAgentScript": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -56,6 +56,7 @@ import (
|
||||||
"github.com/coder/coder/v2/coderd/httpapi"
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
"github.com/coder/coder/v2/coderd/httpmw"
|
"github.com/coder/coder/v2/coderd/httpmw"
|
||||||
"github.com/coder/coder/v2/coderd/metricscache"
|
"github.com/coder/coder/v2/coderd/metricscache"
|
||||||
|
"github.com/coder/coder/v2/coderd/portsharing"
|
||||||
"github.com/coder/coder/v2/coderd/prometheusmetrics"
|
"github.com/coder/coder/v2/coderd/prometheusmetrics"
|
||||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
@ -400,6 +401,7 @@ func New(options *Options) *API {
|
||||||
}
|
}
|
||||||
|
|
||||||
api.AppearanceFetcher.Store(&appearance.DefaultFetcher)
|
api.AppearanceFetcher.Store(&appearance.DefaultFetcher)
|
||||||
|
api.PortSharer.Store(&portsharing.DefaultPortSharer)
|
||||||
api.SiteHandler = site.New(&site.Options{
|
api.SiteHandler = site.New(&site.Options{
|
||||||
BinFS: binFS,
|
BinFS: binFS,
|
||||||
BinHashes: binHashes,
|
BinHashes: binHashes,
|
||||||
|
@ -957,6 +959,14 @@ func New(options *Options) *API {
|
||||||
r.Delete("/favorite", api.deleteFavoriteWorkspace)
|
r.Delete("/favorite", api.deleteFavoriteWorkspace)
|
||||||
r.Put("/autoupdates", api.putWorkspaceAutoupdates)
|
r.Put("/autoupdates", api.putWorkspaceAutoupdates)
|
||||||
r.Get("/resolve-autostart", api.resolveAutostart)
|
r.Get("/resolve-autostart", api.resolveAutostart)
|
||||||
|
r.Route("/port-share", func(r chi.Router) {
|
||||||
|
r.Use(
|
||||||
|
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentSharedPorts),
|
||||||
|
)
|
||||||
|
r.Get("/", api.workspaceAgentPortShares)
|
||||||
|
r.Post("/", api.postWorkspaceAgentPortShare)
|
||||||
|
r.Delete("/", api.deleteWorkspaceAgentPortShare)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
|
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
|
||||||
|
@ -1107,6 +1117,7 @@ type API struct {
|
||||||
// AccessControlStore is a pointer to an atomic pointer since it is
|
// AccessControlStore is a pointer to an atomic pointer since it is
|
||||||
// passed to dbauthz.
|
// passed to dbauthz.
|
||||||
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
||||||
|
PortSharer atomic.Pointer[portsharing.PortSharer]
|
||||||
|
|
||||||
HTTPAuth *HTTPAuthorizer
|
HTTPAuth *HTTPAuthorizer
|
||||||
|
|
||||||
|
|
|
@ -891,6 +891,20 @@ func (q *querier) DeleteTailnetTunnel(ctx context.Context, arg database.DeleteTa
|
||||||
return q.db.DeleteTailnetTunnel(ctx, arg)
|
return q.db.DeleteTailnetTunnel(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg database.DeleteWorkspaceAgentPortShareParams) error {
|
||||||
|
w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleting a workspace port share is more akin to just updating the workspace.
|
||||||
|
if err = q.authorizeContext(ctx, rbac.ActionUpdate, w.RBACObject()); err != nil {
|
||||||
|
return xerrors.Errorf("authorize context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.db.DeleteWorkspaceAgentPortShare(ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) FavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
|
func (q *querier) FavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
|
||||||
fetch := func(ctx context.Context, id uuid.UUID) (database.Workspace, error) {
|
fetch := func(ctx context.Context, id uuid.UUID) (database.Workspace, error) {
|
||||||
return q.db.GetWorkspaceByID(ctx, id)
|
return q.db.GetWorkspaceByID(ctx, id)
|
||||||
|
@ -1868,6 +1882,20 @@ func (q *querier) GetWorkspaceAgentMetadata(ctx context.Context, arg database.Ge
|
||||||
return q.db.GetWorkspaceAgentMetadata(ctx, arg)
|
return q.db.GetWorkspaceAgentMetadata(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) GetWorkspaceAgentPortShare(ctx context.Context, arg database.GetWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||||
|
w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
|
||||||
|
if err != nil {
|
||||||
|
return database.WorkspaceAgentPortShare{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// reading a workspace port share is more akin to just reading the workspace.
|
||||||
|
if err = q.authorizeContext(ctx, rbac.ActionRead, w.RBACObject()); err != nil {
|
||||||
|
return database.WorkspaceAgentPortShare{}, xerrors.Errorf("authorize context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.db.GetWorkspaceAgentPortShare(ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
|
func (q *querier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
|
||||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
|
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -2500,6 +2528,20 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab
|
||||||
return q.db.InsertWorkspaceResourceMetadata(ctx, arg)
|
return q.db.InsertWorkspaceResourceMetadata(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
||||||
|
workspace, err := q.db.GetWorkspaceByID(ctx, workspaceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// listing port shares is more akin to reading the workspace.
|
||||||
|
if err := q.authorizeContext(ctx, rbac.ActionRead, workspace); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.db.ListWorkspaceAgentPortShares(ctx, workspaceID)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) RegisterWorkspaceProxy(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
func (q *querier) RegisterWorkspaceProxy(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||||
fetch := func(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
fetch := func(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||||
return q.db.GetWorkspaceProxyByID(ctx, arg.ID)
|
return q.db.GetWorkspaceProxyByID(ctx, arg.ID)
|
||||||
|
@ -3273,6 +3315,20 @@ func (q *querier) UpsertTailnetTunnel(ctx context.Context, arg database.UpsertTa
|
||||||
return q.db.UpsertTailnetTunnel(ctx, arg)
|
return q.db.UpsertTailnetTunnel(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||||
|
workspace, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
|
||||||
|
if err != nil {
|
||||||
|
return database.WorkspaceAgentPortShare{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace)
|
||||||
|
if err != nil {
|
||||||
|
return database.WorkspaceAgentPortShare{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.db.UpsertWorkspaceAgentPortShare(ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) GetAuthorizedTemplates(ctx context.Context, arg database.GetTemplatesWithFilterParams, _ rbac.PreparedAuthorized) ([]database.Template, error) {
|
func (q *querier) GetAuthorizedTemplates(ctx context.Context, arg database.GetTemplatesWithFilterParams, _ rbac.PreparedAuthorized) ([]database.Template, error) {
|
||||||
// TODO Delete this function, all GetTemplates should be authorized. For now just call getTemplates on the authz querier.
|
// TODO Delete this function, all GetTemplates should be authorized. For now just call getTemplates on the authz querier.
|
||||||
return q.GetTemplatesWithFilter(ctx, arg)
|
return q.GetTemplatesWithFilter(ctx, arg)
|
||||||
|
|
|
@ -822,8 +822,9 @@ func (s *MethodTestSuite) TestTemplate() {
|
||||||
s.Run("InsertTemplate", s.Subtest(func(db database.Store, check *expects) {
|
s.Run("InsertTemplate", s.Subtest(func(db database.Store, check *expects) {
|
||||||
orgID := uuid.New()
|
orgID := uuid.New()
|
||||||
check.Args(database.InsertTemplateParams{
|
check.Args(database.InsertTemplateParams{
|
||||||
Provisioner: "echo",
|
Provisioner: "echo",
|
||||||
OrganizationID: orgID,
|
OrganizationID: orgID,
|
||||||
|
MaxPortSharingLevel: database.AppSharingLevelOwner,
|
||||||
}).Asserts(rbac.ResourceTemplate.InOrg(orgID), rbac.ActionCreate)
|
}).Asserts(rbac.ResourceTemplate.InOrg(orgID), rbac.ActionCreate)
|
||||||
}))
|
}))
|
||||||
s.Run("InsertTemplateVersion", s.Subtest(func(db database.Store, check *expects) {
|
s.Run("InsertTemplateVersion", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
@ -890,7 +891,8 @@ func (s *MethodTestSuite) TestTemplate() {
|
||||||
s.Run("UpdateTemplateMetaByID", s.Subtest(func(db database.Store, check *expects) {
|
s.Run("UpdateTemplateMetaByID", s.Subtest(func(db database.Store, check *expects) {
|
||||||
t1 := dbgen.Template(s.T(), db, database.Template{})
|
t1 := dbgen.Template(s.T(), db, database.Template{})
|
||||||
check.Args(database.UpdateTemplateMetaByIDParams{
|
check.Args(database.UpdateTemplateMetaByIDParams{
|
||||||
ID: t1.ID,
|
ID: t1.ID,
|
||||||
|
MaxPortSharingLevel: "owner",
|
||||||
}).Asserts(t1, rbac.ActionUpdate)
|
}).Asserts(t1, rbac.ActionUpdate)
|
||||||
}))
|
}))
|
||||||
s.Run("UpdateTemplateVersionByID", s.Subtest(func(db database.Store, check *expects) {
|
s.Run("UpdateTemplateVersionByID", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
@ -1601,6 +1603,47 @@ func (s *MethodTestSuite) TestWorkspace() {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MethodTestSuite) TestWorkspacePortSharing() {
|
||||||
|
s.Run("UpsertWorkspaceAgentPortShare", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
u := dbgen.User(s.T(), db, database.User{})
|
||||||
|
ws := dbgen.Workspace(s.T(), db, database.Workspace{OwnerID: u.ID})
|
||||||
|
ps := dbgen.WorkspaceAgentPortShare(s.T(), db, database.WorkspaceAgentPortShare{WorkspaceID: ws.ID})
|
||||||
|
//nolint:gosimple // casting is not a simplification
|
||||||
|
check.Args(database.UpsertWorkspaceAgentPortShareParams{
|
||||||
|
WorkspaceID: ps.WorkspaceID,
|
||||||
|
AgentName: ps.AgentName,
|
||||||
|
Port: ps.Port,
|
||||||
|
ShareLevel: ps.ShareLevel,
|
||||||
|
}).Asserts(ws, rbac.ActionUpdate).Returns(ps)
|
||||||
|
}))
|
||||||
|
s.Run("GetWorkspaceAgentPortShare", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
u := dbgen.User(s.T(), db, database.User{})
|
||||||
|
ws := dbgen.Workspace(s.T(), db, database.Workspace{OwnerID: u.ID})
|
||||||
|
ps := dbgen.WorkspaceAgentPortShare(s.T(), db, database.WorkspaceAgentPortShare{WorkspaceID: ws.ID})
|
||||||
|
check.Args(database.GetWorkspaceAgentPortShareParams{
|
||||||
|
WorkspaceID: ps.WorkspaceID,
|
||||||
|
AgentName: ps.AgentName,
|
||||||
|
Port: ps.Port,
|
||||||
|
}).Asserts(ws, rbac.ActionRead).Returns(ps)
|
||||||
|
}))
|
||||||
|
s.Run("ListWorkspaceAgentPortShares", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
u := dbgen.User(s.T(), db, database.User{})
|
||||||
|
ws := dbgen.Workspace(s.T(), db, database.Workspace{OwnerID: u.ID})
|
||||||
|
ps := dbgen.WorkspaceAgentPortShare(s.T(), db, database.WorkspaceAgentPortShare{WorkspaceID: ws.ID})
|
||||||
|
check.Args(ws.ID).Asserts(ws, rbac.ActionRead).Returns([]database.WorkspaceAgentPortShare{ps})
|
||||||
|
}))
|
||||||
|
s.Run("DeleteWorkspaceAgentPortShare", s.Subtest(func(db database.Store, check *expects) {
|
||||||
|
u := dbgen.User(s.T(), db, database.User{})
|
||||||
|
ws := dbgen.Workspace(s.T(), db, database.Workspace{OwnerID: u.ID})
|
||||||
|
ps := dbgen.WorkspaceAgentPortShare(s.T(), db, database.WorkspaceAgentPortShare{WorkspaceID: ws.ID})
|
||||||
|
check.Args(database.DeleteWorkspaceAgentPortShareParams{
|
||||||
|
WorkspaceID: ps.WorkspaceID,
|
||||||
|
AgentName: ps.AgentName,
|
||||||
|
Port: ps.Port,
|
||||||
|
}).Asserts(ws, rbac.ActionUpdate).Returns()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
func (s *MethodTestSuite) TestExtraMethods() {
|
func (s *MethodTestSuite) TestExtraMethods() {
|
||||||
s.Run("GetProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) {
|
s.Run("GetProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) {
|
||||||
d, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
|
d, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
|
||||||
|
|
|
@ -90,6 +90,7 @@ func Template(t testing.TB, db database.Store, seed database.Template) database.
|
||||||
GroupACL: seed.GroupACL,
|
GroupACL: seed.GroupACL,
|
||||||
DisplayName: takeFirst(seed.DisplayName, namesgenerator.GetRandomName(1)),
|
DisplayName: takeFirst(seed.DisplayName, namesgenerator.GetRandomName(1)),
|
||||||
AllowUserCancelWorkspaceJobs: seed.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: seed.AllowUserCancelWorkspaceJobs,
|
||||||
|
MaxPortSharingLevel: takeFirst(seed.MaxPortSharingLevel, database.AppSharingLevelOwner),
|
||||||
})
|
})
|
||||||
require.NoError(t, err, "insert template")
|
require.NoError(t, err, "insert template")
|
||||||
|
|
||||||
|
@ -133,6 +134,17 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey) (key database
|
||||||
return key, fmt.Sprintf("%s-%s", key.ID, secret)
|
return key, fmt.Sprintf("%s-%s", key.ID, secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WorkspaceAgentPortShare(t testing.TB, db database.Store, orig database.WorkspaceAgentPortShare) database.WorkspaceAgentPortShare {
|
||||||
|
ps, err := db.UpsertWorkspaceAgentPortShare(genCtx, database.UpsertWorkspaceAgentPortShareParams{
|
||||||
|
WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()),
|
||||||
|
AgentName: takeFirst(orig.AgentName, namesgenerator.GetRandomName(1)),
|
||||||
|
Port: takeFirst(orig.Port, 8080),
|
||||||
|
ShareLevel: takeFirst(orig.ShareLevel, database.AppSharingLevelPublic),
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "insert workspace agent")
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgent) database.WorkspaceAgent {
|
func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgent) database.WorkspaceAgent {
|
||||||
agt, err := db.InsertWorkspaceAgent(genCtx, database.InsertWorkspaceAgentParams{
|
agt, err := db.InsertWorkspaceAgent(genCtx, database.InsertWorkspaceAgentParams{
|
||||||
ID: takeFirst(orig.ID, uuid.New()),
|
ID: takeFirst(orig.ID, uuid.New()),
|
||||||
|
|
|
@ -147,6 +147,7 @@ type data struct {
|
||||||
workspaceAgentLogs []database.WorkspaceAgentLog
|
workspaceAgentLogs []database.WorkspaceAgentLog
|
||||||
workspaceAgentLogSources []database.WorkspaceAgentLogSource
|
workspaceAgentLogSources []database.WorkspaceAgentLogSource
|
||||||
workspaceAgentScripts []database.WorkspaceAgentScript
|
workspaceAgentScripts []database.WorkspaceAgentScript
|
||||||
|
workspaceAgentPortShares []database.WorkspaceAgentPortShare
|
||||||
workspaceApps []database.WorkspaceApp
|
workspaceApps []database.WorkspaceApp
|
||||||
workspaceAppStatsLastInsertID int64
|
workspaceAppStatsLastInsertID int64
|
||||||
workspaceAppStats []database.WorkspaceAppStat
|
workspaceAppStats []database.WorkspaceAppStat
|
||||||
|
@ -1322,6 +1323,25 @@ func (*FakeQuerier) DeleteTailnetTunnel(_ context.Context, arg database.DeleteTa
|
||||||
return database.DeleteTailnetTunnelRow{}, ErrUnimplemented
|
return database.DeleteTailnetTunnelRow{}, ErrUnimplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *FakeQuerier) DeleteWorkspaceAgentPortShare(_ context.Context, arg database.DeleteWorkspaceAgentPortShareParams) error {
|
||||||
|
err := validateDatabaseType(arg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
q.mutex.Lock()
|
||||||
|
defer q.mutex.Unlock()
|
||||||
|
|
||||||
|
for i, share := range q.workspaceAgentPortShares {
|
||||||
|
if share.WorkspaceID == arg.WorkspaceID && share.AgentName == arg.AgentName && share.Port == arg.Port {
|
||||||
|
q.workspaceAgentPortShares = append(q.workspaceAgentPortShares[:i], q.workspaceAgentPortShares[i+1:]...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (q *FakeQuerier) FavoriteWorkspace(_ context.Context, arg uuid.UUID) error {
|
func (q *FakeQuerier) FavoriteWorkspace(_ context.Context, arg uuid.UUID) error {
|
||||||
err := validateDatabaseType(arg)
|
err := validateDatabaseType(arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -4159,6 +4179,24 @@ func (q *FakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, arg database.
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *FakeQuerier) GetWorkspaceAgentPortShare(_ context.Context, arg database.GetWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||||
|
err := validateDatabaseType(arg)
|
||||||
|
if err != nil {
|
||||||
|
return database.WorkspaceAgentPortShare{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q.mutex.RLock()
|
||||||
|
defer q.mutex.RUnlock()
|
||||||
|
|
||||||
|
for _, share := range q.workspaceAgentPortShares {
|
||||||
|
if share.WorkspaceID == arg.WorkspaceID && share.AgentName == arg.AgentName && share.Port == arg.Port {
|
||||||
|
return share, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.WorkspaceAgentPortShare{}, sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
func (q *FakeQuerier) GetWorkspaceAgentScriptsByAgentIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
|
func (q *FakeQuerier) GetWorkspaceAgentScriptsByAgentIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
|
||||||
q.mutex.RLock()
|
q.mutex.RLock()
|
||||||
defer q.mutex.RUnlock()
|
defer q.mutex.RUnlock()
|
||||||
|
@ -5374,6 +5412,7 @@ func (q *FakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl
|
||||||
AllowUserCancelWorkspaceJobs: arg.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: arg.AllowUserCancelWorkspaceJobs,
|
||||||
AllowUserAutostart: true,
|
AllowUserAutostart: true,
|
||||||
AllowUserAutostop: true,
|
AllowUserAutostop: true,
|
||||||
|
MaxPortSharingLevel: arg.MaxPortSharingLevel,
|
||||||
}
|
}
|
||||||
q.templates = append(q.templates, template)
|
q.templates = append(q.templates, template)
|
||||||
return nil
|
return nil
|
||||||
|
@ -6006,6 +6045,20 @@ func (q *FakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg dat
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *FakeQuerier) ListWorkspaceAgentPortShares(_ context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
||||||
|
q.mutex.Lock()
|
||||||
|
defer q.mutex.Unlock()
|
||||||
|
|
||||||
|
shares := []database.WorkspaceAgentPortShare{}
|
||||||
|
for _, share := range q.workspaceAgentPortShares {
|
||||||
|
if share.WorkspaceID == workspaceID {
|
||||||
|
shares = append(shares, share)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shares, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (q *FakeQuerier) RegisterWorkspaceProxy(_ context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
func (q *FakeQuerier) RegisterWorkspaceProxy(_ context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
defer q.mutex.Unlock()
|
defer q.mutex.Unlock()
|
||||||
|
@ -6525,6 +6578,7 @@ func (q *FakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd
|
||||||
tpl.Icon = arg.Icon
|
tpl.Icon = arg.Icon
|
||||||
tpl.GroupACL = arg.GroupACL
|
tpl.GroupACL = arg.GroupACL
|
||||||
tpl.AllowUserCancelWorkspaceJobs = arg.AllowUserCancelWorkspaceJobs
|
tpl.AllowUserCancelWorkspaceJobs = arg.AllowUserCancelWorkspaceJobs
|
||||||
|
tpl.MaxPortSharingLevel = arg.MaxPortSharingLevel
|
||||||
q.templates[idx] = tpl
|
q.templates[idx] = tpl
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -7515,6 +7569,35 @@ func (*FakeQuerier) UpsertTailnetTunnel(_ context.Context, arg database.UpsertTa
|
||||||
return database.TailnetTunnel{}, ErrUnimplemented
|
return database.TailnetTunnel{}, ErrUnimplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *FakeQuerier) UpsertWorkspaceAgentPortShare(_ context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||||
|
err := validateDatabaseType(arg)
|
||||||
|
if err != nil {
|
||||||
|
return database.WorkspaceAgentPortShare{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q.mutex.Lock()
|
||||||
|
defer q.mutex.Unlock()
|
||||||
|
|
||||||
|
for i, share := range q.workspaceAgentPortShares {
|
||||||
|
if share.WorkspaceID == arg.WorkspaceID && share.Port == arg.Port && share.AgentName == arg.AgentName {
|
||||||
|
share.ShareLevel = arg.ShareLevel
|
||||||
|
q.workspaceAgentPortShares[i] = share
|
||||||
|
return share, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gosimple // casts are not a simplification
|
||||||
|
psl := database.WorkspaceAgentPortShare{
|
||||||
|
WorkspaceID: arg.WorkspaceID,
|
||||||
|
AgentName: arg.AgentName,
|
||||||
|
Port: arg.Port,
|
||||||
|
ShareLevel: arg.ShareLevel,
|
||||||
|
}
|
||||||
|
q.workspaceAgentPortShares = append(q.workspaceAgentPortShares, psl)
|
||||||
|
|
||||||
|
return psl, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.GetTemplatesWithFilterParams, prepared rbac.PreparedAuthorized) ([]database.Template, error) {
|
func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.GetTemplatesWithFilterParams, prepared rbac.PreparedAuthorized) ([]database.Template, error) {
|
||||||
if err := validateDatabaseType(arg); err != nil {
|
if err := validateDatabaseType(arg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -300,6 +300,13 @@ func (m metricsStore) DeleteTailnetTunnel(ctx context.Context, arg database.Dele
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m metricsStore) DeleteWorkspaceAgentPortShare(ctx context.Context, arg database.DeleteWorkspaceAgentPortShareParams) error {
|
||||||
|
start := time.Now()
|
||||||
|
r0 := m.s.DeleteWorkspaceAgentPortShare(ctx, arg)
|
||||||
|
m.queryLatencies.WithLabelValues("DeleteWorkspaceAgentPortShare").Observe(time.Since(start).Seconds())
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
func (m metricsStore) FavoriteWorkspace(ctx context.Context, arg uuid.UUID) error {
|
func (m metricsStore) FavoriteWorkspace(ctx context.Context, arg uuid.UUID) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0 := m.s.FavoriteWorkspace(ctx, arg)
|
r0 := m.s.FavoriteWorkspace(ctx, arg)
|
||||||
|
@ -1082,6 +1089,13 @@ func (m metricsStore) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAg
|
||||||
return metadata, err
|
return metadata, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m metricsStore) GetWorkspaceAgentPortShare(ctx context.Context, arg database.GetWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.GetWorkspaceAgentPortShare(ctx, arg)
|
||||||
|
m.queryLatencies.WithLabelValues("GetWorkspaceAgentPortShare").Observe(time.Since(start).Seconds())
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m metricsStore) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
|
func (m metricsStore) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.GetWorkspaceAgentScriptsByAgentIDs(ctx, ids)
|
r0, r1 := m.s.GetWorkspaceAgentScriptsByAgentIDs(ctx, ids)
|
||||||
|
@ -1607,6 +1621,13 @@ func (m metricsStore) InsertWorkspaceResourceMetadata(ctx context.Context, arg d
|
||||||
return metadata, err
|
return metadata, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m metricsStore) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.ListWorkspaceAgentPortShares(ctx, workspaceID)
|
||||||
|
m.queryLatencies.WithLabelValues("ListWorkspaceAgentPortShares").Observe(time.Since(start).Seconds())
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m metricsStore) RegisterWorkspaceProxy(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
func (m metricsStore) RegisterWorkspaceProxy(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
proxy, err := m.s.RegisterWorkspaceProxy(ctx, arg)
|
proxy, err := m.s.RegisterWorkspaceProxy(ctx, arg)
|
||||||
|
@ -2122,6 +2143,13 @@ func (m metricsStore) UpsertTailnetTunnel(ctx context.Context, arg database.Upse
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m metricsStore) UpsertWorkspaceAgentPortShare(ctx context.Context, arg database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.UpsertWorkspaceAgentPortShare(ctx, arg)
|
||||||
|
m.queryLatencies.WithLabelValues("UpsertWorkspaceAgentPortShare").Observe(time.Since(start).Seconds())
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m metricsStore) GetAuthorizedTemplates(ctx context.Context, arg database.GetTemplatesWithFilterParams, prepared rbac.PreparedAuthorized) ([]database.Template, error) {
|
func (m metricsStore) GetAuthorizedTemplates(ctx context.Context, arg database.GetTemplatesWithFilterParams, prepared rbac.PreparedAuthorized) ([]database.Template, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
templates, err := m.s.GetAuthorizedTemplates(ctx, arg, prepared)
|
templates, err := m.s.GetAuthorizedTemplates(ctx, arg, prepared)
|
||||||
|
|
|
@ -500,6 +500,20 @@ func (mr *MockStoreMockRecorder) DeleteTailnetTunnel(arg0, arg1 any) *gomock.Cal
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetTunnel", reflect.TypeOf((*MockStore)(nil).DeleteTailnetTunnel), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetTunnel", reflect.TypeOf((*MockStore)(nil).DeleteTailnetTunnel), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteWorkspaceAgentPortShare mocks base method.
|
||||||
|
func (m *MockStore) DeleteWorkspaceAgentPortShare(arg0 context.Context, arg1 database.DeleteWorkspaceAgentPortShareParams) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "DeleteWorkspaceAgentPortShare", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWorkspaceAgentPortShare indicates an expected call of DeleteWorkspaceAgentPortShare.
|
||||||
|
func (mr *MockStoreMockRecorder) DeleteWorkspaceAgentPortShare(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWorkspaceAgentPortShare", reflect.TypeOf((*MockStore)(nil).DeleteWorkspaceAgentPortShare), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// FavoriteWorkspace mocks base method.
|
// FavoriteWorkspace mocks base method.
|
||||||
func (m *MockStore) FavoriteWorkspace(arg0 context.Context, arg1 uuid.UUID) error {
|
func (m *MockStore) FavoriteWorkspace(arg0 context.Context, arg1 uuid.UUID) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -2254,6 +2268,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAgentMetadata(arg0, arg1 any) *gomo
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentMetadata), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentMetadata), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWorkspaceAgentPortShare mocks base method.
|
||||||
|
func (m *MockStore) GetWorkspaceAgentPortShare(arg0 context.Context, arg1 database.GetWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetWorkspaceAgentPortShare", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(database.WorkspaceAgentPortShare)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorkspaceAgentPortShare indicates an expected call of GetWorkspaceAgentPortShare.
|
||||||
|
func (mr *MockStoreMockRecorder) GetWorkspaceAgentPortShare(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentPortShare", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentPortShare), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// GetWorkspaceAgentScriptsByAgentIDs mocks base method.
|
// GetWorkspaceAgentScriptsByAgentIDs mocks base method.
|
||||||
func (m *MockStore) GetWorkspaceAgentScriptsByAgentIDs(arg0 context.Context, arg1 []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
|
func (m *MockStore) GetWorkspaceAgentScriptsByAgentIDs(arg0 context.Context, arg1 []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -3381,6 +3410,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(arg0, arg1 any)
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceResourceMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceResourceMetadata), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceResourceMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceResourceMetadata), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListWorkspaceAgentPortShares mocks base method.
|
||||||
|
func (m *MockStore) ListWorkspaceAgentPortShares(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ListWorkspaceAgentPortShares", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].([]database.WorkspaceAgentPortShare)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListWorkspaceAgentPortShares indicates an expected call of ListWorkspaceAgentPortShares.
|
||||||
|
func (mr *MockStoreMockRecorder) ListWorkspaceAgentPortShares(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListWorkspaceAgentPortShares", reflect.TypeOf((*MockStore)(nil).ListWorkspaceAgentPortShares), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// Ping mocks base method.
|
// Ping mocks base method.
|
||||||
func (m *MockStore) Ping(arg0 context.Context) (time.Duration, error) {
|
func (m *MockStore) Ping(arg0 context.Context) (time.Duration, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -4460,6 +4504,21 @@ func (mr *MockStoreMockRecorder) UpsertTailnetTunnel(arg0, arg1 any) *gomock.Cal
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetTunnel", reflect.TypeOf((*MockStore)(nil).UpsertTailnetTunnel), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetTunnel", reflect.TypeOf((*MockStore)(nil).UpsertTailnetTunnel), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpsertWorkspaceAgentPortShare mocks base method.
|
||||||
|
func (m *MockStore) UpsertWorkspaceAgentPortShare(arg0 context.Context, arg1 database.UpsertWorkspaceAgentPortShareParams) (database.WorkspaceAgentPortShare, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpsertWorkspaceAgentPortShare", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(database.WorkspaceAgentPortShare)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertWorkspaceAgentPortShare indicates an expected call of UpsertWorkspaceAgentPortShare.
|
||||||
|
func (mr *MockStoreMockRecorder) UpsertWorkspaceAgentPortShare(arg0, arg1 any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertWorkspaceAgentPortShare", reflect.TypeOf((*MockStore)(nil).UpsertWorkspaceAgentPortShare), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// Wrappers mocks base method.
|
// Wrappers mocks base method.
|
||||||
func (m *MockStore) Wrappers() []string {
|
func (m *MockStore) Wrappers() []string {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
|
@ -850,7 +850,8 @@ CREATE TABLE templates (
|
||||||
require_active_version boolean DEFAULT false NOT NULL,
|
require_active_version boolean DEFAULT false NOT NULL,
|
||||||
deprecated text DEFAULT ''::text NOT NULL,
|
deprecated text DEFAULT ''::text NOT NULL,
|
||||||
use_max_ttl boolean DEFAULT false NOT NULL,
|
use_max_ttl boolean DEFAULT false NOT NULL,
|
||||||
activity_bump bigint DEFAULT '3600000000000'::bigint NOT NULL
|
activity_bump bigint DEFAULT '3600000000000'::bigint NOT NULL,
|
||||||
|
max_port_sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
|
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
|
||||||
|
@ -901,6 +902,7 @@ CREATE VIEW template_with_users AS
|
||||||
templates.deprecated,
|
templates.deprecated,
|
||||||
templates.use_max_ttl,
|
templates.use_max_ttl,
|
||||||
templates.activity_bump,
|
templates.activity_bump,
|
||||||
|
templates.max_port_sharing_level,
|
||||||
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||||
COALESCE(visible_users.username, ''::text) AS created_by_username
|
COALESCE(visible_users.username, ''::text) AS created_by_username
|
||||||
FROM (public.templates
|
FROM (public.templates
|
||||||
|
@ -958,6 +960,13 @@ CREATE UNLOGGED TABLE workspace_agent_metadata (
|
||||||
|
|
||||||
COMMENT ON COLUMN workspace_agent_metadata.display_order IS 'Specifies the order in which to display agent metadata in user interfaces.';
|
COMMENT ON COLUMN workspace_agent_metadata.display_order IS 'Specifies the order in which to display agent metadata in user interfaces.';
|
||||||
|
|
||||||
|
CREATE TABLE workspace_agent_port_share (
|
||||||
|
workspace_id uuid NOT NULL,
|
||||||
|
agent_name text NOT NULL,
|
||||||
|
port integer NOT NULL,
|
||||||
|
share_level app_sharing_level NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE workspace_agent_scripts (
|
CREATE TABLE workspace_agent_scripts (
|
||||||
workspace_agent_id uuid NOT NULL,
|
workspace_agent_id uuid NOT NULL,
|
||||||
log_source_id uuid NOT NULL,
|
log_source_id uuid NOT NULL,
|
||||||
|
@ -1405,6 +1414,9 @@ ALTER TABLE ONLY workspace_agent_log_sources
|
||||||
ALTER TABLE ONLY workspace_agent_metadata
|
ALTER TABLE ONLY workspace_agent_metadata
|
||||||
ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key);
|
ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY workspace_agent_port_share
|
||||||
|
ADD CONSTRAINT workspace_agent_port_share_pkey PRIMARY KEY (workspace_id, agent_name, port);
|
||||||
|
|
||||||
ALTER TABLE ONLY workspace_agent_logs
|
ALTER TABLE ONLY workspace_agent_logs
|
||||||
ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
@ -1631,6 +1643,9 @@ ALTER TABLE ONLY workspace_agent_log_sources
|
||||||
ALTER TABLE ONLY workspace_agent_metadata
|
ALTER TABLE ONLY workspace_agent_metadata
|
||||||
ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY workspace_agent_port_share
|
||||||
|
ADD CONSTRAINT workspace_agent_port_share_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
ALTER TABLE ONLY workspace_agent_scripts
|
ALTER TABLE ONLY workspace_agent_scripts
|
||||||
ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ const (
|
||||||
ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
ForeignKeyWorkspaceAgentLogSourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_log_sources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||||
ForeignKeyWorkspaceAgentMetadataWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_metadata_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
ForeignKeyWorkspaceAgentMetadataWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_metadata_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||||
|
ForeignKeyWorkspaceAgentPortShareWorkspaceID ForeignKeyConstraint = "workspace_agent_port_share_workspace_id_fkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
|
||||||
ForeignKeyWorkspaceAgentScriptsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_scripts_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
ForeignKeyWorkspaceAgentScriptsWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_scripts_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_scripts ADD CONSTRAINT workspace_agent_scripts_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||||
ForeignKeyWorkspaceAgentStartupLogsAgentID ForeignKeyConstraint = "workspace_agent_startup_logs_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
ForeignKeyWorkspaceAgentStartupLogsAgentID ForeignKeyConstraint = "workspace_agent_startup_logs_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||||
ForeignKeyWorkspaceAgentsResourceID ForeignKeyConstraint = "workspace_agents_resource_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE;
|
ForeignKeyWorkspaceAgentsResourceID ForeignKeyConstraint = "workspace_agents_resource_id_fkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE;
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
DROP TABLE workspace_agent_port_share;
|
||||||
|
DROP VIEW template_with_users;
|
||||||
|
ALTER TABLE templates DROP COLUMN max_port_sharing_level;
|
||||||
|
|
||||||
|
-- Update the template_with_users view by recreating it.
|
||||||
|
|
||||||
|
CREATE VIEW
|
||||||
|
template_with_users
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
templates.*,
|
||||||
|
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||||
|
coalesce(visible_users.username, '') AS created_by_username
|
||||||
|
FROM
|
||||||
|
templates
|
||||||
|
LEFT JOIN
|
||||||
|
visible_users
|
||||||
|
ON
|
||||||
|
templates.created_by = visible_users.id;
|
||||||
|
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
|
|
@ -0,0 +1,27 @@
|
||||||
|
CREATE TABLE workspace_agent_port_share (
|
||||||
|
workspace_id uuid NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE,
|
||||||
|
agent_name text NOT NULL,
|
||||||
|
port integer NOT NULL,
|
||||||
|
share_level app_sharing_level NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE workspace_agent_port_share ADD PRIMARY KEY (workspace_id, agent_name, port);
|
||||||
|
|
||||||
|
ALTER TABLE templates ADD COLUMN max_port_sharing_level app_sharing_level NOT NULL DEFAULT 'owner'::app_sharing_level;
|
||||||
|
|
||||||
|
-- Update the template_with_users view by recreating it.
|
||||||
|
DROP VIEW template_with_users;
|
||||||
|
CREATE VIEW
|
||||||
|
template_with_users
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
templates.*,
|
||||||
|
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||||
|
coalesce(visible_users.username, '') AS created_by_username
|
||||||
|
FROM
|
||||||
|
templates
|
||||||
|
LEFT JOIN
|
||||||
|
visible_users
|
||||||
|
ON
|
||||||
|
templates.created_by = visible_users.id;
|
||||||
|
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
|
4
coderd/database/migrations/testdata/fixtures/000191_ workspace_agent_port_share.up.sql
vendored
Normal file
4
coderd/database/migrations/testdata/fixtures/000191_ workspace_agent_port_share.up.sql
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
INSERT INTO workspace_agent_port_share
|
||||||
|
(workspace_id, agent_name, port, share_level)
|
||||||
|
VALUES
|
||||||
|
('b90547be-8870-4d68-8184-e8b2242b7c01', 'qua', 8080, 'public'::app_sharing_level) RETURNING *;
|
|
@ -91,6 +91,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
|
||||||
&i.Deprecated,
|
&i.Deprecated,
|
||||||
&i.UseMaxTtl,
|
&i.UseMaxTtl,
|
||||||
&i.ActivityBump,
|
&i.ActivityBump,
|
||||||
|
&i.MaxPortSharingLevel,
|
||||||
&i.CreatedByAvatarURL,
|
&i.CreatedByAvatarURL,
|
||||||
&i.CreatedByUsername,
|
&i.CreatedByUsername,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
|
@ -2004,6 +2004,7 @@ type Template struct {
|
||||||
Deprecated string `db:"deprecated" json:"deprecated"`
|
Deprecated string `db:"deprecated" json:"deprecated"`
|
||||||
UseMaxTtl bool `db:"use_max_ttl" json:"use_max_ttl"`
|
UseMaxTtl bool `db:"use_max_ttl" json:"use_max_ttl"`
|
||||||
ActivityBump int64 `db:"activity_bump" json:"activity_bump"`
|
ActivityBump int64 `db:"activity_bump" json:"activity_bump"`
|
||||||
|
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
||||||
CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"`
|
CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"`
|
||||||
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
|
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
|
||||||
}
|
}
|
||||||
|
@ -2044,9 +2045,10 @@ type TemplateTable struct {
|
||||||
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
|
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
|
||||||
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
|
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
|
||||||
// If set to a non empty string, the template will no longer be able to be used. The message will be displayed to the user.
|
// If set to a non empty string, the template will no longer be able to be used. The message will be displayed to the user.
|
||||||
Deprecated string `db:"deprecated" json:"deprecated"`
|
Deprecated string `db:"deprecated" json:"deprecated"`
|
||||||
UseMaxTtl bool `db:"use_max_ttl" json:"use_max_ttl"`
|
UseMaxTtl bool `db:"use_max_ttl" json:"use_max_ttl"`
|
||||||
ActivityBump int64 `db:"activity_bump" json:"activity_bump"`
|
ActivityBump int64 `db:"activity_bump" json:"activity_bump"`
|
||||||
|
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Joins in the username + avatar url of the created by user.
|
// Joins in the username + avatar url of the created by user.
|
||||||
|
@ -2274,6 +2276,13 @@ type WorkspaceAgentMetadatum struct {
|
||||||
DisplayOrder int32 `db:"display_order" json:"display_order"`
|
DisplayOrder int32 `db:"display_order" json:"display_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkspaceAgentPortShare struct {
|
||||||
|
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
||||||
|
AgentName string `db:"agent_name" json:"agent_name"`
|
||||||
|
Port int32 `db:"port" json:"port"`
|
||||||
|
ShareLevel AppSharingLevel `db:"share_level" json:"share_level"`
|
||||||
|
}
|
||||||
|
|
||||||
type WorkspaceAgentScript struct {
|
type WorkspaceAgentScript struct {
|
||||||
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
||||||
LogSourceID uuid.UUID `db:"log_source_id" json:"log_source_id"`
|
LogSourceID uuid.UUID `db:"log_source_id" json:"log_source_id"`
|
||||||
|
|
|
@ -77,6 +77,7 @@ type sqlcQuerier interface {
|
||||||
DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error
|
DeleteTailnetClientSubscription(ctx context.Context, arg DeleteTailnetClientSubscriptionParams) error
|
||||||
DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error)
|
DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error)
|
||||||
DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error)
|
DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error)
|
||||||
|
DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error
|
||||||
FavoriteWorkspace(ctx context.Context, id uuid.UUID) error
|
FavoriteWorkspace(ctx context.Context, id uuid.UUID) error
|
||||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||||
// there is no unique constraint on empty token names
|
// there is no unique constraint on empty token names
|
||||||
|
@ -227,6 +228,7 @@ type sqlcQuerier interface {
|
||||||
GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error)
|
GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error)
|
||||||
GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error)
|
GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error)
|
||||||
GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorkspaceAgentMetadataParams) ([]WorkspaceAgentMetadatum, error)
|
GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorkspaceAgentMetadataParams) ([]WorkspaceAgentMetadatum, error)
|
||||||
|
GetWorkspaceAgentPortShare(ctx context.Context, arg GetWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error)
|
||||||
GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error)
|
GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error)
|
||||||
GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error)
|
GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error)
|
||||||
GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error)
|
GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error)
|
||||||
|
@ -317,6 +319,7 @@ type sqlcQuerier interface {
|
||||||
InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error)
|
InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error)
|
||||||
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
|
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
|
||||||
InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error)
|
InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error)
|
||||||
|
ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error)
|
||||||
RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error)
|
RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error)
|
||||||
RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error
|
RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error
|
||||||
// Non blocking lock. Returns true if the lock was acquired, false otherwise.
|
// Non blocking lock. Returns true if the lock was acquired, false otherwise.
|
||||||
|
@ -400,6 +403,7 @@ type sqlcQuerier interface {
|
||||||
UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error)
|
UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error)
|
||||||
UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error)
|
UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error)
|
||||||
UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error)
|
UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error)
|
||||||
|
UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ sqlcQuerier = (*sqlQuerier)(nil)
|
var _ sqlcQuerier = (*sqlQuerier)(nil)
|
||||||
|
|
|
@ -5728,7 +5728,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem
|
||||||
|
|
||||||
const getTemplateByID = `-- name: GetTemplateByID :one
|
const getTemplateByID = `-- name: GetTemplateByID :one
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, activity_bump, created_by_avatar_url, created_by_username
|
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username
|
||||||
FROM
|
FROM
|
||||||
template_with_users
|
template_with_users
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -5770,6 +5770,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||||
&i.Deprecated,
|
&i.Deprecated,
|
||||||
&i.UseMaxTtl,
|
&i.UseMaxTtl,
|
||||||
&i.ActivityBump,
|
&i.ActivityBump,
|
||||||
|
&i.MaxPortSharingLevel,
|
||||||
&i.CreatedByAvatarURL,
|
&i.CreatedByAvatarURL,
|
||||||
&i.CreatedByUsername,
|
&i.CreatedByUsername,
|
||||||
)
|
)
|
||||||
|
@ -5778,7 +5779,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||||
|
|
||||||
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
|
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, activity_bump, created_by_avatar_url, created_by_username
|
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username
|
||||||
FROM
|
FROM
|
||||||
template_with_users AS templates
|
template_with_users AS templates
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -5828,6 +5829,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||||
&i.Deprecated,
|
&i.Deprecated,
|
||||||
&i.UseMaxTtl,
|
&i.UseMaxTtl,
|
||||||
&i.ActivityBump,
|
&i.ActivityBump,
|
||||||
|
&i.MaxPortSharingLevel,
|
||||||
&i.CreatedByAvatarURL,
|
&i.CreatedByAvatarURL,
|
||||||
&i.CreatedByUsername,
|
&i.CreatedByUsername,
|
||||||
)
|
)
|
||||||
|
@ -5835,7 +5837,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTemplates = `-- name: GetTemplates :many
|
const getTemplates = `-- name: GetTemplates :many
|
||||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, activity_bump, created_by_avatar_url, created_by_username FROM template_with_users AS templates
|
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username FROM template_with_users AS templates
|
||||||
ORDER BY (name, id) ASC
|
ORDER BY (name, id) ASC
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -5878,6 +5880,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||||
&i.Deprecated,
|
&i.Deprecated,
|
||||||
&i.UseMaxTtl,
|
&i.UseMaxTtl,
|
||||||
&i.ActivityBump,
|
&i.ActivityBump,
|
||||||
|
&i.MaxPortSharingLevel,
|
||||||
&i.CreatedByAvatarURL,
|
&i.CreatedByAvatarURL,
|
||||||
&i.CreatedByUsername,
|
&i.CreatedByUsername,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -5896,7 +5899,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||||
|
|
||||||
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
|
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, activity_bump, created_by_avatar_url, created_by_username
|
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username
|
||||||
FROM
|
FROM
|
||||||
template_with_users AS templates
|
template_with_users AS templates
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -5989,6 +5992,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
|
||||||
&i.Deprecated,
|
&i.Deprecated,
|
||||||
&i.UseMaxTtl,
|
&i.UseMaxTtl,
|
||||||
&i.ActivityBump,
|
&i.ActivityBump,
|
||||||
|
&i.MaxPortSharingLevel,
|
||||||
&i.CreatedByAvatarURL,
|
&i.CreatedByAvatarURL,
|
||||||
&i.CreatedByUsername,
|
&i.CreatedByUsername,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -6021,10 +6025,11 @@ INSERT INTO
|
||||||
user_acl,
|
user_acl,
|
||||||
group_acl,
|
group_acl,
|
||||||
display_name,
|
display_name,
|
||||||
allow_user_cancel_workspace_jobs
|
allow_user_cancel_workspace_jobs,
|
||||||
|
max_port_sharing_level
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertTemplateParams struct {
|
type InsertTemplateParams struct {
|
||||||
|
@ -6042,6 +6047,7 @@ type InsertTemplateParams struct {
|
||||||
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
|
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
|
||||||
DisplayName string `db:"display_name" json:"display_name"`
|
DisplayName string `db:"display_name" json:"display_name"`
|
||||||
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
|
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
|
||||||
|
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) error {
|
func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) error {
|
||||||
|
@ -6060,6 +6066,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
|
||||||
arg.GroupACL,
|
arg.GroupACL,
|
||||||
arg.DisplayName,
|
arg.DisplayName,
|
||||||
arg.AllowUserCancelWorkspaceJobs,
|
arg.AllowUserCancelWorkspaceJobs,
|
||||||
|
arg.MaxPortSharingLevel,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -6158,20 +6165,22 @@ SET
|
||||||
icon = $5,
|
icon = $5,
|
||||||
display_name = $6,
|
display_name = $6,
|
||||||
allow_user_cancel_workspace_jobs = $7,
|
allow_user_cancel_workspace_jobs = $7,
|
||||||
group_acl = $8
|
group_acl = $8,
|
||||||
|
max_port_sharing_level = $9
|
||||||
WHERE
|
WHERE
|
||||||
id = $1
|
id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateTemplateMetaByIDParams struct {
|
type UpdateTemplateMetaByIDParams struct {
|
||||||
ID uuid.UUID `db:"id" json:"id"`
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
Description string `db:"description" json:"description"`
|
Description string `db:"description" json:"description"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
Icon string `db:"icon" json:"icon"`
|
Icon string `db:"icon" json:"icon"`
|
||||||
DisplayName string `db:"display_name" json:"display_name"`
|
DisplayName string `db:"display_name" json:"display_name"`
|
||||||
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
|
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
|
||||||
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
|
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
|
||||||
|
MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error {
|
func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error {
|
||||||
|
@ -6184,6 +6193,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
|
||||||
arg.DisplayName,
|
arg.DisplayName,
|
||||||
arg.AllowUserCancelWorkspaceJobs,
|
arg.AllowUserCancelWorkspaceJobs,
|
||||||
arg.GroupACL,
|
arg.GroupACL,
|
||||||
|
arg.MaxPortSharingLevel,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -8141,6 +8151,105 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteWorkspaceAgentPortShare = `-- name: DeleteWorkspaceAgentPortShare :exec
|
||||||
|
DELETE FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteWorkspaceAgentPortShareParams struct {
|
||||||
|
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
||||||
|
AgentName string `db:"agent_name" json:"agent_name"`
|
||||||
|
Port int32 `db:"port" json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteWorkspaceAgentPortShare, arg.WorkspaceID, arg.AgentName, arg.Port)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWorkspaceAgentPortShare = `-- name: GetWorkspaceAgentPortShare :one
|
||||||
|
SELECT workspace_id, agent_name, port, share_level FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetWorkspaceAgentPortShareParams struct {
|
||||||
|
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
||||||
|
AgentName string `db:"agent_name" json:"agent_name"`
|
||||||
|
Port int32 `db:"port" json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) GetWorkspaceAgentPortShare(ctx context.Context, arg GetWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getWorkspaceAgentPortShare, arg.WorkspaceID, arg.AgentName, arg.Port)
|
||||||
|
var i WorkspaceAgentPortShare
|
||||||
|
err := row.Scan(
|
||||||
|
&i.WorkspaceID,
|
||||||
|
&i.AgentName,
|
||||||
|
&i.Port,
|
||||||
|
&i.ShareLevel,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listWorkspaceAgentPortShares = `-- name: ListWorkspaceAgentPortShares :many
|
||||||
|
SELECT workspace_id, agent_name, port, share_level FROM workspace_agent_port_share WHERE workspace_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *sqlQuerier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listWorkspaceAgentPortShares, workspaceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []WorkspaceAgentPortShare
|
||||||
|
for rows.Next() {
|
||||||
|
var i WorkspaceAgentPortShare
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.WorkspaceID,
|
||||||
|
&i.AgentName,
|
||||||
|
&i.Port,
|
||||||
|
&i.ShareLevel,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertWorkspaceAgentPortShare = `-- name: UpsertWorkspaceAgentPortShare :one
|
||||||
|
INSERT INTO workspace_agent_port_share (workspace_id, agent_name, port, share_level)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT (workspace_id, agent_name, port) DO UPDATE SET share_level = $4 RETURNING workspace_id, agent_name, port, share_level
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertWorkspaceAgentPortShareParams struct {
|
||||||
|
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
||||||
|
AgentName string `db:"agent_name" json:"agent_name"`
|
||||||
|
Port int32 `db:"port" json:"port"`
|
||||||
|
ShareLevel AppSharingLevel `db:"share_level" json:"share_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, upsertWorkspaceAgentPortShare,
|
||||||
|
arg.WorkspaceID,
|
||||||
|
arg.AgentName,
|
||||||
|
arg.Port,
|
||||||
|
arg.ShareLevel,
|
||||||
|
)
|
||||||
|
var i WorkspaceAgentPortShare
|
||||||
|
err := row.Scan(
|
||||||
|
&i.WorkspaceID,
|
||||||
|
&i.AgentName,
|
||||||
|
&i.Port,
|
||||||
|
&i.ShareLevel,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteOldWorkspaceAgentLogs = `-- name: DeleteOldWorkspaceAgentLogs :exec
|
const deleteOldWorkspaceAgentLogs = `-- name: DeleteOldWorkspaceAgentLogs :exec
|
||||||
DELETE FROM workspace_agent_logs WHERE agent_id IN
|
DELETE FROM workspace_agent_logs WHERE agent_id IN
|
||||||
(SELECT id FROM workspace_agents WHERE last_connected_at IS NOT NULL
|
(SELECT id FROM workspace_agents WHERE last_connected_at IS NOT NULL
|
||||||
|
@ -11378,7 +11487,7 @@ LEFT JOIN LATERAL (
|
||||||
) latest_build ON TRUE
|
) latest_build ON TRUE
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT
|
SELECT
|
||||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, activity_bump
|
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, use_max_ttl, activity_bump, max_port_sharing_level
|
||||||
FROM
|
FROM
|
||||||
templates
|
templates
|
||||||
WHERE
|
WHERE
|
||||||
|
|
|
@ -83,10 +83,11 @@ INSERT INTO
|
||||||
user_acl,
|
user_acl,
|
||||||
group_acl,
|
group_acl,
|
||||||
display_name,
|
display_name,
|
||||||
allow_user_cancel_workspace_jobs
|
allow_user_cancel_workspace_jobs,
|
||||||
|
max_port_sharing_level
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14);
|
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15);
|
||||||
|
|
||||||
-- name: UpdateTemplateActiveVersionByID :exec
|
-- name: UpdateTemplateActiveVersionByID :exec
|
||||||
UPDATE
|
UPDATE
|
||||||
|
@ -116,7 +117,8 @@ SET
|
||||||
icon = $5,
|
icon = $5,
|
||||||
display_name = $6,
|
display_name = $6,
|
||||||
allow_user_cancel_workspace_jobs = $7,
|
allow_user_cancel_workspace_jobs = $7,
|
||||||
group_acl = $8
|
group_acl = $8,
|
||||||
|
max_port_sharing_level = $9
|
||||||
WHERE
|
WHERE
|
||||||
id = $1
|
id = $1
|
||||||
;
|
;
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- name: GetWorkspaceAgentPortShare :one
|
||||||
|
SELECT * FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3;
|
||||||
|
|
||||||
|
-- name: ListWorkspaceAgentPortShares :many
|
||||||
|
SELECT * FROM workspace_agent_port_share WHERE workspace_id = $1;
|
||||||
|
|
||||||
|
-- name: DeleteWorkspaceAgentPortShare :exec
|
||||||
|
DELETE FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3;
|
||||||
|
|
||||||
|
-- name: UpsertWorkspaceAgentPortShare :one
|
||||||
|
INSERT INTO workspace_agent_port_share (workspace_id, agent_name, port, share_level)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT (workspace_id, agent_name, port) DO UPDATE SET share_level = $4 RETURNING *;
|
|
@ -51,6 +51,7 @@ const (
|
||||||
UniqueUsersPkey UniqueConstraint = "users_pkey" // ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
UniqueUsersPkey UniqueConstraint = "users_pkey" // ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
||||||
UniqueWorkspaceAgentLogSourcesPkey UniqueConstraint = "workspace_agent_log_sources_pkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_pkey PRIMARY KEY (workspace_agent_id, id);
|
UniqueWorkspaceAgentLogSourcesPkey UniqueConstraint = "workspace_agent_log_sources_pkey" // ALTER TABLE ONLY workspace_agent_log_sources ADD CONSTRAINT workspace_agent_log_sources_pkey PRIMARY KEY (workspace_agent_id, id);
|
||||||
UniqueWorkspaceAgentMetadataPkey UniqueConstraint = "workspace_agent_metadata_pkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key);
|
UniqueWorkspaceAgentMetadataPkey UniqueConstraint = "workspace_agent_metadata_pkey" // ALTER TABLE ONLY workspace_agent_metadata ADD CONSTRAINT workspace_agent_metadata_pkey PRIMARY KEY (workspace_agent_id, key);
|
||||||
|
UniqueWorkspaceAgentPortSharePkey UniqueConstraint = "workspace_agent_port_share_pkey" // ALTER TABLE ONLY workspace_agent_port_share ADD CONSTRAINT workspace_agent_port_share_pkey PRIMARY KEY (workspace_id, agent_name, port);
|
||||||
UniqueWorkspaceAgentStartupLogsPkey UniqueConstraint = "workspace_agent_startup_logs_pkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id);
|
UniqueWorkspaceAgentStartupLogsPkey UniqueConstraint = "workspace_agent_startup_logs_pkey" // ALTER TABLE ONLY workspace_agent_logs ADD CONSTRAINT workspace_agent_startup_logs_pkey PRIMARY KEY (id);
|
||||||
UniqueWorkspaceAgentsPkey UniqueConstraint = "workspace_agents_pkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_pkey PRIMARY KEY (id);
|
UniqueWorkspaceAgentsPkey UniqueConstraint = "workspace_agents_pkey" // ALTER TABLE ONLY workspace_agents ADD CONSTRAINT workspace_agents_pkey PRIMARY KEY (id);
|
||||||
UniqueWorkspaceAppStatsPkey UniqueConstraint = "workspace_app_stats_pkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_pkey PRIMARY KEY (id);
|
UniqueWorkspaceAppStatsPkey UniqueConstraint = "workspace_app_stats_pkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_pkey PRIMARY KEY (id);
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package httpmw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RequireExperiment(experiments codersdk.Experiments, experiment codersdk.Experiment) func(next http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !experiments.Enabled(experiment) {
|
||||||
|
httpapi.Write(r.Context(), w, http.StatusForbidden, codersdk.Response{
|
||||||
|
Message: fmt.Sprintf("Experiment '%s' is required but not enabled", experiment),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -420,8 +420,9 @@ func TestCache_BuildTime(t *testing.T) {
|
||||||
|
|
||||||
id := uuid.New()
|
id := uuid.New()
|
||||||
err := db.InsertTemplate(ctx, database.InsertTemplateParams{
|
err := db.InsertTemplate(ctx, database.InsertTemplateParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
Provisioner: database.ProvisionerTypeEcho,
|
Provisioner: database.ProvisionerTypeEcho,
|
||||||
|
MaxPortSharingLevel: database.AppSharingLevelOwner,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
template, err := db.GetTemplateByID(ctx, id)
|
template, err := db.GetTemplateByID(ctx, id)
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package portsharing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PortSharer interface {
|
||||||
|
AuthorizedPortSharingLevel(template database.Template, level codersdk.WorkspaceAgentPortShareLevel) error
|
||||||
|
ValidateTemplateMaxPortSharingLevel(level codersdk.WorkspaceAgentPortShareLevel) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type AGPLPortSharer struct{}
|
||||||
|
|
||||||
|
func (AGPLPortSharer) AuthorizedPortSharingLevel(_ database.Template, _ codersdk.WorkspaceAgentPortShareLevel) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AGPLPortSharer) ValidateTemplateMaxPortSharingLevel(_ codersdk.WorkspaceAgentPortShareLevel) error {
|
||||||
|
return xerrors.New("Restricting port sharing level is an enterprise feature that is not enabled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultPortSharer PortSharer = AGPLPortSharer{}
|
|
@ -350,6 +350,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||||
DisplayName: createTemplate.DisplayName,
|
DisplayName: createTemplate.DisplayName,
|
||||||
Icon: createTemplate.Icon,
|
Icon: createTemplate.Icon,
|
||||||
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
|
||||||
|
MaxPortSharingLevel: database.AppSharingLevelOwner,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("insert template: %s", err)
|
return xerrors.Errorf("insert template: %s", err)
|
||||||
|
@ -539,6 +540,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx = r.Context()
|
ctx = r.Context()
|
||||||
template = httpmw.TemplateParam(r)
|
template = httpmw.TemplateParam(r)
|
||||||
auditor = *api.Auditor.Load()
|
auditor = *api.Auditor.Load()
|
||||||
|
portSharer = *api.PortSharer.Load()
|
||||||
aReq, commitAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
|
aReq, commitAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
|
||||||
Audit: auditor,
|
Audit: auditor,
|
||||||
Log: api.Logger,
|
Log: api.Logger,
|
||||||
|
@ -638,6 +640,15 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||||
if req.TimeTilDormantAutoDeleteMillis < 0 || (req.TimeTilDormantAutoDeleteMillis > 0 && req.TimeTilDormantAutoDeleteMillis < minTTL) {
|
if req.TimeTilDormantAutoDeleteMillis < 0 || (req.TimeTilDormantAutoDeleteMillis > 0 && req.TimeTilDormantAutoDeleteMillis < minTTL) {
|
||||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodelete_ms", Detail: "Value must be at least one minute."})
|
validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodelete_ms", Detail: "Value must be at least one minute."})
|
||||||
}
|
}
|
||||||
|
maxPortShareLevel := template.MaxPortSharingLevel
|
||||||
|
if req.MaxPortShareLevel != nil && *req.MaxPortShareLevel != codersdk.WorkspaceAgentPortShareLevel(maxPortShareLevel) {
|
||||||
|
err := portSharer.ValidateTemplateMaxPortSharingLevel(*req.MaxPortShareLevel)
|
||||||
|
if err != nil {
|
||||||
|
validErrs = append(validErrs, codersdk.ValidationError{Field: "max_port_sharing_level", Detail: err.Error()})
|
||||||
|
} else {
|
||||||
|
maxPortShareLevel = database.AppSharingLevel(*req.MaxPortShareLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(validErrs) > 0 {
|
if len(validErrs) > 0 {
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
@ -667,7 +678,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||||
req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() &&
|
req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() &&
|
||||||
req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() &&
|
req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() &&
|
||||||
req.RequireActiveVersion == template.RequireActiveVersion &&
|
req.RequireActiveVersion == template.RequireActiveVersion &&
|
||||||
(deprecationMessage == template.Deprecated) {
|
(deprecationMessage == template.Deprecated) &&
|
||||||
|
maxPortShareLevel == template.MaxPortSharingLevel {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -692,6 +704,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||||
Icon: req.Icon,
|
Icon: req.Icon,
|
||||||
AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs,
|
||||||
GroupACL: groupACL,
|
GroupACL: groupACL,
|
||||||
|
MaxPortSharingLevel: maxPortShareLevel,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("update template metadata: %w", err)
|
return xerrors.Errorf("update template metadata: %w", err)
|
||||||
|
@ -910,5 +923,6 @@ func (api *API) convertTemplate(
|
||||||
RequireActiveVersion: templateAccessControl.RequireActiveVersion,
|
RequireActiveVersion: templateAccessControl.RequireActiveVersion,
|
||||||
Deprecated: templateAccessControl.IsDeprecated(),
|
Deprecated: templateAccessControl.IsDeprecated(),
|
||||||
DeprecationMessage: templateAccessControl.Deprecated,
|
DeprecationMessage: templateAccessControl.Deprecated,
|
||||||
|
MaxPortShareLevel: codersdk.WorkspaceAgentPortShareLevel(template.MaxPortSharingLevel),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -625,6 +625,36 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||||
assert.Empty(t, updated.DeprecationMessage)
|
assert.Empty(t, updated.DeprecationMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("AGPL_MaxPortShareLevel", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
require.Equal(t, codersdk.WorkspaceAgentPortShareLevelOwner, template.MaxPortShareLevel)
|
||||||
|
|
||||||
|
var level codersdk.WorkspaceAgentPortShareLevel = codersdk.WorkspaceAgentPortShareLevelPublic
|
||||||
|
req := codersdk.UpdateTemplateMeta{
|
||||||
|
MaxPortShareLevel: &level,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||||
|
// AGPL cannot change max port sharing level
|
||||||
|
require.ErrorContains(t, err, "port sharing level is an enterprise feature")
|
||||||
|
|
||||||
|
// Ensure the same value port share level is a no-op
|
||||||
|
level = codersdk.WorkspaceAgentPortShareLevelOwner
|
||||||
|
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
|
Name: template.Name + "2",
|
||||||
|
MaxPortShareLevel: &level,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("NoDefaultTTL", func(t *testing.T) {
|
t.Run("NoDefaultTTL", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
package coderd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
|
"github.com/coder/coder/v2/coderd/httpmw"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Summary Upsert workspace agent port share
|
||||||
|
// @ID upsert-workspace-agent-port-share
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Tags PortSharing
|
||||||
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||||
|
// @Param request body codersdk.UpsertWorkspaceAgentPortShareRequest true "Upsert port sharing level request"
|
||||||
|
// @Success 200 {object} codersdk.WorkspaceAgentPortShare
|
||||||
|
// @Router /workspaces/{workspace}/port-share [post]
|
||||||
|
func (api *API) postWorkspaceAgentPortShare(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
workspace := httpmw.WorkspaceParam(r)
|
||||||
|
portSharer := *api.PortSharer.Load()
|
||||||
|
var req codersdk.UpsertWorkspaceAgentPortShareRequest
|
||||||
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.ShareLevel.ValidPortShareLevel() {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Port sharing level not allowed.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = portSharer.AuthorizedPortSharingLevel(template, req.ShareLevel)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agents, err := api.Database.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspace.ID)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, agent := range agents {
|
||||||
|
if agent.Name == req.AgentName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Agent not found.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
psl, err := api.Database.UpsertWorkspaceAgentPortShare(ctx, database.UpsertWorkspaceAgentPortShareParams{
|
||||||
|
WorkspaceID: workspace.ID,
|
||||||
|
AgentName: req.AgentName,
|
||||||
|
Port: req.Port,
|
||||||
|
ShareLevel: database.AppSharingLevel(req.ShareLevel),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, convertPortShare(psl))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get workspace agent port shares
|
||||||
|
// @ID get-workspace-agent-port-shares
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags PortSharing
|
||||||
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||||
|
// @Success 200 {object} codersdk.WorkspaceAgentPortShares
|
||||||
|
// @Router /workspaces/{workspace}/port-share [get]
|
||||||
|
func (api *API) workspaceAgentPortShares(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
workspace := httpmw.WorkspaceParam(r)
|
||||||
|
|
||||||
|
shares, err := api.Database.ListWorkspaceAgentPortShares(ctx, workspace.ID)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentPortShares{
|
||||||
|
Shares: convertPortShares(shares),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get workspace agent port shares
|
||||||
|
// @ID get-workspace-agent-port-shares
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Accept json
|
||||||
|
// @Tags PortSharing
|
||||||
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
||||||
|
// @Param request body codersdk.DeleteWorkspaceAgentPortShareRequest true "Delete port sharing level request"
|
||||||
|
// @Success 200
|
||||||
|
// @Router /workspaces/{workspace}/port-share [delete]
|
||||||
|
func (api *API) deleteWorkspaceAgentPortShare(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
workspace := httpmw.WorkspaceParam(r)
|
||||||
|
var req codersdk.DeleteWorkspaceAgentPortShareRequest
|
||||||
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := api.Database.GetWorkspaceAgentPortShare(ctx, database.GetWorkspaceAgentPortShareParams{
|
||||||
|
WorkspaceID: workspace.ID,
|
||||||
|
AgentName: req.AgentName,
|
||||||
|
Port: req.Port,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||||
|
Message: "Port share not found.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = api.Database.DeleteWorkspaceAgentPortShare(ctx, database.DeleteWorkspaceAgentPortShareParams{
|
||||||
|
WorkspaceID: workspace.ID,
|
||||||
|
AgentName: req.AgentName,
|
||||||
|
Port: req.Port,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertPortShares(shares []database.WorkspaceAgentPortShare) []codersdk.WorkspaceAgentPortShare {
|
||||||
|
var converted []codersdk.WorkspaceAgentPortShare
|
||||||
|
for _, share := range shares {
|
||||||
|
converted = append(converted, convertPortShare(share))
|
||||||
|
}
|
||||||
|
return converted
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertPortShare(share database.WorkspaceAgentPortShare) codersdk.WorkspaceAgentPortShare {
|
||||||
|
return codersdk.WorkspaceAgentPortShare{
|
||||||
|
WorkspaceID: share.WorkspaceID,
|
||||||
|
AgentName: share.AgentName,
|
||||||
|
Port: share.Port,
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevel(share.ShareLevel),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
package coderd_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPostWorkspaceAgentPortShare(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
dep := coderdtest.DeploymentValues(t)
|
||||||
|
dep.Experiments = append(dep.Experiments, string(codersdk.ExperimentSharedPorts))
|
||||||
|
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: dep,
|
||||||
|
})
|
||||||
|
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||||
|
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
|
||||||
|
OrganizationID: owner.OrganizationID,
|
||||||
|
OwnerID: user.ID,
|
||||||
|
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
|
||||||
|
agents[0].Directory = tmpDir
|
||||||
|
return agents
|
||||||
|
}).Do()
|
||||||
|
agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(user, owner.OrganizationID)), r.Workspace.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// owner level should fail
|
||||||
|
_, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: agents[0].Name,
|
||||||
|
Port: 8080,
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevel("owner"),
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// invalid level should fail
|
||||||
|
_, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: agents[0].Name,
|
||||||
|
Port: 8080,
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevel("invalid"),
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// OK, ignoring template max port share level because we are AGPL
|
||||||
|
ps, err := client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: agents[0].Name,
|
||||||
|
Port: 8080,
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.ShareLevel)
|
||||||
|
|
||||||
|
// update share level
|
||||||
|
ps, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: agents[0].Name,
|
||||||
|
Port: 8080,
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelAuthenticated, ps.ShareLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWorkspaceAgentPortShares(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dep := coderdtest.DeploymentValues(t)
|
||||||
|
dep.Experiments = append(dep.Experiments, string(codersdk.ExperimentSharedPorts))
|
||||||
|
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: dep,
|
||||||
|
})
|
||||||
|
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||||
|
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
|
||||||
|
OrganizationID: owner.OrganizationID,
|
||||||
|
OwnerID: user.ID,
|
||||||
|
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
|
||||||
|
agents[0].Directory = tmpDir
|
||||||
|
return agents
|
||||||
|
}).Do()
|
||||||
|
agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(user, owner.OrganizationID)), r.Workspace.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: agents[0].Name,
|
||||||
|
Port: 8080,
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ps, err := client.GetWorkspaceAgentPortShares(ctx, r.Workspace.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, ps.Shares, 1)
|
||||||
|
require.EqualValues(t, agents[0].Name, ps.Shares[0].AgentName)
|
||||||
|
require.EqualValues(t, 8080, ps.Shares[0].Port)
|
||||||
|
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.Shares[0].ShareLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteWorkspaceAgentPortShare(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dep := coderdtest.DeploymentValues(t)
|
||||||
|
dep.Experiments = append(dep.Experiments, string(codersdk.ExperimentSharedPorts))
|
||||||
|
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||||
|
DeploymentValues: dep,
|
||||||
|
})
|
||||||
|
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||||
|
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
|
||||||
|
OrganizationID: owner.OrganizationID,
|
||||||
|
OwnerID: user.ID,
|
||||||
|
}).WithAgent(func(agents []*proto.Agent) []*proto.Agent {
|
||||||
|
agents[0].Directory = tmpDir
|
||||||
|
return agents
|
||||||
|
}).Do()
|
||||||
|
agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(user, owner.OrganizationID)), r.Workspace.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// create
|
||||||
|
ps, err := client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: agents[0].Name,
|
||||||
|
Port: 8080,
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.ShareLevel)
|
||||||
|
|
||||||
|
// delete
|
||||||
|
err = client.DeleteWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.DeleteWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: agents[0].Name,
|
||||||
|
Port: 8080,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// delete missing
|
||||||
|
err = client.DeleteWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.DeleteWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: agents[0].Name,
|
||||||
|
Port: 8080,
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
_, err = db.GetWorkspaceAgentPortShare(dbauthz.As(ctx, coderdtest.AuthzUserSubject(user, owner.OrganizationID)), database.GetWorkspaceAgentPortShareParams{
|
||||||
|
WorkspaceID: r.Workspace.ID,
|
||||||
|
AgentName: agents[0].Name,
|
||||||
|
Port: 8080,
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
|
@ -909,6 +909,79 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
|
||||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("PortSharingNoShare", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
|
||||||
|
userAppClient := appDetails.AppClient(t)
|
||||||
|
userAppClient.SetSessionToken(userClient.SessionToken())
|
||||||
|
|
||||||
|
resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PortSharingAuthenticatedOK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// we are shadowing the parent since we are changing the state
|
||||||
|
appDetails := setupProxyTest(t, nil)
|
||||||
|
|
||||||
|
port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// set the port we have to be shared with authenticated users
|
||||||
|
_, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: proxyTestAgentName,
|
||||||
|
Port: int32(port),
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
|
||||||
|
userAppClient := appDetails.AppClient(t)
|
||||||
|
userAppClient.SetSessionToken(userClient.SessionToken())
|
||||||
|
|
||||||
|
resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PortSharingPublicOK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// we are shadowing the parent since we are changing the state
|
||||||
|
appDetails := setupProxyTest(t, nil)
|
||||||
|
|
||||||
|
port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// set the port we have to be shared with public
|
||||||
|
_, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: proxyTestAgentName,
|
||||||
|
Port: int32(port),
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
publicAppClient := appDetails.AppClient(t)
|
||||||
|
publicAppClient.SetSessionToken("")
|
||||||
|
|
||||||
|
resp, err := requestWithRetries(ctx, t, publicAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("ProxyError", func(t *testing.T) {
|
t.Run("ProxyError", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package workspaceapps
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -313,6 +314,36 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR
|
||||||
// This is only supported for subdomain-based applications.
|
// This is only supported for subdomain-based applications.
|
||||||
appURL = fmt.Sprintf("http://127.0.0.1:%d", portUint)
|
appURL = fmt.Sprintf("http://127.0.0.1:%d", portUint)
|
||||||
appSharingLevel = database.AppSharingLevelOwner
|
appSharingLevel = database.AppSharingLevelOwner
|
||||||
|
|
||||||
|
// Port sharing authorization
|
||||||
|
agentName := agentNameOrID
|
||||||
|
id, err := uuid.Parse(agentNameOrID)
|
||||||
|
for _, a := range agents {
|
||||||
|
// if err is nil then it's an UUID
|
||||||
|
if err == nil && a.ID == id {
|
||||||
|
agentName = a.Name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// otherwise it's a name
|
||||||
|
if a.Name == agentNameOrID {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if there is a port share for the port
|
||||||
|
ps, err := db.GetWorkspaceAgentPortShare(ctx, database.GetWorkspaceAgentPortShareParams{
|
||||||
|
WorkspaceID: workspace.ID,
|
||||||
|
AgentName: agentName,
|
||||||
|
Port: int32(portUint),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, xerrors.Errorf("get workspace agent port share: %w", err)
|
||||||
|
}
|
||||||
|
// No port share found, so we keep default to owner.
|
||||||
|
} else {
|
||||||
|
appSharingLevel = ps.ShareLevel
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, app := range apps {
|
for _, app := range apps {
|
||||||
if app.Slug == r.AppSlugOrPort {
|
if app.Slug == r.AppSlugOrPort {
|
||||||
|
|
|
@ -257,6 +257,7 @@ func TestWorkspaceApps(t *testing.T) {
|
||||||
deploymentValues.DisablePathApps = clibase.Bool(opts.DisablePathApps)
|
deploymentValues.DisablePathApps = clibase.Bool(opts.DisablePathApps)
|
||||||
deploymentValues.Dangerous.AllowPathAppSharing = clibase.Bool(opts.DangerousAllowPathAppSharing)
|
deploymentValues.Dangerous.AllowPathAppSharing = clibase.Bool(opts.DangerousAllowPathAppSharing)
|
||||||
deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = clibase.Bool(opts.DangerousAllowPathAppSiteOwnerAccess)
|
deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = clibase.Bool(opts.DangerousAllowPathAppSiteOwnerAccess)
|
||||||
|
deploymentValues.Experiments = append(deploymentValues.Experiments, string(codersdk.ExperimentSharedPorts))
|
||||||
|
|
||||||
if opts.DisableSubdomainApps {
|
if opts.DisableSubdomainApps {
|
||||||
opts.AppHost = ""
|
opts.AppHost = ""
|
||||||
|
|
|
@ -52,6 +52,7 @@ const (
|
||||||
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions"
|
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions"
|
||||||
FeatureAccessControl FeatureName = "access_control"
|
FeatureAccessControl FeatureName = "access_control"
|
||||||
FeatureOAuth2Provider FeatureName = "oauth2_provider"
|
FeatureOAuth2Provider FeatureName = "oauth2_provider"
|
||||||
|
FeatureControlSharedPorts FeatureName = "control_shared_ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FeatureNames must be kept in-sync with the Feature enum above.
|
// FeatureNames must be kept in-sync with the Feature enum above.
|
||||||
|
@ -72,6 +73,7 @@ var FeatureNames = []FeatureName{
|
||||||
FeatureWorkspaceBatchActions,
|
FeatureWorkspaceBatchActions,
|
||||||
FeatureAccessControl,
|
FeatureAccessControl,
|
||||||
FeatureOAuth2Provider,
|
FeatureOAuth2Provider,
|
||||||
|
FeatureControlSharedPorts,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Humanize returns the feature name in a human-readable format.
|
// Humanize returns the feature name in a human-readable format.
|
||||||
|
@ -2115,14 +2117,17 @@ type Experiment string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Add new experiments here!
|
// Add new experiments here!
|
||||||
ExperimentExample Experiment = "example" // This isn't used for anything.
|
ExperimentExample Experiment = "example" // This isn't used for anything.
|
||||||
|
ExperimentSharedPorts Experiment = "shared-ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExperimentsAll should include all experiments that are safe for
|
// ExperimentsAll should include all experiments that are safe for
|
||||||
// users to opt-in to via --experimental='*'.
|
// users to opt-in to via --experimental='*'.
|
||||||
// Experiments that are not ready for consumption by all users should
|
// Experiments that are not ready for consumption by all users should
|
||||||
// not be included here and will be essentially hidden.
|
// not be included here and will be essentially hidden.
|
||||||
var ExperimentsAll = Experiments{}
|
var ExperimentsAll = Experiments{
|
||||||
|
ExperimentSharedPorts,
|
||||||
|
}
|
||||||
|
|
||||||
// Experiments is a list of experiments.
|
// Experiments is a list of experiments.
|
||||||
// Multiple experiments may be enabled at the same time.
|
// Multiple experiments may be enabled at the same time.
|
||||||
|
|
|
@ -61,7 +61,8 @@ type Template struct {
|
||||||
|
|
||||||
// RequireActiveVersion mandates that workspaces are built with the active
|
// RequireActiveVersion mandates that workspaces are built with the active
|
||||||
// template version.
|
// template version.
|
||||||
RequireActiveVersion bool `json:"require_active_version"`
|
RequireActiveVersion bool `json:"require_active_version"`
|
||||||
|
MaxPortShareLevel WorkspaceAgentPortShareLevel `json:"max_port_share_level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WeekdaysToBitmap converts a list of weekdays to a bitmap in accordance with
|
// WeekdaysToBitmap converts a list of weekdays to a bitmap in accordance with
|
||||||
|
@ -251,7 +252,8 @@ type UpdateTemplateMeta struct {
|
||||||
// If this is set to true, the template will not be available to all users,
|
// If this is set to true, the template will not be available to all users,
|
||||||
// and must be explicitly granted to users or groups in the permissions settings
|
// and must be explicitly granted to users or groups in the permissions settings
|
||||||
// of the template.
|
// of the template.
|
||||||
DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"`
|
DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"`
|
||||||
|
MaxPortShareLevel *WorkspaceAgentPortShareLevel `json:"max_port_share_level"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateExample struct {
|
type TemplateExample struct {
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package codersdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WorkspaceAgentPortShareLevelOwner WorkspaceAgentPortShareLevel = "owner"
|
||||||
|
WorkspaceAgentPortShareLevelAuthenticated WorkspaceAgentPortShareLevel = "authenticated"
|
||||||
|
WorkspaceAgentPortShareLevelPublic WorkspaceAgentPortShareLevel = "public"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
WorkspaceAgentPortShareLevel string
|
||||||
|
UpsertWorkspaceAgentPortShareRequest struct {
|
||||||
|
AgentName string `json:"agent_name"`
|
||||||
|
Port int32 `json:"port"`
|
||||||
|
ShareLevel WorkspaceAgentPortShareLevel `json:"share_level"`
|
||||||
|
}
|
||||||
|
WorkspaceAgentPortShares struct {
|
||||||
|
Shares []WorkspaceAgentPortShare `json:"shares"`
|
||||||
|
}
|
||||||
|
WorkspaceAgentPortShare struct {
|
||||||
|
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"`
|
||||||
|
AgentName string `json:"agent_name"`
|
||||||
|
Port int32 `json:"port"`
|
||||||
|
ShareLevel WorkspaceAgentPortShareLevel `json:"share_level"`
|
||||||
|
}
|
||||||
|
DeleteWorkspaceAgentPortShareRequest struct {
|
||||||
|
AgentName string `json:"agent_name"`
|
||||||
|
Port int32 `json:"port"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l WorkspaceAgentPortShareLevel) ValidMaxLevel() bool {
|
||||||
|
return l == WorkspaceAgentPortShareLevelOwner ||
|
||||||
|
l == WorkspaceAgentPortShareLevelAuthenticated ||
|
||||||
|
l == WorkspaceAgentPortShareLevelPublic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l WorkspaceAgentPortShareLevel) ValidPortShareLevel() bool {
|
||||||
|
return l == WorkspaceAgentPortShareLevelAuthenticated ||
|
||||||
|
l == WorkspaceAgentPortShareLevelPublic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) (WorkspaceAgentPortShares, error) {
|
||||||
|
var shares WorkspaceAgentPortShares
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/port-share", workspaceID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return shares, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return shares, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shares, json.NewDecoder(res.Body).Decode(&shares)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UpsertWorkspaceAgentPortShare(ctx context.Context, workspaceID uuid.UUID, req UpsertWorkspaceAgentPortShareRequest) (WorkspaceAgentPortShare, error) {
|
||||||
|
var share WorkspaceAgentPortShare
|
||||||
|
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s/port-share", workspaceID), req)
|
||||||
|
if err != nil {
|
||||||
|
return share, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return share, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return share, json.NewDecoder(res.Body).Decode(&share)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeleteWorkspaceAgentPortShare(ctx context.Context, workspaceID uuid.UUID, req DeleteWorkspaceAgentPortShareRequest) error {
|
||||||
|
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/workspaces/%s/port-share", workspaceID), req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -8,20 +8,20 @@ We track the following resources:
|
||||||
|
|
||||||
<!-- Code generated by 'make docs/admin/audit-logs.md'. DO NOT EDIT -->
|
<!-- Code generated by 'make docs/admin/audit-logs.md'. DO NOT EDIT -->
|
||||||
|
|
||||||
| <b>Resource<b> | |
|
| <b>Resource<b> | |
|
||||||
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||||
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||||
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
|
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
|
||||||
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||||
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
|
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
|
||||||
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
||||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_max_ttl</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_max_ttl</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||||
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||||
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
||||||
|
|
||||||
<!-- End generated by 'make docs/admin/audit-logs.md'. -->
|
<!-- End generated by 'make docs/admin/audit-logs.md'. -->
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
# PortSharing
|
||||||
|
|
||||||
|
## Get workspace agent port shares
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X DELETE http://coder-server:8080/api/v2/workspaces/{workspace}/port-share \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`DELETE /workspaces/{workspace}/port-share`
|
||||||
|
|
||||||
|
> Body parameter
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agent_name": "string",
|
||||||
|
"port": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | In | Type | Required | Description |
|
||||||
|
| ----------- | ---- | -------------------------------------------------------------------------------------------------------- | -------- | --------------------------------- |
|
||||||
|
| `workspace` | path | string(uuid) | true | Workspace ID |
|
||||||
|
| `body` | body | [codersdk.DeleteWorkspaceAgentPortShareRequest](schemas.md#codersdkdeleteworkspaceagentportsharerequest) | true | Delete port sharing level request |
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
| ------ | ------------------------------------------------------- | ----------- | ------ |
|
||||||
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | |
|
||||||
|
|
||||||
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
## Upsert workspace agent port share
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Example request using curl
|
||||||
|
curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/port-share \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H 'Coder-Session-Token: API_KEY'
|
||||||
|
```
|
||||||
|
|
||||||
|
`POST /workspaces/{workspace}/port-share`
|
||||||
|
|
||||||
|
> Body parameter
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agent_name": "string",
|
||||||
|
"port": 0,
|
||||||
|
"share_level": "owner"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | In | Type | Required | Description |
|
||||||
|
| ----------- | ---- | -------------------------------------------------------------------------------------------------------- | -------- | --------------------------------- |
|
||||||
|
| `workspace` | path | string(uuid) | true | Workspace ID |
|
||||||
|
| `body` | body | [codersdk.UpsertWorkspaceAgentPortShareRequest](schemas.md#codersdkupsertworkspaceagentportsharerequest) | true | Upsert port sharing level request |
|
||||||
|
|
||||||
|
### Example responses
|
||||||
|
|
||||||
|
> 200 Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agent_name": "string",
|
||||||
|
"port": 0,
|
||||||
|
"share_level": "owner",
|
||||||
|
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
| Status | Meaning | Description | Schema |
|
||||||
|
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------ |
|
||||||
|
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.WorkspaceAgentPortShare](schemas.md#codersdkworkspaceagentportshare) |
|
||||||
|
|
||||||
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
|
@ -2095,6 +2095,22 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||||
| `allow_path_app_sharing` | boolean | false | | |
|
| `allow_path_app_sharing` | boolean | false | | |
|
||||||
| `allow_path_app_site_owner_access` | boolean | false | | |
|
| `allow_path_app_site_owner_access` | boolean | false | | |
|
||||||
|
|
||||||
|
## codersdk.DeleteWorkspaceAgentPortShareRequest
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agent_name": "string",
|
||||||
|
"port": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| ------------ | ------- | -------- | ------------ | ----------- |
|
||||||
|
| `agent_name` | string | false | | |
|
||||||
|
| `port` | integer | false | | |
|
||||||
|
|
||||||
## codersdk.DeploymentConfig
|
## codersdk.DeploymentConfig
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -2901,9 +2917,10 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Value |
|
| Value |
|
||||||
| --------- |
|
| -------------- |
|
||||||
| `example` |
|
| `example` |
|
||||||
|
| `shared-ports` |
|
||||||
|
|
||||||
## codersdk.ExternalAuth
|
## codersdk.ExternalAuth
|
||||||
|
|
||||||
|
@ -4677,6 +4694,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||||
"failure_ttl_ms": 0,
|
"failure_ttl_ms": 0,
|
||||||
"icon": "string",
|
"icon": "string",
|
||||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"max_port_share_level": "owner",
|
||||||
"max_ttl_ms": 0,
|
"max_ttl_ms": 0,
|
||||||
"name": "string",
|
"name": "string",
|
||||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
|
@ -4713,6 +4731,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||||
| `failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
|
| `failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
|
||||||
| `icon` | string | false | | |
|
| `icon` | string | false | | |
|
||||||
| `id` | string | false | | |
|
| `id` | string | false | | |
|
||||||
|
| `max_port_share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | |
|
||||||
| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured |
|
| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured |
|
||||||
| `name` | string | false | | |
|
| `name` | string | false | | |
|
||||||
| `organization_id` | string | false | | |
|
| `organization_id` | string | false | | |
|
||||||
|
@ -5621,6 +5640,24 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||||
| ------ | ------ | -------- | ------------ | ----------- |
|
| ------ | ------ | -------- | ------------ | ----------- |
|
||||||
| `hash` | string | false | | |
|
| `hash` | string | false | | |
|
||||||
|
|
||||||
|
## codersdk.UpsertWorkspaceAgentPortShareRequest
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agent_name": "string",
|
||||||
|
"port": 0,
|
||||||
|
"share_level": "owner"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| ------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- |
|
||||||
|
| `agent_name` | string | false | | |
|
||||||
|
| `port` | integer | false | | |
|
||||||
|
| `share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | |
|
||||||
|
|
||||||
## codersdk.User
|
## codersdk.User
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@ -6535,6 +6572,63 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||||
| `script` | string | false | | |
|
| `script` | string | false | | |
|
||||||
| `timeout` | integer | false | | |
|
| `timeout` | integer | false | | |
|
||||||
|
|
||||||
|
## codersdk.WorkspaceAgentPortShare
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agent_name": "string",
|
||||||
|
"port": 0,
|
||||||
|
"share_level": "owner",
|
||||||
|
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| -------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- |
|
||||||
|
| `agent_name` | string | false | | |
|
||||||
|
| `port` | integer | false | | |
|
||||||
|
| `share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | |
|
||||||
|
| `workspace_id` | string | false | | |
|
||||||
|
|
||||||
|
## codersdk.WorkspaceAgentPortShareLevel
|
||||||
|
|
||||||
|
```json
|
||||||
|
"owner"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
#### Enumerated Values
|
||||||
|
|
||||||
|
| Value |
|
||||||
|
| --------------- |
|
||||||
|
| `owner` |
|
||||||
|
| `authenticated` |
|
||||||
|
| `public` |
|
||||||
|
|
||||||
|
## codersdk.WorkspaceAgentPortShares
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"shares": [
|
||||||
|
{
|
||||||
|
"agent_name": "string",
|
||||||
|
"port": 0,
|
||||||
|
"share_level": "owner",
|
||||||
|
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Name | Type | Required | Restrictions | Description |
|
||||||
|
| -------- | ----------------------------------------------------------------------------- | -------- | ------------ | ----------- |
|
||||||
|
| `shares` | array of [codersdk.WorkspaceAgentPortShare](#codersdkworkspaceagentportshare) | false | | |
|
||||||
|
|
||||||
## codersdk.WorkspaceAgentScript
|
## codersdk.WorkspaceAgentScript
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
|
@ -60,6 +60,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
||||||
"failure_ttl_ms": 0,
|
"failure_ttl_ms": 0,
|
||||||
"icon": "string",
|
"icon": "string",
|
||||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"max_port_share_level": "owner",
|
||||||
"max_ttl_ms": 0,
|
"max_ttl_ms": 0,
|
||||||
"name": "string",
|
"name": "string",
|
||||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
|
@ -113,6 +114,7 @@ Status Code **200**
|
||||||
| `» failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
|
| `» failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
|
||||||
| `» icon` | string | false | | |
|
| `» icon` | string | false | | |
|
||||||
| `» id` | string(uuid) | false | | |
|
| `» id` | string(uuid) | false | | |
|
||||||
|
| `» max_port_share_level` | [codersdk.WorkspaceAgentPortShareLevel](schemas.md#codersdkworkspaceagentportsharelevel) | false | | |
|
||||||
| `» max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured |
|
| `» max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured |
|
||||||
| `» name` | string | false | | |
|
| `» name` | string | false | | |
|
||||||
| `» organization_id` | string(uuid) | false | | |
|
| `» organization_id` | string(uuid) | false | | |
|
||||||
|
@ -125,9 +127,12 @@ Status Code **200**
|
||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
| ------------- | ----------- |
|
| ---------------------- | --------------- |
|
||||||
| `provisioner` | `terraform` |
|
| `max_port_share_level` | `owner` |
|
||||||
|
| `max_port_share_level` | `authenticated` |
|
||||||
|
| `max_port_share_level` | `public` |
|
||||||
|
| `provisioner` | `terraform` |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
@ -222,6 +227,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
|
||||||
"failure_ttl_ms": 0,
|
"failure_ttl_ms": 0,
|
||||||
"icon": "string",
|
"icon": "string",
|
||||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"max_port_share_level": "owner",
|
||||||
"max_ttl_ms": 0,
|
"max_ttl_ms": 0,
|
||||||
"name": "string",
|
"name": "string",
|
||||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
|
@ -362,6 +368,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
||||||
"failure_ttl_ms": 0,
|
"failure_ttl_ms": 0,
|
||||||
"icon": "string",
|
"icon": "string",
|
||||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"max_port_share_level": "owner",
|
||||||
"max_ttl_ms": 0,
|
"max_ttl_ms": 0,
|
||||||
"name": "string",
|
"name": "string",
|
||||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
|
@ -678,6 +685,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \
|
||||||
"failure_ttl_ms": 0,
|
"failure_ttl_ms": 0,
|
||||||
"icon": "string",
|
"icon": "string",
|
||||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"max_port_share_level": "owner",
|
||||||
"max_ttl_ms": 0,
|
"max_ttl_ms": 0,
|
||||||
"name": "string",
|
"name": "string",
|
||||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
|
@ -801,6 +809,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
|
||||||
"failure_ttl_ms": 0,
|
"failure_ttl_ms": 0,
|
||||||
"icon": "string",
|
"icon": "string",
|
||||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||||
|
"max_port_share_level": "owner",
|
||||||
"max_ttl_ms": 0,
|
"max_ttl_ms": 0,
|
||||||
"name": "string",
|
"name": "string",
|
||||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||||
|
|
|
@ -550,6 +550,10 @@
|
||||||
"title": "Organizations",
|
"title": "Organizations",
|
||||||
"path": "./api/organizations.md"
|
"path": "./api/organizations.md"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "PortSharing",
|
||||||
|
"path": "./api/portsharing.md"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Schemas",
|
"title": "Schemas",
|
||||||
"path": "./api/schemas.md"
|
"path": "./api/schemas.md"
|
||||||
|
|
|
@ -88,6 +88,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||||
"time_til_dormant_autodelete": ActionTrack,
|
"time_til_dormant_autodelete": ActionTrack,
|
||||||
"require_active_version": ActionTrack,
|
"require_active_version": ActionTrack,
|
||||||
"deprecated": ActionTrack,
|
"deprecated": ActionTrack,
|
||||||
|
"max_port_sharing_level": ActionTrack,
|
||||||
"activity_bump": ActionTrack,
|
"activity_bump": ActionTrack,
|
||||||
},
|
},
|
||||||
&database.TemplateVersion{}: {
|
&database.TemplateVersion{}: {
|
||||||
|
|
|
@ -15,6 +15,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/appearance"
|
"github.com/coder/coder/v2/coderd/appearance"
|
||||||
|
agplportsharing "github.com/coder/coder/v2/coderd/portsharing"
|
||||||
|
"github.com/coder/coder/v2/enterprise/coderd/portsharing"
|
||||||
|
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
@ -533,6 +535,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||||
codersdk.FeatureWorkspaceProxy: true,
|
codersdk.FeatureWorkspaceProxy: true,
|
||||||
codersdk.FeatureUserRoleManagement: true,
|
codersdk.FeatureUserRoleManagement: true,
|
||||||
codersdk.FeatureAccessControl: true,
|
codersdk.FeatureAccessControl: true,
|
||||||
|
codersdk.FeatureControlSharedPorts: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -690,6 +693,14 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if initial, changed, enabled := featureChanged(codersdk.FeatureControlSharedPorts); shouldUpdate(initial, changed, enabled) {
|
||||||
|
var ps agplportsharing.PortSharer = agplportsharing.DefaultPortSharer
|
||||||
|
if enabled {
|
||||||
|
ps = portsharing.NewEnterprisePortSharer()
|
||||||
|
}
|
||||||
|
api.AGPL.PortSharer.Store(&ps)
|
||||||
|
}
|
||||||
|
|
||||||
// External token encryption is soft-enforced
|
// External token encryption is soft-enforced
|
||||||
featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption]
|
featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption]
|
||||||
featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0
|
featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package portsharing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EnterprisePortSharer struct{}
|
||||||
|
|
||||||
|
func NewEnterprisePortSharer() *EnterprisePortSharer {
|
||||||
|
return &EnterprisePortSharer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (EnterprisePortSharer) AuthorizedPortSharingLevel(template database.Template, level codersdk.WorkspaceAgentPortShareLevel) error {
|
||||||
|
max := codersdk.WorkspaceAgentPortShareLevel(template.MaxPortSharingLevel)
|
||||||
|
switch level {
|
||||||
|
case codersdk.WorkspaceAgentPortShareLevelPublic:
|
||||||
|
if max != codersdk.WorkspaceAgentPortShareLevelPublic {
|
||||||
|
return xerrors.Errorf("port sharing level not allowed. Max level is '%s'", max)
|
||||||
|
}
|
||||||
|
case codersdk.WorkspaceAgentPortShareLevelAuthenticated:
|
||||||
|
if max == codersdk.WorkspaceAgentPortShareLevelOwner {
|
||||||
|
return xerrors.Errorf("port sharing level not allowed. Max level is '%s'", max)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return xerrors.New("port sharing level is invalid.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (EnterprisePortSharer) ValidateTemplateMaxPortSharingLevel(level codersdk.WorkspaceAgentPortShareLevel) error {
|
||||||
|
if !level.ValidMaxLevel() {
|
||||||
|
return xerrors.New("invalid max port sharing level, value must be 'authenticated' or 'public'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -140,6 +140,43 @@ func TestTemplates(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("MaxPortShareLevel", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner, user := coderdenttest.New(t, &coderdenttest.Options{
|
||||||
|
Options: &coderdtest.Options{
|
||||||
|
IncludeProvisionerDaemon: true,
|
||||||
|
},
|
||||||
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureControlSharedPorts: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
client, _ := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// OK
|
||||||
|
var level codersdk.WorkspaceAgentPortShareLevel = codersdk.WorkspaceAgentPortShareLevelPublic
|
||||||
|
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
|
MaxPortShareLevel: &level,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, level, updated.MaxPortShareLevel)
|
||||||
|
|
||||||
|
// Invalid level
|
||||||
|
level = "invalid"
|
||||||
|
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
|
MaxPortShareLevel: &level,
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "invalid max port sharing level")
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("BlockDisablingAutoOffWithMaxTTL", func(t *testing.T) {
|
t.Run("BlockDisablingAutoOffWithMaxTTL", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
package coderd_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||||
|
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWorkspacePortShare(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dep := coderdtest.DeploymentValues(t)
|
||||||
|
dep.Experiments = append(dep.Experiments, string(codersdk.ExperimentSharedPorts))
|
||||||
|
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||||
|
Options: &coderdtest.Options{
|
||||||
|
IncludeProvisionerDaemon: true,
|
||||||
|
DeploymentValues: dep,
|
||||||
|
},
|
||||||
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureControlSharedPorts: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||||
|
workspace, agent := setupWorkspaceAgent(t, client, codersdk.CreateFirstUserResponse{
|
||||||
|
UserID: user.ID,
|
||||||
|
OrganizationID: owner.OrganizationID,
|
||||||
|
}, 0)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// try to update port share with template max port share level owner
|
||||||
|
_, err := client.UpsertWorkspaceAgentPortShare(ctx, workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: agent.Name,
|
||||||
|
Port: 8080,
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
|
||||||
|
})
|
||||||
|
require.Error(t, err, "Port sharing level not allowed")
|
||||||
|
|
||||||
|
// update the template max port share level to public
|
||||||
|
var level codersdk.WorkspaceAgentPortShareLevel = codersdk.WorkspaceAgentPortShareLevelPublic
|
||||||
|
client.UpdateTemplateMeta(ctx, workspace.TemplateID, codersdk.UpdateTemplateMeta{
|
||||||
|
MaxPortShareLevel: &level,
|
||||||
|
})
|
||||||
|
|
||||||
|
// OK
|
||||||
|
ps, err := client.UpsertWorkspaceAgentPortShare(ctx, workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
|
||||||
|
AgentName: agent.Name,
|
||||||
|
Port: 8080,
|
||||||
|
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.ShareLevel)
|
||||||
|
}
|
|
@ -736,6 +736,10 @@ export const updateTemplateSettings = async (
|
||||||
await expect(page).toHaveURL(`/templates/${templateName}/settings`);
|
await expect(page).toHaveURL(`/templates/${templateName}/settings`);
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(templateSettingValues)) {
|
for (const [key, value] of Object.entries(templateSettingValues)) {
|
||||||
|
// Skip max_port_share_level for now since the frontend is not yet able to handle it
|
||||||
|
if (key === "max_port_share_level") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const labelText = capitalize(key).replace("_", " ");
|
const labelText = capitalize(key).replace("_", " ");
|
||||||
await page.getByLabel(labelText, { exact: true }).fill(value);
|
await page.getByLabel(labelText, { exact: true }).fill(value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -369,6 +369,12 @@ export interface DangerousConfig {
|
||||||
readonly allow_all_cors: boolean;
|
readonly allow_all_cors: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/workspaceagentportshare.go
|
||||||
|
export interface DeleteWorkspaceAgentPortShareRequest {
|
||||||
|
readonly agent_name: string;
|
||||||
|
readonly port: number;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
export interface DeploymentConfig {
|
export interface DeploymentConfig {
|
||||||
readonly config?: DeploymentValues;
|
readonly config?: DeploymentValues;
|
||||||
|
@ -1043,6 +1049,7 @@ export interface Template {
|
||||||
readonly time_til_dormant_ms: number;
|
readonly time_til_dormant_ms: number;
|
||||||
readonly time_til_dormant_autodelete_ms: number;
|
readonly time_til_dormant_autodelete_ms: number;
|
||||||
readonly require_active_version: boolean;
|
readonly require_active_version: boolean;
|
||||||
|
readonly max_port_share_level: WorkspaceAgentPortShareLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/templates.go
|
// From codersdk/templates.go
|
||||||
|
@ -1302,6 +1309,7 @@ export interface UpdateTemplateMeta {
|
||||||
readonly require_active_version: boolean;
|
readonly require_active_version: boolean;
|
||||||
readonly deprecation_message?: string;
|
readonly deprecation_message?: string;
|
||||||
readonly disable_everyone_group_access: boolean;
|
readonly disable_everyone_group_access: boolean;
|
||||||
|
readonly max_port_share_level?: WorkspaceAgentPortShareLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/users.go
|
// From codersdk/users.go
|
||||||
|
@ -1362,6 +1370,13 @@ export interface UploadResponse {
|
||||||
readonly hash: string;
|
readonly hash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/workspaceagentportshare.go
|
||||||
|
export interface UpsertWorkspaceAgentPortShareRequest {
|
||||||
|
readonly agent_name: string;
|
||||||
|
readonly port: number;
|
||||||
|
readonly share_level: WorkspaceAgentPortShareLevel;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/users.go
|
// From codersdk/users.go
|
||||||
export interface User {
|
export interface User {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
@ -1611,6 +1626,19 @@ export interface WorkspaceAgentMetadataResult {
|
||||||
readonly error: string;
|
readonly error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/workspaceagentportshare.go
|
||||||
|
export interface WorkspaceAgentPortShare {
|
||||||
|
readonly workspace_id: string;
|
||||||
|
readonly agent_name: string;
|
||||||
|
readonly port: number;
|
||||||
|
readonly share_level: WorkspaceAgentPortShareLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From codersdk/workspaceagentportshare.go
|
||||||
|
export interface WorkspaceAgentPortShares {
|
||||||
|
readonly shares: WorkspaceAgentPortShare[];
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/workspaceagents.go
|
// From codersdk/workspaceagents.go
|
||||||
export interface WorkspaceAgentScript {
|
export interface WorkspaceAgentScript {
|
||||||
readonly log_source_id: string;
|
readonly log_source_id: string;
|
||||||
|
@ -1860,8 +1888,8 @@ export const Entitlements: Entitlement[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
export type Experiment = "example";
|
export type Experiment = "example" | "shared-ports";
|
||||||
export const Experiments: Experiment[] = ["example"];
|
export const Experiments: Experiment[] = ["example", "shared-ports"];
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
export type FeatureName =
|
export type FeatureName =
|
||||||
|
@ -1870,6 +1898,7 @@ export type FeatureName =
|
||||||
| "appearance"
|
| "appearance"
|
||||||
| "audit_log"
|
| "audit_log"
|
||||||
| "browser_only"
|
| "browser_only"
|
||||||
|
| "control_shared_ports"
|
||||||
| "external_provisioner_daemons"
|
| "external_provisioner_daemons"
|
||||||
| "external_token_encryption"
|
| "external_token_encryption"
|
||||||
| "high_availability"
|
| "high_availability"
|
||||||
|
@ -1887,6 +1916,7 @@ export const FeatureNames: FeatureName[] = [
|
||||||
"appearance",
|
"appearance",
|
||||||
"audit_log",
|
"audit_log",
|
||||||
"browser_only",
|
"browser_only",
|
||||||
|
"control_shared_ports",
|
||||||
"external_provisioner_daemons",
|
"external_provisioner_daemons",
|
||||||
"external_token_encryption",
|
"external_token_encryption",
|
||||||
"high_availability",
|
"high_availability",
|
||||||
|
@ -2148,6 +2178,14 @@ export const WorkspaceAgentLifecycles: WorkspaceAgentLifecycle[] = [
|
||||||
"starting",
|
"starting",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// From codersdk/workspaceagentportshare.go
|
||||||
|
export type WorkspaceAgentPortShareLevel = "authenticated" | "owner" | "public";
|
||||||
|
export const WorkspaceAgentPortShareLevels: WorkspaceAgentPortShareLevel[] = [
|
||||||
|
"authenticated",
|
||||||
|
"owner",
|
||||||
|
"public",
|
||||||
|
];
|
||||||
|
|
||||||
// From codersdk/workspaceagents.go
|
// From codersdk/workspaceagents.go
|
||||||
export type WorkspaceAgentStartupScriptBehavior = "blocking" | "non-blocking";
|
export type WorkspaceAgentStartupScriptBehavior = "blocking" | "non-blocking";
|
||||||
export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] =
|
export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] =
|
||||||
|
|
|
@ -48,6 +48,7 @@ const validFormValues: FormValues = {
|
||||||
update_workspace_dormant_at: false,
|
update_workspace_dormant_at: false,
|
||||||
require_active_version: false,
|
require_active_version: false,
|
||||||
disable_everyone_group_access: false,
|
disable_everyone_group_access: false,
|
||||||
|
max_port_share_level: "owner",
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTemplateSettingsPage = async () => {
|
const renderTemplateSettingsPage = async () => {
|
||||||
|
|
|
@ -489,6 +489,7 @@ export const MockTemplate: TypesGen.Template = {
|
||||||
require_active_version: false,
|
require_active_version: false,
|
||||||
deprecated: false,
|
deprecated: false,
|
||||||
deprecation_message: "",
|
deprecation_message: "",
|
||||||
|
max_port_share_level: "owner",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MockTemplateVersionFiles: TemplateVersionFiles = {
|
export const MockTemplateVersionFiles: TemplateVersionFiles = {
|
||||||
|
|
Loading…
Reference in New Issue