Skip to content

feat: make dynamic parameters opt-in by default for new templates #19006

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions cli/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,18 @@ func TestStart(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, template.ID)
workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
}
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Stop the workspace
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop, func(request *codersdk.CreateWorkspaceBuildRequest) {
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
}
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)

inv, root := clitest.New(t, "start", workspace.Name, "--prompt-ephemeral-parameters")
Expand Down
2 changes: 1 addition & 1 deletion cli/testdata/coder_list_--output_json.golden
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"template_allow_user_cancel_workspace_jobs": false,
"template_active_version_id": "============[version ID]============",
"template_require_active_version": false,
"template_use_classic_parameter_flow": true,
"template_use_classic_parameter_flow": false,
"latest_build": {
"id": "========[workspace build ID]========",
"created_at": "====[timestamp]=====",
Expand Down
2 changes: 1 addition & 1 deletion coderd/database/dbgen/dbgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func Template(t testing.TB, db database.Store, seed database.Template) database.
DisplayName: takeFirst(seed.DisplayName, testutil.GetRandomName(t)),
AllowUserCancelWorkspaceJobs: seed.AllowUserCancelWorkspaceJobs,
MaxPortSharingLevel: takeFirst(seed.MaxPortSharingLevel, database.AppSharingLevelOwner),
UseClassicParameterFlow: takeFirst(seed.UseClassicParameterFlow, true),
UseClassicParameterFlow: takeFirst(seed.UseClassicParameterFlow, false),
})
require.NoError(t, err, "insert template")

