Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 1a85240

Browse files
authored
feat: split parsing config from creating env (#234)
- Add an endpoint to parse a WAC template. - Add support for specifying a local file. - Decoupled creating an environment from parsing a template (in the API).
1 parent 0da2ebf commit 1a85240

File tree

4 files changed

+176
-57
lines changed

4 files changed

+176
-57
lines changed

coder-sdk/env.go

+64-18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package coder
22

33
import (
44
"context"
5+
"io"
56
"net/http"
67
"net/url"
78
"time"
@@ -73,17 +74,16 @@ const (
7374

7475
// CreateEnvironmentRequest is used to configure a new environment.
7576
type CreateEnvironmentRequest struct {
76-
Name string `json:"name"`
77-
ImageID string `json:"image_id"`
78-
OrgID string `json:"org_id"`
79-
ImageTag string `json:"image_tag"`
80-
CPUCores float32 `json:"cpu_cores"`
81-
MemoryGB float32 `json:"memory_gb"`
82-
DiskGB int `json:"disk_gb"`
83-
GPUs int `json:"gpus"`
84-
Services []string `json:"services"`
85-
UseContainerVM bool `json:"use_container_vm"`
86-
Template *Template `json:"template"`
77+
Name string `json:"name"`
78+
ImageID string `json:"image_id"`
79+
OrgID string `json:"org_id"`
80+
ImageTag string `json:"image_tag"`
81+
CPUCores float32 `json:"cpu_cores"`
82+
MemoryGB float32 `json:"memory_gb"`
83+
DiskGB int `json:"disk_gb"`
84+
GPUs int `json:"gpus"`
85+
Services []string `json:"services"`
86+
UseContainerVM bool `json:"use_container_vm"`
8787
}
8888

8989
// CreateEnvironment sends a request to create an environment.
@@ -95,14 +95,60 @@ func (c Client) CreateEnvironment(ctx context.Context, req CreateEnvironmentRequ
9595
return &env, nil
9696
}
9797

98-
// Template is used to configure a new environment from a repo.
99-
// It is currently in alpha and subject to API-breaking change.
98+
// ParseTemplateRequest parses a template. If Local is a non-nil reader
99+
// it will obviate any other fields on the request.
100+
type ParseTemplateRequest struct {
101+
RepoURL string `json:"repo_url"`
102+
Ref string `json:"ref"`
103+
Local io.Reader `json:"-"`
104+
}
105+
106+
// Template is a Workspaces As Code (WAC) template.
100107
type Template struct {
101-
RepositoryURL string `json:"repository_url"`
102-
// Optional. The default branch will be used if not provided.
103-
Branch string `json:"branch"`
104-
// Optional. The template name will be used if not provided.
105-
FileName string `json:"file_name"`
108+
Workspace Workspace `json:"workspace"`
109+
}
110+
111+
// Workspace defines values on the workspace that can be configured.
112+
type Workspace struct {
113+
Name string `json:"name"`
114+
Image string `json:"image"`
115+
ContainerBasedVM bool `json:"container-based-vm"`
116+
Resources Resources `json:"resources"`
117+
}
118+
119+
// Resources defines compute values that can be configured for a workspace.
120+
type Resources struct {
121+
CPU float32 `json:"cpu" `
122+
Memory float32 `json:"memory"`
123+
Disk int `json:"disk"`
124+
}
125+
126+
// ParseTemplate parses a template config. It support both remote repositories and local files.
127+
// If a local file is specified then all other values in the request are ignored.
128+
func (c Client) ParseTemplate(ctx context.Context, req ParseTemplateRequest) (Template, error) {
129+
const path = "/api/private/environments/template/parse"
130+
var (
131+
tpl Template
132+
opts []requestOption
133+
headers = http.Header{}
134+
)
135+
136+
if req.Local == nil {
137+
if err := c.requestBody(ctx, http.MethodPost, path, req, &tpl); err != nil {
138+
return tpl, err
139+
}
140+
return tpl, nil
141+
}
142+
143+
headers.Set("Content-Type", "application/octet-stream")
144+
opts = append(opts, withBody(req.Local), withHeaders(headers))
145+
146+
err := c.requestBody(ctx, http.MethodPost, path, nil, &tpl, opts...)
147+
if err != nil {
148+
return tpl, err
149+
}
150+
151+
return tpl, nil
106152
}
107153

108154
// CreateEnvironmentFromRepo sends a request to create an environment from a repository.

coder-sdk/request.go

+43-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,45 @@ import (
77
"fmt"
88
"io"
99
"net/http"
10+
"net/url"
1011

1112
"golang.org/x/xerrors"
1213
)
1314

15+
type requestOptions struct {
16+
BaseURLOverride *url.URL
17+
Query url.Values
18+
Headers http.Header
19+
Reader io.Reader
20+
}
21+
22+
type requestOption func(*requestOptions)
23+
24+
// withQueryParams sets the provided query parameters on the request.
25+
func withQueryParams(q url.Values) func(o *requestOptions) {
26+
return func(o *requestOptions) {
27+
o.Query = q
28+
}
29+
}
30+
31+
func withHeaders(h http.Header) func(o *requestOptions) {
32+
return func(o *requestOptions) {
33+
o.Headers = h
34+
}
35+
}
36+
37+
func withBaseURL(base *url.URL) func(o *requestOptions) {
38+
return func(o *requestOptions) {
39+
o.BaseURLOverride = base
40+
}
41+
}
42+
43+
func withBody(w io.Reader) func(o *requestOptions) {
44+
return func(o *requestOptions) {
45+
o.Reader = w
46+
}
47+
}
48+
1449
// request is a helper to set the cookie, marshal the payload and execute the request.
1550
func (c Client) request(ctx context.Context, method, path string, in interface{}, options ...requestOption) (*http.Response, error) {
1651
// 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{}
3065
if config.Query != nil {
3166
url.RawQuery = config.Query.Encode()
3267
}
33-
3468
url.Path = path
3569

3670
// 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{}
4377
payload = bytes.NewReader(body)
4478
}
4579

80+
if config.Reader != nil {
81+
payload = config.Reader
82+
}
83+
4684
// Create the http request.
4785
req, err := http.NewRequestWithContext(ctx, method, url.String(), payload)
4886
if err != nil {
4987
return nil, xerrors.Errorf("create request: %w", err)
5088
}
5189

90+
if config.Headers != nil {
91+
req.Header = config.Headers
92+
}
93+
5294
// Execute the request.
5395
return client.Do(req)
5496
}

coder-sdk/ws.go

-21
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,10 @@ package coder
33
import (
44
"context"
55
"net/http"
6-
"net/url"
76

87
"nhooyr.io/websocket"
98
)
109

11-
type requestOptions struct {
12-
BaseURLOverride *url.URL
13-
Query url.Values
14-
}
15-
16-
type requestOption func(*requestOptions)
17-
18-
// withQueryParams sets the provided query parameters on the request.
19-
func withQueryParams(q url.Values) func(o *requestOptions) {
20-
return func(o *requestOptions) {
21-
o.Query = q
22-
}
23-
}
24-
25-
func withBaseURL(base *url.URL) func(o *requestOptions) {
26-
return func(o *requestOptions) {
27-
o.BaseURLOverride = base
28-
}
29-
}
30-
3110
// dialWebsocket establish the websocket connection while setting the authentication header.
3211
func (c Client) dialWebsocket(ctx context.Context, path string, options ...requestOption) (*websocket.Conn, error) {
3312
// Make a copy of the url so we can update the scheme to ws(s) without mutating the state.

internal/cmd/envs.go

+69-17
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package cmd
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"fmt"
8+
"io"
9+
"io/ioutil"
10+
"net/url"
711
"os"
812

913
"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
254258

255259
func createEnvFromRepoCmd() *cobra.Command {
256260
var (
257-
branch string
258-
name string
259-
follow bool
261+
ref string
262+
repo string
263+
follow bool
264+
filepath string
265+
org string
260266
)
261267

262268
cmd := &cobra.Command{
263-
Use: "create-from-repo [environment_name]",
264-
Short: "create a new environment from a git repository.",
265-
Args: xcobra.ExactArgs(1),
266-
Long: "Create a new Coder environment from a Git repository.",
269+
Use: "create-from-config",
270+
Short: "create a new environment from a config file.",
271+
Long: "Create a new Coder environment from a config file.",
267272
Hidden: true,
268273
Example: `# create a new environment from git repository template
269-
coder envs create-from-repo github.com/cdr/m
270-
coder envs create-from-repo github.com/cdr/m --branch envs-as-code`,
274+
coder envs create-from-repo --repo-url github.com/cdr/m --branch my-branch
275+
coder envs create-from-repo -f coder.yaml`,
271276
RunE: func(cmd *cobra.Command, args []string) error {
272277
ctx := cmd.Context()
273278

@@ -276,15 +281,60 @@ coder envs create-from-repo github.com/cdr/m --branch envs-as-code`,
276281
return err
277282
}
278283

279-
// ExactArgs(1) ensures our name value can't panic on an out of bounds.
280-
createReq := &coder.Template{
281-
RepositoryURL: args[0],
282-
Branch: branch,
283-
FileName: name,
284+
if repo != "" {
285+
_, err = url.Parse(repo)
286+
if err != nil {
287+
return xerrors.Errorf("'repo' must be a valid url: %w", err)
288+
}
289+
}
290+
291+
multiOrgMember, err := isMultiOrgMember(ctx, client, coder.Me)
292+
if err != nil {
293+
return err
294+
}
295+
296+
if multiOrgMember && org == "" {
297+
return xerrors.New("org is required for multi-org members")
298+
}
299+
300+
var rd io.Reader
301+
if filepath != "" {
302+
b, err := ioutil.ReadFile(filepath)
303+
if err != nil {
304+
return xerrors.Errorf("read local file: %w", err)
305+
}
306+
rd = bytes.NewReader(b)
307+
}
308+
309+
req := coder.ParseTemplateRequest{
310+
RepoURL: repo,
311+
Ref: ref,
312+
Local: rd,
313+
}
314+
315+
tpl, err := client.ParseTemplate(ctx, req)
316+
if err != nil {
317+
return xerrors.Errorf("parse environment template config: %w", err)
318+
}
319+
320+
importedImg, err := findImg(ctx, client, findImgConf{
321+
email: coder.Me,
322+
imgName: tpl.Workspace.Image,
323+
orgName: org,
324+
})
325+
if err != nil {
326+
return err
284327
}
285328

286329
env, err := client.CreateEnvironment(ctx, coder.CreateEnvironmentRequest{
287-
Template: createReq,
330+
Name: tpl.Workspace.Name,
331+
ImageID: importedImg.ID,
332+
OrgID: importedImg.OrganizationID,
333+
ImageTag: importedImg.DefaultTag.Tag,
334+
CPUCores: tpl.Workspace.Resources.CPU,
335+
MemoryGB: tpl.Workspace.Resources.Memory,
336+
DiskGB: tpl.Workspace.Resources.Disk,
337+
UseContainerVM: tpl.Workspace.ContainerBasedVM,
288338
})
289339
if err != nil {
290340
return xerrors.Errorf("create environment: %w", err)
@@ -305,8 +355,10 @@ coder envs create-from-repo github.com/cdr/m --branch envs-as-code`,
305355
return nil
306356
},
307357
}
308-
cmd.Flags().StringVarP(&branch, "branch", "b", "master", "name of the branch to create the environment from.")
309-
cmd.Flags().StringVarP(&name, "name", "n", "coder.yaml", "name of the config file.")
358+
cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the environment should be created under.")
359+
cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "path to local template file.")
360+
cmd.Flags().StringVarP(&ref, "ref", "", "master", "git reference to pull template from. May be a branch, tag, or commit hash.")
361+
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'.")
310362
cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild")
311363
return cmd
312364
}

0 commit comments

Comments
 (0)