From 84a4c7158b4388fab4b321080193334b8e6528d9 Mon Sep 17 00:00:00 2001
From: Charlie Moog <moogcharlie@gmail.com>
Date: Mon, 2 Nov 2020 18:05:40 -0600
Subject: [PATCH] Add more coder-sdk update, create, and delete operations

---
 coder-sdk/env.go             |  9 +++--
 coder-sdk/error.go           |  9 +++--
 coder-sdk/image.go           |  6 ++--
 coder-sdk/org.go             | 59 +++++++++++++++++++++++++------
 coder-sdk/users.go           | 67 ++++++++++++++++++++++++++++++++----
 coder-sdk/util.go            | 35 +++++++++++++++++++
 internal/x/xjson/duration.go | 31 -----------------
 7 files changed, 157 insertions(+), 59 deletions(-)
 create mode 100644 coder-sdk/util.go
 delete mode 100644 internal/x/xjson/duration.go

diff --git a/coder-sdk/env.go b/coder-sdk/env.go
index a15f1caa..42bb6467 100644
--- a/coder-sdk/env.go
+++ b/coder-sdk/env.go
@@ -5,7 +5,6 @@ import (
 	"net/http"
 	"time"
 
-	"cdr.dev/coder-cli/internal/x/xjson"
 	"golang.org/x/xerrors"
 	"nhooyr.io/websocket"
 	"nhooyr.io/websocket/wsjson"
@@ -31,14 +30,14 @@ type Environment struct {
 	UpdatedAt        time.Time        `json:"updated_at"         table:"-"`
 	LastOpenedAt     time.Time        `json:"last_opened_at"     table:"-"`
 	LastConnectionAt time.Time        `json:"last_connection_at" table:"-"`
-	AutoOffThreshold xjson.MSDuration `json:"auto_off_threshold" table:"-"`
+	AutoOffThreshold Duration         `json:"auto_off_threshold" table:"-"`
 }
 
 // RebuildMessage defines the message shown when an Environment requires a rebuild for it can be accessed.
 type RebuildMessage struct {
-	Text             string           `json:"text"`
-	Required         bool             `json:"required"`
-	AutoOffThreshold xjson.MSDuration `json:"auto_off_threshold" table:"-"`
+	Text             string   `json:"text"`
+	Required         bool     `json:"required"`
+	AutoOffThreshold Duration `json:"auto_off_threshold"`
 }
 
 // EnvironmentStat represents the state of an environment
diff --git a/coder-sdk/error.go b/coder-sdk/error.go
index 91ebfa30..2b312378 100644
--- a/coder-sdk/error.go
+++ b/coder-sdk/error.go
@@ -14,9 +14,12 @@ var ErrNotFound = xerrors.Errorf("resource not found")
 
 // APIError is the expected payload format for our errors.
 type APIError struct {
-	Err struct {
-		Msg string `json:"msg"`
-	} `json:"error"`
+	Err APIErrorMsg `json:"error"`
+}
+
+// APIErrorMsg contains the rich error information returned by API errors.
+type APIErrorMsg struct {
+	Msg string `json:"msg"`
 }
 
 // HTTPError represents an error from the Coder API.
diff --git a/coder-sdk/image.go b/coder-sdk/image.go
index c9122cc9..edeecabf 100644
--- a/coder-sdk/image.go
+++ b/coder-sdk/image.go
@@ -26,8 +26,8 @@ type NewRegistryRequest struct {
 	Password     string `json:"password"`
 }
 
-// ImportImageRequest is used to import new images and registries into Coder
-type ImportImageRequest struct {
+// ImportImageReq is used to import new images and registries into Coder
+type ImportImageReq struct {
 	RegistryID      *string             `json:"registry_id"`  // Used to import images to existing registries.
 	NewRegistry     *NewRegistryRequest `json:"new_registry"` // Used when adding a new registry.
 	Repository      string              `json:"repository"`   // Refers to the image. Ex: "codercom/ubuntu".
@@ -40,7 +40,7 @@ type ImportImageRequest struct {
 }
 
 // ImportImage creates a new image and optionally a new registry
-func (c Client) ImportImage(ctx context.Context, orgID string, req ImportImageRequest) (*Image, error) {
+func (c Client) ImportImage(ctx context.Context, orgID string, req ImportImageReq) (*Image, error) {
 	var img Image
 	if err := c.requestBody(ctx, http.MethodPost, "/api/orgs/"+orgID+"/images", req, &img); err != nil {
 		return nil, err
diff --git a/coder-sdk/org.go b/coder-sdk/org.go
index e3fe4583..0caa9d18 100644
--- a/coder-sdk/org.go
+++ b/coder-sdk/org.go
@@ -16,18 +16,15 @@ type Organization struct {
 // OrganizationUser user wraps the basic User type and adds data specific to the user's membership of an organization
 type OrganizationUser struct {
 	User
-	OrganizationRoles []OrganizationRole `json:"organization_roles"`
-	RolesUpdatedAt    time.Time          `json:"roles_updated_at"`
+	OrganizationRoles []Role    `json:"organization_roles"`
+	RolesUpdatedAt    time.Time `json:"roles_updated_at"`
 }
 
-// OrganizationRole defines an organization OrganizationRole
-type OrganizationRole string
-
-// The OrganizationRole enum values
+// Organization Roles
 const (
-	RoleOrgMember  OrganizationRole = "organization-member"
-	RoleOrgAdmin   OrganizationRole = "organization-admin"
-	RoleOrgManager OrganizationRole = "organization-manager"
+	RoleOrgMember  Role = "organization-member"
+	RoleOrgAdmin   Role = "organization-admin"
+	RoleOrgManager Role = "organization-manager"
 )
 
 // Organizations gets all Organizations
@@ -39,11 +36,51 @@ func (c Client) Organizations(ctx context.Context) ([]Organization, error) {
 	return orgs, nil
 }
 
-// OrgMembers get all members of the given organization
-func (c Client) OrgMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) {
+func (c Client) OrganizationByID(ctx context.Context, orgID string) (*Organization, error) {
+	var org Organization
+	err := c.requestBody(ctx, http.MethodGet, "/api/orgs/"+orgID, nil, &org)
+	if err != nil {
+		return nil, err
+	}
+	return &org, nil
+}
+
+// OrganizationMembers get all members of the given organization
+func (c Client) OrganizationMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) {
 	var members []OrganizationUser
 	if err := c.requestBody(ctx, http.MethodGet, "/api/orgs/"+orgID+"/members", nil, &members); err != nil {
 		return nil, err
 	}
 	return members, nil
 }
+
+type UpdateOrganizationReq struct {
+	Name                   *string   `json:"name"`
+	Description            *string   `json:"description"`
+	Default                *bool     `json:"default"`
+	AutoOffThreshold       *Duration `json:"auto_off_threshold"`
+	CPUProvisioningRate    *float32  `json:"cpu_provisioning_rate"`
+	MemoryProvisioningRate *float32  `json:"memory_provisioning_rate"`
+}
+
+func (c Client) UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationReq) error {
+	return c.requestBody(ctx, http.MethodPatch, "/api/orgs/"+orgID, req, nil)
+}
+
+type CreateOrganizationReq struct {
+	Name                   string   `json:"name"`
+	Description            string   `json:"description"`
+	Default                bool     `json:"default"`
+	ResourceNamespace      string   `json:"resource_namespace"`
+	AutoOffThreshold       Duration `json:"auto_off_threshold"`
+	CPUProvisioningRate    float32  `json:"cpu_provisioning_rate"`
+	MemoryProvisioningRate float32  `json:"memory_provisioning_rate"`
+}
+
+func (c Client) CreateOrganization(ctx context.Context, req CreateOrganizationReq) error {
+	return c.requestBody(ctx, http.MethodPost, "/api/orgs", req, nil)
+}
+
+func (c Client) DeleteOrganization(ctx context.Context, orgID string) error {
+	return c.requestBody(ctx, http.MethodDelete, "/api/orgs/"+orgID, nil, nil)
+}
diff --git a/coder-sdk/users.go b/coder-sdk/users.go
index e988a697..6117bace 100644
--- a/coder-sdk/users.go
+++ b/coder-sdk/users.go
@@ -8,14 +8,38 @@ import (
 
 // User describes a Coder user account.
 type User struct {
-	ID        string    `json:"id"         table:"-"`
-	Email     string    `json:"email"      table:"Email"`
-	Username  string    `json:"username"   table:"Username"`
-	Name      string    `json:"name"       table:"Name"`
-	CreatedAt time.Time `json:"created_at" table:"CreatedAt"`
-	UpdatedAt time.Time `json:"updated_at" table:"-"`
+	ID                string    `json:"id"                 table:"-"`
+	Email             string    `json:"email"              table:"Email"`
+	Username          string    `json:"username"           table:"Username"`
+	Name              string    `json:"name"               table:"Name"`
+	Roles             []Role    `json:"roles"              table:"-"`
+	TemporaryPassword bool      `json:"temporary_password" table:"-"`
+	LoginType         string    `json:"login_type"         table:"-"`
+	KeyRegeneratedAt  time.Time `json:"key_regenerated_at" table:"-"`
+	CreatedAt         time.Time `json:"created_at"         table:"CreatedAt"`
+	UpdatedAt         time.Time `json:"updated_at"         table:"-"`
 }
 
+type Role string
+
+type Roles []Role
+
+// Site Roles
+const (
+	SiteAdmin   Role = "site-admin"
+	SiteAuditor Role = "site-auditor"
+	SiteManager Role = "site-manager"
+	SiteMember  Role = "site-member"
+)
+
+type LoginType string
+
+const (
+	LoginTypeBuiltIn LoginType = "built-in"
+	LoginTypeSAML    LoginType = "saml"
+	LoginTypeOIDC    LoginType = "oidc"
+)
+
 // Me gets the details of the authenticated user.
 func (c Client) Me(ctx context.Context) (*User, error) {
 	return c.UserByID(ctx, Me)
@@ -70,3 +94,34 @@ func (c Client) UserByEmail(ctx context.Context, email string) (*User, error) {
 	}
 	return nil, ErrNotFound
 }
+
+// UpdateUserReq defines a modification to the user, updating the
+// value of all non-nil values.
+type UpdateUserReq struct {
+	// TODO(@cmoog) add update password option
+	Revoked        *bool      `json:"revoked,omitempty"`
+	Roles          *Roles     `json:"roles,omitempty"`
+	LoginType      *LoginType `json:"login_type,omitempty"`
+	Name           *string    `json:"name,omitempty"`
+	Username       *string    `json:"username,omitempty"`
+	Email          *string    `json:"email,omitempty"`
+	DotfilesGitURL *string    `json:"dotfiles_git_uri,omitempty"`
+}
+
+func (c Client) UpdateUser(ctx context.Context, userID string, req UpdateUserReq) error {
+	return c.requestBody(ctx, http.MethodPatch, "/api/users/"+userID, req, nil)
+}
+
+type CreateUserReq struct {
+	Name              string    `json:"name"`
+	Username          string    `json:"username"`
+	Email             string    `json:"email"`
+	Password          string    `json:"password"`
+	TemporaryPassword bool      `json:"temporary_password"`
+	LoginType         LoginType `json:"login_type"`
+	OrganizationsIDs  []string  `json:"organizations"`
+}
+
+func (c Client) CreateUser(ctx context.Context, req CreateUserReq) error {
+	return c.requestBody(ctx, http.MethodPost, "/api/users", req, nil)
+}
diff --git a/coder-sdk/util.go b/coder-sdk/util.go
new file mode 100644
index 00000000..882b3ced
--- /dev/null
+++ b/coder-sdk/util.go
@@ -0,0 +1,35 @@
+package coder
+
+import (
+	"encoding/json"
+	"strconv"
+	"time"
+)
+
+func String(s string) *string {
+	return &s
+}
+
+// Duration is a time.Duration wrapper that marshals to millisecond precision.
+// While it looses precision, most javascript applications expect durations to be in milliseconds.
+type Duration time.Duration
+
+// MarshalJSON marshals the duration to millisecond precision.
+func (d Duration) MarshalJSON() ([]byte, error) {
+	du := time.Duration(d)
+	return json.Marshal(du.Milliseconds())
+}
+
+// UnmarshalJSON unmarshals a millisecond-precision integer to
+// a time.Duration.
+func (d *Duration) UnmarshalJSON(b []byte) error {
+	i, err := strconv.ParseInt(string(b), 10, 64)
+	if err != nil {
+		return err
+	}
+
+	*d = Duration(time.Duration(i) * time.Millisecond)
+	return nil
+}
+
+func (d Duration) String() string { return time.Duration(d).String() }
diff --git a/internal/x/xjson/duration.go b/internal/x/xjson/duration.go
deleted file mode 100644
index 04ec1f17..00000000
--- a/internal/x/xjson/duration.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package xjson
-
-import (
-	"encoding/json"
-	"strconv"
-	"time"
-)
-
-// MSDuration is a time.MSDuration that marshals to millisecond precision.
-// While is looses precision, most javascript applications expect durations to be in milliseconds.
-type MSDuration time.Duration
-
-// MarshalJSON marshals the duration to millisecond precision.
-func (d MSDuration) MarshalJSON() ([]byte, error) {
-	du := time.Duration(d)
-	return json.Marshal(du.Milliseconds())
-}
-
-// UnmarshalJSON unmarshals a millisecond-precision integer to
-// a time.Duration.
-func (d *MSDuration) UnmarshalJSON(b []byte) error {
-	i, err := strconv.ParseInt(string(b), 10, 64)
-	if err != nil {
-		return err
-	}
-
-	*d = MSDuration(time.Duration(i) * time.Millisecond)
-	return nil
-}
-
-func (d MSDuration) String() string { return time.Duration(d).String() }
