Ctrl K
ComunidadGuías…

codersdk/templates.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package codersdk

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/google/uuid"
	"golang.org/x/xerrors"
)

// Template is the JSON representation of a Coder template. This type matches the
// database object for now, but is abstracted for ease of change later on.
type Template struct {
	ID                      uuid.UUID       `json:"id" format:"uuid"`
	CreatedAt               time.Time       `json:"created_at" format:"date-time"`
	UpdatedAt               time.Time       `json:"updated_at" format:"date-time"`
	OrganizationID          uuid.UUID       `json:"organization_id" format:"uuid"`
	OrganizationName        string          `json:"organization_name" format:"url"`
	OrganizationDisplayName string          `json:"organization_display_name"`
	OrganizationIcon        string          `json:"organization_icon"`
	Name                    string          `json:"name"`
	DisplayName             string          `json:"display_name"`
	Provisioner             ProvisionerType `json:"provisioner" enums:"terraform"`
	ActiveVersionID         uuid.UUID       `json:"active_version_id" format:"uuid"`
	// ActiveUserCount is set to -1 when loading.
	ActiveUserCount    int                    `json:"active_user_count"`
	BuildTimeStats     TemplateBuildTimeStats `json:"build_time_stats"`
	Description        string                 `json:"description"`
	Deprecated         bool                   `json:"deprecated"`
	DeprecationMessage string                 `json:"deprecation_message"`
	Deleted            bool                   `json:"deleted"`
	Icon               string                 `json:"icon"`
	DefaultTTLMillis   int64                  `json:"default_ttl_ms"`
	ActivityBumpMillis int64                  `json:"activity_bump_ms"`
	// TimeTilAutostopNotifyMillis is the duration before the workspace's
	// autostop deadline at which a reminder notification is sent. 0 disables
	// the notification.
	TimeTilAutostopNotifyMillis int64 `json:"time_til_autostop_notify_ms"`
	// AutostopRequirement and AutostartRequirement are Premium features. Their
	// values are only used if your license is entitled to use the advanced template
	// scheduling feature.
	AutostopRequirement  TemplateAutostopRequirement  `json:"autostop_requirement"`
	AutostartRequirement TemplateAutostartRequirement `json:"autostart_requirement"`
	CreatedByID          uuid.UUID                    `json:"created_by_id" format:"uuid"`
	CreatedByName        string                       `json:"created_by_name"`

	// AllowUserAutostart and AllowUserAutostop are Premium features. Their
	// values are only used if your license is entitled to use the advanced
	// template scheduling feature.
	AllowUserAutostart           bool `json:"allow_user_autostart"`
	AllowUserAutostop            bool `json:"allow_user_autostop"`
	AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs"`

	// FailureTTLMillis, TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are Premium features. Their
	// values are used if your license is entitled to use the advanced
	// template scheduling feature.
	FailureTTLMillis               int64 `json:"failure_ttl_ms"`
	TimeTilDormantMillis           int64 `json:"time_til_dormant_ms"`
	TimeTilDormantAutoDeleteMillis int64 `json:"time_til_dormant_autodelete_ms"`

	// RequireActiveVersion mandates that workspaces are built with the active
	// template version.
	RequireActiveVersion bool                         `json:"require_active_version"`
	MaxPortShareLevel    WorkspaceAgentPortShareLevel `json:"max_port_share_level"`
	CORSBehavior         CORSBehavior                 `json:"cors_behavior"`

	UseClassicParameterFlow bool `json:"use_classic_parameter_flow"`
	AgentsAllowed           bool `json:"agents_allowed"`

	// DisableModuleCache disables the use of cached Terraform modules during
	// provisioning for this template. It is read-only while
	// ModuleCacheDisabledByDeployment is true.
	DisableModuleCache bool `json:"disable_module_cache"`

	// ModuleCacheDisabledByDeployment reports that the deployment disables the
	// Terraform module cache for every template. Templates cannot opt back in,
	// so the effective state is disabled regardless of DisableModuleCache.
	ModuleCacheDisabledByDeployment bool `json:"module_cache_disabled_by_deployment"`

	// AllowWorkspaceRenames permits users to rename workspaces built from this
	// template. Renaming can be destructive for templates whose Terraform
	// references the workspace name.
	AllowWorkspaceRenames bool `json:"allow_workspace_renames"`
}

// WeekdaysToBitmap converts a list of weekdays to a bitmap in accordance with
// the schedule package's rules. The 0th bit is Monday, ..., the 6th bit is
// Sunday. The 7th bit is unused.
func WeekdaysToBitmap(days []string) (uint8, error) {
	var bitmap uint8
	for _, day := range days {
		switch strings.ToLower(day) {
		case "monday":
			bitmap |= 1 << 0
		case "tuesday":
			bitmap |= 1 << 1
		case "wednesday":
			bitmap |= 1 << 2
		case "thursday":
			bitmap |= 1 << 3
		case "friday":
			bitmap |= 1 << 4
		case "saturday":
			bitmap |= 1 << 5
		case "sunday":
			bitmap |= 1 << 6
		default:
			return 0, xerrors.Errorf("invalid weekday %q", day)
		}
	}
	return bitmap, nil
}

// BitmapToWeekdays converts a bitmap to a list of weekdays in accordance with
// the schedule package's rules (see above).
func BitmapToWeekdays(bitmap uint8) []string {
	days := []string{}
	for i := 0; i < 7; i++ {
		if bitmap&(1<<i) != 0 {
			switch i {
			case 0:
				days = append(days, "monday")
			case 1:
				days = append(days, "tuesday")
			case 2:
				days = append(days, "wednesday")
			case 3:
				days = append(days, "thursday")
			case 4:
				days = append(days, "friday")
			case 5:
				days = append(days, "saturday")
			case 6:
				days = append(days, "sunday")
			}
		}
	}
	return days
}

var AllDaysOfWeek = []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}

type TemplateAutostartRequirement struct {
	// DaysOfWeek is a list of days of the week in which autostart is allowed
	// to happen. If no days are specified, autostart is not allowed.
	DaysOfWeek []string `json:"days_of_week" enums:"monday,tuesday,wednesday,thursday,friday,saturday,sunday"`
}

type TemplateAutostopRequirement struct {
	// DaysOfWeek is a list of days of the week on which restarts are required.
	// Restarts happen within the user's quiet hours (in their configured
	// timezone). If no days are specified, restarts are not required. Weekdays
	// cannot be specified twice.
	//
	// Restarts will only happen on weekdays in this list on weeks which line up
	// with Weeks.
	DaysOfWeek []string `json:"days_of_week" enums:"monday,tuesday,wednesday,thursday,friday,saturday,sunday"`
	// Weeks is the number of weeks between required restarts. Weeks are synced
	// across all workspaces (and Coder deployments) using modulo math on a
	// hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023).
	// Values of 0 or 1 indicate weekly restarts. Values of 2 indicate
	// fortnightly restarts, etc.
	Weeks int64 `json:"weeks"`
}

type TransitionStats struct {
	P50 *int64 `example:"123"`
	P95 *int64 `example:"146"`
}

type (
	TemplateBuildTimeStats      map[WorkspaceTransition]TransitionStats
	UpdateActiveTemplateVersion struct {
		ID uuid.UUID `json:"id" validate:"required" format:"uuid"`
	}
)

type ArchiveTemplateVersionsRequest struct {
	// By default, only failed versions are archived. Set this to true
	// to archive all unused versions regardless of job status.
	All bool `json:"all"`
}

type ArchiveTemplateVersionsResponse struct {
	TemplateID  uuid.UUID   `json:"template_id" format:"uuid"`
	ArchivedIDs []uuid.UUID `json:"archived_ids"`
}

// ModuleCacheDisabled reports whether cached Terraform modules must be withheld
// from provisioner jobs. The deployment-wide setting wins: a template cannot opt
// back into the cache once the deployment disables it.
func ModuleCacheDisabled(dv *DeploymentValues, templateDisableModuleCache bool) bool {
	return templateDisableModuleCache || ModuleCacheDisabledByDeployment(dv)
}

// ModuleCacheDisabledByDeployment reports whether the deployment disables the
// Terraform module cache for every template.
func ModuleCacheDisabledByDeployment(dv *DeploymentValues) bool {
	return dv != nil && dv.Provisioner.DisableModuleCache.Value()
}

type TemplateRole string

const (
	TemplateRoleAdmin   TemplateRole = "admin"
	TemplateRoleUse     TemplateRole = "use"
	TemplateRoleDeleted TemplateRole = ""
)

type TemplateACL struct {
	Users  []TemplateUser  `json:"users"`
	Groups []TemplateGroup `json:"group"`
}

type TemplateGroup struct {
	Group
	Role TemplateRole `json:"role" enums:"admin,use"`
}

type TemplateUser struct {
	User
	Role TemplateRole `json:"role" enums:"admin,use"`
}

type UpdateTemplateACL struct {
	// UserPerms is a mapping from valid user UUIDs to the template role they
	// should be granted. To remove a user from the template, use "" as the role
	// (available as a constant named codersdk.TemplateRoleDeleted)
	UserPerms map[string]TemplateRole `json:"user_perms,omitempty" example:"<user_id>:admin,4df59e74-c027-470b-ab4d-cbba8963a5e9:use"`
	// GroupPerms is a mapping from valid group UUIDs to the template role they
	// should be granted. To remove a group from the template, use "" as the role
	// (available as a constant named codersdk.TemplateRoleDeleted)
	GroupPerms map[string]TemplateRole `json:"group_perms,omitempty" example:"<group_id>:admin,8bd26b20-f3e8-48be-a903-46bb920cf671:use"`
}

// ACLAvailable is a list of users and groups that can be added to a resource
// ACL.
type ACLAvailable struct {
	Users  []ReducedUser `json:"users"`
	Groups []Group       `json:"groups"`
}

