mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
feat: Add templates to create working release (#422)
* Add templates
* Move API structs to codersdk
* Back to green tests!
* It all works, but now with tea! 🧋
* It works!
* Add cancellation to provisionerd
* Tests pass!
* Add deletion of workspaces and projects
* Fix agent lock
* Add clog
* Fix linting errors
* Remove unused CLI tests
* Rename daemon to start
* Fix leaking command
* Fix promptui test
* Update agent connection frequency
* Skip login tests on Windows
* Increase tunnel connect timeout
* Fix templater
* Lower test requirements
* Fix embed
* Disable promptui tests for Windows
* Fix write newline
* Fix PTY write newline
* Fix CloseReader
* Fix compilation on Windows
* Fix linting error
* Remove bubbletea
* Cleanup readwriter
* Use embedded templates instead of serving over API
* Move templates to examples
* Improve workspace create flow
* Fix Windows build
* Fix tests
* Fix linting errors
* Fix untar with extracting max size
* Fix newline char
This commit is contained in:
@ -6,28 +6,31 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
)
|
||||
|
||||
const (
|
||||
ContentTypeTar = "application/x-tar"
|
||||
)
|
||||
|
||||
// UploadResponse contains the hash to reference the uploaded file.
|
||||
type UploadResponse struct {
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
// Upload uploads an arbitrary file with the content type provided.
|
||||
// This is used to upload a source-code archive.
|
||||
func (c *Client) Upload(ctx context.Context, contentType string, content []byte) (coderd.UploadResponse, error) {
|
||||
func (c *Client) Upload(ctx context.Context, contentType string, content []byte) (UploadResponse, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/files", content, func(r *http.Request) {
|
||||
r.Header.Set("Content-Type", contentType)
|
||||
})
|
||||
if err != nil {
|
||||
return coderd.UploadResponse{}, err
|
||||
return UploadResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated && res.StatusCode != http.StatusOK {
|
||||
return coderd.UploadResponse{}, readBodyAsError(res)
|
||||
return UploadResponse{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.UploadResponse
|
||||
var resp UploadResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
|
@ -5,25 +5,63 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func (c *Client) Organization(ctx context.Context, id string) (coderd.Organization, error) {
|
||||
// Organization is the JSON representation of a Coder organization.
|
||||
type Organization struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
||||
}
|
||||
|
||||
// CreateProjectVersionRequest enables callers to create a new Project Version.
|
||||
type CreateProjectVersionRequest struct {
|
||||
// ProjectID optionally associates a version with a project.
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
|
||||
StorageMethod database.ProvisionerStorageMethod `json:"storage_method" validate:"oneof=file,required"`
|
||||
StorageSource string `json:"storage_source" validate:"required"`
|
||||
Provisioner database.ProvisionerType `json:"provisioner" validate:"oneof=terraform echo,required"`
|
||||
// ParameterValues allows for additional parameters to be provided
|
||||
// during the dry-run provision stage.
|
||||
ParameterValues []CreateParameterRequest `json:"parameter_values"`
|
||||
}
|
||||
|
||||
// CreateProjectRequest provides options when creating a project.
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name" validate:"username,required"`
|
||||
|
||||
// VersionID is an in-progress or completed job to use as
|
||||
// an initial version of the project.
|
||||
//
|
||||
// This is required on creation to enable a user-flow of validating a
|
||||
// project works. There is no reason the data-model cannot support
|
||||
// empty projects, but it doesn't make sense for users.
|
||||
VersionID uuid.UUID `json:"project_version_id" validate:"required"`
|
||||
ParameterValues []CreateParameterRequest `json:"parameter_values"`
|
||||
}
|
||||
|
||||
func (c *Client) Organization(ctx context.Context, id string) (Organization, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.Organization{}, err
|
||||
return Organization{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Organization{}, readBodyAsError(res)
|
||||
return Organization{}, readBodyAsError(res)
|
||||
}
|
||||
var organization coderd.Organization
|
||||
var organization Organization
|
||||
return organization, json.NewDecoder(res.Body).Decode(&organization)
|
||||
}
|
||||
|
||||
// ProvisionerDaemonsByOrganization returns provisioner daemons available for an organization.
|
||||
func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organization string) ([]coderd.ProvisionerDaemon, error) {
|
||||
func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organization string) ([]ProvisionerDaemon, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons", organization), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -32,41 +70,41 @@ func (c *Client) ProvisionerDaemonsByOrganization(ctx context.Context, organizat
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var daemons []coderd.ProvisionerDaemon
|
||||
var daemons []ProvisionerDaemon
|
||||
return daemons, json.NewDecoder(res.Body).Decode(&daemons)
|
||||
}
|
||||
|
||||
// CreateProjectVersion processes source-code and optionally associates the version with a project.
|
||||
// Executing without a project is useful for validating source-code.
|
||||
func (c *Client) CreateProjectVersion(ctx context.Context, organization string, req coderd.CreateProjectVersionRequest) (coderd.ProjectVersion, error) {
|
||||
func (c *Client) CreateProjectVersion(ctx context.Context, organization string, req CreateProjectVersionRequest) (ProjectVersion, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projectversions", organization), req)
|
||||
if err != nil {
|
||||
return coderd.ProjectVersion{}, err
|
||||
return ProjectVersion{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.ProjectVersion{}, readBodyAsError(res)
|
||||
return ProjectVersion{}, readBodyAsError(res)
|
||||
}
|
||||
var projectVersion coderd.ProjectVersion
|
||||
var projectVersion ProjectVersion
|
||||
return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion)
|
||||
}
|
||||
|
||||
// CreateProject creates a new project inside an organization.
|
||||
func (c *Client) CreateProject(ctx context.Context, organization string, request coderd.CreateProjectRequest) (coderd.Project, error) {
|
||||
func (c *Client) CreateProject(ctx context.Context, organization string, request CreateProjectRequest) (Project, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/projects", organization), request)
|
||||
if err != nil {
|
||||
return coderd.Project{}, err
|
||||
return Project{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.Project{}, readBodyAsError(res)
|
||||
return Project{}, readBodyAsError(res)
|
||||
}
|
||||
var project coderd.Project
|
||||
var project Project
|
||||
return project, json.NewDecoder(res.Body).Decode(&project)
|
||||
}
|
||||
|
||||
// ProjectsByOrganization lists all projects inside of an organization.
|
||||
func (c *Client) ProjectsByOrganization(ctx context.Context, organization string) ([]coderd.Project, error) {
|
||||
func (c *Client) ProjectsByOrganization(ctx context.Context, organization string) ([]Project, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/projects", organization), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -75,20 +113,20 @@ func (c *Client) ProjectsByOrganization(ctx context.Context, organization string
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var projects []coderd.Project
|
||||
var projects []Project
|
||||
return projects, json.NewDecoder(res.Body).Decode(&projects)
|
||||
}
|
||||
|
||||
// ProjectByName finds a project inside the organization provided with a case-insensitive name.
|
||||
func (c *Client) ProjectByName(ctx context.Context, organization, name string) (coderd.Project, error) {
|
||||
func (c *Client) ProjectByName(ctx context.Context, organization, name string) (Project, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/projects/%s", organization, name), nil)
|
||||
if err != nil {
|
||||
return coderd.Project{}, err
|
||||
return Project{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Project{}, readBodyAsError(res)
|
||||
return Project{}, readBodyAsError(res)
|
||||
}
|
||||
var project coderd.Project
|
||||
var project Project
|
||||
return project, json.NewDecoder(res.Body).Decode(&project)
|
||||
}
|
||||
|
@ -5,24 +5,56 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func (c *Client) CreateParameter(ctx context.Context, scope coderd.ParameterScope, id string, req coderd.CreateParameterRequest) (coderd.Parameter, error) {
|
||||
type ParameterScope string
|
||||
|
||||
const (
|
||||
ParameterOrganization ParameterScope = "organization"
|
||||
ParameterProject ParameterScope = "project"
|
||||
ParameterUser ParameterScope = "user"
|
||||
ParameterWorkspace ParameterScope = "workspace"
|
||||
)
|
||||
|
||||
// Parameter represents a set value for the scope.
|
||||
type Parameter struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Scope ParameterScope `db:"scope" json:"scope"`
|
||||
ScopeID string `db:"scope_id" json:"scope_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
SourceScheme database.ParameterSourceScheme `db:"source_scheme" json:"source_scheme"`
|
||||
DestinationScheme database.ParameterDestinationScheme `db:"destination_scheme" json:"destination_scheme"`
|
||||
}
|
||||
|
||||
// CreateParameterRequest is used to create a new parameter value for a scope.
|
||||
type CreateParameterRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
SourceValue string `json:"source_value" validate:"required"`
|
||||
SourceScheme database.ParameterSourceScheme `json:"source_scheme" validate:"oneof=data,required"`
|
||||
DestinationScheme database.ParameterDestinationScheme `json:"destination_scheme" validate:"oneof=environment_variable provisioner_variable,required"`
|
||||
}
|
||||
|
||||
func (c *Client) CreateParameter(ctx context.Context, scope ParameterScope, id string, req CreateParameterRequest) (Parameter, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/parameters/%s/%s", scope, id), req)
|
||||
if err != nil {
|
||||
return coderd.Parameter{}, err
|
||||
return Parameter{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.Parameter{}, readBodyAsError(res)
|
||||
return Parameter{}, readBodyAsError(res)
|
||||
}
|
||||
var param coderd.Parameter
|
||||
var param Parameter
|
||||
return param, json.NewDecoder(res.Body).Decode(¶m)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteParameter(ctx context.Context, scope coderd.ParameterScope, id, name string) error {
|
||||
func (c *Client) DeleteParameter(ctx context.Context, scope ParameterScope, id, name string) error {
|
||||
res, err := c.request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/parameters/%s/%s/%s", scope, id, name), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -34,7 +66,7 @@ func (c *Client) DeleteParameter(ctx context.Context, scope coderd.ParameterScop
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Parameters(ctx context.Context, scope coderd.ParameterScope, id string) ([]coderd.Parameter, error) {
|
||||
func (c *Client) Parameters(ctx context.Context, scope ParameterScope, id string) ([]Parameter, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/parameters/%s/%s", scope, id), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -43,6 +75,6 @@ func (c *Client) Parameters(ctx context.Context, scope coderd.ParameterScope, id
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var parameters []coderd.Parameter
|
||||
var parameters []Parameter
|
||||
return parameters, json.NewDecoder(res.Body).Decode(¶meters)
|
||||
}
|
||||
|
@ -5,28 +5,73 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
// Project is the JSON representation of a Coder project.
|
||||
// This type matches the database object for now, but is
|
||||
// abstracted for ease of change later on.
|
||||
type Project struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
OrganizationID string `json:"organization_id"`
|
||||
Name string `json:"name"`
|
||||
Provisioner database.ProvisionerType `json:"provisioner"`
|
||||
ActiveVersionID uuid.UUID `json:"active_version_id"`
|
||||
WorkspaceOwnerCount uint32 `json:"workspace_owner_count"`
|
||||
}
|
||||
|
||||
type UpdateActiveProjectVersion struct {
|
||||
ID uuid.UUID `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
// Project returns a single project.
|
||||
func (c *Client) Project(ctx context.Context, project uuid.UUID) (coderd.Project, error) {
|
||||
func (c *Client) Project(ctx context.Context, project uuid.UUID) (Project, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s", project), nil)
|
||||
if err != nil {
|
||||
return coderd.Project{}, nil
|
||||
return Project{}, nil
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Project{}, readBodyAsError(res)
|
||||
return Project{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.Project
|
||||
var resp Project
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteProject(ctx context.Context, project uuid.UUID) error {
|
||||
res, err := c.request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/projects/%s", project), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return readBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateActiveProjectVersion updates the active project version to the ID provided.
|
||||
// The project version must be attached to the project.
|
||||
func (c *Client) UpdateActiveProjectVersion(ctx context.Context, project uuid.UUID, req UpdateActiveProjectVersion) error {
|
||||
res, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/projects/%s/versions", project), req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return readBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProjectVersionsByProject lists versions associated with a project.
|
||||
func (c *Client) ProjectVersionsByProject(ctx context.Context, project uuid.UUID) ([]coderd.ProjectVersion, error) {
|
||||
func (c *Client) ProjectVersionsByProject(ctx context.Context, project uuid.UUID) ([]ProjectVersion, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/versions", project), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -35,21 +80,21 @@ func (c *Client) ProjectVersionsByProject(ctx context.Context, project uuid.UUID
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var projectVersion []coderd.ProjectVersion
|
||||
var projectVersion []ProjectVersion
|
||||
return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion)
|
||||
}
|
||||
|
||||
// ProjectVersionByName returns a project version by it's friendly name.
|
||||
// This is used for path-based routing. Like: /projects/example/versions/helloworld
|
||||
func (c *Client) ProjectVersionByName(ctx context.Context, project uuid.UUID, name string) (coderd.ProjectVersion, error) {
|
||||
func (c *Client) ProjectVersionByName(ctx context.Context, project uuid.UUID, name string) (ProjectVersion, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projects/%s/versions/%s", project, name), nil)
|
||||
if err != nil {
|
||||
return coderd.ProjectVersion{}, err
|
||||
return ProjectVersion{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.ProjectVersion{}, readBodyAsError(res)
|
||||
return ProjectVersion{}, readBodyAsError(res)
|
||||
}
|
||||
var projectVersion coderd.ProjectVersion
|
||||
var projectVersion ProjectVersion
|
||||
return projectVersion, json.NewDecoder(res.Body).Decode(&projectVersion)
|
||||
}
|
||||
|
@ -9,25 +9,55 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/parameter"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
// ProjectVersion represents a single version of a project.
|
||||
type ProjectVersion struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Name string `json:"name"`
|
||||
Job ProvisionerJob `json:"job"`
|
||||
}
|
||||
|
||||
// ProjectVersionParameterSchema represents a parameter parsed from project version source.
|
||||
type ProjectVersionParameterSchema database.ParameterSchema
|
||||
|
||||
// ProjectVersionParameter represents a computed parameter value.
|
||||
type ProjectVersionParameter parameter.ComputedValue
|
||||
|
||||
// ProjectVersion returns a project version by ID.
|
||||
func (c *Client) ProjectVersion(ctx context.Context, id uuid.UUID) (coderd.ProjectVersion, error) {
|
||||
func (c *Client) ProjectVersion(ctx context.Context, id uuid.UUID) (ProjectVersion, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.ProjectVersion{}, err
|
||||
return ProjectVersion{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.ProjectVersion{}, readBodyAsError(res)
|
||||
return ProjectVersion{}, readBodyAsError(res)
|
||||
}
|
||||
var version coderd.ProjectVersion
|
||||
var version ProjectVersion
|
||||
return version, json.NewDecoder(res.Body).Decode(&version)
|
||||
}
|
||||
|
||||
// CancelProjectVersion marks a project version job as canceled.
|
||||
func (c *Client) CancelProjectVersion(ctx context.Context, version uuid.UUID) error {
|
||||
res, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/projectversions/%s/cancel", version), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return readBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProjectVersionSchema returns schemas for a project version by ID.
|
||||
func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([]coderd.ProjectVersionParameterSchema, error) {
|
||||
func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([]ProjectVersionParameterSchema, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/schema", version), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -36,12 +66,12 @@ func (c *Client) ProjectVersionSchema(ctx context.Context, version uuid.UUID) ([
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var params []coderd.ProjectVersionParameterSchema
|
||||
var params []ProjectVersionParameterSchema
|
||||
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
||||
}
|
||||
|
||||
// ProjectVersionParameters returns computed parameters for a project version.
|
||||
func (c *Client) ProjectVersionParameters(ctx context.Context, version uuid.UUID) ([]coderd.ProjectVersionParameter, error) {
|
||||
func (c *Client) ProjectVersionParameters(ctx context.Context, version uuid.UUID) ([]ProjectVersionParameter, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/parameters", version), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -50,12 +80,12 @@ func (c *Client) ProjectVersionParameters(ctx context.Context, version uuid.UUID
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var params []coderd.ProjectVersionParameter
|
||||
var params []ProjectVersionParameter
|
||||
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
||||
}
|
||||
|
||||
// ProjectVersionResources returns resources a project version declares.
|
||||
func (c *Client) ProjectVersionResources(ctx context.Context, version uuid.UUID) ([]coderd.WorkspaceResource, error) {
|
||||
func (c *Client) ProjectVersionResources(ctx context.Context, version uuid.UUID) ([]WorkspaceResource, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/projectversions/%s/resources", version), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -64,16 +94,16 @@ func (c *Client) ProjectVersionResources(ctx context.Context, version uuid.UUID)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var resources []coderd.WorkspaceResource
|
||||
var resources []WorkspaceResource
|
||||
return resources, json.NewDecoder(res.Body).Decode(&resources)
|
||||
}
|
||||
|
||||
// ProjectVersionLogsBefore returns logs that occurred before a specific time.
|
||||
func (c *Client) ProjectVersionLogsBefore(ctx context.Context, version uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) {
|
||||
func (c *Client) ProjectVersionLogsBefore(ctx context.Context, version uuid.UUID, before time.Time) ([]ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/projectversions/%s/logs", version), before)
|
||||
}
|
||||
|
||||
// ProjectVersionLogsAfter streams logs for a project version that occurred after a specific time.
|
||||
func (c *Client) ProjectVersionLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
|
||||
func (c *Client) ProjectVersionLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/projectversions/%s/logs", version), after)
|
||||
}
|
||||
|
@ -10,15 +10,48 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/yamux"
|
||||
"golang.org/x/xerrors"
|
||||
"nhooyr.io/websocket"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/provisionerd/proto"
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
type ProvisionerDaemon database.ProvisionerDaemon
|
||||
|
||||
// ProvisionerJobStaus represents the at-time state of a job.
|
||||
type ProvisionerJobStatus string
|
||||
|
||||
const (
|
||||
ProvisionerJobPending ProvisionerJobStatus = "pending"
|
||||
ProvisionerJobRunning ProvisionerJobStatus = "running"
|
||||
ProvisionerJobSucceeded ProvisionerJobStatus = "succeeded"
|
||||
ProvisionerJobCanceling ProvisionerJobStatus = "canceling"
|
||||
ProvisionerJobCanceled ProvisionerJobStatus = "canceled"
|
||||
ProvisionerJobFailed ProvisionerJobStatus = "failed"
|
||||
)
|
||||
|
||||
type ProvisionerJob struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Status ProvisionerJobStatus `json:"status"`
|
||||
WorkerID *uuid.UUID `json:"worker_id,omitempty"`
|
||||
}
|
||||
|
||||
type ProvisionerJobLog struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Source database.LogSource `json:"log_source"`
|
||||
Level database.LogLevel `json:"log_level"`
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
// ListenProvisionerDaemon returns the gRPC service for a provisioner daemon implementation.
|
||||
func (c *Client) ListenProvisionerDaemon(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
|
||||
serverURL, err := c.URL.Parse("/api/v2/provisionerdaemons/me/listen")
|
||||
@ -48,7 +81,7 @@ func (c *Client) ListenProvisionerDaemon(ctx context.Context) (proto.DRPCProvisi
|
||||
// provisionerJobLogsBefore provides log output that occurred before a time.
|
||||
// This is abstracted from a specific job type to provide consistency between
|
||||
// APIs. Logs is the only shared route between jobs.
|
||||
func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, before time.Time) ([]coderd.ProvisionerJobLog, error) {
|
||||
func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, before time.Time) ([]ProvisionerJobLog, error) {
|
||||
values := url.Values{}
|
||||
if !before.IsZero() {
|
||||
values["before"] = []string{strconv.FormatInt(before.UTC().UnixMilli(), 10)}
|
||||
@ -62,12 +95,12 @@ func (c *Client) provisionerJobLogsBefore(ctx context.Context, path string, befo
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var logs []coderd.ProvisionerJobLog
|
||||
var logs []ProvisionerJobLog
|
||||
return logs, json.NewDecoder(res.Body).Decode(&logs)
|
||||
}
|
||||
|
||||
// provisionerJobLogsAfter streams logs that occurred after a specific time.
|
||||
func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
|
||||
func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after time.Time) (<-chan ProvisionerJobLog, error) {
|
||||
afterQuery := ""
|
||||
if !after.IsZero() {
|
||||
afterQuery = fmt.Sprintf("&after=%d", after.UTC().UnixMilli())
|
||||
@ -81,11 +114,11 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
|
||||
logs := make(chan coderd.ProvisionerJobLog)
|
||||
logs := make(chan ProvisionerJobLog)
|
||||
decoder := json.NewDecoder(res.Body)
|
||||
go func() {
|
||||
defer close(logs)
|
||||
var log coderd.ProvisionerJobLog
|
||||
var log ProvisionerJobLog
|
||||
for {
|
||||
err = decoder.Decode(&log)
|
||||
if err != nil {
|
||||
|
@ -5,10 +5,68 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// User represents a user in Coder.
|
||||
type User struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
}
|
||||
|
||||
type CreateFirstUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Organization string `json:"organization" validate:"required,username"`
|
||||
}
|
||||
|
||||
// CreateFirstUserResponse contains IDs for newly created user info.
|
||||
type CreateFirstUserResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
OrganizationID string `json:"organization_id"`
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
OrganizationID string `json:"organization_id" validate:"required"`
|
||||
}
|
||||
|
||||
// LoginWithPasswordRequest enables callers to authenticate with email and password.
|
||||
type LoginWithPasswordRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
// LoginWithPasswordResponse contains a session token for the newly authenticated user.
|
||||
type LoginWithPasswordResponse struct {
|
||||
SessionToken string `json:"session_token" validate:"required"`
|
||||
}
|
||||
|
||||
// GenerateAPIKeyResponse contains an API key for a user.
|
||||
type GenerateAPIKeyResponse struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type CreateOrganizationRequest struct {
|
||||
Name string `json:"name" validate:"required,username"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceRequest provides options for creating a new workspace.
|
||||
type CreateWorkspaceRequest struct {
|
||||
ProjectID uuid.UUID `json:"project_id" validate:"required"`
|
||||
Name string `json:"name" validate:"username,required"`
|
||||
// ParameterValues allows for additional parameters to be provided
|
||||
// during the initial provision.
|
||||
ParameterValues []CreateParameterRequest `json:"parameter_values"`
|
||||
}
|
||||
|
||||
// HasFirstUser returns whether the first user has been created.
|
||||
func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, "/api/v2/users/first", nil)
|
||||
@ -27,35 +85,35 @@ func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
|
||||
|
||||
// CreateFirstUser attempts to create the first user on a Coder deployment.
|
||||
// This initial user has superadmin privileges. If >0 users exist, this request will fail.
|
||||
func (c *Client) CreateFirstUser(ctx context.Context, req coderd.CreateFirstUserRequest) (coderd.CreateFirstUserResponse, error) {
|
||||
func (c *Client) CreateFirstUser(ctx context.Context, req CreateFirstUserRequest) (CreateFirstUserResponse, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/users/first", req)
|
||||
if err != nil {
|
||||
return coderd.CreateFirstUserResponse{}, err
|
||||
return CreateFirstUserResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.CreateFirstUserResponse{}, readBodyAsError(res)
|
||||
return CreateFirstUserResponse{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.CreateFirstUserResponse
|
||||
var resp CreateFirstUserResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// CreateUser creates a new user.
|
||||
func (c *Client) CreateUser(ctx context.Context, req coderd.CreateUserRequest) (coderd.User, error) {
|
||||
func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/users", req)
|
||||
if err != nil {
|
||||
return coderd.User{}, err
|
||||
return User{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.User{}, readBodyAsError(res)
|
||||
return User{}, readBodyAsError(res)
|
||||
}
|
||||
var user coderd.User
|
||||
var user User
|
||||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// CreateAPIKey generates an API key for the user ID provided.
|
||||
func (c *Client) CreateAPIKey(ctx context.Context, id string) (*coderd.GenerateAPIKeyResponse, error) {
|
||||
func (c *Client) CreateAPIKey(ctx context.Context, id string) (*GenerateAPIKeyResponse, error) {
|
||||
if id == "" {
|
||||
id = "me"
|
||||
}
|
||||
@ -67,25 +125,25 @@ func (c *Client) CreateAPIKey(ctx context.Context, id string) (*coderd.GenerateA
|
||||
if res.StatusCode > http.StatusCreated {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
apiKey := &coderd.GenerateAPIKeyResponse{}
|
||||
apiKey := &GenerateAPIKeyResponse{}
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
||||
}
|
||||
|
||||
// LoginWithPassword creates a session token authenticating with an email and password.
|
||||
// Call `SetSessionToken()` to apply the newly acquired token to the client.
|
||||
func (c *Client) LoginWithPassword(ctx context.Context, req coderd.LoginWithPasswordRequest) (coderd.LoginWithPasswordResponse, error) {
|
||||
func (c *Client) LoginWithPassword(ctx context.Context, req LoginWithPasswordRequest) (LoginWithPasswordResponse, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/users/login", req)
|
||||
if err != nil {
|
||||
return coderd.LoginWithPasswordResponse{}, err
|
||||
return LoginWithPasswordResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.LoginWithPasswordResponse{}, readBodyAsError(res)
|
||||
return LoginWithPasswordResponse{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.LoginWithPasswordResponse
|
||||
var resp LoginWithPasswordResponse
|
||||
err = json.NewDecoder(res.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
return coderd.LoginWithPasswordResponse{}, err
|
||||
return LoginWithPasswordResponse{}, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@ -105,24 +163,24 @@ func (c *Client) Logout(ctx context.Context) error {
|
||||
|
||||
// User returns a user for the ID provided.
|
||||
// If the ID string is empty, the current user will be returned.
|
||||
func (c *Client) User(ctx context.Context, id string) (coderd.User, error) {
|
||||
func (c *Client) User(ctx context.Context, id string) (User, error) {
|
||||
if id == "" {
|
||||
id = "me"
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.User{}, err
|
||||
return User{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusOK {
|
||||
return coderd.User{}, readBodyAsError(res)
|
||||
return User{}, readBodyAsError(res)
|
||||
}
|
||||
var user coderd.User
|
||||
var user User
|
||||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// OrganizationsByUser returns all organizations the user is a member of.
|
||||
func (c *Client) OrganizationsByUser(ctx context.Context, id string) ([]coderd.Organization, error) {
|
||||
func (c *Client) OrganizationsByUser(ctx context.Context, id string) ([]Organization, error) {
|
||||
if id == "" {
|
||||
id = "me"
|
||||
}
|
||||
@ -134,62 +192,62 @@ func (c *Client) OrganizationsByUser(ctx context.Context, id string) ([]coderd.O
|
||||
if res.StatusCode > http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var orgs []coderd.Organization
|
||||
var orgs []Organization
|
||||
return orgs, json.NewDecoder(res.Body).Decode(&orgs)
|
||||
}
|
||||
|
||||
func (c *Client) OrganizationByName(ctx context.Context, user, name string) (coderd.Organization, error) {
|
||||
func (c *Client) OrganizationByName(ctx context.Context, user, name string) (Organization, error) {
|
||||
if user == "" {
|
||||
user = "me"
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", user, name), nil)
|
||||
if err != nil {
|
||||
return coderd.Organization{}, err
|
||||
return Organization{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Organization{}, readBodyAsError(res)
|
||||
return Organization{}, readBodyAsError(res)
|
||||
}
|
||||
var org coderd.Organization
|
||||
var org Organization
|
||||
return org, json.NewDecoder(res.Body).Decode(&org)
|
||||
}
|
||||
|
||||
// CreateOrganization creates an organization and adds the provided user as an admin.
|
||||
func (c *Client) CreateOrganization(ctx context.Context, user string, req coderd.CreateOrganizationRequest) (coderd.Organization, error) {
|
||||
func (c *Client) CreateOrganization(ctx context.Context, user string, req CreateOrganizationRequest) (Organization, error) {
|
||||
if user == "" {
|
||||
user = "me"
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/organizations", user), req)
|
||||
if err != nil {
|
||||
return coderd.Organization{}, err
|
||||
return Organization{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.Organization{}, readBodyAsError(res)
|
||||
return Organization{}, readBodyAsError(res)
|
||||
}
|
||||
var org coderd.Organization
|
||||
var org Organization
|
||||
return org, json.NewDecoder(res.Body).Decode(&org)
|
||||
}
|
||||
|
||||
// CreateWorkspace creates a new workspace for the project specified.
|
||||
func (c *Client) CreateWorkspace(ctx context.Context, user string, request coderd.CreateWorkspaceRequest) (coderd.Workspace, error) {
|
||||
func (c *Client) CreateWorkspace(ctx context.Context, user string, request CreateWorkspaceRequest) (Workspace, error) {
|
||||
if user == "" {
|
||||
user = "me"
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/workspaces", user), request)
|
||||
if err != nil {
|
||||
return coderd.Workspace{}, err
|
||||
return Workspace{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.Workspace{}, readBodyAsError(res)
|
||||
return Workspace{}, readBodyAsError(res)
|
||||
}
|
||||
var workspace coderd.Workspace
|
||||
var workspace Workspace
|
||||
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
|
||||
}
|
||||
|
||||
// WorkspacesByUser returns all workspaces the specified user has access to.
|
||||
func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]coderd.Workspace, error) {
|
||||
func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]Workspace, error) {
|
||||
if user == "" {
|
||||
user = "me"
|
||||
}
|
||||
@ -201,22 +259,22 @@ func (c *Client) WorkspacesByUser(ctx context.Context, user string) ([]coderd.Wo
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var workspaces []coderd.Workspace
|
||||
var workspaces []Workspace
|
||||
return workspaces, json.NewDecoder(res.Body).Decode(&workspaces)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceByName(ctx context.Context, user, name string) (coderd.Workspace, error) {
|
||||
func (c *Client) WorkspaceByName(ctx context.Context, user, name string) (Workspace, error) {
|
||||
if user == "" {
|
||||
user = "me"
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspaces/%s", user, name), nil)
|
||||
if err != nil {
|
||||
return coderd.Workspace{}, err
|
||||
return Workspace{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Workspace{}, readBodyAsError(res)
|
||||
return Workspace{}, readBodyAsError(res)
|
||||
}
|
||||
var workspace coderd.Workspace
|
||||
var workspace Workspace
|
||||
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
|
||||
}
|
||||
|
@ -9,26 +9,55 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
// WorkspaceBuild is an at-point representation of a workspace state.
|
||||
// Iterate on before/after to determine a chronological history.
|
||||
type WorkspaceBuild struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
WorkspaceID uuid.UUID `json:"workspace_id"`
|
||||
ProjectVersionID uuid.UUID `json:"project_version_id"`
|
||||
BeforeID uuid.UUID `json:"before_id"`
|
||||
AfterID uuid.UUID `json:"after_id"`
|
||||
Name string `json:"name"`
|
||||
Transition database.WorkspaceTransition `json:"transition"`
|
||||
Initiator string `json:"initiator"`
|
||||
Job ProvisionerJob `json:"job"`
|
||||
}
|
||||
|
||||
// WorkspaceBuild returns a single workspace build for a workspace.
|
||||
// If history is "", the latest version is returned.
|
||||
func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (coderd.WorkspaceBuild, error) {
|
||||
func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.WorkspaceBuild{}, err
|
||||
return WorkspaceBuild{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.WorkspaceBuild{}, readBodyAsError(res)
|
||||
return WorkspaceBuild{}, readBodyAsError(res)
|
||||
}
|
||||
var workspaceBuild coderd.WorkspaceBuild
|
||||
var workspaceBuild WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
||||
// CancelWorkspaceBuild marks a workspace build job as canceled.
|
||||
func (c *Client) CancelWorkspaceBuild(ctx context.Context, id uuid.UUID) error {
|
||||
res, err := c.request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/workspacebuilds/%s/cancel", id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return readBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WorkspaceResourcesByBuild returns resources for a workspace build.
|
||||
func (c *Client) WorkspaceResourcesByBuild(ctx context.Context, build uuid.UUID) ([]coderd.WorkspaceResource, error) {
|
||||
func (c *Client) WorkspaceResourcesByBuild(ctx context.Context, build uuid.UUID) ([]WorkspaceResource, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspacebuilds/%s/resources", build), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -37,16 +66,16 @@ func (c *Client) WorkspaceResourcesByBuild(ctx context.Context, build uuid.UUID)
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var resources []coderd.WorkspaceResource
|
||||
var resources []WorkspaceResource
|
||||
return resources, json.NewDecoder(res.Body).Decode(&resources)
|
||||
}
|
||||
|
||||
// WorkspaceBuildLogsBefore returns logs that occurred before a specific time.
|
||||
func (c *Client) WorkspaceBuildLogsBefore(ctx context.Context, version uuid.UUID, before time.Time) ([]coderd.ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", version), before)
|
||||
func (c *Client) WorkspaceBuildLogsBefore(ctx context.Context, build uuid.UUID, before time.Time) ([]ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsBefore(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", build), before)
|
||||
}
|
||||
|
||||
// WorkspaceBuildLogsAfter streams logs for a workspace build that occurred after a specific time.
|
||||
func (c *Client) WorkspaceBuildLogsAfter(ctx context.Context, version uuid.UUID, after time.Time) (<-chan coderd.ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", version), after)
|
||||
func (c *Client) WorkspaceBuildLogsAfter(ctx context.Context, build uuid.UUID, after time.Time) (<-chan ProvisionerJobLog, error) {
|
||||
return c.provisionerJobLogsAfter(ctx, fmt.Sprintf("/api/v2/workspacebuilds/%s/logs", build), after)
|
||||
}
|
||||
|
@ -8,15 +8,23 @@ import (
|
||||
|
||||
"cloud.google.com/go/compute/metadata"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
)
|
||||
|
||||
type GoogleInstanceIdentityToken struct {
|
||||
JSONWebToken string `json:"json_web_token" validate:"required"`
|
||||
}
|
||||
|
||||
// WorkspaceAgentAuthenticateResponse is returned when an instance ID
|
||||
// has been exchanged for a session token.
|
||||
type WorkspaceAgentAuthenticateResponse struct {
|
||||
SessionToken string `json:"session_token"`
|
||||
}
|
||||
|
||||
// AuthWorkspaceGoogleInstanceIdentity uses the Google Compute Engine Metadata API to
|
||||
// fetch a signed JWT, and exchange it for a session token for a workspace agent.
|
||||
//
|
||||
// The requesting instance must be registered as a resource in the latest history for a workspace.
|
||||
func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (coderd.WorkspaceAgentAuthenticateResponse, error) {
|
||||
func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, serviceAccount string, gcpClient *metadata.Client) (WorkspaceAgentAuthenticateResponse, error) {
|
||||
if serviceAccount == "" {
|
||||
// This is the default name specified by Google.
|
||||
serviceAccount = "default"
|
||||
@ -27,18 +35,18 @@ func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, servic
|
||||
// "format=full" is required, otherwise the responding payload will be missing "instance_id".
|
||||
jwt, err := gcpClient.Get(fmt.Sprintf("instance/service-accounts/%s/identity?audience=coder&format=full", serviceAccount))
|
||||
if err != nil {
|
||||
return coderd.WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err)
|
||||
return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err)
|
||||
}
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceresources/auth/google-instance-identity", coderd.GoogleInstanceIdentityToken{
|
||||
res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceresources/auth/google-instance-identity", GoogleInstanceIdentityToken{
|
||||
JSONWebToken: jwt,
|
||||
})
|
||||
if err != nil {
|
||||
return coderd.WorkspaceAgentAuthenticateResponse{}, err
|
||||
return WorkspaceAgentAuthenticateResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res)
|
||||
return WorkspaceAgentAuthenticateResponse{}, readBodyAsError(res)
|
||||
}
|
||||
var resp coderd.WorkspaceAgentAuthenticateResponse
|
||||
var resp WorkspaceAgentAuthenticateResponse
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
@ -7,13 +7,15 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/yamux"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"golang.org/x/xerrors"
|
||||
"nhooyr.io/websocket"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpmw"
|
||||
"github.com/coder/coder/peer"
|
||||
"github.com/coder/coder/peerbroker"
|
||||
@ -21,16 +23,69 @@ import (
|
||||
"github.com/coder/coder/provisionersdk"
|
||||
)
|
||||
|
||||
func (c *Client) WorkspaceResource(ctx context.Context, id uuid.UUID) (coderd.WorkspaceResource, error) {
|
||||
type WorkspaceAgentStatus string
|
||||
|
||||
const (
|
||||
WorkspaceAgentWaiting WorkspaceAgentStatus = "waiting"
|
||||
WorkspaceAgentConnected WorkspaceAgentStatus = "connected"
|
||||
WorkspaceAgentDisconnected WorkspaceAgentStatus = "disconnected"
|
||||
)
|
||||
|
||||
type WorkspaceResource struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
Transition database.WorkspaceTransition `json:"workspace_transition"`
|
||||
Address string `json:"address"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Agent *WorkspaceAgent `json:"agent,omitempty"`
|
||||
}
|
||||
|
||||
type WorkspaceAgent struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
FirstConnectedAt *time.Time `json:"first_connected_at,omitempty"`
|
||||
LastConnectedAt *time.Time `json:"last_connected_at,omitempty"`
|
||||
DisconnectedAt *time.Time `json:"disconnected_at,omitempty"`
|
||||
Status WorkspaceAgentStatus `json:"status"`
|
||||
ResourceID uuid.UUID `json:"resource_id"`
|
||||
InstanceID string `json:"instance_id,omitempty"`
|
||||
EnvironmentVariables map[string]string `json:"environment_variables"`
|
||||
StartupScript string `json:"startup_script,omitempty"`
|
||||
}
|
||||
|
||||
type WorkspaceAgentResourceMetadata struct {
|
||||
MemoryTotal uint64 `json:"memory_total"`
|
||||
DiskTotal uint64 `json:"disk_total"`
|
||||
CPUCores uint64 `json:"cpu_cores"`
|
||||
CPUModel string `json:"cpu_model"`
|
||||
CPUMhz float64 `json:"cpu_mhz"`
|
||||
}
|
||||
|
||||
type WorkspaceAgentInstanceMetadata struct {
|
||||
JailOrchestrator string `json:"jail_orchestrator"`
|
||||
OperatingSystem string `json:"operating_system"`
|
||||
Platform string `json:"platform"`
|
||||
PlatformFamily string `json:"platform_family"`
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
KernelArchitecture string `json:"kernel_architecture"`
|
||||
Cloud string `json:"cloud"`
|
||||
Jail string `json:"jail"`
|
||||
VNC bool `json:"vnc"`
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceResource(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceresources/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.WorkspaceResource{}, err
|
||||
return WorkspaceResource{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.WorkspaceResource{}, readBodyAsError(res)
|
||||
return WorkspaceResource{}, readBodyAsError(res)
|
||||
}
|
||||
var resource coderd.WorkspaceResource
|
||||
var resource WorkspaceResource
|
||||
return resource, json.NewDecoder(res.Body).Decode(&resource)
|
||||
}
|
||||
|
||||
@ -106,5 +161,9 @@ func (c *Client) ListenWorkspaceAgent(ctx context.Context, opts *peer.ConnOption
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("multiplex client: %w", err)
|
||||
}
|
||||
return peerbroker.Listen(session, nil, opts)
|
||||
return peerbroker.Listen(session, func(ctx context.Context) ([]webrtc.ICEServer, error) {
|
||||
return []webrtc.ICEServer{{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
}}, nil
|
||||
}, opts)
|
||||
}
|
||||
|
@ -5,27 +5,49 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
// Workspace is a per-user deployment of a project. It tracks
|
||||
// project versions, and can be updated.
|
||||
type Workspace struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
LatestBuild WorkspaceBuild `json:"latest_build"`
|
||||
Outdated bool `json:"outdated"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
|
||||
type CreateWorkspaceBuildRequest struct {
|
||||
ProjectVersionID uuid.UUID `json:"project_version_id"`
|
||||
Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
}
|
||||
|
||||
// Workspace returns a single workspace.
|
||||
func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (coderd.Workspace, error) {
|
||||
func (c *Client) Workspace(ctx context.Context, id uuid.UUID) (Workspace, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s", id), nil)
|
||||
if err != nil {
|
||||
return coderd.Workspace{}, err
|
||||
return Workspace{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.Workspace{}, readBodyAsError(res)
|
||||
return Workspace{}, readBodyAsError(res)
|
||||
}
|
||||
var workspace coderd.Workspace
|
||||
var workspace Workspace
|
||||
return workspace, json.NewDecoder(res.Body).Decode(&workspace)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceBuilds(ctx context.Context, workspace uuid.UUID) ([]coderd.WorkspaceBuild, error) {
|
||||
func (c *Client) WorkspaceBuilds(ctx context.Context, workspace uuid.UUID) ([]WorkspaceBuild, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -34,46 +56,33 @@ func (c *Client) WorkspaceBuilds(ctx context.Context, workspace uuid.UUID) ([]co
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var workspaceBuild []coderd.WorkspaceBuild
|
||||
var workspaceBuild []WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
||||
// CreateWorkspaceBuild queues a new build to occur for a workspace.
|
||||
func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID, request coderd.CreateWorkspaceBuildRequest) (coderd.WorkspaceBuild, error) {
|
||||
func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID, request CreateWorkspaceBuildRequest) (WorkspaceBuild, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspace), request)
|
||||
if err != nil {
|
||||
return coderd.WorkspaceBuild{}, err
|
||||
return WorkspaceBuild{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return coderd.WorkspaceBuild{}, readBodyAsError(res)
|
||||
return WorkspaceBuild{}, readBodyAsError(res)
|
||||
}
|
||||
var workspaceBuild coderd.WorkspaceBuild
|
||||
var workspaceBuild WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID, name string) (coderd.WorkspaceBuild, error) {
|
||||
func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID, name string) (WorkspaceBuild, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds/%s", workspace, name), nil)
|
||||
if err != nil {
|
||||
return coderd.WorkspaceBuild{}, err
|
||||
return WorkspaceBuild{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.WorkspaceBuild{}, readBodyAsError(res)
|
||||
return WorkspaceBuild{}, readBodyAsError(res)
|
||||
}
|
||||
var workspaceBuild coderd.WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceBuildLatest(ctx context.Context, workspace uuid.UUID) (coderd.WorkspaceBuild, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds/latest", workspace), nil)
|
||||
if err != nil {
|
||||
return coderd.WorkspaceBuild{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return coderd.WorkspaceBuild{}, readBodyAsError(res)
|
||||
}
|
||||
var workspaceBuild coderd.WorkspaceBuild
|
||||
var workspaceBuild WorkspaceBuild
|
||||
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
|
||||
}
|
||||
|
Reference in New Issue
Block a user