feat: allow setting port share protocol (#12383)

Co-authored-by: Garrett Delfosse <garrett@coder.com>
This commit is contained in:
Dean Sheather 2024-03-06 06:23:57 -08:00 committed by GitHub
parent 23ff807a27
commit 46a2ff1061
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 624 additions and 130 deletions

55
coderd/apidoc/docs.go generated
View File

@ -12619,8 +12619,28 @@ const docTemplate = `{
"port": {
"type": "integer"
},
"protocol": {
"enum": [
"http",
"https"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareProtocol"
}
]
},
"share_level": {
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
"enum": [
"owner",
"authenticated",
"public"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
}
]
}
}
},
@ -13366,8 +13386,28 @@ const docTemplate = `{
"port": {
"type": "integer"
},
"protocol": {
"enum": [
"http",
"https"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareProtocol"
}
]
},
"share_level": {
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
"enum": [
"owner",
"authenticated",
"public"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
}
]
},
"workspace_id": {
"type": "string",
@ -13388,6 +13428,17 @@ const docTemplate = `{
"WorkspaceAgentPortShareLevelPublic"
]
},
"codersdk.WorkspaceAgentPortShareProtocol": {
"type": "string",
"enum": [
"http",
"https"
],
"x-enum-varnames": [
"WorkspaceAgentPortShareProtocolHTTP",
"WorkspaceAgentPortShareProtocolHTTPS"
]
},
"codersdk.WorkspaceAgentPortShares": {
"type": "object",
"properties": {

View File

@ -11442,8 +11442,21 @@
"port": {
"type": "integer"
},
"protocol": {
"enum": ["http", "https"],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareProtocol"
}
]
},
"share_level": {
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
"enum": ["owner", "authenticated", "public"],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
}
]
}
}
},
@ -12164,8 +12177,21 @@
"port": {
"type": "integer"
},
"protocol": {
"enum": ["http", "https"],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareProtocol"
}
]
},
"share_level": {
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
"enum": ["owner", "authenticated", "public"],
"allOf": [
{
"$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel"
}
]
},
"workspace_id": {
"type": "string",
@ -12182,6 +12208,14 @@
"WorkspaceAgentPortShareLevelPublic"
]
},
"codersdk.WorkspaceAgentPortShareProtocol": {
"type": "string",
"enum": ["http", "https"],
"x-enum-varnames": [
"WorkspaceAgentPortShareProtocolHTTP",
"WorkspaceAgentPortShareProtocolHTTPS"
]
},
"codersdk.WorkspaceAgentPortShares": {
"type": "object",
"properties": {

View File

@ -1608,6 +1608,7 @@ func (s *MethodTestSuite) TestWorkspacePortSharing() {
AgentName: ps.AgentName,
Port: ps.Port,
ShareLevel: ps.ShareLevel,
Protocol: ps.Protocol,
}).Asserts(ws, rbac.ActionUpdate).Returns(ps)
}))
s.Run("GetWorkspaceAgentPortShare", s.Subtest(func(db database.Store, check *expects) {

View File

@ -140,6 +140,7 @@ func WorkspaceAgentPortShare(t testing.TB, db database.Store, orig database.Work
AgentName: takeFirst(orig.AgentName, namesgenerator.GetRandomName(1)),
Port: takeFirst(orig.Port, 8080),
ShareLevel: takeFirst(orig.ShareLevel, database.AppSharingLevelPublic),
Protocol: takeFirst(orig.Protocol, database.PortShareProtocolHttp),
})
require.NoError(t, err, "insert workspace agent")
return ps

View File

@ -86,7 +86,7 @@ func New() database.Store {
UpdatedAt: dbtime.Now(),
})
if err != nil {
panic(fmt.Errorf("failed to create default organization: %w", err))
panic(xerrors.Errorf("failed to create default organization: %w", err))
}
q.defaultProxyDisplayName = "Default"
q.defaultProxyIconURL = "/emojis/1f3e1.png"
@ -7933,6 +7933,7 @@ func (q *FakeQuerier) UpsertWorkspaceAgentPortShare(_ context.Context, arg datab
for i, share := range q.workspaceAgentPortShares {
if share.WorkspaceID == arg.WorkspaceID && share.Port == arg.Port && share.AgentName == arg.AgentName {
share.ShareLevel = arg.ShareLevel
share.Protocol = arg.Protocol
q.workspaceAgentPortShares[i] = share
return share, nil
}
@ -7944,6 +7945,7 @@ func (q *FakeQuerier) UpsertWorkspaceAgentPortShare(_ context.Context, arg datab
AgentName: arg.AgentName,
Port: arg.Port,
ShareLevel: arg.ShareLevel,
Protocol: arg.Protocol,
}
q.workspaceAgentPortShares = append(q.workspaceAgentPortShares, psl)

View File

@ -95,6 +95,11 @@ CREATE TYPE parameter_type_system AS ENUM (
'hcl'
);
CREATE TYPE port_share_protocol AS ENUM (
'http',
'https'
);
CREATE TYPE provisioner_job_status AS ENUM (
'pending',
'running',
@ -1027,7 +1032,8 @@ 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
share_level app_sharing_level NOT NULL,
protocol port_share_protocol DEFAULT 'http'::port_share_protocol NOT NULL
);
CREATE TABLE workspace_agent_scripts (

View File

@ -0,0 +1,3 @@
ALTER TABLE workspace_agent_port_share DROP COLUMN protocol;
DROP TYPE port_share_protocol;

View File

@ -0,0 +1,4 @@
CREATE TYPE port_share_protocol AS ENUM ('http', 'https');
ALTER TABLE workspace_agent_port_share
ADD COLUMN protocol port_share_protocol NOT NULL DEFAULT 'http'::port_share_protocol;

View File

@ -898,6 +898,64 @@ func AllParameterTypeSystemValues() []ParameterTypeSystem {
}
}
type PortShareProtocol string
const (
PortShareProtocolHttp PortShareProtocol = "http"
PortShareProtocolHttps PortShareProtocol = "https"
)
func (e *PortShareProtocol) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = PortShareProtocol(s)
case string:
*e = PortShareProtocol(s)
default:
return fmt.Errorf("unsupported scan type for PortShareProtocol: %T", src)
}
return nil
}
type NullPortShareProtocol struct {
PortShareProtocol PortShareProtocol `json:"port_share_protocol"`
Valid bool `json:"valid"` // Valid is true if PortShareProtocol is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullPortShareProtocol) Scan(value interface{}) error {
if value == nil {
ns.PortShareProtocol, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.PortShareProtocol.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullPortShareProtocol) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.PortShareProtocol), nil
}
func (e PortShareProtocol) Valid() bool {
switch e {
case PortShareProtocolHttp,
PortShareProtocolHttps:
return true
}
return false
}
func AllPortShareProtocolValues() []PortShareProtocol {
return []PortShareProtocol{
PortShareProtocolHttp,
PortShareProtocolHttps,
}
}
// Computed status of a provisioner job. Jobs could be stuck in a hung state, these states do not guarantee any transition to another state.
type ProvisionerJobStatus string
@ -2312,10 +2370,11 @@ type WorkspaceAgentMetadatum struct {
}
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"`
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"`
Protocol PortShareProtocol `db:"protocol" json:"protocol"`
}
type WorkspaceAgentScript struct {

View File

@ -8469,7 +8469,12 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
}
const deleteWorkspaceAgentPortShare = `-- name: DeleteWorkspaceAgentPortShare :exec
DELETE FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3
DELETE FROM
workspace_agent_port_share
WHERE
workspace_id = $1
AND agent_name = $2
AND port = $3
`
type DeleteWorkspaceAgentPortShareParams struct {
@ -8484,7 +8489,17 @@ func (q *sqlQuerier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg Dele
}
const deleteWorkspaceAgentPortSharesByTemplate = `-- name: DeleteWorkspaceAgentPortSharesByTemplate :exec
DELETE FROM workspace_agent_port_share WHERE workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1)
DELETE FROM
workspace_agent_port_share
WHERE
workspace_id IN (
SELECT
id
FROM
workspaces
WHERE
template_id = $1
)
`
func (q *sqlQuerier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error {
@ -8493,7 +8508,14 @@ func (q *sqlQuerier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Contex
}
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
SELECT
workspace_id, agent_name, port, share_level, protocol
FROM
workspace_agent_port_share
WHERE
workspace_id = $1
AND agent_name = $2
AND port = $3
`
type GetWorkspaceAgentPortShareParams struct {
@ -8510,12 +8532,18 @@ func (q *sqlQuerier) GetWorkspaceAgentPortShare(ctx context.Context, arg GetWork
&i.AgentName,
&i.Port,
&i.ShareLevel,
&i.Protocol,
)
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
SELECT
workspace_id, agent_name, port, share_level, protocol
FROM
workspace_agent_port_share
WHERE
workspace_id = $1
`
func (q *sqlQuerier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) {
@ -8532,6 +8560,7 @@ func (q *sqlQuerier) ListWorkspaceAgentPortShares(ctx context.Context, workspace
&i.AgentName,
&i.Port,
&i.ShareLevel,
&i.Protocol,
); err != nil {
return nil, err
}
@ -8547,7 +8576,20 @@ func (q *sqlQuerier) ListWorkspaceAgentPortShares(ctx context.Context, workspace
}
const reduceWorkspaceAgentShareLevelToAuthenticatedByTemplate = `-- name: ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate :exec
UPDATE workspace_agent_port_share SET share_level = 'authenticated' WHERE share_level = 'public' AND workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1)
UPDATE
workspace_agent_port_share
SET
share_level = 'authenticated'
WHERE
share_level = 'public'
AND workspace_id IN (
SELECT
id
FROM
workspaces
WHERE
template_id = $1
)
`
func (q *sqlQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
@ -8556,16 +8598,38 @@ func (q *sqlQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx
}
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
INSERT INTO
workspace_agent_port_share (
workspace_id,
agent_name,
port,
share_level,
protocol
)
VALUES (
$1,
$2,
$3,
$4,
$5
)
ON CONFLICT (
workspace_id,
agent_name,
port
)
DO UPDATE SET
share_level = $4,
protocol = $5
RETURNING workspace_id, agent_name, port, share_level, protocol
`
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"`
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"`
Protocol PortShareProtocol `db:"protocol" json:"protocol"`
}
func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) {
@ -8574,6 +8638,7 @@ func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg Upse
arg.AgentName,
arg.Port,
arg.ShareLevel,
arg.Protocol,
)
var i WorkspaceAgentPortShare
err := row.Scan(
@ -8581,6 +8646,7 @@ func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg Upse
&i.AgentName,
&i.Port,
&i.ShareLevel,
&i.Protocol,
)
return i, err
}

View File

@ -1,19 +1,80 @@
-- name: GetWorkspaceAgentPortShare :one
SELECT * FROM workspace_agent_port_share WHERE workspace_id = $1 AND agent_name = $2 AND port = $3;
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;
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;
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 *;
INSERT INTO
workspace_agent_port_share (
workspace_id,
agent_name,
port,
share_level,
protocol
)
VALUES (
$1,
$2,
$3,
$4,
$5
)
ON CONFLICT (
workspace_id,
agent_name,
port
)
DO UPDATE SET
share_level = $4,
protocol = $5
RETURNING *;
-- name: ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate :exec
UPDATE workspace_agent_port_share SET share_level = 'authenticated' WHERE share_level = 'public' AND workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1);
UPDATE
workspace_agent_port_share
SET
share_level = 'authenticated'
WHERE
share_level = 'public'
AND workspace_id IN (
SELECT
id
FROM
workspaces
WHERE
template_id = $1
);
-- name: DeleteWorkspaceAgentPortSharesByTemplate :exec
DELETE FROM workspace_agent_port_share WHERE workspace_id IN (SELECT id FROM workspaces WHERE template_id = $1);
DELETE FROM
workspace_agent_port_share
WHERE
workspace_id IN (
SELECT
id
FROM
workspaces
WHERE
template_id = $1
);

View File

@ -4,6 +4,7 @@ import (
"database/sql"
"errors"
"net/http"
"slices"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/httpapi"
@ -55,6 +56,12 @@ func (api *API) postWorkspaceAgentPortShare(rw http.ResponseWriter, r *http.Requ
})
return
}
if !req.Protocol.ValidPortProtocol() {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Port protocol not allowed.",
})
return
}
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
if err != nil {
@ -95,6 +102,7 @@ func (api *API) postWorkspaceAgentPortShare(rw http.ResponseWriter, r *http.Requ
AgentName: req.AgentName,
Port: req.Port,
ShareLevel: database.AppSharingLevel(req.ShareLevel),
Protocol: database.PortShareProtocol(req.Protocol),
})
if err != nil {
httpapi.InternalServerError(rw, err)
@ -179,6 +187,9 @@ func convertPortShares(shares []database.WorkspaceAgentPortShare) []codersdk.Wor
for _, share := range shares {
converted = append(converted, convertPortShare(share))
}
slices.SortFunc(converted, func(i, j codersdk.WorkspaceAgentPortShare) int {
return (int)(i.Port - j.Port)
})
return converted
}
@ -188,5 +199,6 @@ func convertPortShare(share database.WorkspaceAgentPortShare) codersdk.Workspace
AgentName: share.AgentName,
Port: share.Port,
ShareLevel: codersdk.WorkspaceAgentPortShareLevel(share.ShareLevel),
Protocol: codersdk.WorkspaceAgentPortShareProtocol(share.Protocol),
}
}