Expand Down
18 changes: 10 additions & 8 deletions coderd/insights_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,10 +665,11 @@ func TestTemplateInsights_Golden(t *testing.T) {
// where we can control the template ID.
// createdTemplate := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
createdTemplate := dbgen.Template(t, db, database.Template{
ID: template.id,
ActiveVersionID: version.ID,
OrganizationID: firstUser.OrganizationID,
CreatedBy: firstUser.UserID,
ID: template.id,
ActiveVersionID: version.ID,
OrganizationID: firstUser.OrganizationID,
CreatedBy: firstUser.UserID,
UseClassicParameterFlow: true, // Required for testing classic parameter flow behavior
GroupACL: database.TemplateACL{
firstUser.OrganizationID.String(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
},
Expand Down Expand Up @@ -1556,10 +1557,11 @@ func TestUserActivityInsights_Golden(t *testing.T) {
// where we can control the template ID.
// createdTemplate := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
createdTemplate := dbgen.Template(t, db, database.Template{
ID: template.id,
ActiveVersionID: version.ID,
OrganizationID: firstUser.OrganizationID,
CreatedBy: firstUser.UserID,
ID: template.id,
ActiveVersionID: version.ID,
OrganizationID: firstUser.OrganizationID,
CreatedBy: firstUser.UserID,
UseClassicParameterFlow: true, // Required for parameter usage tracking in this test
GroupACL: database.TemplateACL{
firstUser.OrganizationID.String(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
},
Expand Down
4 changes: 2 additions & 2 deletions coderd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
return
}

// Default is true until dynamic parameters are promoted to stable.
useClassicParameterFlow := ptr.NilToDefault(createTemplate.UseClassicParameterFlow, true)
// Default is false as dynamic parameters are now the preferred approach.
useClassicParameterFlow := ptr.NilToDefault(createTemplate.UseClassicParameterFlow, false)

// Make a temporary struct to represent the template. This is used for
// auditing if any of the following checks fail. It will be overwritten when
Expand Down
4 changes: 2 additions & 2 deletions coderd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
assert.Equal(t, expected.Name, got.Name)
assert.Equal(t, expected.Description, got.Description)
assert.Equal(t, expected.ActivityBumpMillis, got.ActivityBumpMillis)
assert.Equal(t, expected.UseClassicParameterFlow, true) // Current default is true
assert.Equal(t, expected.UseClassicParameterFlow, false) // Current default is false

require.Len(t, auditor.AuditLogs(), 3)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[0].Action)
Expand Down Expand Up @@ -1551,7 +1551,7 @@ func TestPatchTemplateMeta(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.True(t, template.UseClassicParameterFlow, "default is true")
require.False(t, template.UseClassicParameterFlow, "default is false")

bTrue := true
bFalse := false
Expand Down
6 changes: 3 additions & 3 deletions coderd/workspaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,9 @@ func TestWorkspace(t *testing.T) {

// Test Utility variables
templateVersionParameters := []*proto.RichParameter{
{Name: "param1", Type: "string", Required: false},
{Name: "param2", Type: "string", Required: false},
{Name: "param3", Type: "string", Required: false},
{Name: "param1", Type: "string", Required: false, DefaultValue: "default1"},
{Name: "param2", Type: "string", Required: false, DefaultValue: "default2"},
{Name: "param3", Type: "string", Required: false, DefaultValue: "default3"},
}
presetParameters := []*proto.PresetParameter{
{Name: "param1", Value: "value1"},
Expand Down
Empty file added provisioner/echo/parameter.tpl
Empty file.
99 changes: 99 additions & 0 deletions provisioner/echo/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"strings"
"text/template"

"github.com/google/uuid"
"golang.org/x/xerrors"
Expand Down Expand Up @@ -377,6 +378,45 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response

logger.Debug(context.Background(), "extra file written", slog.F("name", name), slog.F("bytes_written", n))
}

// Write a main.tf with the appropriate parameters. This is to write terraform
// that matches the parameters defined in the responses. Dynamic parameters
// parsed these, even in the echo provisioner.
var mainTF bytes.Buffer
for _, respPlan := range responses.ProvisionPlan {
plan := respPlan.GetPlan()
if plan == nil {
continue
}

for _, param := range plan.Parameters {
paramTF, err := ParameterTerraform(param)
if err != nil {
return nil, xerrors.Errorf("parameter terraform: %w", err)
}
_, _ = mainTF.WriteString(paramTF)
}
}

if mainTF.Len() > 0 {
mainTFData := `
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
` + mainTF.String()

_ = writer.WriteHeader(&tar.Header{
Name: `main.tf`,
Size: int64(len(mainTFData)),
Mode: 0o644,
})
_, _ = writer.Write([]byte(mainTFData))
}

// `writer.Close()` function flushes the writer buffer, and adds extra padding to create a legal tarball.
err := writer.Close()
if err != nil {
Expand All @@ -385,6 +425,65 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
return buffer.Bytes(), nil
}

// ParameterTerraform will create a Terraform data block for the provided parameter.
func ParameterTerraform(param *proto.RichParameter) (string, error) {
tmpl := template.Must(template.New("parameter").Funcs(map[string]any{
"showValidation": func(v *proto.RichParameter) bool {
return v != nil && (v.ValidationMax != nil || v.ValidationMin != nil ||
v.ValidationError != "" || v.ValidationRegex != "" ||
v.ValidationMonotonic != "")
},
}).Parse(`
data "coder_parameter" "{{ .Name }}" {
name = "{{ .Name }}"
display_name = "{{ .DisplayName }}"
description = "{{ .Description }}"
icon = "{{ .Icon }}"
mutable = {{ .Mutable }}
ephemeral = {{ .Ephemeral }}
order = {{ .Order }}
{{- if .DefaultValue }}
default = {{ .DefaultValue }}
{{- end }}
{{- if .Type }}
type = "{{ .Type }}"
{{- end }}
{{- if .FormType }}
form_type = "{{ .FormType }}"
{{- end }}
{{- range .Options }}
option {
name = "{{ .Name }}"
value = "{{ .Value }}"
}
{{- end }}
{{- if showValidation .}}
validation {
{{- if .ValidationRegex }}
regex = "{{ .ValidationRegex }}"
{{- end }}
{{- if .ValidationError }}
error = "{{ .ValidationError }}"
{{- end }}
{{- if .ValidationMin }}
min = {{ .ValidationMin }}
{{- end }}
{{- if .ValidationMax }}
max = {{ .ValidationMax }}
{{- end }}
{{- if .ValidationMonotonic }}
monotonic = {{ .ValidationMonotonic }}
{{- end }}
}
{{- end }}
}
`))

var buf bytes.Buffer
err := tmpl.Execute(&buf, param)
return buf.String(), err
}

func WithResources(resources []*proto.Resource) *Responses {
return &Responses{
Parse: ParseComplete,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render, screen } from "@testing-library/react";
import { ClassicParameterFlowDeprecationWarning } from "./ClassicParameterFlowDeprecationWarning";

jest.mock("modules/navigation", () => ({
useLinks: () => () => "/mock-link",
linkToTemplate: () => "/mock-template-link",
}));

describe("ClassicParameterFlowDeprecationWarning", () => {
const defaultProps = {
organizationName: "test-org",
templateName: "test-template",
};

it("renders warning when enabled and user has template update permissions", () => {
render(
<ClassicParameterFlowDeprecationWarning
{...defaultProps}
isEnabled={true}
/>,
);

expect(screen.getByText("deprecated")).toBeInTheDocument();
expect(screen.getByText("Go to Template Settings")).toBeInTheDocument();
});

it("does not render when enabled is false", () => {
const { container } = render(
<ClassicParameterFlowDeprecationWarning
{...defaultProps}
isEnabled={false}
/>,
);

expect(container.firstChild).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Alert } from "components/Alert/Alert";
import { Link } from "components/Link/Link";
import { linkToTemplate, useLinks } from "modules/navigation";
import type { FC } from "react";
import { docs } from "utils/docs";

interface ClassicParameterFlowDeprecationWarningProps {
organizationName: string;
templateName: string;
isEnabled: boolean;
}

export const ClassicParameterFlowDeprecationWarning: FC<
ClassicParameterFlowDeprecationWarningProps
> = ({ organizationName, templateName, isEnabled }) => {
const getLink = useLinks();

if (!isEnabled) {
return null;
}

const templateSettingsLink = `${getLink(
linkToTemplate(organizationName, templateName),
)}/settings`;

return (
<Alert severity="warning" className="mb-2">
<div>
This template is using the classic parameter flow, which will be{" "}
<strong>deprecated</strong> and removed in a future release. Please
migrate to{" "}
<a
href={docs("/admin/templates/extending-templates/dynamic-parameters")}
className="text-content-link"
>
dynamic parameters
</a>{" "}
on template settings for improved functionality.
</div>

<Link className="text-xs" href={templateSettingsLink}>
Go to Template Settings
</Link>
</Alert>
);
};
24 changes: 22 additions & 2 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ const CreateWorkspacePage: FC = () => {
}),
enabled: !!templateQuery.data,
});
const templatePermissionsQuery = useQuery({
...checkAuthorization({
checks: {
canUpdateTemplate: {
object: {
resource_type: "template",
resource_id: templateQuery.data?.id ?? "",
},
action: "update",
},
},
}),
enabled: !!templateQuery.data,
});
const realizedVersionId =
customVersionId ?? templateQuery.data?.active_version_id;
const organizationId = templateQuery.data?.organization_id;
Expand All @@ -93,9 +107,13 @@ const CreateWorkspacePage: FC = () => {
const isLoadingFormData =
templateQuery.isLoading ||
permissionsQuery.isLoading ||
templatePermissionsQuery.isLoading ||
richParametersQuery.isLoading;
const loadFormDataError =
templateQuery.error ?? permissionsQuery.error ?? richParametersQuery.error;
templateQuery.error ??
permissionsQuery.error ??
templatePermissionsQuery.error ??
richParametersQuery.error;

const title = autoCreateWorkspaceMutation.isPending
? "Creating workspace..."
Expand Down Expand Up @@ -211,7 +229,9 @@ const CreateWorkspacePage: FC = () => {
startPollingExternalAuth={startPollingExternalAuth}
hasAllRequiredExternalAuth={hasAllRequiredExternalAuth}
permissions={permissionsQuery.data as CreateWorkspacePermissions}
canUpdateTemplate={permissionsQuery.data?.canUpdateTemplate}
templatePermissions={
templatePermissionsQuery.data as { canUpdateTemplate: boolean }
}
parameters={realizedParameters as TemplateVersionParameter[]}
presets={templateVersionPresetsQuery.data ?? []}
creatingWorkspace={createWorkspaceMutation.isPending}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const meta: Meta<typeof CreateWorkspacePageView> = {
canUpdateTemplate: false,
},
onCancel: action("onCancel"),
templatePermissions: { canUpdateTemplate: true },
},
};

Expand Down
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy