Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: validate presets on template import#18844

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
SasSwart wants to merge2 commits intomain
base:main
Choose a base branch
Loading
fromjjs/17333
Open
Show file tree
Hide file tree
Changes from1 commit
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
NextNext commit
feat: validate presets on template import
  • Loading branch information
@SasSwart
SasSwart committedJul 14, 2025
commitac7af5e9600401892bf04cede5640871763071ae
8 changes: 8 additions & 0 deletionscoderd/dynamicparameters/error.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,14 @@ func tagValidationError(diags hcl.Diagnostics) *DiagnosticError {
}
}

funcpresetValidationError(diags hcl.Diagnostics)*DiagnosticError {
return&DiagnosticError{
Message:"Unable to parse presets",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

nit:

Suggested change
Message:"Unable toparse presets",
Message:"Unable tovalidate presets",

Diagnostics:diags,
KeyedDiagnostics:make(map[string]hcl.Diagnostics),
}
}

typeDiagnosticErrorstruct {
// Message is the human-readable message that will be returned to the user.
Messagestring
Expand Down
24 changes: 24 additions & 0 deletionscoderd/dynamicparameters/presets.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
package dynamicparameters

import (
"github.com/hashicorp/hcl/v2"

"github.com/coder/preview"
)

// CheckPresets extracts the preset related diagnostics from a template version preset
func CheckPresets(output *preview.Output, diags hcl.Diagnostics) *DiagnosticError {
de := presetValidationError(diags)
presets := output.Presets
for _, preset := range presets {
if hcl.Diagnostics(preset.Diagnostics).HasErrors() {
de.Extend(preset.Name, hcl.Diagnostics(preset.Diagnostics))
}
}

if de.HasError() {
return de
}

return nil
}
27 changes: 24 additions & 3 deletionscoderd/parameters_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ package coderd_test
import (
"context"
"os"
"sync"
"testing"

"github.com/google/uuid"
Expand DownExpand Up@@ -193,20 +194,23 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) {
t.Parallel()

db, ps := dbtestutil.NewDB(t)
dbReject := &dbRejectGitSSHKey{Store: db}
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/modules/main.tf")
require.NoError(t, err)

modulesArchive, err := terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules"))
require.NoError(t, err)

setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
db:&dbRejectGitSSHKey{Store: db},
db:dbReject,
ps: ps,
provisionerDaemonVersion: provProto.CurrentVersion.String(),
mainTF: dynamicParametersTerraformSource,
modulesArchive: modulesArchive,
})

dbReject.SetReject(true)

stream := setup.stream
previews := stream.Chan()

Expand DownExpand Up@@ -412,8 +416,25 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn
// that is generally impossible to force an error.
type dbRejectGitSSHKey struct {
database.Store
rejectMu sync.RWMutex
reject bool
}

// SetReject toggles whether GetGitSSHKey should return an error or passthrough to the underlying store.
func (d *dbRejectGitSSHKey) SetReject(reject bool) {
d.rejectMu.Lock()
defer d.rejectMu.Unlock()
d.reject = reject
Comment on lines +419 to +427
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

nit: Is this a fix for a separate flake? If so, might be no harm to separate in its own PR.

}

func (*dbRejectGitSSHKey) GetGitSSHKey(_ context.Context, _ uuid.UUID) (database.GitSSHKey, error) {
return database.GitSSHKey{}, xerrors.New("forcing a fake error")
func (d *dbRejectGitSSHKey) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (database.GitSSHKey, error) {
d.rejectMu.RLock()
reject := d.reject
d.rejectMu.RUnlock()

if reject {
return database.GitSSHKey{}, xerrors.New("forcing a fake error")
}

return d.Store.GetGitSSHKey(ctx, userID)
}
101 changes: 56 additions & 45 deletionscoderd/templateversions.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@ import (

"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/hashicorp/hcl/v2"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/sqlc-dev/pqtype"
"golang.org/x/xerrors"
Expand DownExpand Up@@ -1582,10 +1583,63 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
}
}

varfiles fs.FS
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This was previously only necessary for dynamic parameters, but we now need it in both dynamic and classic templates because we validate presets in both cases. It feels a tad wasteful if considered in isolation, but I'd like to update the classic template case that usestfparse to use thisfs.FS as well. That would be a small first step in getting rid of tfparse.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

tfparse will eventually be removed, likely in a release or two.

Maybe we should only do preset validation if using dynamic parameters. It is overloading the feature a little bit, as calling itterraform-preview or something would be more accurate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I'd like to update the classic template case that uses tfparse to use this fs.FS as well.

There is atfconfig.LoadModuleFromFilesystem but IIRC I tried using this before and ran into issues.