View File

@ -43,6 +43,7 @@ func TestPostWorkspaceAgentPortShare(t *testing.T) {
AgentName: agents[0].Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevel("owner"),
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.Error(t, err)
@ -51,6 +52,16 @@ func TestPostWorkspaceAgentPortShare(t *testing.T) {
AgentName: agents[0].Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevel("invalid"),
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.Error(t, err)
// invalid protocol should fail
_, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
AgentName: agents[0].Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
Protocol: codersdk.WorkspaceAgentPortShareProtocol("invalid"),
})
require.Error(t, err)
@ -59,6 +70,7 @@ func TestPostWorkspaceAgentPortShare(t *testing.T) {
AgentName: agents[0].Name,
Port: 0,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.Error(t, err)
_, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
@ -73,18 +85,54 @@ func TestPostWorkspaceAgentPortShare(t *testing.T) {
AgentName: agents[0].Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTPS,
})
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.ShareLevel)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareProtocolHTTPS, ps.Protocol)
// update share level
// list
list, err := client.GetWorkspaceAgentPortShares(ctx, r.Workspace.ID)
require.NoError(t, err)
require.Len(t, list.Shares, 1)
require.EqualValues(t, agents[0].Name, list.Shares[0].AgentName)
require.EqualValues(t, 8080, list.Shares[0].Port)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, list.Shares[0].ShareLevel)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareProtocolHTTPS, list.Shares[0].Protocol)
// update share level and protocol
ps, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
AgentName: agents[0].Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelAuthenticated, ps.ShareLevel)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareProtocolHTTP, ps.Protocol)
// list
list, err = client.GetWorkspaceAgentPortShares(ctx, r.Workspace.ID)
require.NoError(t, err)
require.Len(t, list.Shares, 1)
require.EqualValues(t, agents[0].Name, list.Shares[0].AgentName)
require.EqualValues(t, 8080, list.Shares[0].Port)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelAuthenticated, list.Shares[0].ShareLevel)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareProtocolHTTP, list.Shares[0].Protocol)
// list 2 ordered by port
ps, err = client.UpsertWorkspaceAgentPortShare(ctx, r.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
AgentName: agents[0].Name,
Port: 8081,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTPS,
})
require.NoError(t, err)
list, err = client.GetWorkspaceAgentPortShares(ctx, r.Workspace.ID)
require.NoError(t, err)
require.Len(t, list.Shares, 2)
require.EqualValues(t, 8080, list.Shares[0].Port)
require.EqualValues(t, 8081, list.Shares[1].Port)
}
func TestGetWorkspaceAgentPortShares(t *testing.T) {
@ -115,6 +163,7 @@ func TestGetWorkspaceAgentPortShares(t *testing.T) {
AgentName: agents[0].Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.NoError(t, err)
@ -155,6 +204,7 @@ func TestDeleteWorkspaceAgentPortShare(t *testing.T) {
AgentName: agents[0].Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.ShareLevel)

View File

@ -909,79 +909,6 @@ 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()
@ -1119,6 +1046,108 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
})
})
t.Run("PortSharing", func(t *testing.T) {
t.Run("NoShare", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
appDetails := setupProxyTest(t, nil)
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("AuthenticatedOK", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
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,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
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("PublicOK", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
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,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
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("HTTPS", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
appDetails := setupProxyTest(t, &DeploymentOptions{
ServeHTTPS: true,
})
port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32)
require.NoError(t, err)
_, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
AgentName: proxyTestAgentName,
Port: int32(port),
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTPS,
})
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("AppSharing", func(t *testing.T) {
t.Parallel()

View File

@ -342,6 +342,10 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR
}
// No port share found, so we keep default to owner.
} else {
if ps.Protocol == database.PortShareProtocolHttps {
// Apply HTTPS protocol if specified.
appURL = fmt.Sprintf("https://127.0.0.1:%d", portUint)
}
appSharingLevel = ps.ShareLevel
}
} else {

View File

@ -269,6 +269,3 @@ func Test_RequestValidate(t *testing.T) {
})
}
}
// getDatabase is tested heavily in auth_test.go, so we don't have specific
// tests for it here.