// UpdateTemplateMeta is the request body for the PATCH /templates/{template}
// endpoint. All fields are optional. Fields that are nil are not modified.
type UpdateTemplateMeta struct {
	Name             *string `json:"name,omitempty" validate:"omitempty,template_name"`
	DisplayName      *string `json:"display_name,omitempty" validate:"omitempty,template_display_name"`
	Description      *string `json:"description,omitempty"`
	Icon             *string `json:"icon,omitempty"`
	DefaultTTLMillis *int64  `json:"default_ttl_ms,omitempty"`
	// ActivityBumpMillis allows optionally specifying the activity bump
	// duration for all workspaces created from this template. Defaults to 1h
	// but can be set to 0 to disable activity bumping.
	ActivityBumpMillis *int64 `json:"activity_bump_ms,omitempty"`
	// TimeTilAutostopNotifyMillis allows optionally specifying the duration
	// before the autostop deadline at which a reminder notification is sent for
	// workspaces created from this template. Defaults to 0 (disabled). Omitting
	// the field keeps the existing value.
	TimeTilAutostopNotifyMillis *int64 `json:"time_til_autostop_notify_ms,omitempty"`
	// AutostopRequirement and AutostartRequirement can only be set if your license
	// includes the advanced template scheduling feature. If you attempt to set this
	// value while unlicensed, it will be ignored.
	AutostopRequirement            *TemplateAutostopRequirement  `json:"autostop_requirement,omitempty"`
	AutostartRequirement           *TemplateAutostartRequirement `json:"autostart_requirement,omitempty"`
	AllowUserAutostart             *bool                         `json:"allow_user_autostart,omitempty"`
	AllowUserAutostop              *bool                         `json:"allow_user_autostop,omitempty"`
	AllowUserCancelWorkspaceJobs   *bool                         `json:"allow_user_cancel_workspace_jobs,omitempty"`
	FailureTTLMillis               *int64                        `json:"failure_ttl_ms,omitempty"`
	TimeTilDormantMillis           *int64                        `json:"time_til_dormant_ms,omitempty"`
	TimeTilDormantAutoDeleteMillis *int64                        `json:"time_til_dormant_autodelete_ms,omitempty"`
	// UpdateWorkspaceLastUsedAt updates the last_used_at field of workspaces
	// spawned from the template. This is useful for preventing workspaces being
	// immediately locked when updating the inactivity_ttl field to a new, shorter
	// value.
	UpdateWorkspaceLastUsedAt *bool `json:"update_workspace_last_used_at,omitempty"`
	// UpdateWorkspaceDormantAt updates the dormant_at field of workspaces spawned
	// from the template. This is useful for preventing dormant workspaces being
	// immediately deleted when updating the dormant_ttl field to a new, shorter
	// value.
	UpdateWorkspaceDormantAt *bool `json:"update_workspace_dormant_at,omitempty"`
	// RequireActiveVersion mandates workspaces built using this template
	// use the active version of the template. This option has no
	// effect on template admins.
	RequireActiveVersion *bool `json:"require_active_version,omitempty"`
	// DeprecationMessage if set, will mark the template as deprecated and block
	// any new workspaces from using this template.
	// If passed an empty string, will remove the deprecated message, making
	// the template usable for new workspaces again.
	DeprecationMessage *string `json:"deprecation_message,omitempty"`
	// DisableEveryoneGroupAccess allows optionally disabling the default
	// behavior of granting the 'everyone' group access to use the template.
	// If this is set to true, the template will not be available to all users,
	// and must be explicitly granted to users or groups in the permissions settings
	// of the template.
	DisableEveryoneGroupAccess *bool                         `json:"disable_everyone_group_access,omitempty"`
	MaxPortShareLevel          *WorkspaceAgentPortShareLevel `json:"max_port_share_level,omitempty"`
	CORSBehavior               *CORSBehavior                 `json:"cors_behavior,omitempty"`
	// UseClassicParameterFlow is a flag that switches the default behavior to use the classic
	// parameter flow when creating a workspace. This only affects deployments with the experiment
	// "dynamic-parameters" enabled. This setting will live for a period after the experiment is
	// made the default.
	// An "opt-out" is present in case the new feature breaks some existing templates.
	UseClassicParameterFlow *bool `json:"use_classic_parameter_flow,omitempty"`
	// DisableModuleCache disables the using of cached Terraform modules during
	// provisioning. It is ignored while the deployment disables the module
	// cache for all templates. It is recommended not to disable this.
	DisableModuleCache *bool `json:"disable_module_cache,omitempty"`
	// AgentsAllowed controls whether Coder Agents can create workspaces using
	// this template. If omitted, the current value is preserved.
	AgentsAllowed *bool `json:"agents_allowed,omitempty"`
	// AllowWorkspaceRenames permits users to rename workspaces built from this
	// template. Renaming can be destructive for templates whose Terraform
	// references the workspace name.
	AllowWorkspaceRenames *bool `json:"allow_workspace_renames,omitempty"`
}

type TemplateExample struct {
	ID          string   `json:"id" format:"uuid"`
	URL         string   `json:"url"`
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Icon        string   `json:"icon"`
	Tags        []string `json:"tags"`
	Markdown    string   `json:"markdown"`
}

// Template returns a single template.
func (c *Client) Template(ctx context.Context, template uuid.UUID) (Template, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s", template), nil)
	if err != nil {
		return Template{}, xerrors.Errorf("do request: %w", err)
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return Template{}, ReadBodyAsError(res)
	}
	var resp Template
	return resp, ReadBodyAsJSON(res, &resp)
}

func (c *Client) ArchiveTemplateVersions(ctx context.Context, template uuid.UUID, all bool) (ArchiveTemplateVersionsResponse, error) {
	res, err := c.Request(ctx, http.MethodPost,
		fmt.Sprintf("/api/v2/templates/%s/versions/archive", template),
		ArchiveTemplateVersionsRequest{
			All: all,
		},
	)
	if err != nil {
		return ArchiveTemplateVersionsResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return ArchiveTemplateVersionsResponse{}, ReadBodyAsError(res)
	}
	var resp ArchiveTemplateVersionsResponse
	return resp, ReadBodyAsJSON(res, &resp)
}

//nolint:revive
func (c *Client) SetArchiveTemplateVersion(ctx context.Context, templateVersion uuid.UUID, archive bool) error {
	u := fmt.Sprintf("/api/v2/templateversions/%s", templateVersion.String())
	if archive {
		u += "/archive"
	} else {
		u += "/unarchive"
	}
	res, err := c.Request(ctx, http.MethodPost, u, nil)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return ReadBodyAsError(res)
	}

	return nil
}

func (c *Client) DeleteTemplate(ctx context.Context, template uuid.UUID) error {
	res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/templates/%s", template), nil)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return ReadBodyAsError(res)
	}
	return nil
}

func (c *Client) UpdateTemplateMeta(ctx context.Context, templateID uuid.UUID, req UpdateTemplateMeta) (Template, error) {
	res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/templates/%s", templateID), req)
	if err != nil {
		return Template{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return Template{}, ReadBodyAsError(res)
	}
	var updated Template
	return updated, ReadBodyAsJSON(res, &updated)
}

func (c *Client) UpdateTemplateACL(ctx context.Context, templateID uuid.UUID, req UpdateTemplateACL) error {
	res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/templates/%s/acl", templateID), req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return ReadBodyAsError(res)
	}
	return nil
}

// TemplateACLAvailable returns available users + groups that can be assigned
// template perms. The optional req controls the q/limit/offset query
// parameters applied server-side; pass codersdk.UsersRequest{} when no
// filtering is desired.
func (c *Client) TemplateACLAvailable(ctx context.Context, templateID uuid.UUID, req UsersRequest) (ACLAvailable, error) {
	res, err := c.Request(
		ctx,
		http.MethodGet,
		fmt.Sprintf("/api/v2/templates/%s/acl/available", templateID),
		nil,
		req.Pagination.asRequestOption(),
		req.asRequestOption(),
	)
	if err != nil {
		return ACLAvailable{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return ACLAvailable{}, ReadBodyAsError(res)
	}
	var acl ACLAvailable
	return acl, ReadBodyAsJSON(res, &acl)
}

func (c *Client) TemplateACL(ctx context.Context, templateID uuid.UUID) (TemplateACL, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/acl", templateID), nil)
	if err != nil {
		return TemplateACL{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return TemplateACL{}, ReadBodyAsError(res)
	}
	var acl TemplateACL
	return acl, ReadBodyAsJSON(res, &acl)
}

// UpdateActiveTemplateVersion updates the active template version to the ID provided.
// The template version must be attached to the template.
func (c *Client) UpdateActiveTemplateVersion(ctx context.Context, template uuid.UUID, req UpdateActiveTemplateVersion) error {
	res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/templates/%s/versions", template), req)
	if err != nil {
		return nil
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return ReadBodyAsError(res)
	}
	return nil
}

// TemplateVersionsByTemplateRequest defines the request parameters for
// TemplateVersionsByTemplate.
type TemplateVersionsByTemplateRequest struct {
	TemplateID      uuid.UUID `json:"template_id" validate:"required" format:"uuid"`
	IncludeArchived bool      `json:"include_archived"`
	Pagination
}

// TemplateVersionsByTemplate lists versions associated with a template.
func (c *Client) TemplateVersionsByTemplate(ctx context.Context, req TemplateVersionsByTemplateRequest) ([]TemplateVersion, error) {
	u := fmt.Sprintf("/api/v2/templates/%s/versions", req.TemplateID)
	if req.IncludeArchived {
		u += "?include_archived=true"
	}
	res, err := c.Request(ctx, http.MethodGet, u, nil, req.asRequestOption())
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return nil, ReadBodyAsError(res)
	}
	var templateVersion []TemplateVersion
	return templateVersion, ReadBodyAsJSON(res, &templateVersion)
}

// TemplateVersionByName returns a template version by it's friendly name.
// This is used for path-based routing. Like: /templates/example/versions/helloworld
func (c *Client) TemplateVersionByName(ctx context.Context, template uuid.UUID, name string) (TemplateVersion, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/versions/%s", template, name), nil)
	if err != nil {
		return TemplateVersion{}, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return TemplateVersion{}, ReadBodyAsError(res)
	}
	var templateVersion TemplateVersion
	return templateVersion, ReadBodyAsJSON(res, &templateVersion)
}

func (c *Client) TemplateDAUsLocalTZ(ctx context.Context, templateID uuid.UUID) (*DAUsResponse, error) {
	return c.TemplateDAUs(ctx, templateID, TimezoneOffsetHour(time.Local))
}

// TemplateDAUs requires a tzOffset in hours. Use 0 for UTC, and TimezoneOffsetHour(time.Local) for the
// local timezone.
func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID, tzOffset int) (*DAUsResponse, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/daus", templateID), nil, DAURequest{
		TZHourOffset: tzOffset,
	}.asRequestOption())
	if err != nil {
		return nil, xerrors.Errorf("execute request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return nil, ReadBodyAsError(res)
	}

	var resp DAUsResponse
	return &resp, ReadBodyAsJSON(res, &resp)
}

