From 4bdb63a68654d58a13bf7d0f5c498b16271d6421 Mon Sep 17 00:00:00 2001 From: Philip Reichenberger Date: Tue, 30 Mar 2021 23:52:53 -0400 Subject: [PATCH] Clean up gitignore --- .circleci/config.yml | 22 +- main.go => cmd/main.go | 0 pkg/.gitignore | 4 - pkg/provider/.gitignore | 4 + pkg/provider/Makefile | 17 + pkg/provider/client.go | 362 +++++++++++++++++++++ pkg/provider/datasource_job_storage.go | 63 ++++ pkg/provider/datasource_network.go | 178 ++++++++++ pkg/provider/datasource_template.go | 191 +++++++++++ pkg/provider/datasource_user.go | 165 ++++++++++ pkg/provider/main.tf | 45 +++ pkg/provider/provider.go | 117 +++++++ pkg/provider/resource_autoscaling_group.go | 178 ++++++++++ pkg/provider/resource_machine.go | 321 ++++++++++++++++++ pkg/provider/resource_network.go | 159 +++++++++ pkg/provider/resource_script.go | 262 +++++++++++++++ src/.gitignore | 2 - 17 files changed, 2063 insertions(+), 27 deletions(-) rename main.go => cmd/main.go (100%) create mode 100644 pkg/provider/.gitignore create mode 100644 pkg/provider/Makefile create mode 100644 pkg/provider/client.go create mode 100644 pkg/provider/datasource_job_storage.go create mode 100644 pkg/provider/datasource_network.go create mode 100644 pkg/provider/datasource_template.go create mode 100644 pkg/provider/datasource_user.go create mode 100644 pkg/provider/main.tf create mode 100644 pkg/provider/provider.go create mode 100644 pkg/provider/resource_autoscaling_group.go create mode 100644 pkg/provider/resource_machine.go create mode 100644 pkg/provider/resource_network.go create mode 100644 pkg/provider/resource_script.go delete mode 100644 src/.gitignore diff --git a/.circleci/config.yml b/.circleci/config.yml index 9a378f4..92cd5e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,16 +13,6 @@ jobs: - checkout - run: command: go test ./... - upload_assets: - docker: - - image: golang:1.14 - steps: - - checkout - - run: apt update && apt install file jq -y - - run: - command: bin/build - - run: - command: bin/upload ${CIRCLE_TAG} workflows: version: 2 @@ -39,14 +29,4 @@ workflows: context: semantic-release filters: branches: - only: master - - tag: - jobs: - - upload_assets: - context: semantic-release - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ + only: master \ No newline at end of file diff --git a/main.go b/cmd/main.go similarity index 100% rename from main.go rename to cmd/main.go diff --git a/pkg/.gitignore b/pkg/.gitignore index 5e7d273..e69de29 100644 --- a/pkg/.gitignore +++ b/pkg/.gitignore @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore diff --git a/pkg/provider/.gitignore b/pkg/provider/.gitignore new file mode 100644 index 0000000..f60f8fe --- /dev/null +++ b/pkg/provider/.gitignore @@ -0,0 +1,4 @@ +terraform +terraform-provider-paperspace +terraform.tfstate +terraform.tfstate.backup diff --git a/pkg/provider/Makefile b/pkg/provider/Makefile new file mode 100644 index 0000000..50340b5 --- /dev/null +++ b/pkg/provider/Makefile @@ -0,0 +1,17 @@ +build: + go mod tidy + go build -o terraform-provider-paperspace + +build-linux: + go mod tidy + GOOS=linux GOARCH=amd64 go build -o terraform-provider-paperspace-linux-amd64 + +build-darwin: + go mod tidy + GOOS=darwin GOARCH=amd64 go build -o terraform-provider-paperspace-darwin-amd64 + +build-windows: + go mod tidy + GOOS=windows GOARCH=amd64 go build -o terraform-provider-paperspace-windows-amd64.exe + +build-all: build-linux build-darwin build-windows diff --git a/pkg/provider/client.go b/pkg/provider/client.go new file mode 100644 index 0000000..e2f4844 --- /dev/null +++ b/pkg/provider/client.go @@ -0,0 +1,362 @@ +package provider + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "reflect" + "strconv" + "strings" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +var MachineNotFoundError = "Error on GetMachine: machine not found" +var MachineDeleteNotFoundError = "Error on DeleteMachine: machine not found" + +var RegionMap = map[string]int{ + "East Coast (NY2)": 1, + "West Coast (CA1)": 2, + "Europe (AMS1)": 3, +} + +type JobStorage struct { + Handle string `json:"handle"` + TeamID int `json:"teamId"` + Server JobStorageServer `json:"jobStorageServer"` +} + +type JobStorageServer struct { + IP string `json:"ipAddress"` + StorageRegion StorageRegion `json:"storageRegion"` +} + +type StorageRegion struct { + Name string `json:"name"` +} + +type Network struct { + ID int `json:"id"` + Handle string `json:"handle"` + IsTaken bool `json:"isTaken"` + Network string `json:"network"` + Netmask string `json:"netmask"` + VlanID int `json:"vlanId"` +} + +type NamedNetwork struct { + Name string `json:"name"` + Network Network `json:"network"` +} + +type CreateTeamNamedNetworkParams struct { + Name string `json:"name"` + RegionId int `json:"regionId"` +} + +type MapIf map[string]interface{} + +func (m *MapIf) Append(d *schema.ResourceData, k string) { + v := d.Get(k) + (*m)[k] = v +} + +func (m *MapIf) AppendAs(d *schema.ResourceData, k, nk string) { + v := d.Get(k) + (*m)[nk] = v +} + +func (m *MapIf) AppendV(d *schema.ResourceData, k, v string) { + (*m)[k] = v +} + +func (m *MapIf) AppendIfSet(d *schema.ResourceData, k string) { + v := d.Get(k) + if reflect.ValueOf(v).Interface() != reflect.Zero(reflect.TypeOf(v)).Interface() { + (*m)[k] = v + } +} + +func (m *MapIf) AppendAsIfSet(d *schema.ResourceData, k, nk string) { + v := d.Get(k) + if reflect.ValueOf(v).Interface() != reflect.Zero(reflect.TypeOf(v)).Interface() { + (*m)[nk] = v + } +} + +func SetResDataFrom(d *schema.ResourceData, m map[string]interface{}, dn, n string) { + v, ok := m[n] + //log.Printf("%v %v\n", n, v) + if ok { + d.Set(dn, v) + } +} + +func SetResData(d *schema.ResourceData, m map[string]interface{}, n string) { + SetResDataFrom(d, m, n, n) +} + +func logHttpRequestConstruction(operationType string, url string, data *bytes.Buffer) { + log.Printf("Constructing %s request to url: %s, data: %v", operationType, url, data) +} + +// LogHttpResponse logs http response fields +func LogHttpResponse(reqDesc string, reqURL *url.URL, resp *http.Response, body interface{}, err error) { + log.Printf("Request: %v", reqDesc) + log.Printf("Request URL: %v", reqURL) + log.Printf("Response Status: %v", resp.Status) + log.Printf("Response: %v", resp) + log.Printf("Response Body: %s", spew.Sdump(body)) + log.Printf("Error: %v", err) +} + +type ClientConfig struct { + APIKey string + APIHost string + Region string +} + +type PaperspaceClient struct { + APIKey string + APIHost string + Region string + HttpClient *http.Client +} + +func (c *ClientConfig) Client() (paperspaceClient PaperspaceClient) { + timeout := 30 * time.Second + client := &http.Client{ + Timeout: timeout, + } + + paperspaceClient = PaperspaceClient{ + APIKey: c.APIKey, + APIHost: c.APIHost, + Region: c.Region, + HttpClient: client, + } + + log.Printf("[DEBUG] Paperspace client config %v", paperspaceClient) + + return paperspaceClient +} + +func (paperspaceClient *PaperspaceClient) NewHttpRequest(method, url string, buf io.Reader) (*http.Request, error) { + req, err := http.NewRequest(method, url, buf) + if err != nil { + return nil, err + } + + req.Header.Add("x-api-key", paperspaceClient.APIKey) + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + req.Header.Add("User-Agent", "terraform-provider-paperspace") + req.Header.Add("ps_client_name", "terraform-provider-paperspace") + + return req, nil +} + +func (paperspaceClient *PaperspaceClient) RequestInterface(method string, url string, params, result interface{}) (res *http.Response, err error) { + var data []byte + body := bytes.NewReader(make([]byte, 0)) + + if params != nil { + data, err = json.Marshal(params) + if err != nil { + return res, err + } + + body = bytes.NewReader(data) + } + + buf := bytes.NewBuffer(data) + logHttpRequestConstruction(method, url, buf) + + req, err := paperspaceClient.NewHttpRequest(method, url, body) + if err != nil { + return nil, err + } + + resp, err := paperspaceClient.HttpClient.Do(req) + if err != nil { + return resp, err + } + defer resp.Body.Close() + + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return resp, err + } + + LogHttpResponse("", req.URL, resp, result, err) + return resp, nil +} + +func (paperspaceClient *PaperspaceClient) Request(method string, url string, data []byte) (body map[string]interface{}, statusCode int, err error) { + buf := bytes.NewBuffer(data) + + logHttpRequestConstruction(method, url, buf) + + req, err := paperspaceClient.NewHttpRequest(method, url, buf) + if err != nil { + return nil, statusCode, fmt.Errorf("Error constructing request: %s", err) + } + + resp, err := paperspaceClient.HttpClient.Do(req) + if err != nil { + if resp != nil { + statusCode = resp.StatusCode + } + return nil, statusCode, fmt.Errorf("Error completing request: %s", err) + } + defer resp.Body.Close() + + err = json.NewDecoder(resp.Body).Decode(&body) + if err != nil { + if resp != nil { + statusCode = resp.StatusCode + } + return nil, statusCode, fmt.Errorf("Error decoding response body: %s", err) + } + + LogHttpResponse("", req.URL, resp, body, err) + + return body, resp.StatusCode, nil +} + +func (paperspaceClient *PaperspaceClient) GetMachine(id string) (body map[string]interface{}, err error) { + url := fmt.Sprintf("%s/machines/getMachinePublic?machineId=%s", paperspaceClient.APIHost, id) + body, statusCode, err := paperspaceClient.Request("GET", url, nil) + if err != nil { + return nil, err + } + + if statusCode != 404 && statusCode != 200 { + return nil, fmt.Errorf("Error on GetMachine response: statusCode: %d", statusCode) + } + + nextID, _ := body["id"].(string) + if statusCode == 404 || nextID == "" { + return nil, fmt.Errorf(MachineNotFoundError) + } + + return body, nil +} + +func (paperspaceClient *PaperspaceClient) CreateMachine(data []byte) (id string, err error) { + url := fmt.Sprintf("%s/machines/createSingleMachinePublic", paperspaceClient.APIHost) + body, statusCode, err := paperspaceClient.Request("POST", url, data) + if err != nil { + return "", err + } + + if statusCode != 200 { + jsonBody, err := json.Marshal(body) + if err != nil { + return "", fmt.Errorf("Error unmarshaling response body: %v", err) + } + + return "", fmt.Errorf("Error on CreateMachine: Status Code %d, Response Body: %s", statusCode, jsonBody) + } + + id, _ = body["id"].(string) + + if id == "" { + return "", fmt.Errorf("Error on CreateMachine: id not found") + } + + return id, nil +} + +func (paperspaceClient *PaperspaceClient) DeleteMachine(id string) (err error) { + url := fmt.Sprintf("%s/machines/%s/destroyMachine", paperspaceClient.APIHost, id) + _, statusCode, err := paperspaceClient.Request("POST", url, nil) + // /destroyMachine returns the string "EOF" if it was successful, which can't be JSON-decoded + if err != nil && !strings.Contains(err.Error(), "EOF") { + return err + } + + if statusCode != 204 { + return fmt.Errorf("Error deleting machine") + } + if statusCode != 404 { + return fmt.Errorf(MachineDeleteNotFoundError) + } + + return nil +} + +func (paperspaceClient *PaperspaceClient) CreateTeamNamedNetwork(teamID int, createNamedNetworkParams CreateTeamNamedNetworkParams) error { + var network Network + url := fmt.Sprintf("%s/teams/%d/createPrivateNetwork", paperspaceClient.APIHost, teamID) + + _, err := paperspaceClient.RequestInterface("POST", url, createNamedNetworkParams, &network) + if err != nil && strings.Contains(err.Error(), "EOF") { + return nil + } + return err +} + +func (paperspaceClient *PaperspaceClient) GetTeamNamedNetworks(teamID int) ([]NamedNetwork, error) { + var namedNetworks []NamedNetwork + url := fmt.Sprintf("%s/teams/%d/getNetworks", paperspaceClient.APIHost, teamID) + + _, err := paperspaceClient.RequestInterface("GET", url, nil, &namedNetworks) + + return namedNetworks, err +} + +func (paperspaceClient *PaperspaceClient) GetTeamNamedNetwork(teamID int, name string) (*NamedNetwork, error) { + namedNetworks, err := paperspaceClient.GetTeamNamedNetworks(teamID) + if err != nil { + return nil, err + } + + for _, namedNetwork := range namedNetworks { + if namedNetwork.Name == name { + return &namedNetwork, nil + } + } + + return nil, fmt.Errorf("Error getting private network by name: %s", name) +} + +func (paperspaceClient *PaperspaceClient) GetTeamNamedNetworkById(teamID int, id string) (*NamedNetwork, error) { + namedNetworks, err := paperspaceClient.GetTeamNamedNetworks(teamID) + if err != nil { + return nil, err + } + + for _, namedNetwork := range namedNetworks { + if strconv.Itoa(namedNetwork.Network.ID) == id { + return &namedNetwork, nil + } + } + + return nil, fmt.Errorf("Error getting private network by id: %s", id) +} + +func (paperspaceClient *PaperspaceClient) GetJobStorageByRegion(teamID int, region string) (JobStorage, error) { + var jobStorage JobStorage + var jobStorages []JobStorage + url := fmt.Sprintf("%s/accounts/team/%d/getJobStorage", paperspaceClient.APIHost, teamID) + + _, err := paperspaceClient.RequestInterface("GET", url, nil, &jobStorages) + if err != nil { + return jobStorage, err + } + + for _, jobStorageInstance := range jobStorages { + if jobStorageInstance.Server.StorageRegion.Name == region { + return jobStorageInstance, nil + } + } + + return jobStorage, nil +} diff --git a/pkg/provider/datasource_job_storage.go b/pkg/provider/datasource_job_storage.go new file mode 100644 index 0000000..473b88e --- /dev/null +++ b/pkg/provider/datasource_job_storage.go @@ -0,0 +1,63 @@ +package provider + +import ( + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func dataSourceJobStorageRead(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + region := paperspaceClient.Region + + teamID, ok := d.Get("team_id").(int) + if !ok { + return fmt.Errorf("team_id is not a int") + } + regionData, ok := d.Get("region").(string) + if !ok { + return fmt.Errorf("region is not a string") + } + + if regionData != "" { + region = regionData + } + + jobStorage, err := paperspaceClient.GetJobStorageByRegion(teamID, region) + if err != nil { + return err + } + if jobStorage.Handle == "" { + return errors.New("Could not find job storage") + } + + d.SetId(jobStorage.Handle) + updateJobStorageSchema(d, jobStorage) + + return nil +} + +func updateJobStorageSchema(d *schema.ResourceData, jobStorage JobStorage) { + d.Set("handle", jobStorage.Handle) +} + +func dataSourceJobStorage() *schema.Resource { + return &schema.Resource{ + Read: dataSourceJobStorageRead, + Schema: map[string]*schema.Schema{ + "team_id": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "handle": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "region": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} diff --git a/pkg/provider/datasource_network.go b/pkg/provider/datasource_network.go new file mode 100644 index 0000000..0296b06 --- /dev/null +++ b/pkg/provider/datasource_network.go @@ -0,0 +1,178 @@ +package provider + +import ( + "encoding/json" + "fmt" + "log" + "net/http/httputil" + "net/url" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func dataSourceNetworkRead(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + + log.Printf("[INFO] paperspace dataSourceNetworkRead Client ready") + + queryParam := false + queryStr := "?" + id, ok := d.GetOk("id") + if ok { + queryStr += "id=" + url.QueryEscape(id.(string)) + queryParam = true + } + name, ok := d.GetOk("name") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "name=" + url.QueryEscape(name.(string)) + queryParam = true + } + region, ok := d.GetOk("region") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "region=" + url.QueryEscape(region.(string)) + queryParam = true + } + dtCreated, ok := d.GetOk("dt_created") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "dtCreated=" + url.QueryEscape(dtCreated.(string)) + queryParam = true + } + network, ok := d.GetOk("network") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "network=" + url.QueryEscape(network.(string)) + queryParam = true + } + netmask, ok := d.GetOk("netmask") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "netmask=" + url.QueryEscape(netmask.(string)) + queryParam = true + } + teamId, ok := d.GetOk("team_id") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "teamId=" + url.QueryEscape(teamId.(string)) + queryParam = true + } + if !queryParam { + return fmt.Errorf("Error reading paperspace network: must specify query filter properties") + } + + url := fmt.Sprintf("%s/networks/getNetworks%s", paperspaceClient.APIHost, queryStr) + req, err := paperspaceClient.NewHttpRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("Error constructing GetTeamNamedNetworks request: %s", err) + } + requestDump, err := httputil.DumpRequest(req, true) + if err != nil { + return fmt.Errorf("Error constructing GetNetwork request: %s", err) + } + log.Print("[INFO] Request:", string(requestDump)) + + resp, err := paperspaceClient.HttpClient.Do(req) + if err != nil { + return fmt.Errorf("Error reading paperspace network: %s", err) + } + defer resp.Body.Close() + + statusCode := resp.StatusCode + log.Printf("[INFO] paperspace dataSourceNetworkRead StatusCode: %v", statusCode) + if statusCode == 404 { + return fmt.Errorf("Error reading paperspace network: networks not found") + } + if statusCode != 200 { + responseDump, _ := httputil.DumpResponse(resp, true) + return fmt.Errorf("Error reading paperspace network: Response: %s", string(responseDump)) + } + + var f interface{} + err = json.NewDecoder(resp.Body).Decode(&f) + if err != nil { + return fmt.Errorf("Error decoding GetTeamNamedNetworks response body: %s", err) + } + LogHttpResponse("paperspace dataSourceNetworkRead", req.URL, resp, f, err) + + mpa := f.([]interface{}) + if len(mpa) > 1 { + return fmt.Errorf("Error reading paperspace network: found more than one network matching given properties") + } + if len(mpa) == 0 { + return fmt.Errorf("Error reading paperspace network: no network found matching given properties") + } + + mp, ok := mpa[0].(map[string]interface{}) + if !ok { + return fmt.Errorf("Error unmarshalling paperspace network read response: no networks not found") + } + + idr, _ := mp["id"].(string) + if idr == "" { + return fmt.Errorf("Error unmarshalling paperspace network read response: no network id found for network") + } + + log.Printf("[INFO] paperspace dataSourceNetworkRead network id: %v", idr) + + SetResData(d, mp, "name") + SetResData(d, mp, "region") + SetResDataFrom(d, mp, "dt_created", "dtCreated") + SetResData(d, mp, "network") + SetResData(d, mp, "netmask") + SetResDataFrom(d, mp, "team_id", "teamId") + + d.SetId(idr) + + return nil +} + +func dataSourceNetwork() *schema.Resource { + return &schema.Resource{ + Read: dataSourceNetworkRead, + + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "region": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "dt_created": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "network": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "netmask": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "team_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} diff --git a/pkg/provider/datasource_template.go b/pkg/provider/datasource_template.go new file mode 100644 index 0000000..cd163ce --- /dev/null +++ b/pkg/provider/datasource_template.go @@ -0,0 +1,191 @@ +package provider + +import ( + "encoding/json" + "fmt" + "log" + "net/http/httputil" + "net/url" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func dataSourceTemplateRead(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + + log.Printf("[INFO] paperspace dataSourceTemplateRead Client ready") + + queryParam := false + queryStr := "?" + id, ok := d.GetOk("id") + if ok { + queryStr += "id=" + url.QueryEscape(id.(string)) + queryParam = true + } + name, ok := d.GetOk("name") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "name=" + url.QueryEscape(name.(string)) + queryParam = true + } + label, ok := d.GetOk("label") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "label=" + url.QueryEscape(label.(string)) + queryParam = true + } + os, ok := d.GetOk("os") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "os=" + url.QueryEscape(os.(string)) + queryParam = true + } + dtCreated, ok := d.GetOk("dt_created") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "dtCreated=" + url.QueryEscape(dtCreated.(string)) + queryParam = true + } + teamId, ok := d.GetOk("team_id") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "teamId=" + url.QueryEscape(teamId.(string)) + queryParam = true + } + userId, ok := d.GetOk("user_id") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "userId=" + url.QueryEscape(userId.(string)) + queryParam = true + } + region, ok := d.GetOk("region") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "region=" + url.QueryEscape(region.(string)) + queryParam = true + } + if !queryParam { + return fmt.Errorf("Error reading paperspace template: must specify query filter properties") + } + + url := fmt.Sprintf("%s/templates/getTemplates%s", paperspaceClient.APIHost, queryStr) + req, err := paperspaceClient.NewHttpRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("Error constructing GetTemplates request: %s", err) + } + requestDump, err := httputil.DumpRequest(req, true) + if err != nil { + return fmt.Errorf("Error constructing GetTemplates request: %s", err) + } + log.Print("[INFO] Request:", string(requestDump)) + + resp, err := paperspaceClient.HttpClient.Do(req) + if err != nil { + return fmt.Errorf("Error reading paperspace template: %s", err) + } + defer resp.Body.Close() + + statusCode := resp.StatusCode + log.Printf("[INFO] paperspace dataSourceTemplateRead StatusCode: %v", statusCode) + if statusCode == 404 { + return fmt.Errorf("Error reading paperspace template: templates not found") + } + if statusCode != 200 { + responseDump, _ := httputil.DumpResponse(resp, true) + return fmt.Errorf("Error reading paperspace template: Response: %s", string(responseDump)) + } + + var f interface{} + err = json.NewDecoder(resp.Body).Decode(&f) + if err != nil { + return fmt.Errorf("Error decoding GetTemplate response body: %s", err) + } + LogHttpResponse("paperspace dataSourceTemplateRead", req.URL, resp, f, err) + + mpa := f.([]interface{}) + if len(mpa) > 1 { + return fmt.Errorf("Error reading paperspace template: found more than one template matching given properties") + } + if len(mpa) == 0 { + return fmt.Errorf("Error reading paperspace template: no template found matching given properties") + } + + mp, ok := mpa[0].(map[string]interface{}) + if !ok { + return fmt.Errorf("Error unmarshalling paperspace template read response: no templates not found") + } + + idr, _ := mp["id"].(string) + if idr == "" { + return fmt.Errorf("Error unmarshalling paperspace template read response: no template id found for template") + } + + log.Printf("[INFO] paperspace dataSourceTemplateRead template id: %v", idr) + + SetResData(d, mp, "name") + SetResData(d, mp, "label") + SetResData(d, mp, "os") + SetResDataFrom(d, mp, "dt_created", "dtCreated") + SetResDataFrom(d, mp, "team_id", "teamId") + SetResDataFrom(d, mp, "user_id", "userId") + SetResData(d, mp, "region") + + d.SetId(idr) + + return nil +} + +func dataSourceTemplate() *schema.Resource { + return &schema.Resource{ + Read: dataSourceTemplateRead, + + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "label": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "os": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "dt_created": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "team_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "user_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "region": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} diff --git a/pkg/provider/datasource_user.go b/pkg/provider/datasource_user.go new file mode 100644 index 0000000..bfb9d58 --- /dev/null +++ b/pkg/provider/datasource_user.go @@ -0,0 +1,165 @@ +package provider + +import ( + "encoding/json" + "fmt" + "log" + "net/http/httputil" + "net/url" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func dataSourceUserRead(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + + log.Printf("[INFO] paperspace dataSourceUserRead Client ready") + + queryParam := false + queryStr := "?" + id, ok := d.GetOk("id") + if ok { + queryStr += "id=" + url.QueryEscape(id.(string)) + queryParam = true + } + email, ok := d.GetOk("email") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "email=" + url.QueryEscape(email.(string)) + queryParam = true + } + firstname, ok := d.GetOk("firstname") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "firstname=" + url.QueryEscape(firstname.(string)) + queryParam = true + } + lastname, ok := d.GetOk("lastname") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "lastname=" + url.QueryEscape(lastname.(string)) + queryParam = true + } + dtCreated, ok := d.GetOk("dt_created") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "dtCreated=" + url.QueryEscape(dtCreated.(string)) + queryParam = true + } + teamId, ok := d.GetOk("team_id") + if ok { + if queryParam { + queryStr += "&" + } + queryStr += "teamId=" + url.QueryEscape(teamId.(string)) + queryParam = true + } + if !queryParam { + return fmt.Errorf("Error reading paperspace user: must specify query filter properties") + } + + url := fmt.Sprintf("%s/users/getUsers%s", paperspaceClient.APIHost, queryStr) + req, err := paperspaceClient.NewHttpRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("Error constructing GetUsers request: %s", err) + } + requestDump, err := httputil.DumpRequest(req, true) + if err != nil { + return fmt.Errorf("Error constructing GetUsers request: %s", err) + } + log.Print("[INFO] Request:", string(requestDump)) + + resp, err := paperspaceClient.HttpClient.Do(req) + if err != nil { + return fmt.Errorf("Error reading paperspace user: %s", err) + } + defer resp.Body.Close() + + statusCode := resp.StatusCode + log.Printf("[INFO] paperspace dataSourceUserRead StatusCode: %v", statusCode) + if statusCode == 404 { + return fmt.Errorf("Error reading paperspace user: users not found") + } + if statusCode != 200 { + responseDump, _ := httputil.DumpResponse(resp, true) + return fmt.Errorf("Error reading paperspace user: Response: %s", string(responseDump)) + } + + var f interface{} + err = json.NewDecoder(resp.Body).Decode(&f) + if err != nil { + return fmt.Errorf("Error decoding GetUsers response body: %s", err) + } + LogHttpResponse("paperspace dataSourceUserRead", req.URL, resp, f, err) + + mpa := f.([]interface{}) + if len(mpa) > 1 { + return fmt.Errorf("Error reading paperspace user: found more than one user matching given properties") + } + if len(mpa) == 0 { + return fmt.Errorf("Error reading paperspace user: no user found matching given properties") + } + + mp, ok := mpa[0].(map[string]interface{}) + if !ok { + return fmt.Errorf("Error unmarshalling paperspace user read response: no users not found") + } + + idr, _ := mp["id"].(string) + if idr == "" { + return fmt.Errorf("Error unmarshalling paperspace user read response: no user id found for user") + } + + log.Printf("[INFO] paperspace dataSourceUserRead user id: %v", idr) + + SetResData(d, mp, "email") + SetResData(d, mp, "firstname") + SetResData(d, mp, "lastname") + SetResDataFrom(d, mp, "dt_created", "dtCreated") + SetResDataFrom(d, mp, "team_id", "teamId") + + d.SetId(idr) + + return nil +} + +func dataSourceUser() *schema.Resource { + return &schema.Resource{ + Read: dataSourceUserRead, + + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "email": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "firstname": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "lastname": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "dt_created": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "team_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} diff --git a/pkg/provider/main.tf b/pkg/provider/main.tf new file mode 100644 index 0000000..046a2d9 --- /dev/null +++ b/pkg/provider/main.tf @@ -0,0 +1,45 @@ +provider "paperspace" { + region = "East Coast (NY2)" + api_key = "1be4f97..." // modify this to use your actual api key +} + +data "paperspace_template" "my-template-1" { + id = "t04azgph" // this is one of the Ubuntu Server 18.04 templates +} + +data "paperspace_user" "my-user-1" { + email = "me@mycompany.com" // change to the email address of a user on your paperspace team + team_id = "te1234567" +} + +resource "paperspace_script" "my-script-1" { + name = "My Script" + description = "a short description" + script_text = < index.html +ufw allow 8080 +nohup busybox httpd -f -p 8080 & +EOF + is_enabled = true + run_once = false +} + +resource "paperspace_machine" "my-machine-1" { + region = "East Coast (NY2)" // optional, defaults to provider region if not specified + name = "Terraform Test" + machine_type = "C1" + size = 50 + billing_type = "hourly" + assign_public_ip = true // optional, remove if you don't want a public ip assigned + template_id = data.paperspace_template.my-template-1.id + user_id = data.paperspace_user.my-user-1.id // optional, remove to default + team_id = data.paperspace_user.my-user-1.team_id + script_id = paperspace_script.my-script-1.id // optional, remove for no script + shutdown_timeout_in_hours = 42 + # live_forever = true # enable this to make the machine have no shutdown timeout +} + +resource "paperspace_network" "network" { + team_id = 00000 // change to your team's actual database id (unlike team_id everywhere else, which is your team handle) +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go new file mode 100644 index 0000000..e29dbf7 --- /dev/null +++ b/pkg/provider/provider.go @@ -0,0 +1,117 @@ +package provider + +import ( + "log" + "os" + + "github.com/Paperspace/paperspace-go" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func Provider() *schema.Provider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "api_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: envDefaultFunc("PAPERSPACE_API_KEY"), + }, + "api_host": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: envDefaultFuncAllowMissingDefault("PAPERSPACE_API_HOST", "https://api.paperspace.io"), + }, + "region": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: envDefaultFuncAllowMissing("PAPERSPACE_REGION"), + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "paperspace_autoscaling_group": resourceAutoscalingGroup(), + "paperspace_machine": resourceMachine(), + "paperspace_network": resourceNetwork(), + "paperspace_script": resourceScript(), + }, + + DataSourcesMap: map[string]*schema.Resource{ + "paperspace_job_storage": dataSourceJobStorage(), + "paperspace_network": dataSourceNetwork(), + "paperspace_template": dataSourceTemplate(), + "paperspace_user": dataSourceUser(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func envDefaultFunc(k string) schema.SchemaDefaultFunc { + return func() (interface{}, error) { + if v := os.Getenv(k); v != "" { + return v, nil + } + + return nil, nil + } +} + +func envDefaultFuncAllowMissing(k string) schema.SchemaDefaultFunc { + return func() (interface{}, error) { + v := os.Getenv(k) + return v, nil + } +} + +func envDefaultFuncAllowMissingDefault(k string, d string) schema.SchemaDefaultFunc { + return func() (interface{}, error) { + if v := os.Getenv(k); v != "" { + return v, nil + } + + return d, nil + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := ClientConfig{ + APIKey: d.Get("api_key").(string), + APIHost: d.Get("api_host").(string), + Region: d.Get("region").(string), + } + + log.Printf("[INFO] paperspace provider api_key %v", config.APIKey) + log.Printf("[INFO] paperspace provider api_host %v", config.APIHost) + if config.Region != "" { + log.Printf("[INFO] paperspace provider region %v", config.Region) + } + + return config, nil +} + +func newInternalPaperspaceClient(v interface{}) PaperspaceClient { + config, ok := v.(ClientConfig) + if !ok { + return PaperspaceClient{} + } + + return config.Client() +} + +func newPaperspaceClient(v interface{}) *paperspace.Client { + client := paperspace.NewClient() + config, ok := v.(ClientConfig) + if !ok { + return paperspace.NewClient() + } + + apiBackend := paperspace.NewAPIBackend() + if config.APIHost != "" { + apiBackend.BaseURL = config.APIHost + } + + client = paperspace.NewClientWithBackend(paperspace.Backend(apiBackend)) + client.APIKey = config.APIKey + + return client +} diff --git a/pkg/provider/resource_autoscaling_group.go b/pkg/provider/resource_autoscaling_group.go new file mode 100644 index 0000000..9b6f68b --- /dev/null +++ b/pkg/provider/resource_autoscaling_group.go @@ -0,0 +1,178 @@ +package provider + +import ( + "time" + + "github.com/Paperspace/paperspace-go" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func ErrNotFound(err error) bool { + paperspaceError, ok := err.(*paperspace.PaperspaceError) + if ok { + if paperspaceError.Status == 404 { + return true + } + } + + return false +} + +func resourceAutoscalingGroupCreate(d *schema.ResourceData, m interface{}) error { + var autoscalingGroup paperspace.AutoscalingGroup + + paperspaceClient := newPaperspaceClient(m) + autoscalingGroupCreateParams := paperspace.AutoscalingGroupCreateParams{ + Name: d.Get("name").(string), + ClusterID: d.Get("cluster_id").(string), + Min: d.Get("min").(int), + Max: d.Get("max").(int), + MachineType: d.Get("machine_type").(string), + TemplateID: d.Get("template_id").(string), + NetworkID: d.Get("network_id").(string), + ScriptID: d.Get("startup_script_id").(string), + } + + err := resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + var err error + autoscalingGroup, err = paperspaceClient.CreateAutoscalingGroup(autoscalingGroupCreateParams) + if err != nil { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(nil) + }) + if err != nil { + return err + } + + d.SetId(autoscalingGroup.ID) + + return resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + if err := resourceAutoscalingGroupRead(d, m); err != nil { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(nil) + }) +} + +func resourceAutoscalingGroupRead(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newPaperspaceClient(m) + + autoscalingGroup, err := paperspaceClient.GetAutoscalingGroup(d.Id(), paperspace.AutoscalingGroupGetParams{}) + if err != nil { + if ErrNotFound(err) { + d.SetId("") + return nil + } + + return err + } + + d.Set("name", autoscalingGroup.Name) + d.Set("machine_type", autoscalingGroup.MachineType) + d.Set("template_id", autoscalingGroup.TemplateID) + d.Set("network_id", autoscalingGroup.NetworkID) + d.Set("startup_script_id", autoscalingGroup.ScriptID) + + return nil +} + +func resourceAutoscalingGroupUpdate(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newPaperspaceClient(m) + autoscalingGroupUpdateParams := paperspace.AutoscalingGroupUpdateParams{ + Attributes: paperspace.AutoscalingGroupUpdateAttributeParams{ + Name: d.Get("name").(string), + TemplateID: d.Get("template_id").(string), + NetworkID: d.Get("network_id").(string), + ScriptID: d.Get("startup_script_id").(string), + }, + } + + err := resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + if err := paperspaceClient.UpdateAutoscalingGroup(d.Id(), autoscalingGroupUpdateParams); err != nil { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(nil) + }) + if err != nil { + return err + } + + return resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + if err := resourceAutoscalingGroupRead(d, m); err != nil { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(nil) + }) + +} + +func resourceAutoscalingGroupDelete(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newPaperspaceClient(m) + + return resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError { + if err := paperspaceClient.DeleteAutoscalingGroup(d.Id(), paperspace.AutoscalingGroupDeleteParams{}); err != nil { + if ErrNotFound(err) { + return resource.NonRetryableError(nil) + } + return resource.RetryableError(err) + } + + return resource.NonRetryableError(nil) + }) +} + +func resourceAutoscalingGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAutoscalingGroupCreate, + Read: resourceAutoscalingGroupRead, + Update: resourceAutoscalingGroupUpdate, + Delete: resourceAutoscalingGroupDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "min": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "max": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "cluster_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "machine_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "template_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "network_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "startup_script_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(1 * time.Minute), + Delete: schema.DefaultTimeout(1 * time.Minute), + }, + } +} diff --git a/pkg/provider/resource_machine.go b/pkg/provider/resource_machine.go new file mode 100644 index 0000000..3141e98 --- /dev/null +++ b/pkg/provider/resource_machine.go @@ -0,0 +1,321 @@ +package provider + +import ( + "encoding/json" + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceMachineCreate(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + + region := paperspaceClient.Region + if r, ok := d.GetOk("region"); ok { + region = r.(string) + } + if region == "" { + return fmt.Errorf("Error creating paperspace machine: missing region") + } + + body := make(MapIf) + body.AppendV(d, "region", region) + body.AppendAs(d, "machine_type", "machineType") + body.Append(d, "size") + body.AppendAs(d, "billing_type", "billingType") + body.AppendAs(d, "name", "machineName") + body.AppendAs(d, "template_id", "templateId") + body.AppendAsIfSet(d, "assign_public_ip", "assignPublicIp") + body.AppendAsIfSet(d, "user_id", "userId") + body.AppendAsIfSet(d, "team_id", "teamId") + body.AppendAsIfSet(d, "script_id", "scriptId") + body.AppendAsIfSet(d, "network_id", "networkId") + body.AppendAsIfSet(d, "shutdown_timeout_in_hours", "shutdownTimeoutInHours") + body.AppendAsIfSet(d, "is_managed", "isManaged") + + s := d.Get("live_forever") + if s.(bool) == true { + body["shutdownTimeoutInHours"] = nil + } + + // fields not tested when this project was picked back up for https://github.com/Paperspace/terraform-provider-paperspace/pull/3 + body.AppendIfSet(d, "email") + body.AppendIfSet(d, "password") + body.AppendAsIfSet(d, "firstname", "firstName") + body.AppendAsIfSet(d, "lastname", "lastName") + body.AppendAsIfSet(d, "notification_email", "notificationEmail") + + data, _ := json.MarshalIndent(body, "", " ") + + id, err := paperspaceClient.CreateMachine(data) + if err != nil { + return err + } + d.SetId(id) + + return resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + body, err := paperspaceClient.GetMachine(id) + if err != nil { + return resource.RetryableError(err) + } + + state, ok := body["state"].(string) + if !ok { + return resource.RetryableError(fmt.Errorf("[WARNING] Expected machine to be ready but found no state")) + } + if state != "ready" { + return resource.RetryableError(fmt.Errorf("[INFO] Expected machine to be ready but was in state %s", state)) + } + + return resource.NonRetryableError(resourceMachineRead(d, m)) + }) +} + +func resourceMachineRead(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + + body, err := paperspaceClient.GetMachine(d.Id()) + if err != nil { + if err.Error() == MachineNotFoundError { + d.SetId("") + return nil + } + + return err + } + + SetResData(d, body, "name") + SetResData(d, body, "os") + SetResData(d, body, "ram") + SetResData(d, body, "cpus") + SetResData(d, body, "gpu") + SetResDataFrom(d, body, "storage_total", "storageTotal") + SetResDataFrom(d, body, "storage_used", "storageUsed") + SetResDataFrom(d, body, "usage_rate", "usageRate") + + shutdown_timeout := d.Get("shutdown_timeout_in_hours") + _, ok := shutdown_timeout.(int32) + if ok { + SetResDataFrom(d, body, "shutdown_timeout_in_hours", "shutdownTimeoutInHours") + } + + SetResDataFrom(d, body, "shutdown_timeout_forces", "shutdownTimeoutForces") + SetResDataFrom(d, body, "perform_auto_snapshot", "performAutoSnapshot") + SetResDataFrom(d, body, "auto_snapshot_frequency", "autoSnapshotFrequency") + SetResDataFrom(d, body, "auto_snapshot_save_count", "autoSnapshotSaveCount") + SetResDataFrom(d, body, "agent_type", "agentType") + SetResDataFrom(d, body, "dt_created", "dtCreated") + SetResData(d, body, "state") + SetResDataFrom(d, body, "network_id", "networkId") //overlays with null initially + SetResDataFrom(d, body, "private_ip_address", "privateIpAddress") + SetResDataFrom(d, body, "public_ip_address", "publicIpAddress") + SetResData(d, body, "region") //overlays with null initially + SetResDataFrom(d, body, "user_id", "userId") + SetResDataFrom(d, body, "team_id", "teamId") + SetResDataFrom(d, body, "script_id", "scriptId") + SetResDataFrom(d, body, "dt_last_run", "dtLastRun") + SetResDataFrom(d, body, "is_managed", "isManaged") + + return nil +} + +func resourceMachineUpdate(d *schema.ResourceData, m interface{}) error { + return resourceMachineRead(d, m) +} + +func resourceMachineDelete(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + + err := paperspaceClient.DeleteMachine(d.Id()) + if err != nil { + if err.Error() == MachineDeleteNotFoundError { + d.SetId("") + return nil + } + return err + } + + return resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError { + body, err := paperspaceClient.GetMachine(d.Id()) + log.Printf("\nbody: %v\nerr: %v", body, err) + if err != nil { + if strings.Contains(err.Error(), "machine not found") { + return resource.NonRetryableError(nil) + } + return resource.RetryableError(err) + } + + return resource.RetryableError(fmt.Errorf("Expected machine to be deleted but still exists")) + }) +} + +func resourceMachine() *schema.Resource { + return &schema.Resource{ + Create: resourceMachineCreate, + Read: resourceMachineRead, + Update: resourceMachineUpdate, + Delete: resourceMachineDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "machine_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "size": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "billing_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "template_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "assign_public_ip": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "network_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "team_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "user_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "email": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "password": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "firstname": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "lastname": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "notification_email": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "script_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "dt_last_run": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "os": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "ram": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "cpus": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + "gpu": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "storage_total": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "storage_used": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "usage_rate": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "shutdown_timeout_in_hours": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "shutdown_timeout_forces": &schema.Schema{ + Type: schema.TypeBool, + Computed: true, + }, + "perform_auto_snapshot": &schema.Schema{ + Type: schema.TypeBool, + Computed: true, + }, + "auto_snapshot_frequency": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "auto_snapshot_save_count": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "agent_type": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "dt_created": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "state": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "private_ip_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "public_ip_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "live_forever": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "is_managed": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + } +} diff --git a/pkg/provider/resource_network.go b/pkg/provider/resource_network.go new file mode 100644 index 0000000..f328279 --- /dev/null +++ b/pkg/provider/resource_network.go @@ -0,0 +1,159 @@ +package provider + +import ( + "fmt" + "math/rand" + "strconv" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +// adopted from https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go/22892986#22892986 +var chars = []rune("0123456789abcdefghijklmnopqrstuvwxyz") +var networkCreateTimeout = "2m" +var networkDefaultTimeout = "1m" + +func randSeq(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = chars[rand.Intn(len(chars))] + } + return string(b) +} + +func networkHandle() string { + rand.Seed(time.Now().UnixNano()) + + return fmt.Sprint("managed_network_" + randSeq(7)) +} + +func updateNetworkSchema(d *schema.ResourceData, network Network, name string) { + d.Set("handle", network.Handle) + d.Set("is_taken", network.IsTaken) + d.Set("name", name) + d.Set("netmask", network.Netmask) + d.Set("network", network.Network) + d.Set("vlan_id", network.VlanID) +} + +func resourceNetworkCreate(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + teamID, ok := d.Get("team_id").(int) + if !ok { + return fmt.Errorf("team_id is not an int") + } + + regionId, ok := RegionMap[paperspaceClient.Region] + if !ok { + return fmt.Errorf("Region %s not found", paperspaceClient.Region) + } + + name := networkHandle() + + createNamedNetworkParams := CreateTeamNamedNetworkParams{ + Name: name, + RegionId: regionId, + } + + if err := paperspaceClient.CreateTeamNamedNetwork(teamID, createNamedNetworkParams); err != nil { + return fmt.Errorf("Error creating private network: %s", err) + } + + return resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + paperspaceClient := newInternalPaperspaceClient(m) + + // XXX: potential race condition for multiple networks created with the name concurrently + // Add sync API response to API + namedNetwork, err := paperspaceClient.GetTeamNamedNetwork(teamID, name) + if err != nil { + return resource.RetryableError(fmt.Errorf("Error creating private network: %s", err)) + } + + d.SetId(strconv.Itoa(namedNetwork.Network.ID)) + return resource.NonRetryableError(resourceNetworkRead(d, m)) + }) +} + +func resourceNetworkRead(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + teamID, ok := d.Get("team_id").(int) + if !ok { + return fmt.Errorf("team_id is not an int") + } + + namedNetwork, err := paperspaceClient.GetTeamNamedNetworkById(teamID, d.Id()) + if err != nil { + d.SetId("") + return err + } + + d.SetId(strconv.Itoa(namedNetwork.Network.ID)) + updateNetworkSchema(d, namedNetwork.Network, namedNetwork.Name) + + return nil +} + +func resourceNetworkUpdate(d *schema.ResourceData, m interface{}) error { + // TODO: implement; api doesn't exist yet + return resourceNetworkRead(d, m) +} + +func resourceNetworkDelete(d *schema.ResourceData, m interface{}) error { + // TODO: implement; api doesn't exist yet + d.SetId("") + return nil +} + +func resourceNetwork() *schema.Resource { + createTimeout, _ := time.ParseDuration(networkCreateTimeout) + defaultTimeout, _ := time.ParseDuration(networkDefaultTimeout) + return &schema.Resource{ + Create: resourceNetworkCreate, + Read: resourceNetworkRead, + Update: resourceNetworkUpdate, + Delete: resourceNetworkDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Timeouts: &schema.ResourceTimeout{ + Create: &createTimeout, + Default: &defaultTimeout, + }, + + Schema: map[string]*schema.Schema{ + "team_id": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "handle": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "is_taken": &schema.Schema{ + Type: schema.TypeBool, + Computed: true, + }, + "netmask": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "network": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "vlan_id": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + // name is not on the network schema but rather part of what we're calling here + // the "named network response", which comes from /getNetworks and includes the + // network and its name as joined with the network_owners table. + "name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} diff --git a/pkg/provider/resource_script.go b/pkg/provider/resource_script.go new file mode 100644 index 0000000..46186cf --- /dev/null +++ b/pkg/provider/resource_script.go @@ -0,0 +1,262 @@ +package provider + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http/httputil" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceScriptCreate(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + + log.Printf("[INFO] paperspace resourceScriptCreate Client ready") + + region := paperspaceClient.Region + if r, ok := d.GetOk("region"); ok { + region = r.(string) + } + if region == "" { + return fmt.Errorf("Error creating paperspace script: missing region") + } + + body := make(MapIf) + body.AppendAs(d, "name", "scriptName") + body.AppendAs(d, "script_text", "scriptText") + body.AppendAsIfSet(d, "description", "scriptDescription") + body.AppendAsIfSet(d, "is_enabled", "isEnabled") + body.AppendAsIfSet(d, "run_once", "runOnce") + + data, _ := json.MarshalIndent(body, "", " ") + log.Println(string(data)) + + url := fmt.Sprintf("%s/scripts/createScript", paperspaceClient.APIHost) + req, err := paperspaceClient.NewHttpRequest("POST", url, bytes.NewBuffer(data)) + if err != nil { + return fmt.Errorf("Error constructing CreateScript request: %s", err) + } + requestDump, err := httputil.DumpRequest(req, true) + if err != nil { + return fmt.Errorf("Error constructing CreateScript request: %s", err) + } + log.Print("[INFO] Request:", string(requestDump)) + + resp, err := paperspaceClient.HttpClient.Do(req) + if err != nil { + return fmt.Errorf("Error creating paperspace script: %s", err) + } + defer resp.Body.Close() + + statusCode := resp.StatusCode + log.Printf("[INFO] paperspace resourceScriptCreate StatusCode: %v", statusCode) + if statusCode != 200 { + responseDump, _ := httputil.DumpResponse(resp, true) + return fmt.Errorf("Error reading paperspace script: Response: %s", string(responseDump)) + } + + var f interface{} + err = json.NewDecoder(resp.Body).Decode(&f) + if err != nil { + return fmt.Errorf("Error decoding GetScript response body: %s", err) + } + LogHttpResponse("paperspace dataSourceGetScript", req.URL, resp, f, err) + + mp := f.(map[string]interface{}) + id, _ := mp["id"].(string) + + if id == "" { + return fmt.Errorf("Error in paperspace script create data: id not found") + } + + log.Printf("[INFO] paperspace resourceScriptCreate returned id: %v", id) + + SetResData(d, mp, "name") + SetResData(d, mp, "description") + SetResDataFrom(d, mp, "owner_type", "ownerType") + SetResDataFrom(d, mp, "owner_id", "ownerId") + SetResDataFrom(d, mp, "dt_created", "dtCreated") + SetResDataFrom(d, mp, "is_enabled", "isEnabled") + SetResDataFrom(d, mp, "run_once", "runOnce") + + d.SetId(id) + + return resourceScriptRead(d, m) +} + +func resourceScriptRead(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + + log.Printf("[INFO] paperspace resourceScriptRead Client ready") + + url := fmt.Sprintf("%s/scripts/getScript?scriptId=%s", paperspaceClient.APIHost, d.Id()) + req, err := paperspaceClient.NewHttpRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("Error constructing GetScript request: %s", err) + } + + resp, err := paperspaceClient.HttpClient.Do(req) + if err != nil { + return fmt.Errorf("Error completing GetScript request: %s", err) + } + defer resp.Body.Close() + + statusCode := resp.StatusCode + log.Printf("[INFO] paperspace resourceScriptRead StatusCode: %v", statusCode) + if statusCode == 404 { + log.Printf("[INFO] paperspace resourceScriptRead scriptId not found; removing resource %s", d.Id()) + d.SetId("") + return nil + } + var body interface{} + json.NewDecoder(resp.Body).Decode(&body) + LogHttpResponse("paperspace resourceScriptCreate", req.URL, resp, body, err) + + if statusCode != 200 { + return fmt.Errorf("Error reading paperspace script: Response: %s", body) + } + + if err != nil { + return fmt.Errorf("Error unmarshalling paperspace script read response: %s", err) + } + + mp := body.(map[string]interface{}) + id, _ := mp["id"].(string) + + if id == "" { + log.Printf("[WARNING] paperspace resourceScriptRead script id not found; removing resource %s", d.Id()) + d.SetId("") + return nil + } + + log.Printf("[INFO] paperspace resourceScriptRead returned id: %v", id) + + SetResData(d, mp, "name") + SetResData(d, mp, "description") + SetResDataFrom(d, mp, "owner_type", "ownerType") + SetResDataFrom(d, mp, "owner_id", "ownerId") + SetResDataFrom(d, mp, "dt_created", "dtCreated") + SetResDataFrom(d, mp, "is_enabled", "isEnabled") + SetResDataFrom(d, mp, "run_once", "runOnce") + + url = fmt.Sprintf("%s/scripts/getScriptText?scriptId=%s", paperspaceClient.APIHost, d.Id()) + req, err = paperspaceClient.NewHttpRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("Error constructing GetScriptText request: %s", err) + } + + resp, err = paperspaceClient.HttpClient.Do(req) + if err != nil { + return fmt.Errorf("Error reading paperspace script text: %s", err) + } + defer resp.Body.Close() + + statusCode = resp.StatusCode + log.Printf("[INFO] paperspace resourceScriptRead text StatusCode: %v", statusCode) + + json.NewDecoder(resp.Body).Decode(&body) + s, err := json.Marshal(body) + LogHttpResponse("paperspace resourceScriptCreate", req.URL, resp, body, err) + + if statusCode == 404 { + log.Printf("[INFO] paperspace resourceScriptRead text scriptId not found") + return nil + } + if statusCode != 200 { + return fmt.Errorf("Error reading paperspace script text: Response: %s", body) + } + + d.Set("script_text", s) + + return nil +} + +func resourceScriptUpdate(d *schema.ResourceData, m interface{}) error { + + log.Printf("[INFO] paperspace resourceScriptUpdate Client ready") + + return resourceScriptRead(d, m) +} + +func resourceScriptDelete(d *schema.ResourceData, m interface{}) error { + paperspaceClient := newInternalPaperspaceClient(m) + + log.Printf("[INFO] paperspace resourceScriptDelete Client ready") + + url := fmt.Sprintf("%s/scripts/%s/destroy", paperspaceClient.APIHost, d.Id()) + req, err := paperspaceClient.NewHttpRequest("POST", url, nil) + if err != nil { + return fmt.Errorf("Error constructing DeleteScript request: %s", err) + } + resp, err := paperspaceClient.HttpClient.Do(req) + if err != nil { + return fmt.Errorf("Error deleting paperspace script: %s", err) + } + defer resp.Body.Close() + + statusCode := resp.StatusCode + log.Printf("[INFO] paperspace resourceScriptDelete StatusCode: %v", statusCode) + LogHttpResponse("paperspace resourceScriptDelete", req.URL, resp, nil, err) + if statusCode != 204 && statusCode != 404 { + return fmt.Errorf("Error deleting paperspace script: Response: %s", resp.Body) + } + if statusCode == 204 { + log.Printf("[INFO] paperspace resourceScriptDelete script deleted successfully, StatusCode: %v", statusCode) + } + if statusCode == 404 { + log.Printf("[INFO] paperspace resourceScriptDelete script already deleted, StatusCode: %v", statusCode) + } + + return nil +} + +func resourceScript() *schema.Resource { + return &schema.Resource{ + Create: resourceScriptCreate, + Read: resourceScriptRead, + Update: resourceScriptUpdate, + Delete: resourceScriptDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "script_text": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "owner_type": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "owner_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "dt_created": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "is_enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "run_once": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + }, + } +} diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index d6f3e57..0000000 --- a/src/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/* -!/terraform-provider-paperspace