Skip to content

Commit

Permalink
feat(networks): create private networks as a resource (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
paperspace-philip authored Jun 30, 2020
1 parent 99aaa27 commit 523e927
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@
.terraform
terraform.tfstate
crash.log
*terraform.tfstate.*.backup

# Mac
.DS_Store
117 changes: 114 additions & 3 deletions src/terraform-provider-paperspace/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,31 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

var RegionMap = map[string]int{
"East Coast (NY2)": 1,
"West Coast (CA1)": 2,
"Europe (AMS1)": 3,
}

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) {
Expand Down Expand Up @@ -138,12 +163,48 @@ func (h withHeader) RoundTrip(req *http.Request) (*http.Response, error) {
return h.transport.RoundTrip(req)
}

func (paperspaceClient *PaperspaceClient) Request(operationType string, url string, data []byte) (body map[string]interface{}, statusCode int, err error) {
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 := http.NewRequest(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(operationType, url, buf)
logHttpRequestConstruction(method, url, buf)

req, err := http.NewRequest(operationType, url, buf)
req, err := http.NewRequest(method, url, buf)
if err != nil {
return nil, 0, fmt.Errorf("Error constructing request: %s", err)
}
Expand Down Expand Up @@ -222,3 +283,53 @@ func (paperspaceClient *PaperspaceClient) DeleteMachine(id string) (err error) {

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: %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 string(namedNetwork.Network.ID) == id {
return &namedNetwork, nil
}
}

return nil, fmt.Errorf("Error getting private network: %s", id)
}
4 changes: 2 additions & 2 deletions src/terraform-provider-paperspace/datasource_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func dataSourceNetworkRead(d *schema.ResourceData, m interface{}) error {
url := fmt.Sprintf("%s/networks/getNetworks%s", paperspaceClient.APIHost, queryStr)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return fmt.Errorf("Error constructing GetNetworks request: %s", err)
return fmt.Errorf("Error constructing GetTeamNamedNetworks request: %s", err)
}
requestDump, err := httputil.DumpRequest(req, true)
if err != nil {
Expand All @@ -105,7 +105,7 @@ func dataSourceNetworkRead(d *schema.ResourceData, m interface{}) error {
var f interface{}
err = json.NewDecoder(resp.Body).Decode(&f)
if err != nil {
return fmt.Errorf("Error decoding GetNetworks response body: %s", err)
return fmt.Errorf("Error decoding GetTeamNamedNetworks response body: %s", err)
}
LogHttpResponse("paperspace dataSourceNetworkRead", req.URL, resp, f, err)

Expand Down
4 changes: 4 additions & 0 deletions src/terraform-provider-paperspace/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ resource "paperspace_machine" "my-machine-1" {
team_id = data.paperspace_user.my-user-1.team_id
script_id = paperspace_script.my-script-1.id // optional, remove for no script
}

resource "paperspace_network" "network" {
team_id = 00000 // change to your actual integer team id
}
1 change: 1 addition & 0 deletions src/terraform-provider-paperspace/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func Provider() *schema.Provider {

ResourcesMap: map[string]*schema.Resource{
"paperspace_machine": resourceMachine(),
"paperspace_network": resourceNetwork(),
"paperspace_script": resourceScript(),
},

Expand Down
150 changes: 150 additions & 0 deletions src/terraform-provider-paperspace/resource_network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package main

import (
"fmt"
"math/rand"
"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")

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 := m.(PaperspaceClient)
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 := m.(PaperspaceClient)

// 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(string(namedNetwork.Network.ID))
return resource.NonRetryableError(resourceNetworkRead(d, m))
})
}

func resourceNetworkRead(d *schema.ResourceData, m interface{}) error {
paperspaceClient := m.(PaperspaceClient)
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(string(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 {
return &schema.Resource{
Create: resourceNetworkCreate,
Read: resourceNetworkRead,
Update: resourceNetworkUpdate,
Delete: resourceNetworkDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

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,
},
},
}
}

0 comments on commit 523e927

Please sign in to comment.