// AgentStatsReportRequest is a WebSocket request by coderd
// to the agent for stats.
// @typescript-ignore AgentStatsReportRequest
type AgentStatsReportRequest struct{}

// AgentStatsReportResponse is returned for each report
// request by the agent.
type AgentStatsReportResponse struct {
	NumConns int64 `json:"num_comms"`
	// RxBytes is the number of received bytes.
	RxBytes int64 `json:"rx_bytes"`
	// TxBytes is the number of transmitted bytes.
	TxBytes int64 `json:"tx_bytes"`
}

// TemplateExamples lists example templates available in Coder.
//
// Deprecated: Use StarterTemplates instead.
func (c *Client) TemplateExamples(ctx context.Context, _ uuid.UUID) ([]TemplateExample, error) {
	return c.StarterTemplates(ctx)
}

// StarterTemplates lists example templates available in Coder.
func (c *Client) StarterTemplates(ctx context.Context) ([]TemplateExample, error) {
	res, err := c.Request(ctx, http.MethodGet, "/api/v2/templates/examples", nil)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return nil, ReadBodyAsError(res)
	}
	var templateExamples []TemplateExample
	return templateExamples, ReadBodyAsJSON(res, &templateExamples)
}

type InvalidatePresetsResponse struct {
	Invalidated []InvalidatedPreset `json:"invalidated"`
}

type InvalidatedPreset struct {
	TemplateName        string `json:"template_name"`
	TemplateVersionName string `json:"template_version_name"`
	PresetName          string `json:"preset_name"`
}

// InvalidateTemplatePresets invalidates all presets for the
// template's active version by setting last_invalidated_at timestamp.
// The reconciler will then mark these prebuilds as expired and create new ones.
func (c *Client) InvalidateTemplatePresets(ctx context.Context, template uuid.UUID) (InvalidatePresetsResponse, error) {
	res, err := c.Request(ctx, http.MethodPost,
		fmt.Sprintf("/api/v2/templates/%s/prebuilds/invalidate", template),
		nil,
	)
	if err != nil {
		return InvalidatePresetsResponse{}, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return InvalidatePresetsResponse{}, ReadBodyAsError(res)
	}

	var response InvalidatePresetsResponse
	return response, ReadBodyAsJSON(res, &response)
}