Maybe we should only do preset validation if using dynamic parameters.

@Emyrk what potential issues do we avoid by doing this? Tying a behaviour relating to a 'GA' feature to a per-template 'Beta' feature feels like it would be unexpected.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@johnstcnpreview is a large part of the dynamic parameters feature. It just feels premature to pushpreview to GA for this parsing, while the rest of its use is still getting tested in Beta.

If there is a bug inpreview, we will know when Beta is in the wild

switchfile.Mimetype {
case"application/x-tar":
files=archivefs.FromTarReader(bytes.NewBuffer(file.Data))
case"application/zip":
files,err=archivefs.FromZipReader(bytes.NewReader(file.Data),int64(len(file.Data)))
iferr!=nil {
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
Message:"Internal error reading file",
Detail:"extract zip archive: "+err.Error(),
})
return
}
default:
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
Message:"Unsupported file type",
Detail:fmt.Sprintf("Mimetype %q is not supported",file.Mimetype),
})
return
}
ownerData,err:=dynamicparameters.WorkspaceOwner(ctx,api.Database,organization.ID,apiKey.UserID)
iferr!=nil {
ifhttpapi.Is404Error(err) {
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
Message:"Internal error checking workspace tags",
Detail:fmt.Sprintf("Owner not found, uuid=%s",apiKey.UserID.String()),
})
return
}
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
Message:"Internal error checking workspace tags",
Detail:"fetch owner data: "+err.Error(),
})
return
}

previewInput:= preview.Input{
PlanJSON:nil,// Template versions are before `terraform plan`
ParameterValues:nil,// No user-specified parameters
Owner:*ownerData,
Logger:stdslog.New(stdslog.DiscardHandler),
}
previewOutput,previewDiags:=preview.Preview(ctx,previewInput,files)

// Validate presets on template version import to avoid errors that would
// have caused workspace creation to fail:
Comment on lines +1630 to +1631
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

small nit:

Suggested change
// Validate presets on template version import to avoid errors that would
// have caused workspace creation to fail:
// Fails early if presets are invalid to prevent downstream workspace creation errors

presetErr:=dynamicparameters.CheckPresets(previewOutput,nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Is there any case where we callCheckPresets with ahcl.Diagnostics? Should we maybe remove this parameter?

ifpresetErr!=nil {
code,resp:=presetErr.Response()
httpapi.Write(ctx,rw,code,resp)
return
}

varparsedTagsmap[string]string
varokbool
ifdynamicTemplate {
parsedTags,ok=api.dynamicTemplateVersionTags(ctx,rw,organization.ID,apiKey.UserID,file)
parsedTags,ok=api.dynamicTemplateVersionTags(ctx,rw,previewOutput,previewDiags)
Comment on lines +1587 to +1642
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Could we leave this extracted to its own function and just rename it with different outputs?

I ask because thispostTemplateVersionsByOrganization is kind of long already. And ideally, we only usepreview ifDynamicParameters is enabled on the template. Since the usage ofpreview is still in Beta.

func (api*API)templatePreview(ctx context.Context,rw http.ResponseWriter,orgID uuid.UUID,owner uuid.UUID,file database.File) (ParsedOutput,bool)

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

do you feel strongly that the only public interface of preview should bepreview.Preview()? Some of this discomfort that you're describing here is imo a result of the preview API being too course. If we split Params, Tags and Presets into three separate functions in the preview project then we have the granular control we need to be able to decouple things and leave most of this logic down where it was.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@SasSwart We might want to split it. If we can keep it as a singlepreview.Preview, that is ideal. But I understand what you are saying.

if!ok {
return
}
Expand DownExpand Up@@ -1762,50 +1816,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
warnings))
}

func (api*API)dynamicTemplateVersionTags(ctx context.Context,rw http.ResponseWriter,orgID uuid.UUID,owner uuid.UUID,file database.File) (map[string]string,bool) {
ownerData,err:=dynamicparameters.WorkspaceOwner(ctx,api.Database,orgID,owner)
iferr!=nil {
ifhttpapi.Is404Error(err) {
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
Message:"Internal error checking workspace tags",
Detail:fmt.Sprintf("Owner not found, uuid=%s",owner.String()),
})
returnnil,false
}
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
Message:"Internal error checking workspace tags",
Detail:"fetch owner data: "+err.Error(),
})
returnnil,false
}

