Ctrl K
ComunidadGuías…

codersdk/organizations.go

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

Archivo original ↗Preguntar a KiBot
package codersdk

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

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

// DefaultOrganization is used as a replacement for the default organization.
var DefaultOrganization = "default"

type ProvisionerStorageMethod string

const (
	ProvisionerStorageMethodFile ProvisionerStorageMethod = "file"
)

type ProvisionerType string

const (
	ProvisionerTypeEcho      ProvisionerType = "echo"
	ProvisionerTypeTerraform ProvisionerType = "terraform"
)

// ProvisionerTypeValid accepts string or ProvisionerType for easier usage.
// Will validate the enum is in the set.
func ProvisionerTypeValid[T ProvisionerType | string](pt T) error {
	switch string(pt) {
	case string(ProvisionerTypeEcho), string(ProvisionerTypeTerraform):
		return nil
	default:
		return xerrors.Errorf("provisioner type '%s' is not supported", pt)
	}
}

type MinimalOrganization struct {
	ID          uuid.UUID `table:"id" json:"id" validate:"required" format:"uuid"`
	Name        string    `table:"name,default_sort" json:"name"`
	DisplayName string    `table:"display name" json:"display_name"`
	Icon        string    `table:"icon" json:"icon"`
}

// Organization is the JSON representation of a Coder organization.
type Organization struct {
	MinimalOrganization `table:"m,recursive_inline"`
	Description         string    `table:"description" json:"description"`
	CreatedAt           time.Time `table:"created at" json:"created_at" validate:"required" format:"date-time"`
	UpdatedAt           time.Time `table:"updated at" json:"updated_at" validate:"required" format:"date-time"`
	IsDefault           bool      `table:"default" json:"is_default" validate:"required"`
	// DefaultOrgMemberRoles are unioned into every member's effective
	// roles at request time. Changes propagate to all members on the
	// next request.
	DefaultOrgMemberRoles []string `table:"default org member roles" json:"default_org_member_roles"`
}

func (o Organization) HumanName() string {
	if o.DisplayName == "" {
		return o.Name
	}
	return o.DisplayName
}

type OrganizationMember struct {
	UserID         uuid.UUID  `table:"user id" json:"user_id" format:"uuid"`
	OrganizationID uuid.UUID  `table:"organization id" json:"organization_id" format:"uuid"`
	CreatedAt      time.Time  `table:"created at" json:"created_at" format:"date-time"`
	UpdatedAt      time.Time  `table:"updated at" json:"updated_at" format:"date-time"`
	Roles          []SlimRole `table:"organization roles" json:"roles"`
}

type OrganizationMemberWithUserData struct {
	Username         string     `table:"username,default_sort" json:"username"`
	Name             string     `table:"name" json:"name,omitempty"`
	AvatarURL        string     `json:"avatar_url,omitempty"`
	Email            string     `json:"email"`
	Status           UserStatus `json:"status"`
	LoginType        LoginType  `json:"login_type"`
	LastSeenAt       time.Time  `table:"last seen at" json:"last_seen_at,omitempty" format:"date-time"`
	UserCreatedAt    time.Time  `table:"user created at" json:"user_created_at" format:"date-time"`
	UserUpdatedAt    time.Time  `table:"user updated at" json:"user_updated_at" format:"date-time"`
	IsServiceAccount bool       `json:"is_service_account,omitempty"`
	GlobalRoles      []SlimRole `json:"global_roles"`
	// HasAISeat intentionally omits omitempty so the API always includes the
	// field, even when false.
	HasAISeat          bool `json:"has_ai_seat"`
	OrganizationMember `table:"m,recursive_inline"`
}

type PaginatedMembersRequest struct {
	Limit  int `json:"limit,omitempty"`
	Offset int `json:"offset,omitempty"`
}

type PaginatedMembersResponse struct {
	Members []OrganizationMemberWithUserData `json:"members"`
	Count   int                              `json:"count"`
}