View File

@ -13,23 +13,29 @@ const (
WorkspaceAgentPortShareLevelOwner WorkspaceAgentPortShareLevel = "owner"
WorkspaceAgentPortShareLevelAuthenticated WorkspaceAgentPortShareLevel = "authenticated"
WorkspaceAgentPortShareLevelPublic WorkspaceAgentPortShareLevel = "public"
WorkspaceAgentPortShareProtocolHTTP WorkspaceAgentPortShareProtocol = "http"
WorkspaceAgentPortShareProtocolHTTPS WorkspaceAgentPortShareProtocol = "https"
)
type (
WorkspaceAgentPortShareLevel string
WorkspaceAgentPortShareProtocol string
UpsertWorkspaceAgentPortShareRequest struct {
AgentName string `json:"agent_name"`
Port int32 `json:"port"`
ShareLevel WorkspaceAgentPortShareLevel `json:"share_level"`
AgentName string `json:"agent_name"`
Port int32 `json:"port"`
ShareLevel WorkspaceAgentPortShareLevel `json:"share_level" enums:"owner,authenticated,public"`
Protocol WorkspaceAgentPortShareProtocol `json:"protocol" enums:"http,https"`
}
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"`
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"`
AgentName string `json:"agent_name"`
Port int32 `json:"port"`
ShareLevel WorkspaceAgentPortShareLevel `json:"share_level" enums:"owner,authenticated,public"`
Protocol WorkspaceAgentPortShareProtocol `json:"protocol" enums:"http,https"`
}
DeleteWorkspaceAgentPortShareRequest struct {
AgentName string `json:"agent_name"`
@ -48,6 +54,11 @@ func (l WorkspaceAgentPortShareLevel) ValidPortShareLevel() bool {
l == WorkspaceAgentPortShareLevelPublic
}
func (p WorkspaceAgentPortShareProtocol) ValidPortProtocol() bool {
return p == WorkspaceAgentPortShareProtocolHTTP ||
p == WorkspaceAgentPortShareProtocolHTTPS
}
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)

View File

@ -57,6 +57,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/port-share \
{
"agent_name": "string",
"port": 0,
"protocol": "http",
"share_level": "owner"
}
```
@ -76,6 +77,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/port-share \
{
"agent_name": "string",
"port": 0,
"protocol": "http",
"share_level": "owner",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
}

62
docs/api/schemas.md generated
View File

@ -6613,17 +6613,29 @@ If the schedule is empty, the user will be updated to use the default schedule.|
{
"agent_name": "string",
"port": 0,
"protocol": "http",
"share_level": "owner"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- |
| `agent_name` | string | false | | |
| `port` | integer | false | | |
| `share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | |
| Name | Type | Required | Restrictions | Description |
| ------------- | ------------------------------------------------------------------------------------ | -------- | ------------ | ----------- |
| `agent_name` | string | false | | |
| `port` | integer | false | | |
| `protocol` | [codersdk.WorkspaceAgentPortShareProtocol](#codersdkworkspaceagentportshareprotocol) | false | | |
| `share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | |
#### Enumerated Values
| Property | Value |
| ------------- | --------------- |
| `protocol` | `http` |
| `protocol` | `https` |
| `share_level` | `owner` |
| `share_level` | `authenticated` |
| `share_level` | `public` |
## codersdk.User
@ -7579,6 +7591,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
{
"agent_name": "string",
"port": 0,
"protocol": "http",
"share_level": "owner",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
}
@ -7586,12 +7599,23 @@ If the schedule is empty, the user will be updated to use the default schedule.|
### Properties
| Name | Type | Required | Restrictions | Description |
| -------------- | ------------------------------------------------------------------------------ | -------- | ------------ | ----------- |
| `agent_name` | string | false | | |
| `port` | integer | false | | |
| `share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | |
| `workspace_id` | string | false | | |
| Name | Type | Required | Restrictions | Description |
| -------------- | ------------------------------------------------------------------------------------ | -------- | ------------ | ----------- |
| `agent_name` | string | false | | |
| `port` | integer | false | | |
| `protocol` | [codersdk.WorkspaceAgentPortShareProtocol](#codersdkworkspaceagentportshareprotocol) | false | | |
| `share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | |
| `workspace_id` | string | false | | |
#### Enumerated Values
| Property | Value |
| ------------- | --------------- |
| `protocol` | `http` |
| `protocol` | `https` |
| `share_level` | `owner` |
| `share_level` | `authenticated` |
| `share_level` | `public` |
## codersdk.WorkspaceAgentPortShareLevel
@ -7609,6 +7633,21 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `authenticated` |
| `public` |
## codersdk.WorkspaceAgentPortShareProtocol
```json
"http"
```
### Properties
#### Enumerated Values
| Value |
| ------- |
| `http` |
| `https` |
## codersdk.WorkspaceAgentPortShares
```json
@ -7617,6 +7656,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
{
"agent_name": "string",
"port": 0,
"protocol": "http",
"share_level": "owner",
"workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9"
}

View File

@ -219,6 +219,7 @@ func TestTemplates(t *testing.T) {
AgentName: ws.LatestBuild.Resources[0].Agents[0].Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.NoError(t, err)

View File

@ -43,6 +43,7 @@ func TestWorkspacePortShare(t *testing.T) {
AgentName: r.sdkAgent.Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.Error(t, err, "Port sharing level not allowed")
@ -57,6 +58,7 @@ func TestWorkspacePortShare(t *testing.T) {
AgentName: r.sdkAgent.Name,
Port: 8080,
ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
Protocol: codersdk.WorkspaceAgentPortShareProtocolHTTP,
})
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAgentPortShareLevelPublic, ps.ShareLevel)

View File

@ -1504,6 +1504,7 @@ export interface UpsertWorkspaceAgentPortShareRequest {
readonly agent_name: string;
readonly port: number;
readonly share_level: WorkspaceAgentPortShareLevel;
readonly protocol: WorkspaceAgentPortShareProtocol;
}
// From codersdk/users.go
@ -1762,6 +1763,7 @@ export interface WorkspaceAgentPortShare {
readonly agent_name: string;
readonly port: number;
readonly share_level: WorkspaceAgentPortShareLevel;
readonly protocol: WorkspaceAgentPortShareProtocol;
}
// From codersdk/workspaceagentportshare.go
@ -2351,6 +2353,11 @@ export const WorkspaceAgentPortShareLevels: WorkspaceAgentPortShareLevel[] = [
"public",
];
// From codersdk/workspaceagentportshare.go
export type WorkspaceAgentPortShareProtocol = "http" | "https";
export const WorkspaceAgentPortShareProtocols: WorkspaceAgentPortShareProtocol[] =
["http", "https"];
// From codersdk/workspaceagents.go
export type WorkspaceAgentStartupScriptBehavior = "blocking" | "non-blocking";
export const WorkspaceAgentStartupScriptBehaviors: WorkspaceAgentStartupScriptBehavior[] =

View File

@ -26,10 +26,11 @@ import {
} from "api/queries/workspaceportsharing";
import {
type Template,
type UpsertWorkspaceAgentPortShareRequest,
type WorkspaceAgent,
type WorkspaceAgentListeningPort,
type WorkspaceAgentPortShareLevel,
type UpsertWorkspaceAgentPortShareRequest,
type WorkspaceAgentPortShareProtocol,
WorkspaceAppSharingLevels,
} from "api/typesGenerated";
import {
@ -162,6 +163,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
initialValues: {
agent_name: agent.name,
port: undefined,
protocol: "http",
share_level: "authenticated",
},
validationSchema,
@ -325,6 +327,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
await upsertSharedPortMutation.mutateAsync({
agent_name: agent.name,
port: port.port,
protocol: "http",
share_level: "authenticated",
});
await sharedPortsQuery.refetch();
@ -384,8 +387,31 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
)}
{label}
</Link>
<FormControl size="small" css={styles.protocolFormControl}>
<Select
css={styles.shareLevelSelect}
value={share.protocol}
onChange={async (event) => {
await upsertSharedPortMutation.mutateAsync({
agent_name: agent.name,
port: share.port,
protocol: event.target
.value as WorkspaceAgentPortShareProtocol,
share_level: share.share_level,
});
await sharedPortsQuery.refetch();
}}
>
<MenuItem value="http">HTTP</MenuItem>
<MenuItem value="https">HTTPS</MenuItem>
</Select>
</FormControl>
<Stack direction="row" justifyContent="flex-end">
<FormControl size="small">
<FormControl
size="small"
css={styles.shareLevelFormControl}
>
<Select
css={styles.shareLevelSelect}
value={share.share_level}
@ -393,6 +419,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
await upsertSharedPortMutation.mutateAsync({
agent_name: agent.name,
port: share.port,
protocol: share.protocol,
share_level: event.target
.value as WorkspaceAgentPortShareLevel,
});
@ -452,6 +479,17 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
type="number"
value={form.values.port}
/>
<TextField
{...getFieldHelpers("protocol")}
disabled={isSubmitting}
fullWidth
select
value={form.values.protocol}
label="Protocol"
>
<MenuItem value="http">HTTP</MenuItem>
<MenuItem value="https">HTTPS</MenuItem>
</TextField>
<TextField
{...getFieldHelpers("share_level")}
disabled={isSubmitting}
@ -486,7 +524,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
const classNames = {
paper: (css, theme) => css`
padding: 0;
width: 304px;
width: 404px;
color: ${theme.palette.text.secondary};
margin-top: 4px;
`,
@ -515,6 +553,7 @@ const styles = {
paddingTop: 8,
paddingBottom: 8,
fontWeight: 500,
minWidth: 80,
}),
portNumber: (theme) => ({
@ -563,4 +602,13 @@ const styles = {
display: "block",
width: "100%",
}),
sharedPortLink: () => ({
minWidth: 80,
}),
protocolFormControl: () => ({
minWidth: 90,
}),
shareLevelFormControl: () => ({
minWidth: 140,
}),
} satisfies Record<string, Interpolation<Theme>>;

View File

@ -15,7 +15,7 @@ const meta: Meta<typeof PortForwardPopoverView> = {
(Story) => (
<div
css={(theme) => ({
width: 304,
width: 404,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
backgroundColor: theme.palette.background.paper,

View File

@ -3276,18 +3276,21 @@ export const MockSharedPortsResponse: TypesGen.WorkspaceAgentPortShares = {
agent_name: "a-workspace-agent",
port: 4000,
share_level: "authenticated",
protocol: "http",
},
{
workspace_id: MockWorkspace.id,
agent_name: "a-workspace-agent",
port: 8080,
port: 65535,
share_level: "authenticated",
protocol: "https",
},
{
workspace_id: MockWorkspace.id,
agent_name: "a-workspace-agent",
port: 8081,
share_level: "public",
protocol: "http",
},
],
};