diff --git a/docs/data-sources/template.md b/docs/data-sources/template.md new file mode 100644 index 0000000..986f359 --- /dev/null +++ b/docs/data-sources/template.md @@ -0,0 +1,44 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coderd_template Data Source - coderd" +subcategory: "" +description: |- + An existing template on the coder deployment +--- + +# coderd_template (Data Source) + +An existing template on the coder deployment + + + + +## Schema + +### Optional + +- `id` (String) The ID of the template to retrieve. This field will be populated if a template name is supplied. +- `name` (String) The name of the template to retrieve. This field will be populated if an ID is supplied. +- `organization_id` (String) ID of the organization the template is associated with. + +### Read-Only + +- `active_user_count` (Number) Number of active users using the template. +- `active_version_id` (String) ID of the active version of the template. +- `activity_bump_ms` (Number) Duration to bump the deadline of a workspace when it receives activity. +- `allow_user_autostart` (Boolean) Whether users can autostart workspaces created from the template. +- `allow_user_autostop` (Boolean) Whether users can customize autostop behavior for workspaces created from the template. +- `allow_user_cancel_workspace_jobs` (Boolean) Whether users can cancel jobs in workspaces created from the template. +- `created_at` (Number) Unix timestamp of when the template was created. +- `created_by_user_id` (String) ID of the user who created the template. +- `default_ttl_ms` (Number) Default time-to-live for workspaces created from the template. +- `deprecated` (Boolean) Whether the template is deprecated. +- `deprecation_message` (String) Message to display when the template is deprecated. +- `description` (String) Description of the template. +- `display_name` (String) Display name of the template. +- `failure_ttl_ms` (Number) Automatic cleanup TTL for failed workspace builds. +- `icon` (String) URL of the template's icon. +- `require_active_version` (Boolean) Whether workspaces created from the template must be up-to-datae on the latest active version. +- `time_til_dormant_autodelete_ms` (Number) Duration of inactivity after the workspace becomes dormant before a workspace is automatically deleted. +- `time_til_dormant_ms` (Number) Duration of inactivity before a workspace is considered dormant. +- `updated_at` (Number) Unix timestamp of when the template was last updated. diff --git a/integration/template-test/main.tf b/integration/template-test/main.tf index 9bafe8a..e7cfa01 100644 --- a/integration/template-test/main.tf +++ b/integration/template-test/main.tf @@ -17,7 +17,6 @@ resource "coderd_user" "ethan" { suspended = false } - data "coderd_organization" "default" { is_default = true } @@ -64,4 +63,9 @@ resource "coderd_template" "sample" { ] } ] -} \ No newline at end of file +} + +data "coderd_template" "sample" { + organization_id = data.coderd_organization.default.id + name = coderd_template.sample.name +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5eb103c..6f9c29b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -132,6 +132,7 @@ func (p *CoderdProvider) DataSources(ctx context.Context) []func() datasource.Da NewGroupDataSource, NewUserDataSource, NewOrganizationDataSource, + NewTemplateDataSource, } } diff --git a/internal/provider/template_data_source.go b/internal/provider/template_data_source.go new file mode 100644 index 0000000..9b5d4a2 --- /dev/null +++ b/internal/provider/template_data_source.go @@ -0,0 +1,272 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/coder/coder/v2/codersdk" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &TemplateDataSource{} + +func NewTemplateDataSource() datasource.DataSource { + return &TemplateDataSource{} +} + +// TemplateDataSource defines the data source implementation. +type TemplateDataSource struct { + data *CoderdProviderData +} + +// TemplateDataSourceModel describes the data source data model. +type TemplateDataSourceModel struct { + // ((Organization and Name) or ID) must be set + OrganizationID UUID `tfsdk:"organization_id"` + ID UUID `tfsdk:"id"` + Name types.String `tfsdk:"name"` + + DisplayName types.String `tfsdk:"display_name"` + // TODO: Provisioner + Description types.String `tfsdk:"description"` + ActiveVersionID UUID `tfsdk:"active_version_id"` + ActiveUserCount types.Int64 `tfsdk:"active_user_count"` + Deprecated types.Bool `tfsdk:"deprecated"` + DeprecationMessage types.String `tfsdk:"deprecation_message"` + Icon types.String `tfsdk:"icon"` + + DefaultTTLMillis types.Int64 `tfsdk:"default_ttl_ms"` + ActivityBumpMillis types.Int64 `tfsdk:"activity_bump_ms"` + // TODO: AutostopRequirement + // TODO: AutostartRequirement + + AllowUserAutostart types.Bool `tfsdk:"allow_user_autostart"` + AllowUserAutostop types.Bool `tfsdk:"allow_user_autostop"` + AllowUserCancelWorkspaceJobs types.Bool `tfsdk:"allow_user_cancel_workspace_jobs"` + + FailureTTLMillis types.Int64 `tfsdk:"failure_ttl_ms"` + TimeTilDormantMillis types.Int64 `tfsdk:"time_til_dormant_ms"` + TimeTilDormantAutoDeleteMillis types.Int64 `tfsdk:"time_til_dormant_autodelete_ms"` + + RequireActiveVersion types.Bool `tfsdk:"require_active_version"` + // TODO: MaxPortShareLevel + + CreatedByUserID UUID `tfsdk:"created_by_user_id"` + CreatedAt types.Int64 `tfsdk:"created_at"` // Unix timestamp + UpdatedAt types.Int64 `tfsdk:"updated_at"` // Unix timestamp + + // TODO: ACL-related stuff +} + +func (d *TemplateDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_template" +} + +func (d *TemplateDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "An existing template on the Coder deployment.", + + Attributes: map[string]schema.Attribute{ + "organization_id": schema.StringAttribute{ + MarkdownDescription: "ID of the organization the template is associated with. This field will be populated if an ID is supplied. Defaults to the provider default organization ID.", + CustomType: UUIDType, + Optional: true, + Computed: true, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of the template to retrieve. This field will be populated if a template name is supplied.", + CustomType: UUIDType, + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("name"), + }...), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the template to retrieve. This field will be populated if an ID is supplied.", + Optional: true, + Computed: true, + }, + + "display_name": schema.StringAttribute{ + MarkdownDescription: "Display name of the template.", + Computed: true, + }, + "description": schema.StringAttribute{ + MarkdownDescription: "Description of the template.", + Computed: true, + }, + "active_version_id": schema.StringAttribute{ + MarkdownDescription: "ID of the active version of the template.", + CustomType: UUIDType, + Computed: true, + }, + "active_user_count": schema.Int64Attribute{ + MarkdownDescription: "Number of active users using the template.", + Computed: true, + }, + "deprecated": schema.BoolAttribute{ + MarkdownDescription: "Whether the template is deprecated.", + Computed: true, + }, + "deprecation_message": schema.StringAttribute{ + MarkdownDescription: "Message to display when the template is deprecated.", + Computed: true, + }, + "icon": schema.StringAttribute{ + MarkdownDescription: "URL of the template's icon.", + Computed: true, + }, + "default_ttl_ms": schema.Int64Attribute{ + MarkdownDescription: "Default time-to-live for workspaces created from the template.", + Computed: true, + }, + "activity_bump_ms": schema.Int64Attribute{ + MarkdownDescription: "Duration to bump the deadline of a workspace when it receives activity.", + Computed: true, + }, + "allow_user_autostart": schema.BoolAttribute{ + MarkdownDescription: "Whether users can autostart workspaces created from the template.", + Computed: true, + }, + "allow_user_autostop": schema.BoolAttribute{ + MarkdownDescription: "Whether users can customize autostop behavior for workspaces created from the template.", + Computed: true, + }, + "allow_user_cancel_workspace_jobs": schema.BoolAttribute{ + MarkdownDescription: "Whether users can cancel jobs in workspaces created from the template.", + Computed: true, + }, + "failure_ttl_ms": schema.Int64Attribute{ + MarkdownDescription: "Automatic cleanup TTL for failed workspace builds.", + Computed: true, + }, + "time_til_dormant_ms": schema.Int64Attribute{ + MarkdownDescription: "Duration of inactivity before a workspace is considered dormant.", + Computed: true, + }, + "time_til_dormant_autodelete_ms": schema.Int64Attribute{ + MarkdownDescription: "Duration of inactivity after the workspace becomes dormant before a workspace is automatically deleted.", + Computed: true, + }, + "require_active_version": schema.BoolAttribute{ + MarkdownDescription: "Whether workspaces created from the template must be up-to-date on the latest active version.", + Computed: true, + }, + "created_by_user_id": schema.StringAttribute{ + MarkdownDescription: "ID of the user who created the template.", + CustomType: UUIDType, + Computed: true, + }, + "created_at": schema.Int64Attribute{ + MarkdownDescription: "Unix timestamp of when the template was created.", + Computed: true, + }, + "updated_at": schema.Int64Attribute{ + MarkdownDescription: "Unix timestamp of when the template was last updated.", + Computed: true, + }, + }, + } +} + +func (d *TemplateDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + data, ok := req.ProviderData.(*CoderdProviderData) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.data = data +} + +func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data TemplateDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + client := d.data.Client + + var ( + template codersdk.Template + err error + ) + if data.ID.ValueUUID() != uuid.Nil { + template, err = client.Template(ctx, data.ID.ValueUUID()) + } else { + if data.OrganizationID.ValueUUID() == uuid.Nil { + data.OrganizationID = UUIDValue(d.data.DefaultOrganizationID) + } + if data.OrganizationID.ValueUUID() == uuid.Nil { + resp.Diagnostics.AddError("Client Error", "name requires organization_id to be set") + return + } + template, err = client.TemplateByName(ctx, data.OrganizationID.ValueUUID(), data.Name.ValueString()) + } + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get template, got error: %s", err)) + return + } + if !data.ID.IsNull() && template.ID.String() != data.ID.ValueString() { + resp.Diagnostics.AddError("Client Error", "Retrieved Template's ID does not match the provided ID") + return + } + if !data.Name.IsNull() && template.Name != data.Name.ValueString() { + resp.Diagnostics.AddError("Client Error", "Retrieved Template's name does not match the provided name") + return + } + if !data.OrganizationID.IsNull() && template.OrganizationID.String() != data.OrganizationID.ValueString() { + resp.Diagnostics.AddError("Client Error", "Retrieved Template's organization ID does not match the provided organization ID") + return + } + + data.OrganizationID = UUIDValue(template.OrganizationID) + data.ID = UUIDValue(template.ID) + data.Name = types.StringValue(template.Name) + data.DisplayName = types.StringValue(template.DisplayName) + data.Description = types.StringValue(template.Description) + data.ActiveVersionID = UUIDValue(template.ActiveVersionID) + data.ActiveUserCount = types.Int64Value(int64(template.ActiveUserCount)) + data.Deprecated = types.BoolValue(template.Deprecated) + data.DeprecationMessage = types.StringValue(template.DeprecationMessage) + data.Icon = types.StringValue(template.Icon) + data.DefaultTTLMillis = types.Int64Value(template.DefaultTTLMillis) + data.ActivityBumpMillis = types.Int64Value(template.ActivityBumpMillis) + data.AllowUserAutostart = types.BoolValue(template.AllowUserAutostart) + data.AllowUserAutostop = types.BoolValue(template.AllowUserAutostop) + data.AllowUserCancelWorkspaceJobs = types.BoolValue(template.AllowUserCancelWorkspaceJobs) + data.FailureTTLMillis = types.Int64Value(template.FailureTTLMillis) + data.TimeTilDormantMillis = types.Int64Value(template.TimeTilDormantMillis) + data.TimeTilDormantAutoDeleteMillis = types.Int64Value(template.TimeTilDormantAutoDeleteMillis) + data.RequireActiveVersion = types.BoolValue(template.RequireActiveVersion) + data.CreatedByUserID = UUIDValue(template.CreatedByID) + data.CreatedAt = types.Int64Value(template.CreatedAt.Unix()) + data.UpdatedAt = types.Int64Value(template.UpdatedAt.Unix()) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/template_data_source_test.go b/internal/provider/template_data_source_test.go new file mode 100644 index 0000000..0c1e40d --- /dev/null +++ b/internal/provider/template_data_source_test.go @@ -0,0 +1,238 @@ +package provider + +import ( + "context" + "os" + "regexp" + "strconv" + "strings" + "testing" + "text/template" + "time" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" + "github.com/coder/terraform-provider-coderd/integration" +) + +func TestAccTemplateDataSource(t *testing.T) { + if os.Getenv("TF_ACC") == "" { + t.Skip("Acceptance tests are disabled.") + } + ctx := context.Background() + client := integration.StartCoder(ctx, t, "template_data_acc", true) + firstUser, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) + orgID := firstUser.OrganizationIDs[0] + + version, err := newVersion(ctx, client, newVersionRequest{ + OrganizationID: orgID, + Version: &TemplateVersion{ + Name: types.StringValue("main"), + Message: types.StringValue("Initial commit"), + Directory: types.StringValue("../../integration/template-test/example-template/"), + TerraformVariables: []Variable{ + { + Name: types.StringValue("name"), + Value: types.StringValue("world"), + }, + }, + }, + }) + require.NoError(t, err) + tpl, err := client.CreateTemplate(ctx, orgID, codersdk.CreateTemplateRequest{ + Name: "example-template", + DisplayName: "Example Template", + Description: "An example template", + Icon: "/path/to/icon.png", + VersionID: version.ID, + DefaultTTLMillis: PtrTo((10 * time.Hour).Milliseconds()), + ActivityBumpMillis: PtrTo((4 * time.Hour).Milliseconds()), + AutostopRequirement: &codersdk.TemplateAutostopRequirement{ + DaysOfWeek: []string{"sunday"}, + Weeks: 1, + }, + AutostartRequirement: &codersdk.TemplateAutostartRequirement{ + DaysOfWeek: []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, + }, + AllowUserCancelWorkspaceJobs: PtrTo(true), + AllowUserAutostart: PtrTo(true), + AllowUserAutostop: PtrTo(true), + FailureTTLMillis: PtrTo((1 * time.Hour).Milliseconds()), + TimeTilDormantMillis: PtrTo((7 * 24 * time.Hour).Milliseconds()), + TimeTilDormantAutoDeleteMillis: PtrTo((30 * 24 * time.Hour).Milliseconds()), + DisableEveryoneGroupAccess: true, + RequireActiveVersion: true, + }) + require.NoError(t, err) + + // Can't set some fields on create, like deprecated. + tpl, err = client.UpdateTemplateMeta(ctx, tpl.ID, codersdk.UpdateTemplateMeta{ + Name: tpl.Name, + DisplayName: tpl.DisplayName, + Description: tpl.Description, + Icon: tpl.Icon, + DefaultTTLMillis: tpl.DefaultTTLMillis, + ActivityBumpMillis: tpl.ActivityBumpMillis, + AutostopRequirement: &codersdk.TemplateAutostopRequirement{ + DaysOfWeek: tpl.AutostopRequirement.DaysOfWeek, + Weeks: tpl.AutostopRequirement.Weeks, + }, + AutostartRequirement: &codersdk.TemplateAutostartRequirement{ + DaysOfWeek: tpl.AutostartRequirement.DaysOfWeek, + }, + AllowUserAutostart: tpl.AllowUserAutostart, + AllowUserAutostop: tpl.AllowUserAutostop, + AllowUserCancelWorkspaceJobs: tpl.AllowUserCancelWorkspaceJobs, + FailureTTLMillis: tpl.FailureTTLMillis, + TimeTilDormantMillis: tpl.TimeTilDormantMillis, + TimeTilDormantAutoDeleteMillis: tpl.TimeTilDormantAutoDeleteMillis, + UpdateWorkspaceLastUsedAt: false, + UpdateWorkspaceDormantAt: false, + RequireActiveVersion: tpl.RequireActiveVersion, + DeprecationMessage: PtrTo("This template is deprecated"), + DisableEveryoneGroupAccess: true, + MaxPortShareLevel: PtrTo(codersdk.WorkspaceAgentPortShareLevelOwner), + }) + require.NoError(t, err) + + checkFn := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.coderd_template.test", "organization_id", tpl.OrganizationID.String()), + resource.TestCheckResourceAttr("data.coderd_template.test", "id", tpl.ID.String()), + resource.TestCheckResourceAttr("data.coderd_template.test", "name", tpl.Name), + resource.TestCheckResourceAttr("data.coderd_template.test", "display_name", tpl.DisplayName), + resource.TestCheckResourceAttr("data.coderd_template.test", "description", tpl.Description), + resource.TestCheckResourceAttr("data.coderd_template.test", "active_version_id", tpl.ActiveVersionID.String()), + resource.TestCheckResourceAttr("data.coderd_template.test", "active_user_count", strconv.Itoa(tpl.ActiveUserCount)), + resource.TestCheckResourceAttr("data.coderd_template.test", "deprecated", strconv.FormatBool(tpl.Deprecated)), + resource.TestCheckResourceAttr("data.coderd_template.test", "deprecation_message", tpl.DeprecationMessage), + resource.TestCheckResourceAttr("data.coderd_template.test", "icon", tpl.Icon), + resource.TestCheckResourceAttr("data.coderd_template.test", "default_ttl_ms", strconv.FormatInt(tpl.DefaultTTLMillis, 10)), + resource.TestCheckResourceAttr("data.coderd_template.test", "activity_bump_ms", strconv.FormatInt(tpl.ActivityBumpMillis, 10)), + resource.TestCheckResourceAttr("data.coderd_template.test", "allow_user_autostart", strconv.FormatBool(tpl.AllowUserAutostart)), + resource.TestCheckResourceAttr("data.coderd_template.test", "allow_user_autostop", strconv.FormatBool(tpl.AllowUserAutostop)), + resource.TestCheckResourceAttr("data.coderd_template.test", "allow_user_cancel_workspace_jobs", strconv.FormatBool(tpl.AllowUserCancelWorkspaceJobs)), + resource.TestCheckResourceAttr("data.coderd_template.test", "failure_ttl_ms", strconv.FormatInt(tpl.FailureTTLMillis, 10)), + resource.TestCheckResourceAttr("data.coderd_template.test", "time_til_dormant_ms", strconv.FormatInt(tpl.TimeTilDormantMillis, 10)), + resource.TestCheckResourceAttr("data.coderd_template.test", "time_til_dormant_autodelete_ms", strconv.FormatInt(tpl.TimeTilDormantAutoDeleteMillis, 10)), + resource.TestCheckResourceAttr("data.coderd_template.test", "require_active_version", strconv.FormatBool(tpl.RequireActiveVersion)), + resource.TestCheckResourceAttr("data.coderd_template.test", "created_by_user_id", firstUser.ID.String()), + resource.TestCheckResourceAttr("data.coderd_template.test", "created_at", strconv.Itoa(int(tpl.CreatedAt.Unix()))), + resource.TestCheckResourceAttr("data.coderd_template.test", "updated_at", strconv.Itoa(int(tpl.UpdatedAt.Unix()))), + ) + + t.Run("TemplateByOrgAndNameOK", func(t *testing.T) { + cfg := testAccTemplateDataSourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + OrganizationID: PtrTo(orgID.String()), + Name: PtrTo(tpl.Name), + } + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: cfg.String(t), + Check: checkFn, + }, + }, + }) + }) + + t.Run("TemplateByIDOK", func(t *testing.T) { + cfg := testAccTemplateDataSourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + ID: PtrTo(tpl.ID.String()), + } + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: cfg.String(t), + Check: checkFn, + }, + }, + }) + }) + + t.Run("NeitherIDNorNameError", func(t *testing.T) { + cfg := testAccTemplateDataSourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + } + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: cfg.String(t), + ExpectError: regexp.MustCompile(`At least one attribute out of \[name,id\] must be specified`), + }, + }, + }) + }) + + t.Run("NameWithoutOrgUsesDefaultOrg", func(t *testing.T) { + cfg := testAccTemplateDataSourceConfig{ + URL: client.URL.String(), + Token: client.SessionToken(), + Name: PtrTo(tpl.Name), + } + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: cfg.String(t), + Check: checkFn, + }, + }, + }) + }) +} + +type testAccTemplateDataSourceConfig struct { + URL string + Token string + + OrganizationID *string + ID *string + Name *string +} + +func (c testAccTemplateDataSourceConfig) String(t *testing.T) string { + t.Helper() + tpl := ` +provider coderd { + url = "{{.URL}}" + token = "{{.Token}}" +} + +data "coderd_template" "test" { + organization_id = {{orNull .OrganizationID}} + id = {{orNull .ID}} + name = {{orNull .Name}} +}` + + funcMap := template.FuncMap{ + "orNull": PrintOrNull, + } + + buf := strings.Builder{} + tmpl, err := template.New("templateDataSource").Funcs(funcMap).Parse(tpl) + require.NoError(t, err) + + err = tmpl.Execute(&buf, c) + require.NoError(t, err) + return buf.String() +}