diff --git a/coder-sdk/env.go b/coder-sdk/env.go index 8678344a..d28e6fb7 100644 --- a/coder-sdk/env.go +++ b/coder-sdk/env.go @@ -2,6 +2,7 @@ package coder import ( "context" + "io" "net/http" "net/url" "time" @@ -73,17 +74,16 @@ const ( // CreateEnvironmentRequest is used to configure a new environment. type CreateEnvironmentRequest struct { - Name string `json:"name"` - ImageID string `json:"image_id"` - OrgID string `json:"org_id"` - ImageTag string `json:"image_tag"` - CPUCores float32 `json:"cpu_cores"` - MemoryGB float32 `json:"memory_gb"` - DiskGB int `json:"disk_gb"` - GPUs int `json:"gpus"` - Services []string `json:"services"` - UseContainerVM bool `json:"use_container_vm"` - Template *Template `json:"template"` + Name string `json:"name"` + ImageID string `json:"image_id"` + OrgID string `json:"org_id"` + ImageTag string `json:"image_tag"` + CPUCores float32 `json:"cpu_cores"` + MemoryGB float32 `json:"memory_gb"` + DiskGB int `json:"disk_gb"` + GPUs int `json:"gpus"` + Services []string `json:"services"` + UseContainerVM bool `json:"use_container_vm"` } // CreateEnvironment sends a request to create an environment. @@ -95,14 +95,60 @@ func (c Client) CreateEnvironment(ctx context.Context, req CreateEnvironmentRequ return &env, nil } -// Template is used to configure a new environment from a repo. -// It is currently in alpha and subject to API-breaking change. +// ParseTemplateRequest parses a template. If Local is a non-nil reader +// it will obviate any other fields on the request. +type ParseTemplateRequest struct { + RepoURL string `json:"repo_url"` + Ref string `json:"ref"` + Local io.Reader `json:"-"` +} + +// Template is a Workspaces As Code (WAC) template. type Template struct { - RepositoryURL string `json:"repository_url"` - // Optional. The default branch will be used if not provided. - Branch string `json:"branch"` - // Optional. The template name will be used if not provided. - FileName string `json:"file_name"` + Workspace Workspace `json:"workspace"` +} + +// Workspace defines values on the workspace that can be configured. +type Workspace struct { + Name string `json:"name"` + Image string `json:"image"` + ContainerBasedVM bool `json:"container-based-vm"` + Resources Resources `json:"resources"` +} + +// Resources defines compute values that can be configured for a workspace. +type Resources struct { + CPU float32 `json:"cpu" ` + Memory float32 `json:"memory"` + Disk int `json:"disk"` +} + +// ParseTemplate parses a template config. It support both remote repositories and local files. +// If a local file is specified then all other values in the request are ignored. +func (c Client) ParseTemplate(ctx context.Context, req ParseTemplateRequest) (Template, error) { + const path = "/api/private/environments/template/parse" + var ( + tpl Template + opts []requestOption + headers = http.Header{} + ) + + if req.Local == nil { + if err := c.requestBody(ctx, http.MethodPost, path, req, &tpl); err != nil { + return tpl, err + } + return tpl, nil + } + + headers.Set("Content-Type", "application/octet-stream") + opts = append(opts, withBody(req.Local), withHeaders(headers)) + + err := c.requestBody(ctx, http.MethodPost, path, nil, &tpl, opts...) + if err != nil { + return tpl, err + } + + return tpl, nil } // CreateEnvironmentFromRepo sends a request to create an environment from a repository. diff --git a/coder-sdk/request.go b/coder-sdk/request.go index d256aff2..4a3fbc97 100644 --- a/coder-sdk/request.go +++ b/coder-sdk/request.go @@ -7,10 +7,45 @@ import ( "fmt" "io" "net/http" + "net/url" "golang.org/x/xerrors" ) +type requestOptions struct { + BaseURLOverride *url.URL + Query url.Values + Headers http.Header + Reader io.Reader +} + +type requestOption func(*requestOptions) + +// withQueryParams sets the provided query parameters on the request. +func withQueryParams(q url.Values) func(o *requestOptions) { + return func(o *requestOptions) { + o.Query = q + } +} + +func withHeaders(h http.Header) func(o *requestOptions) { + return func(o *requestOptions) { + o.Headers = h + } +} + +func withBaseURL(base *url.URL) func(o *requestOptions) { + return func(o *requestOptions) { + o.BaseURLOverride = base + } +} + +func withBody(w io.Reader) func(o *requestOptions) { + return func(o *requestOptions) { + o.Reader = w + } +} + // request is a helper to set the cookie, marshal the payload and execute the request. func (c Client) request(ctx context.Context, method, path string, in interface{}, options ...requestOption) (*http.Response, error) { // Create a default http client with the auth in the cookie. @@ -30,7 +65,6 @@ func (c Client) request(ctx context.Context, method, path string, in interface{} if config.Query != nil { url.RawQuery = config.Query.Encode() } - url.Path = path // If we have incoming data, encode it as json. @@ -43,12 +77,20 @@ func (c Client) request(ctx context.Context, method, path string, in interface{} payload = bytes.NewReader(body) } + if config.Reader != nil { + payload = config.Reader + } + // Create the http request. req, err := http.NewRequestWithContext(ctx, method, url.String(), payload) if err != nil { return nil, xerrors.Errorf("create request: %w", err) } + if config.Headers != nil { + req.Header = config.Headers + } + // Execute the request. return client.Do(req) } diff --git a/coder-sdk/ws.go b/coder-sdk/ws.go index 81eeabf9..2a27ed99 100644 --- a/coder-sdk/ws.go +++ b/coder-sdk/ws.go @@ -3,31 +3,10 @@ package coder import ( "context" "net/http" - "net/url" "nhooyr.io/websocket" ) -type requestOptions struct { - BaseURLOverride *url.URL - Query url.Values -} - -type requestOption func(*requestOptions) - -// withQueryParams sets the provided query parameters on the request. -func withQueryParams(q url.Values) func(o *requestOptions) { - return func(o *requestOptions) { - o.Query = q - } -} - -func withBaseURL(base *url.URL) func(o *requestOptions) { - return func(o *requestOptions) { - o.BaseURLOverride = base - } -} - // dialWebsocket establish the websocket connection while setting the authentication header. func (c Client) dialWebsocket(ctx context.Context, path string, options ...requestOption) (*websocket.Conn, error) { // Make a copy of the url so we can update the scheme to ws(s) without mutating the state. diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go index d0ca2f03..ae14b528 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/envs.go @@ -1,9 +1,13 @@ package cmd import ( + "bytes" "context" "encoding/json" "fmt" + "io" + "io/ioutil" + "net/url" "os" "cdr.dev/coder-cli/coder-sdk" @@ -254,20 +258,21 @@ coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ub func createEnvFromRepoCmd() *cobra.Command { var ( - branch string - name string - follow bool + ref string + repo string + follow bool + filepath string + org string ) cmd := &cobra.Command{ - Use: "create-from-repo [environment_name]", - Short: "create a new environment from a git repository.", - Args: xcobra.ExactArgs(1), - Long: "Create a new Coder environment from a Git repository.", + Use: "create-from-config", + Short: "create a new environment from a config file.", + Long: "Create a new Coder environment from a config file.", Hidden: true, Example: `# create a new environment from git repository template -coder envs create-from-repo github.com/cdr/m -coder envs create-from-repo github.com/cdr/m --branch envs-as-code`, +coder envs create-from-repo --repo-url github.com/cdr/m --branch my-branch +coder envs create-from-repo -f coder.yaml`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -276,15 +281,60 @@ coder envs create-from-repo github.com/cdr/m --branch envs-as-code`, return err } - // ExactArgs(1) ensures our name value can't panic on an out of bounds. - createReq := &coder.Template{ - RepositoryURL: args[0], - Branch: branch, - FileName: name, + if repo != "" { + _, err = url.Parse(repo) + if err != nil { + return xerrors.Errorf("'repo' must be a valid url: %w", err) + } + } + + multiOrgMember, err := isMultiOrgMember(ctx, client, coder.Me) + if err != nil { + return err + } + + if multiOrgMember && org == "" { + return xerrors.New("org is required for multi-org members") + } + + var rd io.Reader + if filepath != "" { + b, err := ioutil.ReadFile(filepath) + if err != nil { + return xerrors.Errorf("read local file: %w", err) + } + rd = bytes.NewReader(b) + } + + req := coder.ParseTemplateRequest{ + RepoURL: repo, + Ref: ref, + Local: rd, + } + + tpl, err := client.ParseTemplate(ctx, req) + if err != nil { + return xerrors.Errorf("parse environment template config: %w", err) + } + + importedImg, err := findImg(ctx, client, findImgConf{ + email: coder.Me, + imgName: tpl.Workspace.Image, + orgName: org, + }) + if err != nil { + return err } env, err := client.CreateEnvironment(ctx, coder.CreateEnvironmentRequest{ - Template: createReq, + Name: tpl.Workspace.Name, + ImageID: importedImg.ID, + OrgID: importedImg.OrganizationID, + ImageTag: importedImg.DefaultTag.Tag, + CPUCores: tpl.Workspace.Resources.CPU, + MemoryGB: tpl.Workspace.Resources.Memory, + DiskGB: tpl.Workspace.Resources.Disk, + UseContainerVM: tpl.Workspace.ContainerBasedVM, }) if err != nil { return xerrors.Errorf("create environment: %w", err) @@ -305,8 +355,10 @@ coder envs create-from-repo github.com/cdr/m --branch envs-as-code`, return nil }, } - cmd.Flags().StringVarP(&branch, "branch", "b", "master", "name of the branch to create the environment from.") - cmd.Flags().StringVarP(&name, "name", "n", "coder.yaml", "name of the config file.") + cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the environment should be created under.") + cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "path to local template file.") + cmd.Flags().StringVarP(&ref, "ref", "", "master", "git reference to pull template from. May be a branch, tag, or commit hash.") + cmd.Flags().StringVarP(&repo, "repo-url", "r", "", "URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'.") cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") return cmd }