type CreateOrganizationRequest struct {
	Name string `json:"name" validate:"required,organization_name"`
	// DisplayName will default to the same value as `Name` if not provided.
	DisplayName string `json:"display_name,omitempty" validate:"omitempty,organization_display_name"`
	Description string `json:"description,omitempty"`
	Icon        string `json:"icon,omitempty"`
}

type UpdateOrganizationRequest struct {
	Name        string  `json:"name,omitempty" validate:"omitempty,organization_name"`
	DisplayName string  `json:"display_name,omitempty" validate:"omitempty,organization_display_name"`
	Description *string `json:"description,omitempty"`
	Icon        *string `json:"icon,omitempty"`
	// DefaultOrgMemberRoles, when non-nil, replaces the org's default
	// member roles.
	DefaultOrgMemberRoles *[]string `json:"default_org_member_roles,omitempty"`
}

// CreateTemplateVersionRequest enables callers to create a new Template Version.
type CreateTemplateVersionRequest struct {
	Name    string `json:"name,omitempty" validate:"omitempty,template_version_name"`
	Message string `json:"message,omitempty" validate:"lt=1048577"`
	// TemplateID optionally associates a version with a template.
	TemplateID      uuid.UUID                `json:"template_id,omitempty" format:"uuid"`
	StorageMethod   ProvisionerStorageMethod `json:"storage_method" validate:"oneof=file,required"`
	FileID          uuid.UUID                `json:"file_id,omitempty" validate:"required_without=ExampleID" format:"uuid"`
	ExampleID       string                   `json:"example_id,omitempty" validate:"required_without=FileID"`
	Provisioner     ProvisionerType          `json:"provisioner" validate:"oneof=terraform echo,required"`
	ProvisionerTags map[string]string        `json:"tags"`

	UserVariableValues []VariableValue `json:"user_variable_values,omitempty"`
}

type VariableValue struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

// CreateTemplateRequest provides options when creating a template.
type CreateTemplateRequest struct {
	// Name is the name of the template.
	Name string `json:"name" validate:"template_name,required"`
	// DisplayName is the displayed name of the template.
	DisplayName string `json:"display_name,omitempty" validate:"template_display_name"`
	// Description is a description of what the template contains. It must be
	// no longer than 128 Unicode code points.
	Description string `json:"description,omitempty" validate:"lte=128"`
	// Icon is a relative path or external URL that specifies
	// an icon to be displayed in the dashboard.
	Icon string `json:"icon,omitempty"`

	// VersionID is an in-progress or completed job to use as an initial version
	// of the template.
	//
	// This is required on creation to enable a user-flow of validating a
	// template works. There is no reason the data-model cannot support empty
	// templates, but it doesn't make sense for users.
	VersionID uuid.UUID `json:"template_version_id" validate:"required" format:"uuid"`

	// DefaultTTLMillis allows optionally specifying the default TTL
	// for all workspaces created from this template.
	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).
	TimeTilAutostopNotifyMillis *int64 `json:"time_til_autostop_notify_ms,omitempty"`
	// AutostopRequirement allows optionally specifying the autostop requirement
	// for workspaces created from this template. This is a Premium feature.
	AutostopRequirement *TemplateAutostopRequirement `json:"autostop_requirement,omitempty"`
	// AutostartRequirement allows optionally specifying the autostart allowed days
	// for workspaces created from this template. This is a Premium feature.
	AutostartRequirement *TemplateAutostartRequirement `json:"autostart_requirement,omitempty"`

	// Allow users to cancel in-progress workspace jobs.
	// *bool as the default value is "true".
	AllowUserCancelWorkspaceJobs *bool `json:"allow_user_cancel_workspace_jobs"`

	// AllowUserAutostart allows users to set a schedule for autostarting their
	// workspace. By default this is true. This can only be disabled with a
	// Premium license.
	AllowUserAutostart *bool `json:"allow_user_autostart,omitempty"`

	// AllowUserAutostop allows users to set a custom workspace TTL to use in
	// place of the template's DefaultTTL field. By default this is true. If
	// false, the DefaultTTL will always be used. This can only be disabled with a
	// Premium license.
	AllowUserAutostop *bool `json:"allow_user_autostop,omitempty"`

	// FailureTTLMillis allows optionally specifying the max lifetime before Coder
	// stops all resources for failed workspaces created from this template.
	FailureTTLMillis *int64 `json:"failure_ttl_ms,omitempty"`
	// TimeTilDormantMillis allows optionally specifying the max lifetime before Coder
	// locks inactive workspaces created from this template.
	TimeTilDormantMillis *int64 `json:"dormant_ttl_ms,omitempty"`
	// TimeTilDormantAutoDeleteMillis allows optionally specifying the max lifetime before Coder
	// permanently deletes dormant workspaces created from this template.
	TimeTilDormantAutoDeleteMillis *int64 `json:"delete_ttl_ms,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"`

	// RequireActiveVersion mandates that workspaces are built with the active
	// template version.
	RequireActiveVersion bool `json:"require_active_version"`

	// MaxPortShareLevel allows optionally specifying the maximum port share level
	// for workspaces created from the template.
	MaxPortShareLevel *WorkspaceAgentPortShareLevel `json:"max_port_share_level"`

	// UseClassicParameterFlow allows optionally specifying whether
	// the template should use the classic parameter flow. The default if unset is
	// true, and is why `*bool` is used here. When dynamic parameters becomes
	// the default, this will default to false.
	UseClassicParameterFlow *bool `json:"template_use_classic_parameter_flow,omitempty"`

	// CORSBehavior allows optionally specifying the CORS behavior for all shared ports.
	CORSBehavior *CORSBehavior `json:"cors_behavior"`

	// AgentsAllowed controls whether Coder Agents can create workspaces using
	// this template. Defaults to true.
	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, so this defaults to false.
	AllowWorkspaceRenames *bool `json:"allow_workspace_renames,omitempty"`
}

// CreateWorkspaceRequest provides options for creating a new workspace.
// Either TemplateID or TemplateVersionID must be specified. They cannot both be present.
// @Description CreateWorkspaceRequest provides options for creating a new workspace.
// @Description Only one of TemplateID or TemplateVersionID can be specified, not both.
// @Description If TemplateID is specified, the active version of the template will be used.
// @Description Workspace names:
// @Description - Must start with a letter or number
// @Description - Can only contain letters, numbers, and hyphens
// @Description - Cannot contain spaces or special characters
// @Description - Cannot be named `new` or `create`
// @Description - Must be unique within your workspaces
// @Description - Maximum length of 32 characters
type CreateWorkspaceRequest struct {
	// TemplateID specifies which template should be used for creating the workspace.
	TemplateID uuid.UUID `json:"template_id,omitempty" validate:"required_without=TemplateVersionID,excluded_with=TemplateVersionID" format:"uuid"`
	// TemplateVersionID can be used to specify a specific version of a template for creating the workspace.
	TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" validate:"required_without=TemplateID,excluded_with=TemplateID" format:"uuid"`
	Name              string    `json:"name" validate:"workspace_name,required"`
	AutostartSchedule *string   `json:"autostart_schedule,omitempty"`
	TTLMillis         *int64    `json:"ttl_ms,omitempty"`
	// RichParameterValues allows for additional parameters to be provided
	// during the initial provision.
	RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"`
	// Secrets sets the workspace's initial secrets.
	Secrets                 []WorkspaceSecretInput `json:"secrets,omitempty"`
	AutomaticUpdates        AutomaticUpdates       `json:"automatic_updates,omitempty"`
	TemplateVersionPresetID uuid.UUID              `json:"template_version_preset_id,omitempty" format:"uuid"`
}

func (c *Client) OrganizationByName(ctx context.Context, name string) (Organization, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", name), nil)
	if err != nil {
		return Organization{}, xerrors.Errorf("execute request: %w", err)
	}
	defer res.Body.Close()

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

	var organization Organization
	return organization, ReadBodyAsJSON(res, &organization)
}

func (c *Client) Organizations(ctx context.Context) ([]Organization, error) {
	res, err := c.Request(ctx, http.MethodGet, "/api/v2/organizations", nil)
	if err != nil {
		return []Organization{}, xerrors.Errorf("execute request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return []Organization{}, ReadBodyAsError(res)
	}

	var organizations []Organization
	return organizations, ReadBodyAsJSON(res, &organizations)
}

func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) {
	// OrganizationByName uses the exact same endpoint. It accepts a name or uuid.
	// We just provide this function for type safety.
	return c.OrganizationByName(ctx, id.String())
}

// CreateOrganization creates an organization and adds the user making the request as an owner.
func (c *Client) CreateOrganization(ctx context.Context, req CreateOrganizationRequest) (Organization, error) {
	res, err := c.Request(ctx, http.MethodPost, "/api/v2/organizations", req)
	if err != nil {
		return Organization{}, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusCreated {
		return Organization{}, ReadBodyAsError(res)
	}

	var org Organization
	return org, ReadBodyAsJSON(res, &org)
}

// UpdateOrganization will update information about the corresponding organization, based on
// the UUID/name provided as `orgID`.
func (c *Client) UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationRequest) (Organization, error) {
	res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s", orgID), req)
	if err != nil {
		return Organization{}, xerrors.Errorf("execute request: %w", err)
	}
	defer res.Body.Close()

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

	var organization Organization
	return organization, ReadBodyAsJSON(res, &organization)
}

// DeleteOrganization will remove the corresponding organization from the deployment, based on
// the UUID/name provided as `orgID`.
func (c *Client) DeleteOrganization(ctx context.Context, orgID string) error {
	res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/organizations/%s", orgID), nil)
	if err != nil {
		return xerrors.Errorf("execute request: %w", err)
	}
	defer res.Body.Close()

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

	return nil
}

// ProvisionerDaemons returns provisioner daemons available.
func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
	res, err := c.Request(ctx, http.MethodGet,
		// TODO: the organization path parameter is currently ignored.
		"/api/v2/organizations/default/provisionerdaemons",
		nil,
	)
	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 daemons []ProvisionerDaemon
	return daemons, ReadBodyAsJSON(res, &daemons)
}

type OrganizationProvisionerDaemonsOptions struct {
	Limit   int
	Offline bool
	Status  []ProvisionerDaemonStatus
	MaxAge  time.Duration
	IDs     []uuid.UUID
	Tags    map[string]string
}

func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizationID uuid.UUID, opts *OrganizationProvisionerDaemonsOptions) ([]ProvisionerDaemon, error) {
	qp := url.Values{}
	if opts != nil {
		if opts.Limit > 0 {
			qp.Add("limit", strconv.Itoa(opts.Limit))
		}
		if opts.Offline {
			qp.Add("offline", "true")
		}
		if len(opts.Status) > 0 {
			qp.Add("status", joinSlice(opts.Status))
		}
		if opts.MaxAge > 0 {
			qp.Add("max_age", opts.MaxAge.String())
		}
		if len(opts.IDs) > 0 {
			qp.Add("ids", joinSliceStringer(opts.IDs))
		}
		if len(opts.Tags) > 0 {
			tagsRaw, err := json.Marshal(opts.Tags)
			if err != nil {
				return nil, xerrors.Errorf("marshal tags: %w", err)
			}
			qp.Add("tags", string(tagsRaw))
		}
	}

	res, err := c.Request(ctx, http.MethodGet,
		fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons?%s", organizationID.String(), qp.Encode()),
		nil,
	)
	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 daemons []ProvisionerDaemon
	return daemons, ReadBodyAsJSON(res, &daemons)
}

type OrganizationProvisionerJobsOptions struct {
	Limit     int
	IDs       []uuid.UUID
	Status    []ProvisionerJobStatus
	Tags      map[string]string
	Initiator string
}

func (c *Client) OrganizationProvisionerJobs(ctx context.Context, organizationID uuid.UUID, opts *OrganizationProvisionerJobsOptions) ([]ProvisionerJob, error) {
	qp := url.Values{}
	if opts != nil {
		if opts.Limit > 0 {
			qp.Add("limit", strconv.Itoa(opts.Limit))
		}
		if len(opts.IDs) > 0 {
			qp.Add("ids", joinSliceStringer(opts.IDs))
		}
		if len(opts.Status) > 0 {
			qp.Add("status", joinSlice(opts.Status))
		}
		if len(opts.Tags) > 0 {
			tagsRaw, err := json.Marshal(opts.Tags)
			if err != nil {
				return nil, xerrors.Errorf("marshal tags: %w", err)
			}
			qp.Add("tags", string(tagsRaw))
		}
		if opts.Initiator != "" {
			qp.Add("initiator", opts.Initiator)
		}
	}

	res, err := c.Request(ctx, http.MethodGet,
		fmt.Sprintf("/api/v2/organizations/%s/provisionerjobs?%s", organizationID.String(), qp.Encode()),
		nil,
	)
	if err != nil {
		return nil, xerrors.Errorf("make request: %w", err)
	}
	defer res.Body.Close()

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

	var jobs []ProvisionerJob
	return jobs, ReadBodyAsJSON(res, &jobs)
}

func (c *Client) OrganizationProvisionerJob(ctx context.Context, organizationID, jobID uuid.UUID) (job ProvisionerJob, err error) {
	res, err := c.Request(ctx, http.MethodGet,
		fmt.Sprintf("/api/v2/organizations/%s/provisionerjobs/%s", organizationID.String(), jobID.String()),
		nil,
	)
	if err != nil {
		return job, xerrors.Errorf("make request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return job, ReadBodyAsError(res)
	}
	return job, ReadBodyAsJSON(res, &job)
}

func joinSlice[T ~string](s []T) string {
	var ss []string
	for _, v := range s {
		ss = append(ss, string(v))
	}
	return strings.Join(ss, ",")
}

func joinSliceStringer[T fmt.Stringer](s []T) string {
	var ss []string
	for _, v := range s {
		ss = append(ss, v.String())
	}
	return strings.Join(ss, ",")
}

// CreateTemplateVersion processes source-code and optionally associates the version with a template.
// Executing without a template is useful for validating source-code.
func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid.UUID, req CreateTemplateVersionRequest) (TemplateVersion, error) {
	res, err := c.Request(ctx, http.MethodPost,
		fmt.Sprintf("/api/v2/organizations/%s/templateversions", organizationID.String()),
		req,
	)
	if err != nil {
		return TemplateVersion{}, xerrors.Errorf("execute request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusCreated {
		return TemplateVersion{}, ReadBodyAsError(res)
	}

	var templateVersion TemplateVersion
	return templateVersion, ReadBodyAsJSON(res, &templateVersion)
}

func (c *Client) TemplateVersionByOrganizationAndName(ctx context.Context, organizationID uuid.UUID, templateName, versionName string) (TemplateVersion, error) {
	res, err := c.Request(ctx, http.MethodGet,
		fmt.Sprintf("/api/v2/organizations/%s/templates/%s/versions/%s", organizationID.String(), templateName, versionName),
		nil,
	)
	if err != nil {
		return TemplateVersion{}, xerrors.Errorf("execute request: %w", err)
	}
	defer res.Body.Close()

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

	var templateVersion TemplateVersion
	return templateVersion, ReadBodyAsJSON(res, &templateVersion)
}

// CreateTemplate creates a new template inside an organization.
func (c *Client) CreateTemplate(ctx context.Context, organizationID uuid.UUID, request CreateTemplateRequest) (Template, error) {
	res, err := c.Request(ctx, http.MethodPost,
		fmt.Sprintf("/api/v2/organizations/%s/templates", organizationID.String()),
		request,
	)
	if err != nil {
		return Template{}, xerrors.Errorf("execute request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusCreated {
		return Template{}, ReadBodyAsError(res)
	}

	var template Template
	return template, ReadBodyAsJSON(res, &template)
}

// TemplatesByOrganization lists all templates inside of an organization.
func (c *Client) TemplatesByOrganization(ctx context.Context, organizationID uuid.UUID) ([]Template, error) {
	res, err := c.Request(ctx, http.MethodGet,
		fmt.Sprintf("/api/v2/organizations/%s/templates", organizationID.String()),
		nil,
	)
	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 templates []Template
	return templates, ReadBodyAsJSON(res, &templates)
}

type TemplateFilter struct {
	OrganizationID uuid.UUID `typescript:"-"`
	ExactName      string    `typescript:"-"`
	FuzzyName      string    `typescript:"-"`
	AuthorUsername string    `typescript:"-"`
	SearchQuery    string    `json:"q,omitempty"`
}

// asRequestOption returns a function that can be used in (*Client).Request.
// It modifies the request query parameters.
func (f TemplateFilter) asRequestOption() RequestOption {
	return func(r *http.Request) {
		var params []string
		// Make sure all user input is quoted to ensure it's parsed as a single
		// string.
		if f.OrganizationID != uuid.Nil {
			params = append(params, fmt.Sprintf("organization:%q", f.OrganizationID.String()))
		}

		if f.ExactName != "" {
			params = append(params, fmt.Sprintf("exact_name:%q", f.ExactName))
		}

		if f.FuzzyName != "" {
			params = append(params, fmt.Sprintf("name:%q", f.FuzzyName))
		}

		if f.AuthorUsername != "" {
			params = append(params, fmt.Sprintf("author:%q", f.AuthorUsername))
		}

		if f.SearchQuery != "" {
			params = append(params, f.SearchQuery)
		}

		q := r.URL.Query()
		q.Set("q", strings.Join(params, " "))
		r.URL.RawQuery = q.Encode()
	}
}

// Templates lists all viewable templates
func (c *Client) Templates(ctx context.Context, filter TemplateFilter) ([]Template, error) {
	res, err := c.Request(ctx, http.MethodGet,
		"/api/v2/templates",
		nil,
		filter.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 templates []Template
	return templates, ReadBodyAsJSON(res, &templates)
}

// TemplateByName finds a template inside the organization provided with a case-insensitive name.
func (c *Client) TemplateByName(ctx context.Context, organizationID uuid.UUID, name string) (Template, error) {
	if name == "" {
		return Template{}, xerrors.Errorf("template name cannot be empty")
	}
	res, err := c.Request(ctx, http.MethodGet,
		fmt.Sprintf("/api/v2/organizations/%s/templates/%s", organizationID.String(), name),
		nil,
	)
	if err != nil {
		return Template{}, xerrors.Errorf("execute request: %w", err)
	}
	defer res.Body.Close()

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

	var template Template
	return template, ReadBodyAsJSON(res, &template)
}

// CreateWorkspace creates a new workspace for the template specified.
//
// Deprecated: Use CreateUserWorkspace instead.
func (c *Client) CreateWorkspace(ctx context.Context, _ uuid.UUID, user string, request CreateWorkspaceRequest) (Workspace, error) {
	return c.CreateUserWorkspace(ctx, user, request)
}

// CreateUserWorkspace creates a new workspace for the template specified.
func (c *Client) CreateUserWorkspace(ctx context.Context, user string, request CreateWorkspaceRequest) (Workspace, error) {
	// Validate before URL resolution can interpret user input as a path or query.
	if err := validateWorkspacePathSegment(user); err != nil {
		return Workspace{}, xerrors.Errorf("invalid user: %w", err)
	}
	res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/workspaces", user), request)
	if err != nil {
		return Workspace{}, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusCreated {
		return Workspace{}, ReadBodyAsError(res)
	}

	var workspace Workspace
	return workspace, ReadBodyAsJSON(res, &workspace)
}