varfiles fs.FS
switchfile.Mimetype {
case"application/x-tar":
files=archivefs.FromTarReader(bytes.NewBuffer(file.Data))
case"application/zip":
files,err=archivefs.FromZipReader(bytes.NewReader(file.Data),int64(len(file.Data)))
iferr!=nil {
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
Message:"Internal error checking workspace tags",
Detail:"extract zip archive: "+err.Error(),
})
returnnil,false
}
default:
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
Message:"Unsupported file type for dynamic template version tags",
Detail:fmt.Sprintf("Mimetype %q is not supported for dynamic template version tags",file.Mimetype),
})
returnnil,false
}

output,diags:=preview.Preview(ctx, preview.Input{
PlanJSON:nil,// Template versions are before `terraform plan`
ParameterValues:nil,// No user-specified parameters
Owner:*ownerData,
Logger:stdslog.New(stdslog.DiscardHandler),
},files)
func (*API)dynamicTemplateVersionTags(ctx context.Context,rw http.ResponseWriter,output*preview.Output,diags hcl.Diagnostics) (map[string]string,bool) {
tagErr:=dynamicparameters.CheckTags(output,diags)
iftagErr!=nil {
code,resp:=tagErr.Response()
Expand Down
112 changes: 112 additions & 0 deletionscoderd/templateversions_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -641,6 +641,118 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
})
}
})

t.Run("Presets", func(t *testing.T) {
t.Parallel()
store, ps := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
})
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())

for _, tt := range []struct {
Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

The different failure cases are more exhaustively tested incoder/preview#149, so I'm only doing a happy and sad path here.

Emyrk and ssncferreira reacted with thumbs up emoji
name string
files map[string]string
expectError string
}{
{
name: "valid preset",
files: map[string]string{
`main.tf`: `
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.8.0"
}
}
}
data "coder_parameter" "valid_parameter" {
name = "valid_parameter_name"
default = "valid_option_value"
option {
name = "valid_option_name"
value = "valid_option_value"
}
}
data "coder_workspace_preset" "valid_preset" {
name = "valid_preset"
parameters = {
"valid_parameter_name" = "valid_option_value"
}
}
`,
},
},
{
name: "invalid preset",
files: map[string]string{
`main.tf`: `
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.8.0"
}
}
}
data "coder_parameter" "valid_parameter" {
name = "valid_parameter_name"
default = "valid_option_value"
option {
name = "valid_option_name"
value = "valid_option_value"
}
}
data "coder_workspace_preset" "invalid_parameter_name" {
name = "invalid_parameter_name"
parameters = {
"invalid_parameter_name" = "irrelevant_value"
}
}
`,
},
expectError: "Undefined Parameter",
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)

// Create an archive from the files provided in the test case.
tarFile := testutil.CreateTar(t, tt.files)

// Post the archive file
fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarFile))
require.NoError(t, err)

// Create a template version from the archive
tvName := testutil.GetRandomNameHyphenated(t)
tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
Name: tvName,
StorageMethod: codersdk.ProvisionerStorageMethodFile,
Provisioner: codersdk.ProvisionerTypeTerraform,
FileID: fi.ID,
})

if tt.expectError == "" {
require.NoError(t, err)
// Assert the expected provisioner job is created from the template version import
pj, err := store.GetProvisionerJobByID(ctx, tv.Job.ID)
require.NoError(t, err)
require.NotNil(t, pj)
// Also assert that we get the expected information back from the API endpoint
require.Zero(t, tv.MatchedProvisioners.Count)
require.Zero(t, tv.MatchedProvisioners.Available)
require.Zero(t, tv.MatchedProvisioners.MostRecentlySeen.Time)
} else {
require.ErrorContains(t, err, tt.expectError)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Should we check that no provisioner job was created?

}
})
}
})
}

func TestPatchCancelTemplateVersion(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletionsgo.mod
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -488,6 +488,8 @@ require (
github.com/mark3labs/mcp-go v0.32.0
)

replace github.com/coder/preview => ../preview

require (
cel.dev/expr v0.23.0 // indirect
cloud.google.com/go v0.120.0 // indirect
Expand Down
2 changes: 0 additions & 2 deletionsgo.sum
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -916,8 +916,6 @@ github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102 h1:ahTJlTRmTogsubgRVGO
github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
github.com/coder/preview v1.0.3-0.20250701142654-c3d6e86b9393 h1:l+m2liikn8JoEv6C22QIV4qseolUfvNsyUNA6JJsD6Y=
github.com/coder/preview v1.0.3-0.20250701142654-c3d6e86b9393/go.mod h1:efDWGlO/PZPrvdt5QiDhMtTUTkPxejXo9c0wmYYLLjM=
github.com/coder/quartz v0.2.1 h1:QgQ2Vc1+mvzewg2uD/nj8MJ9p9gE+QhGJm+Z+NGnrSE=
github.com/coder/quartz v0.2.1/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA=
github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc=
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp