coder/codersdk/templates.go

485 lines
18 KiB
Go

package codersdk
import (
"context"
"encoding/json"
"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"`
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"`
Icon string `json:"icon"`
DefaultTTLMillis int64 `json:"default_ttl_ms"`
ActivityBumpMillis int64 `json:"activity_bump_ms"`
// AutostopRequirement and AutostartRequirement are enterprise features. Its
// value is 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 enterprise-only. 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 enterprise-only. 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"`
}
// 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
}
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"`
}
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 should be a mapping of user id to role. The user id must be the
// uuid of the user, not a username or email address.
UserPerms map[string]TemplateRole `json:"user_perms,omitempty" example:"<group_id>:admin,4df59e74-c027-470b-ab4d-cbba8963a5e9:use"`
// GroupPerms should be a mapping of group id to role.
GroupPerms map[string]TemplateRole `json:"group_perms,omitempty" example:"<user_id>>:admin,8bd26b20-f3e8-48be-a903-46bb920cf671:use"`
}
// ACLAvailable is a list of users and groups that can be added to a template
// ACL.
type ACLAvailable struct {
Users []ReducedUser `json:"users"`
Groups []Group `json:"groups"`
}
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"`
// 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"`
// UpdateWorkspaceDormant 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"`
// 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"`
// 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"`
MaxPortShareLevel *WorkspaceAgentPortShareLevel `json:"max_port_share_level"`
}
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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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.StatusNotModified {
return Template{}, xerrors.New("template metadata not modified")
}
if res.StatusCode != http.StatusOK {
return Template{}, ReadBodyAsError(res)
}
var updated Template
return updated, json.NewDecoder(res.Body).Decode(&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
func (c *Client) TemplateACLAvailable(ctx context.Context, templateID uuid.UUID) (ACLAvailable, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/acl/available", templateID), nil)
if err != nil {
return ACLAvailable{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ACLAvailable{}, ReadBodyAsError(res)
}
var acl ACLAvailable
return acl, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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.Pagination.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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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, json.NewDecoder(res.Body).Decode(&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 embedded in coder.
func (c *Client) TemplateExamples(ctx context.Context, organizationID uuid.UUID) ([]TemplateExample, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/templates/examples", organizationID), 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, json.NewDecoder(res.Body).Decode(&templateExamples)
}