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

feat: split parsing config from creating env #234

Merged
merged 1 commit into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 64 additions & 18 deletions coder-sdk/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package coder

import (
"context"
"io"
"net/http"
"net/url"
"time"
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
44 changes: 43 additions & 1 deletion coder-sdk/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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)
}
Expand Down
21 changes: 0 additions & 21 deletions coder-sdk/ws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
86 changes: 69 additions & 17 deletions internal/cmd/envs.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package cmd

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"

"cdr.dev/coder-cli/coder-sdk"
Expand Down Expand Up @@ -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()

Expand All @@ -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)
Expand All @@ -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
}
Expand Down