This commit is contained in:
Garrett Delfosse 2024-05-01 21:47:29 +00:00 committed by GitHub
commit 94d7cd4721
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 211 additions and 266 deletions

2
coderd/apidoc/docs.go generated
View File

@ -9517,7 +9517,6 @@ const docTemplate = `{
"type": "string",
"enum": [
"example",
"shared-ports",
"auto-fill-parameters"
],
"x-enum-comments": {
@ -9526,7 +9525,6 @@ const docTemplate = `{
},
"x-enum-varnames": [
"ExperimentExample",
"ExperimentSharedPorts",
"ExperimentAutoFillParameters"
]
},

View File

@ -8516,16 +8516,12 @@
},
"codersdk.Experiment": {
"type": "string",
"enum": ["example", "shared-ports", "auto-fill-parameters"],
"enum": ["example", "auto-fill-parameters"],
"x-enum-comments": {
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
"ExperimentExample": "This isn't used for anything."
},
"x-enum-varnames": [
"ExperimentExample",
"ExperimentSharedPorts",
"ExperimentAutoFillParameters"
]
"x-enum-varnames": ["ExperimentExample", "ExperimentAutoFillParameters"]
},
"codersdk.ExternalAuth": {
"type": "object",

View File

@ -1055,9 +1055,6 @@ func New(options *Options) *API {
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)

View File

@ -511,6 +511,10 @@ func TestAgentStats(t *testing.T) {
func TestExperimentsMetric(t *testing.T) {
t.Parallel()
if len(codersdk.ExperimentsAll) == 0 {
t.Skip("No experiments are currently defined; skipping test.")
}
tests := []struct {
name string
experiments codersdk.Experiments
@ -518,16 +522,16 @@ func TestExperimentsMetric(t *testing.T) {
}{
{
name: "Enabled experiment is exported in metrics",
experiments: codersdk.Experiments{codersdk.ExperimentSharedPorts},
experiments: codersdk.Experiments{codersdk.ExperimentExample},
expected: map[codersdk.Experiment]float64{
codersdk.ExperimentSharedPorts: 1,
codersdk.ExperimentExample: 1,
},
},
{
name: "Disabled experiment is exported in metrics",
experiments: codersdk.Experiments{},
expected: map[codersdk.Experiment]float64{
codersdk.ExperimentSharedPorts: 0,
codersdk.ExperimentExample: 0,
},
},
{
@ -549,7 +553,6 @@ func TestExperimentsMetric(t *testing.T) {
out, err := reg.Gather()
require.NoError(t, err)
require.Lenf(t, out, 1, "unexpected number of registered metrics")
seen := make(map[codersdk.Experiment]float64)
for _, metric := range out[0].GetMetric() {

View File

@ -19,11 +19,7 @@ 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,
})
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
@ -140,11 +136,7 @@ func TestGetWorkspaceAgentPortShares(t *testing.T) {
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,
})
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
@ -180,11 +172,7 @@ func TestDeleteWorkspaceAgentPortShare(t *testing.T) {
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,
})
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)

View File

@ -257,7 +257,6 @@ func TestWorkspaceApps(t *testing.T) {
deploymentValues.DisablePathApps = serpent.Bool(opts.DisablePathApps)
deploymentValues.Dangerous.AllowPathAppSharing = serpent.Bool(opts.DangerousAllowPathAppSharing)
deploymentValues.Dangerous.AllowPathAppSiteOwnerAccess = serpent.Bool(opts.DangerousAllowPathAppSiteOwnerAccess)
deploymentValues.Experiments = append(deploymentValues.Experiments, string(codersdk.ExperimentSharedPorts))
if opts.DisableSubdomainApps {
opts.AppHost = ""

View File

@ -2192,8 +2192,7 @@ type Experiment string
const (
// Add new experiments here!
ExperimentExample Experiment = "example" // This isn't used for anything.
ExperimentSharedPorts Experiment = "shared-ports"
ExperimentExample Experiment = "example" // This isn't used for anything.
ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature.
)
@ -2201,9 +2200,7 @@ const (
// users to opt-in to via --experimental='*'.
// Experiments that are not ready for consumption by all users should
// not be included here and will be essentially hidden.
var ExperimentsAll = Experiments{
ExperimentSharedPorts,
}
var ExperimentsAll = Experiments{}
// Experiments is a list of experiments.
// Multiple experiments may be enabled at the same time.

1
docs/api/schemas.md generated
View File

@ -2666,7 +2666,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| Value |
| ---------------------- |
| `example` |
| `shared-ports` |
| `auto-fill-parameters` |
## codersdk.ExternalAuth

View File

@ -17,12 +17,9 @@ import (
func TestWorkspacePortShare(t *testing.T) {
t.Parallel()
dep := coderdtest.DeploymentValues(t)
dep.Experiments = append(dep.Experiments, string(codersdk.ExperimentSharedPorts))
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
DeploymentValues: dep,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{

View File

@ -1898,12 +1898,8 @@ export const Entitlements: Entitlement[] = [
];
// From codersdk/deployment.go
export type Experiment = "auto-fill-parameters" | "example" | "shared-ports";
export const Experiments: Experiment[] = [
"auto-fill-parameters",
"example",
"shared-ports",
];
export type Experiment = "auto-fill-parameters" | "example";
export const Experiments: Experiment[] = ["auto-fill-parameters", "example"];
// From codersdk/deployment.go
export type FeatureName =

View File

@ -65,7 +65,7 @@ export interface PortForwardButtonProps {
export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
const { agent } = props;
const { entitlements, experiments } = useDashboard();
const { entitlements } = useDashboard();
const paper = useClassName(classNames.paper, []);
const portsQuery = useQuery({
@ -103,7 +103,6 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
<PortForwardPopoverView
{...props}
listeningPorts={portsQuery.data?.ports}
portSharingExperimentEnabled={experiments.includes("shared-ports")}
portSharingControlsEnabled={
entitlements.features.control_shared_ports.enabled
}
@ -121,7 +120,6 @@ const getValidationSchema = (): Yup.AnyObjectSchema =>
interface PortForwardPopoverViewProps extends PortForwardButtonProps {
listeningPorts?: readonly WorkspaceAgentListeningPort[];
portSharingExperimentEnabled: boolean;
portSharingControlsEnabled: boolean;
}
@ -135,7 +133,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
template,
username,
listeningPorts,
portSharingExperimentEnabled,
portSharingControlsEnabled,
}) => {
const theme = useTheme();
@ -200,9 +197,9 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
filteredSharedPorts.every((sharedPort) => sharedPort.port !== port.port),
);
// only disable the form if shared port controls are entitled and the template doesn't allow sharing ports
const canSharePorts =
portSharingExperimentEnabled &&
!(portSharingControlsEnabled && template.max_port_share_level === "owner");
const canSharePorts = !(
portSharingControlsEnabled && template.max_port_share_level === "owner"
);
const canSharePortsPublic =
canSharePorts && template.max_port_share_level === "public";
@ -396,183 +393,179 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
})}
</Stack>
</div>
{portSharingExperimentEnabled && (
<div
css={{
padding: 20,
borderTop: `1px solid ${theme.palette.divider}`,
}}
>
<HelpTooltipTitle>Shared Ports</HelpTooltipTitle>
<HelpTooltipText css={{ color: theme.palette.text.secondary }}>
{canSharePorts
? "Ports can be shared with other Coder users or with the public. Changing the protocol may take up to 1 minute to take effect."
: "This workspace template does not allow sharing ports. Contact a template administrator to enable port sharing."}
</HelpTooltipText>
{canSharePorts && (
<div>
{filteredSharedPorts?.map((share) => {
const url = portForwardURL(
host,
share.port,
agent.name,
workspaceName,
username,
share.protocol,
);
const label = share.port;
return (
<Stack
key={share.port}
direction="row"
justifyContent="space-between"
alignItems="center"
<div
css={{
padding: 20,
borderTop: `1px solid ${theme.palette.divider}`,
}}
>
<HelpTooltipTitle>Shared Ports</HelpTooltipTitle>
<HelpTooltipText css={{ color: theme.palette.text.secondary }}>
{canSharePorts
? "Ports can be shared with other Coder users or with the public. Changing the protocol may take up to 1 minute to take effect."
: "This workspace template does not allow sharing ports. Contact a template administrator to enable port sharing."}
</HelpTooltipText>
{canSharePorts && (
<div>
{filteredSharedPorts?.map((share) => {
const url = portForwardURL(
host,
share.port,
agent.name,
workspaceName,
username,
share.protocol,
);
const label = share.port;
return (
<Stack
key={share.port}
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Link
underline="none"
css={styles.portLink}
href={url}
target="_blank"
rel="noreferrer"
>
<Link
underline="none"
css={styles.portLink}
href={url}
target="_blank"
rel="noreferrer"
{share.share_level === "public" ? (
<LockOpenIcon css={{ width: 14, height: 14 }} />
) : (
<LockIcon css={{ width: 14, height: 14 }} />
)}
{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"
css={styles.shareLevelFormControl}
>
{share.share_level === "public" ? (
<LockOpenIcon css={{ width: 14, height: 14 }} />
) : (
<LockIcon css={{ width: 14, height: 14 }} />
)}
{label}
</Link>
<FormControl size="small" css={styles.protocolFormControl}>
<Select
css={styles.shareLevelSelect}
value={share.protocol}
value={share.share_level}
onChange={async (event) => {
await upsertSharedPortMutation.mutateAsync({
agent_name: agent.name,
port: share.port,
protocol: event.target
.value as WorkspaceAgentPortShareProtocol,
share_level: share.share_level,
protocol: share.protocol,
share_level: event.target
.value as WorkspaceAgentPortShareLevel,
});
await sharedPortsQuery.refetch();
}}
>
<MenuItem value="http">HTTP</MenuItem>
<MenuItem value="https">HTTPS</MenuItem>
<MenuItem value="authenticated">Authenticated</MenuItem>
{canSharePortsPublic ? (
<MenuItem value="public">Public</MenuItem>
) : (
disabledPublicMenuItem
)}
</Select>
</FormControl>
<Stack direction="row" justifyContent="flex-end">
<FormControl
size="small"
css={styles.shareLevelFormControl}
>
<Select
css={styles.shareLevelSelect}
value={share.share_level}
onChange={async (event) => {
await upsertSharedPortMutation.mutateAsync({
agent_name: agent.name,
port: share.port,
protocol: share.protocol,
share_level: event.target
.value as WorkspaceAgentPortShareLevel,
});
await sharedPortsQuery.refetch();
}}
>
<MenuItem value="authenticated">
Authenticated
</MenuItem>
{canSharePortsPublic ? (
<MenuItem value="public">Public</MenuItem>
) : (
disabledPublicMenuItem
)}
</Select>
</FormControl>
<Button
size="small"
variant="text"
css={styles.deleteButton}
onClick={async () => {
await deleteSharedPortMutation.mutateAsync({
agent_name: agent.name,
port: share.port,
});
await sharedPortsQuery.refetch();
<Button
size="small"
variant="text"
css={styles.deleteButton}
onClick={async () => {
await deleteSharedPortMutation.mutateAsync({
agent_name: agent.name,
port: share.port,
});
await sharedPortsQuery.refetch();
}}
>
<CloseIcon
css={{
width: 14,
height: 14,
color: theme.palette.text.primary,
}}
>
<CloseIcon
css={{
width: 14,
height: 14,
color: theme.palette.text.primary,
}}
/>
</Button>
</Stack>
/>
</Button>
</Stack>
);
})}
<form onSubmit={form.handleSubmit}>
<Stack
direction="column"
gap={2}
justifyContent="flex-end"
sx={{
marginTop: 2,
}}
>
<TextField
{...getFieldHelpers("port")}
disabled={isSubmitting}
label="Port"
size="small"
variant="outlined"
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}
fullWidth
select
value={form.values.share_level}
label="Sharing Level"
>
<MenuItem value="authenticated">Authenticated</MenuItem>
{canSharePortsPublic ? (
<MenuItem value="public">Public</MenuItem>
) : (
disabledPublicMenuItem
)}
</TextField>
<LoadingButton
variant="contained"
type="submit"
loading={isSubmitting}
disabled={!form.isValid}
>
Share Port
</LoadingButton>
</Stack>
</form>
</div>
)}
</div>
)}
);
})}
<form onSubmit={form.handleSubmit}>
<Stack
direction="column"
gap={2}
justifyContent="flex-end"
sx={{
marginTop: 2,
}}
>
<TextField
{...getFieldHelpers("port")}
disabled={isSubmitting}
label="Port"
size="small"
variant="outlined"
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}
fullWidth
select
value={form.values.share_level}
label="Sharing Level"
>
<MenuItem value="authenticated">Authenticated</MenuItem>
{canSharePortsPublic ? (
<MenuItem value="public">Public</MenuItem>
) : (
disabledPublicMenuItem
)}
</TextField>
<LoadingButton
variant="contained"
type="submit"
loading={isSubmitting}
disabled={!form.isValid}
>
Share Port
</LoadingButton>
</Stack>
</form>
</div>
)}
</div>
</>
);
};

View File

@ -29,7 +29,6 @@ const meta: Meta<typeof PortForwardPopoverView> = {
agent: MockWorkspaceAgent,
template: MockTemplate,
workspaceID: MockWorkspace.id,
portSharingExperimentEnabled: true,
portSharingControlsEnabled: true,
},
};
@ -83,13 +82,6 @@ export const Empty: Story = {
},
};
export const NoPortSharingExperiment: Story = {
args: {
listeningPorts: MockListeningPortsResponse.ports,
portSharingExperimentEnabled: false,
},
};
export const AGPLPortSharing: Story = {
args: {
listeningPorts: MockListeningPortsResponse.ports,

View File

@ -21,7 +21,6 @@ describe("Port Forward Popover View", () => {
template={MockTemplate}
workspaceID={MockWorkspace.id}
listeningPorts={MockListeningPortsResponse.ports}
portSharingExperimentEnabled
portSharingControlsEnabled
host="host"
username="username"

View File

@ -61,7 +61,6 @@ export interface TemplateSettingsForm {
initialTouched?: FormikTouched<UpdateTemplateMeta>;
accessControlEnabled: boolean;
advancedSchedulingEnabled: boolean;
portSharingExperimentEnabled: boolean;
portSharingControlsEnabled: boolean;
}
@ -74,7 +73,6 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
initialTouched,
accessControlEnabled,
advancedSchedulingEnabled,
portSharingExperimentEnabled,
portSharingControlsEnabled,
}) => {
const validationSchema = getValidationSchema();
@ -258,45 +256,43 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
</FormFields>
</FormSection>
{portSharingExperimentEnabled && (
<FormSection
title="Port Sharing"
description="Shared ports with the Public sharing level can be accessed by anyone,
<FormSection
title="Port Sharing"
description="Shared ports with the Public sharing level can be accessed by anyone,
while ports with the Authenticated sharing level can only be accessed
by authenticated Coder users. Ports with the Owner sharing level can
only be accessed by the workspace owner."
>
<FormFields>
<TextField
{...getFieldHelpers("max_port_share_level", {
helperText:
"The maximum level of port sharing allowed for workspaces.",
})}
disabled={isSubmitting || !portSharingControlsEnabled}
fullWidth
select
value={
portSharingControlsEnabled
? form.values.max_port_share_level
: "public"
}
label="Maximum Port Sharing Level"
>
<MenuItem value="owner">Owner</MenuItem>
<MenuItem value="authenticated">Authenticated</MenuItem>
<MenuItem value="public">Public</MenuItem>
</TextField>
{!portSharingControlsEnabled && (
<Stack direction="row" spacing={2} alignItems="center">
<EnterpriseBadge />
<FormHelperText>
Enterprise license required to control max port sharing level.
</FormHelperText>
</Stack>
)}
</FormFields>
</FormSection>
)}
>
<FormFields>
<TextField
{...getFieldHelpers("max_port_share_level", {
helperText:
"The maximum level of port sharing allowed for workspaces.",
})}
disabled={isSubmitting || !portSharingControlsEnabled}
fullWidth
select
value={
portSharingControlsEnabled
? form.values.max_port_share_level
: "public"
}
label="Maximum Port Sharing Level"
>
<MenuItem value="owner">Owner</MenuItem>
<MenuItem value="authenticated">Authenticated</MenuItem>
<MenuItem value="public">Public</MenuItem>
</TextField>
{!portSharingControlsEnabled && (
<Stack direction="row" spacing={2} alignItems="center">
<EnterpriseBadge />
<FormHelperText>
Enterprise license required to control max port sharing level.
</FormHelperText>
</Stack>
)}
</FormFields>
</FormSection>
<FormFooter onCancel={onCancel} isLoading={isSubmitting} />
</HorizontalForm>

View File

@ -18,11 +18,10 @@ export const TemplateSettingsPage: FC = () => {
const { organizationId } = useAuthenticated();
const { template } = useTemplateSettings();
const queryClient = useQueryClient();
const { entitlements, experiments } = useDashboard();
const { entitlements } = useDashboard();
const accessControlEnabled = entitlements.features.access_control.enabled;
const advancedSchedulingEnabled =
entitlements.features.advanced_template_scheduling.enabled;
const sharedPortsExperimentEnabled = experiments.includes("shared-ports");
const sharedPortControlsEnabled =
entitlements.features.control_shared_ports.enabled;
@ -73,7 +72,6 @@ export const TemplateSettingsPage: FC = () => {
}}
accessControlEnabled={accessControlEnabled}
advancedSchedulingEnabled={advancedSchedulingEnabled}
sharedPortsExperimentEnabled={sharedPortsExperimentEnabled}
sharedPortControlsEnabled={sharedPortControlsEnabled}
/>
</>

View File

@ -14,7 +14,6 @@ export interface TemplateSettingsPageViewProps {
>["initialTouched"];
accessControlEnabled: boolean;
advancedSchedulingEnabled: boolean;
sharedPortsExperimentEnabled: boolean;
sharedPortControlsEnabled: boolean;
}
@ -27,7 +26,6 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
initialTouched,
accessControlEnabled,
advancedSchedulingEnabled,
sharedPortsExperimentEnabled,
sharedPortControlsEnabled,
}) => {
return (
@ -45,7 +43,6 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
error={submitError}
accessControlEnabled={accessControlEnabled}
advancedSchedulingEnabled={advancedSchedulingEnabled}
portSharingExperimentEnabled={sharedPortsExperimentEnabled}
portSharingControlsEnabled={sharedPortControlsEnabled}
/>
</>