mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat!: extract provisioner tags from coder_workspace_tags data source (#15578)
Relates to https://github.com/coder/coder/issues/15087 and https://github.com/coder/coder/issues/15427 - Extracts provisioner job tags from `coder_workspace_tags` on template version creation using `provisioner/terraform/tfparse` added in https://github.com/coder/coder/pull/15236 - Drops a WARN log in coderd if no matching provisioners found. - Also drops a warning message in the CLI if no provisioners are found. - To support both CLI and UI warnings, added a `codersdk.MatchedProvisioners` struct to the `TemplateVersion` response containing details of how many provisioners were around at the time of the insert. Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
This commit is contained in:
co-authored by
Mathias Fredriksson
parent
648cdd006c
commit
1cdc3e8921
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/externalauth"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
@@ -221,6 +222,253 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("WorkspaceTags", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// This test ensures that when creating a template version from an archive continaining a coder_workspace_tags
|
||||
// data source, we automatically assign some "reasonable" provisioner tag values to the resulting template
|
||||
// import job.
|
||||
// TODO(Cian): I'd also like to assert that the correct raw tag values are stored in the database,
|
||||
// but in order to do this, we need to actually run the job! This isn't straightforward right now.
|
||||
|
||||
store, ps := dbtestutil.NewDB(t)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
Database: store,
|
||||
Pubsub: ps,
|
||||
})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
templateAdmin, templateAdminUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
reqTags map[string]string
|
||||
wantTags map[string]string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
wantTags: map[string]string{"owner": "", "scope": "organization"},
|
||||
},
|
||||
{
|
||||
name: "main.tf with no tags",
|
||||
files: map[string]string{
|
||||
`main.tf`: `
|
||||
variable "a" {
|
||||
type = string
|
||||
default = "1"
|
||||
}
|
||||
data "coder_parameter" "b" {
|
||||
type = string
|
||||
default = "2"
|
||||
}
|
||||
resource "null_resource" "test" {}`,
|
||||
},
|
||||
wantTags: map[string]string{"owner": "", "scope": "organization"},
|
||||
},
|
||||
{
|
||||
name: "main.tf with empty workspace tags",
|
||||
files: map[string]string{
|
||||
`main.tf`: `
|
||||
variable "a" {
|
||||
type = string
|
||||
default = "1"
|
||||
}
|
||||
data "coder_parameter" "b" {
|
||||
type = string
|
||||
default = "2"
|
||||
}
|
||||
resource "null_resource" "test" {}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {}
|
||||
}`,
|
||||
},
|
||||
wantTags: map[string]string{"owner": "", "scope": "organization"},
|
||||
},
|
||||
{
|
||||
name: "main.tf with workspace tags",
|
||||
files: map[string]string{
|
||||
`main.tf`: `
|
||||
variable "a" {
|
||||
type = string
|
||||
default = "1"
|
||||
}
|
||||
data "coder_parameter" "b" {
|
||||
type = string
|
||||
default = "2"
|
||||
}
|
||||
resource "null_resource" "test" {}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"foo": "bar",
|
||||
"a": var.a,
|
||||
"b": data.coder_parameter.b.value,
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantTags: map[string]string{"owner": "", "scope": "organization", "foo": "bar", "a": "1", "b": "2"},
|
||||
},
|
||||
{
|
||||
name: "main.tf with workspace tags and request tags",
|
||||
files: map[string]string{
|
||||
`main.tf`: `
|
||||
variable "a" {
|
||||
type = string
|
||||
default = "1"
|
||||
}
|
||||
data "coder_parameter" "b" {
|
||||
type = string
|
||||
default = "2"
|
||||
}
|
||||
resource "null_resource" "test" {}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"foo": "bar",
|
||||
"a": var.a,
|
||||
"b": data.coder_parameter.b.value,
|
||||
}
|
||||
}`,
|
||||
},
|
||||
reqTags: map[string]string{"baz": "zap", "foo": "noclobber"},
|
||||
wantTags: map[string]string{"owner": "", "scope": "organization", "foo": "bar", "baz": "zap", "a": "1", "b": "2"},
|
||||
},
|
||||
{
|
||||
name: "main.tf with disallowed workspace tag value",
|
||||
files: map[string]string{
|
||||
`main.tf`: `
|
||||
variable "a" {
|
||||
type = string
|
||||
default = "1"
|
||||
}
|
||||
data "coder_parameter" "b" {
|
||||
type = string
|
||||
default = "2"
|
||||
}
|
||||
resource "null_resource" "test" {
|
||||
name = "foo"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"foo": "bar",
|
||||
"a": var.a,
|
||||
"b": data.coder_parameter.b.value,
|
||||
"test": null_resource.test.name,
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectError: `Unknown variable; There is no variable named "null_resource".`,
|
||||
},
|
||||
{
|
||||
name: "main.tf with disallowed function in tag value",
|
||||
files: map[string]string{
|
||||
`main.tf`: `
|
||||
variable "a" {
|
||||
type = string
|
||||
default = "1"
|
||||
}
|
||||
data "coder_parameter" "b" {
|
||||
type = string
|
||||
default = "2"
|
||||
}
|
||||
resource "null_resource" "test" {
|
||||
name = "foo"
|
||||
}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"foo": "bar",
|
||||
"a": var.a,
|
||||
"b": data.coder_parameter.b.value,
|
||||
"test": try(null_resource.test.name, "whatever"),
|
||||
}
|
||||
}`,
|
||||
},
|
||||
expectError: `Function calls not allowed; Functions may not be called here.`,
|
||||
},
|
||||
// We will allow coder_workspace_tags to set the scope on a template version import job
|
||||
// BUT the user ID will be ultimately determined by the API key in the scope.
|
||||
// TODO(Cian): Is this what we want? Or should we just ignore these provisioner
|
||||
// tags entirely?
|
||||
{
|
||||
name: "main.tf with workspace tags that attempts to set user scope",
|
||||
files: map[string]string{
|
||||
`main.tf`: `
|
||||
resource "null_resource" "test" {}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"scope": "user",
|
||||
"owner": "12345678-1234-1234-1234-1234567890ab",
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantTags: map[string]string{"owner": templateAdminUser.ID.String(), "scope": "user"},
|
||||
},
|
||||
{
|
||||
name: "main.tf with workspace tags that attempt to clobber org ID",
|
||||
files: map[string]string{
|
||||
`main.tf`: `
|
||||
resource "null_resource" "test" {}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"scope": "organization",
|
||||
"owner": "12345678-1234-1234-1234-1234567890ab",
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantTags: map[string]string{"owner": "", "scope": "organization"},
|
||||
},
|
||||
{
|
||||
name: "main.tf with workspace tags that set scope=user",
|
||||
files: map[string]string{
|
||||
`main.tf`: `
|
||||
resource "null_resource" "test" {}
|
||||
data "coder_workspace_tags" "tags" {
|
||||
tags = {
|
||||
"scope": "user",
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantTags: map[string]string{"owner": templateAdminUser.ID.String(), "scope": "user"},
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
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 := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-")
|
||||
tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
Name: tvName,
|
||||
StorageMethod: codersdk.ProvisionerStorageMethodFile,
|
||||
Provisioner: codersdk.ProvisionerTypeTerraform,
|
||||
FileID: fi.ID,
|
||||
ProvisionerTags: tt.reqTags,
|
||||
})
|
||||
|
||||
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.EqualValues(t, tt.wantTags, pj.Tags)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.expectError)
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCancelTemplateVersion(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user