mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add port-sharing backend (#11939)
This commit is contained in:
Generated
+193
-2
@@ -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": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -9028,6 +9147,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.DeleteWorkspaceAgentPortShareRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.DeploymentConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9337,13 +9467,15 @@ const docTemplate = `{
|
||||
"codersdk.Experiment": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"example"
|
||||
"example",
|
||||
"shared-ports"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"ExperimentExample": "This isn't used for anything."
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
"ExperimentExample"
|
||||
"ExperimentExample",
|
||||
"ExperimentSharedPorts"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
@@ -11022,6 +11154,9 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"max_port_share_level": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
|
||||
},
|
||||
"max_ttl_ms": {
|
||||
"description": "TODO(@dean): remove max_ttl once autostop_requirement is matured",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+173
-2
@@ -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": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -8064,6 +8169,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.DeleteWorkspaceAgentPortShareRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.DeploymentConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8368,11 +8484,11 @@
|
||||
},
|
||||
"codersdk.Experiment": {
|
||||
"type": "string",
|
||||
"enum": ["example"],
|
||||
"enum": ["example", "shared-ports"],
|
||||
"x-enum-comments": {
|
||||
"ExperimentExample": "This isn't used for anything."
|
||||
},
|
||||
"x-enum-varnames": ["ExperimentExample"]
|
||||
"x-enum-varnames": ["ExperimentExample", "ExperimentSharedPorts"]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
"type": "object",
|
||||
@@ -9964,6 +10080,9 @@
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"max_port_share_level": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
|
||||
},
|
||||
"max_ttl_ms": {
|
||||
"description": "TODO(@dean): remove max_ttl once autostop_requirement is matured",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -56,6 +56,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"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/provisionerdserver"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
@@ -400,6 +401,7 @@ func New(options *Options) *API {
|
||||
}
|
||||
|
||||
api.AppearanceFetcher.Store(&appearance.DefaultFetcher)
|
||||
api.PortSharer.Store(&portsharing.DefaultPortSharer)
|
||||
api.SiteHandler = site.New(&site.Options{
|
||||
BinFS: binFS,
|
||||
BinHashes: binHashes,
|
||||
@@ -957,6 +959,14 @@ func New(options *Options) *API {
|
||||
r.Delete("/favorite", api.deleteFavoriteWorkspace)
|
||||
r.Put("/autoupdates", api.putWorkspaceAutoupdates)
|
||||
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) {
|
||||
@@ -1107,6 +1117,7 @@ type API struct {
|
||||
// AccessControlStore is a pointer to an atomic pointer since it is
|
||||
// passed to dbauthz.
|
||||
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
||||
PortSharer atomic.Pointer[portsharing.PortSharer]
|
||||
|
||||
HTTPAuth *HTTPAuthorizer
|
||||
|
||||
|
||||
@@ -891,6 +891,20 @@ func (q *querier) DeleteTailnetTunnel(ctx context.Context, arg database.DeleteTa
|
||||
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 {
|
||||
fetch := func(ctx context.Context, id uuid.UUID) (database.Workspace, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return nil, err
|
||||
@@ -2500,6 +2528,20 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab
|
||||
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) {
|
||||
fetch := func(ctx context.Context, arg database.RegisterWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
// TODO Delete this function, all GetTemplates should be authorized. For now just call getTemplates on the authz querier.
|
||||
return q.GetTemplatesWithFilter(ctx, arg)
|
||||
|
||||
@@ -822,8 +822,9 @@ func (s *MethodTestSuite) TestTemplate() {
|
||||
s.Run("InsertTemplate", s.Subtest(func(db database.Store, check *expects) {
|
||||
orgID := uuid.New()
|
||||
check.Args(database.InsertTemplateParams{
|
||||
Provisioner: "echo",
|
||||
OrganizationID: orgID,
|
||||
Provisioner: "echo",
|
||||
OrganizationID: orgID,
|
||||
MaxPortSharingLevel: database.AppSharingLevelOwner,
|
||||
}).Asserts(rbac.ResourceTemplate.InOrg(orgID), rbac.ActionCreate)
|
||||
}))
|
||||
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) {
|
||||
t1 := dbgen.Template(s.T(), db, database.Template{})
|
||||
check.Args(database.UpdateTemplateMetaByIDParams{
|
||||
ID: t1.ID,
|
||||
ID: t1.ID,
|
||||
MaxPortSharingLevel: "owner",
|
||||
}).Asserts(t1, rbac.ActionUpdate)
|
||||
}))
|
||||
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() {
|
||||
s.Run("GetProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) {
|
||||
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,
|
||||
DisplayName: takeFirst(seed.DisplayName, namesgenerator.GetRandomName(1)),
|
||||
AllowUserCancelWorkspaceJobs: seed.AllowUserCancelWorkspaceJobs,
|
||||
MaxPortSharingLevel: takeFirst(seed.MaxPortSharingLevel, database.AppSharingLevelOwner),
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
agt, err := db.InsertWorkspaceAgent(genCtx, database.InsertWorkspaceAgentParams{
|
||||
ID: takeFirst(orig.ID, uuid.New()),
|
||||
|
||||
@@ -147,6 +147,7 @@ type data struct {
|
||||
workspaceAgentLogs []database.WorkspaceAgentLog
|
||||
workspaceAgentLogSources []database.WorkspaceAgentLogSource
|
||||
workspaceAgentScripts []database.WorkspaceAgentScript
|
||||
workspaceAgentPortShares []database.WorkspaceAgentPortShare
|
||||
workspaceApps []database.WorkspaceApp
|
||||
workspaceAppStatsLastInsertID int64
|
||||
workspaceAppStats []database.WorkspaceAppStat
|
||||
@@ -1322,6 +1323,25 @@ func (*FakeQuerier) DeleteTailnetTunnel(_ context.Context, arg database.DeleteTa
|
||||
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 {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
@@ -4159,6 +4179,24 @@ func (q *FakeQuerier) GetWorkspaceAgentMetadata(_ context.Context, arg database.
|
||||
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) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -5374,6 +5412,7 @@ func (q *FakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl
|
||||
AllowUserCancelWorkspaceJobs: arg.AllowUserCancelWorkspaceJobs,
|
||||
AllowUserAutostart: true,
|
||||
AllowUserAutostop: true,
|
||||
MaxPortSharingLevel: arg.MaxPortSharingLevel,
|
||||
}
|
||||
q.templates = append(q.templates, template)
|
||||
return nil
|
||||
@@ -6006,6 +6045,20 @@ func (q *FakeQuerier) InsertWorkspaceResourceMetadata(_ context.Context, arg dat
|
||||
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) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
@@ -6525,6 +6578,7 @@ func (q *FakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd
|
||||
tpl.Icon = arg.Icon
|
||||
tpl.GroupACL = arg.GroupACL
|
||||
tpl.AllowUserCancelWorkspaceJobs = arg.AllowUserCancelWorkspaceJobs
|
||||
tpl.MaxPortSharingLevel = arg.MaxPortSharingLevel
|
||||
q.templates[idx] = tpl
|
||||
return nil
|
||||
}
|
||||
@@ -7515,6 +7569,35 @@ func (*FakeQuerier) UpsertTailnetTunnel(_ context.Context, arg database.UpsertTa
|
||||
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) {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -300,6 +300,13 @@ func (m metricsStore) DeleteTailnetTunnel(ctx context.Context, arg database.Dele
|
||||
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 {
|
||||
start := time.Now()
|
||||
r0 := m.s.FavoriteWorkspace(ctx, arg)
|
||||
@@ -1082,6 +1089,13 @@ func (m metricsStore) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAg
|
||||
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) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetWorkspaceAgentScriptsByAgentIDs(ctx, ids)
|
||||
@@ -1607,6 +1621,13 @@ func (m metricsStore) InsertWorkspaceResourceMetadata(ctx context.Context, arg d
|
||||
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) {
|
||||
start := time.Now()
|
||||
proxy, err := m.s.RegisterWorkspaceProxy(ctx, arg)
|
||||
@@ -2122,6 +2143,13 @@ func (m metricsStore) UpsertTailnetTunnel(ctx context.Context, arg database.Upse
|
||||
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) {
|
||||
start := time.Now()
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockStore) FavoriteWorkspace(arg0 context.Context, arg1 uuid.UUID) error {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockStore) GetWorkspaceAgentScriptsByAgentIDs(arg0 context.Context, arg1 []uuid.UUID) ([]database.WorkspaceAgentScript, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockStore) Ping(arg0 context.Context) (time.Duration, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockStore) Wrappers() []string {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+16
-1
@@ -850,7 +850,8 @@ CREATE TABLE templates (
|
||||
require_active_version boolean DEFAULT false NOT NULL,
|
||||
deprecated text DEFAULT ''::text 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.';
|
||||
@@ -901,6 +902,7 @@ CREATE VIEW template_with_users AS
|
||||
templates.deprecated,
|
||||
templates.use_max_ttl,
|
||||
templates.activity_bump,
|
||||
templates.max_port_sharing_level,
|
||||
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||
COALESCE(visible_users.username, ''::text) AS created_by_username
|
||||
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.';
|
||||
|
||||
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 (
|
||||
workspace_agent_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
|
||||
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
|
||||
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
|
||||
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
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
|
||||
@@ -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
@@ -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.UseMaxTtl,
|
||||
&i.ActivityBump,
|
||||
&i.MaxPortSharingLevel,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
|
||||
@@ -2004,6 +2004,7 @@ type Template struct {
|
||||
Deprecated string `db:"deprecated" json:"deprecated"`
|
||||
UseMaxTtl bool `db:"use_max_ttl" json:"use_max_ttl"`
|
||||
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"`
|
||||
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"`
|
||||
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.
|
||||
Deprecated string `db:"deprecated" json:"deprecated"`
|
||||
UseMaxTtl bool `db:"use_max_ttl" json:"use_max_ttl"`
|
||||
ActivityBump int64 `db:"activity_bump" json:"activity_bump"`
|
||||
Deprecated string `db:"deprecated" json:"deprecated"`
|
||||
UseMaxTtl bool `db:"use_max_ttl" json:"use_max_ttl"`
|
||||
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.
|
||||
@@ -2274,6 +2276,13 @@ type WorkspaceAgentMetadatum struct {
|
||||
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 {
|
||||
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_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
|
||||
DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error)
|
||||
DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error)
|
||||
DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error
|
||||
FavoriteWorkspace(ctx context.Context, id uuid.UUID) error
|
||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||
// 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)
|
||||
GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, 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)
|
||||
GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, 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)
|
||||
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, 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)
|
||||
RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error
|
||||
// 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)
|
||||
UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error)
|
||||
UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error)
|
||||
UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error)
|
||||
}
|
||||
|
||||
var _ sqlcQuerier = (*sqlQuerier)(nil)
|
||||
|
||||
+125
-16
@@ -5728,7 +5728,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem
|
||||
|
||||
const getTemplateByID = `-- name: GetTemplateByID :one
|
||||
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
|
||||
template_with_users
|
||||
WHERE
|
||||
@@ -5770,6 +5770,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
&i.Deprecated,
|
||||
&i.UseMaxTtl,
|
||||
&i.ActivityBump,
|
||||
&i.MaxPortSharingLevel,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
@@ -5778,7 +5779,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
|
||||
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
|
||||
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
|
||||
template_with_users AS templates
|
||||
WHERE
|
||||
@@ -5828,6 +5829,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
&i.Deprecated,
|
||||
&i.UseMaxTtl,
|
||||
&i.ActivityBump,
|
||||
&i.MaxPortSharingLevel,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
@@ -5835,7 +5837,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
}
|
||||
|
||||
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
|
||||
`
|
||||
|
||||
@@ -5878,6 +5880,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
&i.Deprecated,
|
||||
&i.UseMaxTtl,
|
||||
&i.ActivityBump,
|
||||
&i.MaxPortSharingLevel,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
@@ -5896,7 +5899,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
|
||||
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :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
|
||||
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
|
||||
WHERE
|
||||
@@ -5989,6 +5992,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
|
||||
&i.Deprecated,
|
||||
&i.UseMaxTtl,
|
||||
&i.ActivityBump,
|
||||
&i.MaxPortSharingLevel,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
@@ -6021,10 +6025,11 @@ INSERT INTO
|
||||
user_acl,
|
||||
group_acl,
|
||||
display_name,
|
||||
allow_user_cancel_workspace_jobs
|
||||
allow_user_cancel_workspace_jobs,
|
||||
max_port_sharing_level
|
||||
)
|
||||
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 {
|
||||
@@ -6042,6 +6047,7 @@ type InsertTemplateParams struct {
|
||||
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
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 {
|
||||
@@ -6060,6 +6066,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
|
||||
arg.GroupACL,
|
||||
arg.DisplayName,
|
||||
arg.AllowUserCancelWorkspaceJobs,
|
||||
arg.MaxPortSharingLevel,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -6158,20 +6165,22 @@ SET
|
||||
icon = $5,
|
||||
display_name = $6,
|
||||
allow_user_cancel_workspace_jobs = $7,
|
||||
group_acl = $8
|
||||
group_acl = $8,
|
||||
max_port_sharing_level = $9
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateTemplateMetaByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
|
||||
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
|
||||
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 {
|
||||
@@ -6184,6 +6193,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
|
||||
arg.DisplayName,
|
||||
arg.AllowUserCancelWorkspaceJobs,
|
||||
arg.GroupACL,
|
||||
arg.MaxPortSharingLevel,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -8141,6 +8151,105 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
|
||||
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
|
||||
DELETE FROM workspace_agent_logs WHERE agent_id IN
|
||||
(SELECT id FROM workspace_agents WHERE last_connected_at IS NOT NULL
|
||||
@@ -11378,7 +11487,7 @@ LEFT JOIN LATERAL (
|
||||
) latest_build ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
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
|
||||
templates
|
||||
WHERE
|
||||
|
||||
@@ -83,10 +83,11 @@ INSERT INTO
|
||||
user_acl,
|
||||
group_acl,
|
||||
display_name,
|
||||
allow_user_cancel_workspace_jobs
|
||||
allow_user_cancel_workspace_jobs,
|
||||
max_port_sharing_level
|
||||
)
|
||||
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
|
||||
UPDATE
|
||||
@@ -116,7 +117,8 @@ SET
|
||||
icon = $5,
|
||||
display_name = $6,
|
||||
allow_user_cancel_workspace_jobs = $7,
|
||||
group_acl = $8
|
||||
group_acl = $8,
|
||||
max_port_sharing_level = $9
|
||||
WHERE
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
|
||||
@@ -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()
|
||||
err := db.InsertTemplate(ctx, database.InsertTemplateParams{
|
||||
ID: id,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
ID: id,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
MaxPortSharingLevel: database.AppSharingLevelOwner,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
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{}
|
||||
+15
-1
@@ -350,6 +350,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
DisplayName: createTemplate.DisplayName,
|
||||
Icon: createTemplate.Icon,
|
||||
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
|
||||
MaxPortSharingLevel: database.AppSharingLevelOwner,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert template: %s", err)
|
||||
@@ -539,6 +540,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx = r.Context()
|
||||
template = httpmw.TemplateParam(r)
|
||||
auditor = *api.Auditor.Load()
|
||||
portSharer = *api.PortSharer.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.Template](rw, &audit.RequestParams{
|
||||
Audit: auditor,
|
||||
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) {
|
||||
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 {
|
||||
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.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() &&
|
||||
req.RequireActiveVersion == template.RequireActiveVersion &&
|
||||
(deprecationMessage == template.Deprecated) {
|
||||
(deprecationMessage == template.Deprecated) &&
|
||||
maxPortShareLevel == template.MaxPortSharingLevel {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -692,6 +704,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
Icon: req.Icon,
|
||||
AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs,
|
||||
GroupACL: groupACL,
|
||||
MaxPortSharingLevel: maxPortShareLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update template metadata: %w", err)
|
||||
@@ -910,5 +923,6 @@ func (api *API) convertTemplate(
|
||||
RequireActiveVersion: templateAccessControl.RequireActiveVersion,
|
||||
Deprecated: templateAccessControl.IsDeprecated(),
|
||||
DeprecationMessage: templateAccessControl.Deprecated,
|
||||
MaxPortShareLevel: codersdk.WorkspaceAgentPortShareLevel(template.MaxPortSharingLevel),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,6 +625,36 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
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.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)
|
||||
})
|
||||
|
||||
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.Parallel()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package workspaceapps
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@@ -313,6 +314,36 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR
|
||||
// This is only supported for subdomain-based applications.
|
||||
appURL = fmt.Sprintf("http://127.0.0.1:%d", portUint)
|
||||
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 {
|
||||
for _, app := range apps {
|
||||
if app.Slug == r.AppSlugOrPort {
|
||||
|
||||
@@ -257,6 +257,7 @@ func TestWorkspaceApps(t *testing.T) {
|
||||
deploymentValues.DisablePathApps = clibase.Bool(opts.DisablePathApps)
|
||||
deploymentValues.Dangerous.AllowPathAppSharing = clibase.Bool(opts.DangerousAllowPathAppSharing)
|
||||
deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = clibase.Bool(opts.DangerousAllowPathAppSiteOwnerAccess)
|
||||
deploymentValues.Experiments = append(deploymentValues.Experiments, string(codersdk.ExperimentSharedPorts))
|
||||
|
||||
if opts.DisableSubdomainApps {
|
||||
opts.AppHost = ""
|
||||
|
||||
Reference in New Issue
Block a user