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 }
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: