From 23415ca2ff3b441798ffb34e41b83a22d7349729 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Thu, 31 May 2018 15:40:02 +1000 Subject: [PATCH] add member resource --- README.md | 2 +- zerotier/client.go | 120 ++++++++++++-- zerotier/provider.go | 1 + zerotier/resource_zerotier_member.go | 221 ++++++++++++++++++++++++++ zerotier/resource_zerotier_network.go | 5 +- 5 files changed, 332 insertions(+), 17 deletions(-) create mode 100644 zerotier/resource_zerotier_member.go diff --git a/README.md b/README.md index 1b2c853..0b9e87d 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ resource "zerotier_network" "your_network" { route { target = "${var.zt_cidr}" } - rules_source = "${file(ztr.conf)}" + rules_source = "${file("ztr.conf")}" } ``` diff --git a/zerotier/client.go b/zerotier/client.go index 0211f0e..00e0a85 100644 --- a/zerotier/client.go +++ b/zerotier/client.go @@ -92,18 +92,31 @@ type TagByName struct { Flags map[string]int `json:"flags"` } -func (n *Network) Compile() error { - return nil - // compiled, err := CompileRulesSource([]byte(n.RulesSource)) - // if err != nil { - // return err - // } - // n.Config.Rules = compiled.Config.Rules - // n.Config.Tags = compiled.Config.Tags - // n.Config.Capabilities = compiled.Config.Capabilities - // n.TagsByName = compiled.TagsByName - // n.CapabilitiesByName = compiled.CapabilitiesByName - // return nil +type Member struct { + Id string `json:"id"` + NetworkId string `json:"networkId"` + NodeId string `json:"nodeId"` + OfflineNotifyDelay int `json:"offlineNotifyDelay"` // milliseconds + Name string `json:"name"` + Description string `json:"description"` + Hidden bool `json:"hidden"` + Config *MemberConfig `json:"config"` +} +type MemberConfig struct { + Authorized bool `json:"authorized"` + Capabilities []int `json:"capabilities"` + Tags [][]int `json:"tags"` // array of [tag id, value] tuples + ActiveBridge bool `json:"activeBridge"` + NoAutoAssignIps bool `json:"noAutoAssignIps"` + IpAssignments []string `json:"ipAssignments"` +} +type MemberConfigReadOnly struct { + CreationTime int `json:"creationTime"` + LastAuthorizedTime int `json:"lastAuthorizedTime"` + VMajor int `json:"vMajor"` + VMinor int `json:"vMinor"` + VRev int `json:"vRev"` + VProto int `json:"vProto"` } func CIDRToRange(cidr string) (net.IP, net.IP, error) { @@ -269,3 +282,86 @@ func (client *ZeroTierClient) DeleteNetwork(id string) error { _, err = client.doRequest("DeleteNetwork", req) return err } + +///////////// +// members // +///////////// + +func (client *ZeroTierClient) GetMember(nwid string, nodeId string) (*Member, error) { + url := fmt.Sprintf(baseUrl+"/network/%s/member/%s", nwid, nodeId) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + bytes, err := client.doRequest("GetMember", req) + if err != nil { + return nil, err + } + var data Member + err = json.Unmarshal(bytes, &data) + if err != nil { + return nil, err + } + return &data, nil +} + +func (client *ZeroTierClient) postMember(member *Member, reqName string) (*Member, error) { + url := fmt.Sprintf(baseUrl+"/network/%s/member/%s", member.NetworkId, member.NodeId) + j, err := json.Marshal(member) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", url, bytes.NewBuffer(j)) + if err != nil { + return nil, err + } + bytes, err := client.doRequest(reqName, req) + if err != nil { + return nil, err + } + var data Member + err = json.Unmarshal(bytes, &data) + if err != nil { + return nil, err + } + return &data, nil +} + +func (client *ZeroTierClient) CreateMember(member *Member) (*Member, error) { + return client.postMember(member, "CreateMember") +} + +func (client *ZeroTierClient) UpdateMember(member *Member) (*Member, error) { + return client.postMember(member, "UpdateMember") +} + +// Careful: this one isn't documented in the Zt API, +// but this is what the Central web client does. +func (client *ZeroTierClient) DeleteMember(member *Member) error { + url := fmt.Sprintf(baseUrl+"/network/%s/member/%s", member.NetworkId, member.NodeId) + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + _, err = client.doRequest("DeleteMember", req) + return err +} + +func (client *ZeroTierClient) CheckMemberExists(nwid string, nodeId string) (bool, error) { + url := fmt.Sprintf(baseUrl+"/network/%s/member/%s", nwid, nodeId) + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return false, err + } + resp, err := client.headRequest(req) + if resp.StatusCode == 404 { + return false, nil + } + if resp.StatusCode == 403 { + return false, fmt.Errorf("CheckMemberExists received a %s response. Check your ZEROTIER_API_KEY.", resp.Status) + } + if resp.StatusCode != 200 { + return false, fmt.Errorf("CheckMemberExists received response: %s", resp.Status) + } + return true, err +} diff --git a/zerotier/provider.go b/zerotier/provider.go index ca135e3..77a5312 100644 --- a/zerotier/provider.go +++ b/zerotier/provider.go @@ -16,6 +16,7 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ "zerotier_network": resourceZeroTierNetwork(), + "zerotier_member": resourceZeroTierMember(), }, ConfigureFunc: configureProvider, } diff --git a/zerotier/resource_zerotier_member.go b/zerotier/resource_zerotier_member.go new file mode 100644 index 0000000..5cd2d42 --- /dev/null +++ b/zerotier/resource_zerotier_member.go @@ -0,0 +1,221 @@ +package main + +import ( + "fmt" + "strconv" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceZeroTierMember() *schema.Resource { + return &schema.Resource{ + Create: resourceMemberCreate, + Read: resourceMemberRead, + Update: resourceMemberUpdate, + Delete: resourceMemberDelete, + Exists: resourceMemberExists, + + Schema: map[string]*schema.Schema{ + "network_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "node_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Default: "Managed by Terraform", + }, + "hidden": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "offline_notify_delay": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + "authorized": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "allow_ethernet_bridging": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "no_auto_assign_ips": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "ip_assignments": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "capabilities": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + }, + "tags": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + }, + }, + } +} + +func resourceMemberCreate(d *schema.ResourceData, m interface{}) error { + client := m.(*ZeroTierClient) + stored, err := memberFromResourceData(d) + if err != nil { + return err + } + created, err := client.CreateMember(stored) + if err != nil { + return err + } + d.SetId(created.Id) + setTags(d, created) + return nil +} + +func resourceMemberUpdate(d *schema.ResourceData, m interface{}) error { + client := m.(*ZeroTierClient) + stored, err := memberFromResourceData(d) + if err != nil { + return err + } + updated, err := client.UpdateMember(stored) + if err != nil { + return fmt.Errorf("unable to update member using ZeroTier API: %s", err) + } + setTags(d, updated) + return nil +} + +func setTags(d *schema.ResourceData, member *Member) { + rawTags := map[string]int{} + for _, tuple := range member.Config.Tags { + key := fmt.Sprintf("%d", tuple[0]) + val := tuple[1] + rawTags[key] = val + } +} + +func resourceMemberDelete(d *schema.ResourceData, m interface{}) error { + client := m.(*ZeroTierClient) + member, err := memberFromResourceData(d) + if err != nil { + return err + } + err = client.DeleteMember(member) + return err +} + +func memberFromResourceData(d *schema.ResourceData) (*Member, error) { + tags := d.Get("tags").(map[string]interface{}) + tagTuples := [][]int{} + for key, val := range tags { + i, err := strconv.Atoi(key) + if err != nil { + break + } + tagTuples = append(tagTuples, []int{i, val.(int)}) + } + capsRaw := d.Get("capabilities").([]interface{}) + caps := make([]int, len(capsRaw)) + for i := range capsRaw { + caps[i] = capsRaw[i].(int) + } + ipsRaw := d.Get("ip_assignments").([]interface{}) + ips := make([]string, len(ipsRaw)) + for i := range ipsRaw { + ips[i] = ipsRaw[i].(string) + } + n := &Member{ + Id: d.Id(), + NetworkId: d.Get("network_id").(string), + NodeId: d.Get("node_id").(string), + Hidden: d.Get("hidden").(bool), + OfflineNotifyDelay: d.Get("offline_notify_delay").(int), + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Config: &MemberConfig{ + Authorized: d.Get("authorized").(bool), + ActiveBridge: d.Get("allow_ethernet_bridging").(bool), + NoAutoAssignIps: d.Get("no_auto_assign_ips").(bool), + Capabilities: caps, + Tags: tagTuples, + IpAssignments: ips, + }, + } + return n, nil +} +func resourceMemberRead(d *schema.ResourceData, m interface{}) error { + client := m.(*ZeroTierClient) + + // Attempt to read from an upstream API + nwid := d.Get("network_id").(string) + nodeId := d.Get("node_id").(string) + member, err := client.GetMember(nwid, nodeId) + + // If the resource does not exist, inform Terraform. We want to immediately + // return here to prevent further processing. + if err != nil { + return fmt.Errorf("unable to read network from API: %s", err) + } + if member == nil { + d.SetId("") + return nil + } + + d.SetId(member.Id) + d.Set("name", member.Name) + d.Set("description", member.Description) + d.Set("hidden", member.Hidden) + d.Set("offline_notify_delay", member.OfflineNotifyDelay) + d.Set("authorized", member.Config.Authorized) + d.Set("allow_ethernet_bridging", member.Config.ActiveBridge) + d.Set("no_auto_assign_ips", member.Config.NoAutoAssignIps) + d.Set("ip_assignments", member.Config.IpAssignments) + d.Set("capabilities", member.Config.Capabilities) + setTags(d, member) + + return nil +} + +func resourceMemberExists(d *schema.ResourceData, m interface{}) (b bool, e error) { + client := m.(*ZeroTierClient) + nwid := d.Get("network_id").(string) + nodeId := d.Get("node_id").(string) + exists, err := client.CheckMemberExists(nwid, nodeId) + if err != nil { + return exists, err + } + + if !exists { + d.SetId("") + } + return exists, nil +} diff --git a/zerotier/resource_zerotier_network.go b/zerotier/resource_zerotier_network.go index e13f11c..5375072 100644 --- a/zerotier/resource_zerotier_network.go +++ b/zerotier/resource_zerotier_network.go @@ -149,9 +149,6 @@ func fromResourceData(d *schema.ResourceData) (*Network, error) { IpAssignmentPools: pools, }, } - if err := n.Compile(); err != nil { - return nil, err - } return n, nil } @@ -232,7 +229,7 @@ func resourceNetworkUpdate(d *schema.ResourceData, m interface{}) error { updated, err := client.UpdateNetwork(d.Id(), n) if err != nil { stringify, _ := json.Marshal(n) - return fmt.Errorf("unable to update network from API: %s\n\n%s", err, stringify) + return fmt.Errorf("unable to update network using ZeroTier API: %s\n\n%s", err, stringify) } setAssignmentPools(d, updated) return nil