diff --git a/v3/ecl/network/v2/ports/requests.go b/v3/ecl/network/v2/ports/requests.go index 1cfcce21..9b314ae4 100644 --- a/v3/ecl/network/v2/ports/requests.go +++ b/v3/ecl/network/v2/ports/requests.go @@ -80,6 +80,7 @@ type CreateOpts struct { MACAddress string `json:"mac_address,omitempty"` Name string `json:"name,omitempty"` NetworkID string `json:"network_id"` + SecurityGroups *[]string `json:"security_groups,omitempty"` SegmentationID int `json:"segmentation_id,omitempty"` SegmentationType string `json:"segmentation_type,omitempty"` Tags map[string]string `json:"tags,omitempty"` @@ -118,6 +119,7 @@ type UpdateOpts struct { DeviceOwner *string `json:"device_owner,omitempty"` FixedIPs interface{} `json:"fixed_ips,omitempty"` Name *string `json:"name,omitempty"` + SecurityGroups *[]string `json:"security_groups,omitempty"` SegmentationID *int `json:"segmentation_id,omitempty"` SegmentationType *string `json:"segmentation_type,omitempty"` Tags *map[string]string `json:"tags,omitempty"` diff --git a/v3/ecl/network/v2/ports/results.go b/v3/ecl/network/v2/ports/results.go index c2f50c86..014e2461 100644 --- a/v3/ecl/network/v2/ports/results.go +++ b/v3/ecl/network/v2/ports/results.go @@ -94,6 +94,9 @@ type Port struct { // Network that this port is associated with. NetworkID string `json:"network_id"` + // SecurityGroups is the IDs of security groups applied to the port. + SecurityGroups []string `json:"security_groups"` + // SegmentationID is the segmenation ID used for this port (i.e. for vlan type it is vlan tag) SegmentationID int `json:"segmentation_id"` diff --git a/v3/ecl/network/v2/ports/testing/fixtures.go b/v3/ecl/network/v2/ports/testing/fixtures.go index c622b719..858e8a9a 100644 --- a/v3/ecl/network/v2/ports/testing/fixtures.go +++ b/v3/ecl/network/v2/ports/testing/fixtures.go @@ -288,3 +288,83 @@ var Port2 = ports.Port{ } var ExpectedPortSlice = []ports.Port{Port1, Port2} + +const CreateWithSecurityGroupsRequest = ` +{ + "port": + { + "admin_state_up": true, + "fixed_ips": [ + { + "ip_address": "192.168.2.40", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "name": "port_with_sg", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "security_groups": ["85cc3048-abc3-43cc-89b3-377341426ac5"], + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } +}` + +const CreateWithSecurityGroupsResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.40", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "port-with-sg-id", + "mac_address": "fa:16:3e:b0:ca:f2", + "name": "port_with_sg", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "security_groups": ["85cc3048-abc3-43cc-89b3-377341426ac5"], + "segmentation_id": null, + "segmentation_type": null, + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } +}` + +const UpdateWithSecurityGroupsRequest = ` +{ + "port": { + "name": "port_with_updated_sg", + "security_groups": ["85cc3048-abc3-43cc-89b3-377341426ac5", "c0e1482e-2e3c-497e-8964-e4f818071700"] + } +}` + +const UpdateWithSecurityGroupsResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.40", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "port-with-sg-id", + "mac_address": "fa:16:3e:b0:ca:f2", + "name": "port_with_updated_sg", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "security_groups": ["85cc3048-abc3-43cc-89b3-377341426ac5", "c0e1482e-2e3c-497e-8964-e4f818071700"], + "segmentation_id": null, + "segmentation_type": null, + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } +}` diff --git a/v3/ecl/network/v2/ports/testing/request_test.go b/v3/ecl/network/v2/ports/testing/request_test.go index 0fb45d8c..f1e15e4a 100644 --- a/v3/ecl/network/v2/ports/testing/request_test.go +++ b/v3/ecl/network/v2/ports/testing/request_test.go @@ -219,3 +219,71 @@ func TestDeletePort(t *testing.T) { res := ports.Delete(fake.ServiceClient(), "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730") th.AssertNoErr(t, res.Err) } + +func TestCreatePortWithSecurityGroups(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateWithSecurityGroupsRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateWithSecurityGroupsResponse) + }) + + asu := true + securityGroups := []string{"85cc3048-abc3-43cc-89b3-377341426ac5"} + + options := &ports.CreateOpts{ + AdminStateUp: &asu, + FixedIPs: []ports.IP{{ + IPAddress: "192.168.2.40", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + }}, + Name: "port_with_sg", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + SecurityGroups: &securityGroups, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", + } + p, err := ports.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, "port_with_sg", p.Name) + th.CheckDeepEquals(t, securityGroups, p.SecurityGroups) +} + +func TestUpdatePortWithSecurityGroups(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/port-with-sg-id", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateWithSecurityGroupsRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateWithSecurityGroupsResponse) + }) + + name := "port_with_updated_sg" + securityGroups := []string{"85cc3048-abc3-43cc-89b3-377341426ac5", "c0e1482e-2e3c-497e-8964-e4f818071700"} + + options := ports.UpdateOpts{ + Name: &name, + SecurityGroups: &securityGroups, + } + p, err := ports.Update(fake.ServiceClient(), "port-with-sg-id", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, name, p.Name) + th.CheckDeepEquals(t, securityGroups, p.SecurityGroups) +} diff --git a/v3/ecl/network/v2/security_group_rules/doc.go b/v3/ecl/network/v2/security_group_rules/doc.go new file mode 100644 index 00000000..cb002741 --- /dev/null +++ b/v3/ecl/network/v2/security_group_rules/doc.go @@ -0,0 +1,51 @@ +/* +Package security_group_rules contains functionality for working with ECL Security Group Rule resources. + +Security Group Rules define specific ingress and egress traffic rules for Security Groups. + +Example to List Security Group Rules + + listOpts := security_group_rules.ListOpts{ + SecurityGroupID: "security-group-id", + } + + allPages, err := security_group_rules.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRules, err := security_group_rules.ExtractSecurityGroupRules(allPages) + if err != nil { + panic(err) + } + + for _, rule := range allRules { + fmt.Printf("%+v\n", rule) + } + +Example to Create a Security Group Rule + + createOpts := security_group_rules.CreateOpts{ + Direction: "ingress", + SecurityGroupID: "security-group-id", + Ethertype: "IPv4", + Protocol: "tcp", + PortRangeMin: &[]int{22}[0], + PortRangeMax: &[]int{22}[0], + RemoteIPPrefix: &[]string{"0.0.0.0/0"}[0], + } + + rule, err := security_group_rules.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Security Group Rule + + ruleID := "rule-id" + err := security_group_rules.Delete(networkClient, ruleID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package security_group_rules diff --git a/v3/ecl/network/v2/security_group_rules/requests.go b/v3/ecl/network/v2/security_group_rules/requests.go new file mode 100644 index 00000000..e47589a8 --- /dev/null +++ b/v3/ecl/network/v2/security_group_rules/requests.go @@ -0,0 +1,100 @@ +package security_group_rules + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSecurityGroupRuleListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the security group rule attributes you want to see returned. +type ListOpts struct { + Description string `q:"description"` + Direction string `q:"direction"` + Ethertype string `q:"ethertype"` + ID string `q:"id"` + PortRangeMax int `q:"port_range_max"` + PortRangeMin int `q:"port_range_min"` + Protocol string `q:"protocol"` + RemoteGroupID string `q:"remote_group_id"` + RemoteIPPrefix string `q:"remote_ip_prefix"` + SecurityGroupID string `q:"security_group_id"` + TenantID string `q:"tenant_id"` +} + +// ToSecurityGroupRuleListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSecurityGroupRuleListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// security group rules. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSecurityGroupRuleListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SecurityGroupRulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific security group rule based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecurityGroupRuleCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a security group rule. +type CreateOpts struct { + Description string `json:"description,omitempty"` + Direction string `json:"direction" required:"true"` + Ethertype string `json:"ethertype,omitempty"` + PortRangeMax *int `json:"port_range_max,omitempty"` + PortRangeMin *int `json:"port_range_min,omitempty"` + Protocol string `json:"protocol,omitempty"` + RemoteGroupID *string `json:"remote_group_id,omitempty"` + RemoteIPPrefix *string `json:"remote_ip_prefix,omitempty"` + SecurityGroupID string `json:"security_group_id" required:"true"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToSecurityGroupRuleCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSecurityGroupRuleCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "security_group_rule") +} + +// Create accepts a CreateOpts struct and creates a new security group rule. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecurityGroupRuleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// Delete accepts a unique ID and deletes the security group rule associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} diff --git a/v3/ecl/network/v2/security_group_rules/results.go b/v3/ecl/network/v2/security_group_rules/results.go new file mode 100644 index 00000000..41c9462d --- /dev/null +++ b/v3/ecl/network/v2/security_group_rules/results.go @@ -0,0 +1,77 @@ +package security_group_rules + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*SecurityGroupRule, error) { + var s SecurityGroupRule + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "security_group_rule") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +// SecurityGroupRule represents a security group rule +type SecurityGroupRule struct { + Description string `json:"description"` + Direction string `json:"direction"` + Ethertype string `json:"ethertype"` + ID string `json:"id"` + PortRangeMax *int `json:"port_range_max"` + PortRangeMin *int `json:"port_range_min"` + Protocol string `json:"protocol"` + RemoteGroupID *string `json:"remote_group_id"` + RemoteIPPrefix *string `json:"remote_ip_prefix"` + SecurityGroupID string `json:"security_group_id"` + TenantID string `json:"tenant_id"` +} + +type SecurityGroupRulePage struct { + pagination.LinkedPageBase +} + +func (r SecurityGroupRulePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"security_group_rules_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r SecurityGroupRulePage) IsEmpty() (bool, error) { + is, err := ExtractSecurityGroupRules(r) + return len(is) == 0, err +} + +func ExtractSecurityGroupRules(r pagination.Page) ([]SecurityGroupRule, error) { + var s []SecurityGroupRule + err := ExtractSecurityGroupRulesInto(r, &s) + return s, err +} + +func ExtractSecurityGroupRulesInto(r pagination.Page, v interface{}) error { + return r.(SecurityGroupRulePage).Result.ExtractIntoSlicePtr(v, "security_group_rules") +} diff --git a/v3/ecl/network/v2/security_group_rules/testing/fixtures.go b/v3/ecl/network/v2/security_group_rules/testing/fixtures.go new file mode 100644 index 00000000..89875f83 --- /dev/null +++ b/v3/ecl/network/v2/security_group_rules/testing/fixtures.go @@ -0,0 +1,155 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/security_group_rules" +) + +const ListResponse = ` +{ + "security_group_rules": [ + { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "2bc0accf-312e-429a-956e-e4407625eb62", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "description": "Allow HTTPS", + "direction": "ingress", + "ethertype": "IPv4", + "id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "port_range_max": 443, + "port_range_min": 443, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "10.0.0.0/8", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ] +}` + +const GetResponse = ` +{ + "security_group_rule": { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "2bc0accf-312e-429a-956e-e4407625eb62", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +}` + +const CreateRequest = ` +{ + "security_group_rule": { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +}` + +const CreateResponse = ` +{ + "security_group_rule": { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "2bc0accf-312e-429a-956e-e4407625eb62", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +}` + +const CreateRequestWithRemoteGroup = ` +{ + "security_group_rule": { + "description": "Allow from same group", + "direction": "ingress", + "ethertype": "IPv4", + "protocol": "tcp", + "remote_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a" + } +}` + +const CreateResponseWithRemoteGroup = ` +{ + "security_group_rule": { + "description": "Allow from same group", + "direction": "ingress", + "ethertype": "IPv4", + "id": "rule-with-remote-group-id", + "port_range_max": null, + "port_range_min": null, + "protocol": "tcp", + "remote_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "remote_ip_prefix": null, + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +}` + +var port22 = 22 +var port443 = 443 + +var SecurityGroupRule1 = security_group_rules.SecurityGroupRule{ + Description: "Allow SSH", + Direction: "ingress", + Ethertype: "IPv4", + ID: "2bc0accf-312e-429a-956e-e4407625eb62", + PortRangeMax: &port22, + PortRangeMin: &port22, + Protocol: "tcp", + RemoteGroupID: nil, + RemoteIPPrefix: strPtr("0.0.0.0/0"), + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", +} + +var SecurityGroupRule2 = security_group_rules.SecurityGroupRule{ + Description: "Allow HTTPS", + Direction: "ingress", + Ethertype: "IPv4", + ID: "c0e1482e-2e3c-497e-8964-e4f818071700", + PortRangeMax: &port443, + PortRangeMin: &port443, + Protocol: "tcp", + RemoteGroupID: nil, + RemoteIPPrefix: strPtr("10.0.0.0/8"), + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", +} + +var ExpectedSecurityGroupRuleSlice = []security_group_rules.SecurityGroupRule{ + SecurityGroupRule1, + SecurityGroupRule2, +} + +func strPtr(s string) *string { + return &s +} diff --git a/v3/ecl/network/v2/security_group_rules/testing/request_test.go b/v3/ecl/network/v2/security_group_rules/testing/request_test.go new file mode 100644 index 00000000..b57d3647 --- /dev/null +++ b/v3/ecl/network/v2/security_group_rules/testing/request_test.go @@ -0,0 +1,182 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/security_group_rules" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListSecurityGroupRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + security_group_rules.List(client, security_group_rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := security_group_rules.ExtractSecurityGroupRules(page) + if err != nil { + t.Errorf("Failed to extract security group rules: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedSecurityGroupRuleSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetSecurityGroupRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/2bc0accf-312e-429a-956e-e4407625eb62", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + rule, err := security_group_rules.Get(fake.ServiceClient(), "2bc0accf-312e-429a-956e-e4407625eb62").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecurityGroupRule1, rule) +} + +func TestCreateSecurityGroupRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + portRangeMax := 22 + portRangeMin := 22 + remoteIPPrefix := "0.0.0.0/0" + + options := &security_group_rules.CreateOpts{ + Description: "Allow SSH", + Direction: "ingress", + Ethertype: "IPv4", + PortRangeMax: &portRangeMax, + PortRangeMin: &portRangeMin, + Protocol: "tcp", + RemoteIPPrefix: &remoteIPPrefix, + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + } + rule, err := security_group_rules.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &SecurityGroupRule1, rule) +} + +func TestCreateSecurityGroupRuleWithRemoteGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequestWithRemoteGroup) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponseWithRemoteGroup) + }) + + remoteGroupID := "a7734e61-b545-452d-a3cd-0189cbd9747a" + + options := &security_group_rules.CreateOpts{ + Description: "Allow from same group", + Direction: "ingress", + Ethertype: "IPv4", + Protocol: "tcp", + RemoteGroupID: &remoteGroupID, + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + } + rule, err := security_group_rules.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + expected := &security_group_rules.SecurityGroupRule{ + Description: "Allow from same group", + Direction: "ingress", + Ethertype: "IPv4", + ID: "rule-with-remote-group-id", + PortRangeMax: nil, + PortRangeMin: nil, + Protocol: "tcp", + RemoteGroupID: &remoteGroupID, + RemoteIPPrefix: nil, + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + } + th.AssertDeepEquals(t, expected, rule) +} + +func TestRequiredCreateOptsSecurityGroupRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + // Test missing direction + res := security_group_rules.Create(fake.ServiceClient(), security_group_rules.CreateOpts{ + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + }) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + // Test missing security_group_id + res = security_group_rules.Create(fake.ServiceClient(), security_group_rules.CreateOpts{ + Direction: "ingress", + }) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestDeleteSecurityGroupRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/2bc0accf-312e-429a-956e-e4407625eb62", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := security_group_rules.Delete(fake.ServiceClient(), "2bc0accf-312e-429a-956e-e4407625eb62") + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/network/v2/security_group_rules/urls.go b/v3/ecl/network/v2/security_group_rules/urls.go new file mode 100644 index 00000000..275d096d --- /dev/null +++ b/v3/ecl/network/v2/security_group_rules/urls.go @@ -0,0 +1,27 @@ +package security_group_rules + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("security-group-rules", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("security-group-rules") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v3/ecl/network/v2/security_groups/doc.go b/v3/ecl/network/v2/security_groups/doc.go new file mode 100644 index 00000000..ea2cd534 --- /dev/null +++ b/v3/ecl/network/v2/security_groups/doc.go @@ -0,0 +1,63 @@ +/* +Package security_groups contains functionality for working with ECL Security Group resources. + +Security Groups provide a way to define network access rules to control +inbound and outbound traffic to instances. + +Example to List Security Groups + + listOpts := security_groups.ListOpts{ + TenantID: "tenant-id", + } + + allPages, err := security_groups.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allSecurityGroups, err := security_groups.ExtractSecurityGroups(allPages) + if err != nil { + panic(err) + } + + for _, sg := range allSecurityGroups { + fmt.Printf("%+v\n", sg) + } + +Example to Create a Security Group + + createOpts := security_groups.CreateOpts{ + Name: "example-security-group", + Description: "Example security group", + } + + sg, err := security_groups.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Security Group + + securityGroupID := "security-group-id" + + name := "updated-name" + description := "updated description" + updateOpts := security_groups.UpdateOpts{ + Name: &name, + Description: &description, + } + + sg, err := security_groups.Update(networkClient, securityGroupID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Security Group + + securityGroupID := "security-group-id" + err := security_groups.Delete(networkClient, securityGroupID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package security_groups diff --git a/v3/ecl/network/v2/security_groups/requests.go b/v3/ecl/network/v2/security_groups/requests.go new file mode 100644 index 00000000..46e84545 --- /dev/null +++ b/v3/ecl/network/v2/security_groups/requests.go @@ -0,0 +1,122 @@ +package security_groups + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSecurityGroupListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the security group attributes you want to see returned. +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` +} + +// ToSecurityGroupListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSecurityGroupListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// security groups. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSecurityGroupListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SecurityGroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific security group based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecurityGroupCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a security group. +type CreateOpts struct { + Description string `json:"description,omitempty"` + Name string `json:"name,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToSecurityGroupCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSecurityGroupCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "security_group") +} + +// Create accepts a CreateOpts struct and creates a new security group using +// the values provided. This operation does not actually require a request +// body, i.e. the CreateOpts struct argument can be empty. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecurityGroupCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSecurityGroupUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options used to update a security group. +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` +} + +// ToSecurityGroupUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSecurityGroupUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "security_group") +} + +// Update accepts a UpdateOpts struct and updates an existing security group +// using the values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSecurityGroupUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete accepts a unique ID and deletes the security group associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} diff --git a/v3/ecl/network/v2/security_groups/results.go b/v3/ecl/network/v2/security_groups/results.go new file mode 100644 index 00000000..ad3a04a6 --- /dev/null +++ b/v3/ecl/network/v2/security_groups/results.go @@ -0,0 +1,92 @@ +package security_groups + +import ( + "github.com/nttcom/eclcloud/v3" + "github.com/nttcom/eclcloud/v3/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*SecurityGroup, error) { + var s SecurityGroup + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "security_group") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +// SecurityGroupRule represents a rule within a security group +type SecurityGroupRule struct { + Description string `json:"description"` + Direction string `json:"direction"` + Ethertype string `json:"ethertype"` + ID string `json:"id"` + PortRangeMax *int `json:"port_range_max"` + PortRangeMin *int `json:"port_range_min"` + Protocol string `json:"protocol"` + RemoteGroupID *string `json:"remote_group_id"` + RemoteIPPrefix *string `json:"remote_ip_prefix"` + SecurityGroupID string `json:"security_group_id"` + TenantID string `json:"tenant_id"` +} + +// SecurityGroup represents a security group +type SecurityGroup struct { + Description string `json:"description"` + ID string `json:"id"` + Name string `json:"name"` + SecurityGroupRules []SecurityGroupRule `json:"security_group_rules"` + Status string `json:"status"` + Tags map[string]string `json:"tags"` + TenantID string `json:"tenant_id"` +} + +type SecurityGroupPage struct { + pagination.LinkedPageBase +} + +func (r SecurityGroupPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"security_groups_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r SecurityGroupPage) IsEmpty() (bool, error) { + is, err := ExtractSecurityGroups(r) + return len(is) == 0, err +} + +func ExtractSecurityGroups(r pagination.Page) ([]SecurityGroup, error) { + var s []SecurityGroup + err := ExtractSecurityGroupsInto(r, &s) + return s, err +} + +func ExtractSecurityGroupsInto(r pagination.Page, v interface{}) error { + return r.(SecurityGroupPage).Result.ExtractIntoSlicePtr(v, "security_groups") +} diff --git a/v3/ecl/network/v2/security_groups/testing/fixtures.go b/v3/ecl/network/v2/security_groups/testing/fixtures.go new file mode 100644 index 00000000..3633faa6 --- /dev/null +++ b/v3/ecl/network/v2/security_groups/testing/fixtures.go @@ -0,0 +1,185 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v3/ecl/network/v2/security_groups" +) + +const ListResponse = ` +{ + "security_groups": [ + { + "description": "default security group", + "id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "name": "default", + "security_group_rules": [], + "status": "ACTIVE", + "tags": {}, + "tenant_id": "6f70656e737461636b20342065766572" + }, + { + "description": "Test security group", + "id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "name": "test-sg", + "security_group_rules": [ + { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "rule-id-1", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "tenant_id": "6f70656e737461636b20342065766572" + } + ], + "status": "ACTIVE", + "tags": { + "env": "test" + }, + "tenant_id": "6f70656e737461636b20342065766572" + } + ] +}` + +const GetResponse = ` +{ + "security_group": { + "description": "Test security group", + "id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "name": "test-sg", + "security_group_rules": [ + { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "rule-id-1", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "tenant_id": "6f70656e737461636b20342065766572" + } + ], + "status": "ACTIVE", + "tags": { + "env": "test" + }, + "tenant_id": "6f70656e737461636b20342065766572" + } +}` + +const CreateRequest = ` +{ + "security_group": { + "description": "Test security group", + "name": "test-sg", + "tags": { + "env": "test" + }, + "tenant_id": "6f70656e737461636b20342065766572" + } +}` + +const CreateResponse = ` +{ + "security_group": { + "description": "Test security group", + "id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "name": "test-sg", + "security_group_rules": [], + "status": "ACTIVE", + "tags": { + "env": "test" + }, + "tenant_id": "6f70656e737461636b20342065766572" + } +}` + +const UpdateRequest = ` +{ + "security_group": { + "description": "Updated security group", + "name": "updated-sg", + "tags": { + "env": "production" + } + } +}` + +const UpdateResponse = ` +{ + "security_group": { + "description": "Updated security group", + "id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "name": "updated-sg", + "security_group_rules": [ + { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "rule-id-1", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "tenant_id": "6f70656e737461636b20342065766572" + } + ], + "status": "ACTIVE", + "tags": { + "env": "production" + }, + "tenant_id": "6f70656e737461636b20342065766572" + } +}` + +var port22 = 22 + +var SecurityGroup1 = security_groups.SecurityGroup{ + Description: "default security group", + ID: "85cc3048-abc3-43cc-89b3-377341426ac5", + Name: "default", + SecurityGroupRules: []security_groups.SecurityGroupRule{}, + Status: "ACTIVE", + Tags: map[string]string{}, + TenantID: "6f70656e737461636b20342065766572", +} + +var SecurityGroup2 = security_groups.SecurityGroup{ + Description: "Test security group", + ID: "c0e1482e-2e3c-497e-8964-e4f818071700", + Name: "test-sg", + SecurityGroupRules: []security_groups.SecurityGroupRule{ + { + Description: "Allow SSH", + Direction: "ingress", + Ethertype: "IPv4", + ID: "rule-id-1", + PortRangeMax: &port22, + PortRangeMin: &port22, + Protocol: "tcp", + RemoteGroupID: nil, + RemoteIPPrefix: strPtr("0.0.0.0/0"), + SecurityGroupID: "c0e1482e-2e3c-497e-8964-e4f818071700", + TenantID: "6f70656e737461636b20342065766572", + }, + }, + Status: "ACTIVE", + Tags: map[string]string{ + "env": "test", + }, + TenantID: "6f70656e737461636b20342065766572", +} + +var ExpectedSecurityGroupSlice = []security_groups.SecurityGroup{SecurityGroup1, SecurityGroup2} + +func strPtr(s string) *string { + return &s +} diff --git a/v3/ecl/network/v2/security_groups/testing/request_test.go b/v3/ecl/network/v2/security_groups/testing/request_test.go new file mode 100644 index 00000000..740de154 --- /dev/null +++ b/v3/ecl/network/v2/security_groups/testing/request_test.go @@ -0,0 +1,157 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v3/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v3/ecl/network/v2/security_groups" + "github.com/nttcom/eclcloud/v3/pagination" + th "github.com/nttcom/eclcloud/v3/testhelper" +) + +func TestListSecurityGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + security_groups.List(client, security_groups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := security_groups.ExtractSecurityGroups(page) + if err != nil { + t.Errorf("Failed to extract security groups: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedSecurityGroupSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetSecurityGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/c0e1482e-2e3c-497e-8964-e4f818071700", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + sg, err := security_groups.Get(fake.ServiceClient(), "c0e1482e-2e3c-497e-8964-e4f818071700").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecurityGroup2, sg) +} + +func TestCreateSecurityGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := &security_groups.CreateOpts{ + Description: "Test security group", + Name: "test-sg", + Tags: map[string]string{ + "env": "test", + }, + TenantID: "6f70656e737461636b20342065766572", + } + sg, err := security_groups.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + expected := &security_groups.SecurityGroup{ + Description: "Test security group", + ID: "c0e1482e-2e3c-497e-8964-e4f818071700", + Name: "test-sg", + SecurityGroupRules: []security_groups.SecurityGroupRule{}, + Status: "ACTIVE", + Tags: map[string]string{ + "env": "test", + }, + TenantID: "6f70656e737461636b20342065766572", + } + th.AssertDeepEquals(t, expected, sg) +} + +func TestUpdateSecurityGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/c0e1482e-2e3c-497e-8964-e4f818071700", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + description := "Updated security group" + name := "updated-sg" + tags := map[string]string{ + "env": "production", + } + + options := &security_groups.UpdateOpts{ + Description: &description, + Name: &name, + Tags: &tags, + } + sg, err := security_groups.Update(fake.ServiceClient(), "c0e1482e-2e3c-497e-8964-e4f818071700", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, description, sg.Description) + th.CheckEquals(t, name, sg.Name) + th.CheckDeepEquals(t, tags, sg.Tags) +} + +func TestDeleteSecurityGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/c0e1482e-2e3c-497e-8964-e4f818071700", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := security_groups.Delete(fake.ServiceClient(), "c0e1482e-2e3c-497e-8964-e4f818071700") + th.AssertNoErr(t, res.Err) +} diff --git a/v3/ecl/network/v2/security_groups/urls.go b/v3/ecl/network/v2/security_groups/urls.go new file mode 100644 index 00000000..5978997b --- /dev/null +++ b/v3/ecl/network/v2/security_groups/urls.go @@ -0,0 +1,31 @@ +package security_groups + +import "github.com/nttcom/eclcloud/v3" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("security-groups", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("security-groups") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/ports/requests.go b/v4/ecl/network/v2/ports/requests.go index 1029b4d1..1ba58dc6 100644 --- a/v4/ecl/network/v2/ports/requests.go +++ b/v4/ecl/network/v2/ports/requests.go @@ -80,6 +80,7 @@ type CreateOpts struct { MACAddress string `json:"mac_address,omitempty"` Name string `json:"name,omitempty"` NetworkID string `json:"network_id"` + SecurityGroups *[]string `json:"security_groups,omitempty"` SegmentationID int `json:"segmentation_id,omitempty"` SegmentationType string `json:"segmentation_type,omitempty"` Tags map[string]string `json:"tags,omitempty"` @@ -118,6 +119,7 @@ type UpdateOpts struct { DeviceOwner *string `json:"device_owner,omitempty"` FixedIPs interface{} `json:"fixed_ips,omitempty"` Name *string `json:"name,omitempty"` + SecurityGroups *[]string `json:"security_groups,omitempty"` SegmentationID *int `json:"segmentation_id,omitempty"` SegmentationType *string `json:"segmentation_type,omitempty"` Tags *map[string]string `json:"tags,omitempty"` diff --git a/v4/ecl/network/v2/ports/results.go b/v4/ecl/network/v2/ports/results.go index b2628946..6964a708 100644 --- a/v4/ecl/network/v2/ports/results.go +++ b/v4/ecl/network/v2/ports/results.go @@ -94,6 +94,9 @@ type Port struct { // Network that this port is associated with. NetworkID string `json:"network_id"` + // SecurityGroups is the IDs of security groups applied to the port. + SecurityGroups []string `json:"security_groups"` + // SegmentationID is the segmenation ID used for this port (i.e. for vlan type it is vlan tag) SegmentationID int `json:"segmentation_id"` diff --git a/v4/ecl/network/v2/ports/testing/fixtures.go b/v4/ecl/network/v2/ports/testing/fixtures.go index 2a8c1665..9535c781 100644 --- a/v4/ecl/network/v2/ports/testing/fixtures.go +++ b/v4/ecl/network/v2/ports/testing/fixtures.go @@ -288,3 +288,83 @@ var Port2 = ports.Port{ } var ExpectedPortSlice = []ports.Port{Port1, Port2} + +const CreateWithSecurityGroupsRequest = ` +{ + "port": + { + "admin_state_up": true, + "fixed_ips": [ + { + "ip_address": "192.168.2.40", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "name": "port_with_sg", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "security_groups": ["85cc3048-abc3-43cc-89b3-377341426ac5"], + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } +}` + +const CreateWithSecurityGroupsResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.40", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "port-with-sg-id", + "mac_address": "fa:16:3e:b0:ca:f2", + "name": "port_with_sg", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "security_groups": ["85cc3048-abc3-43cc-89b3-377341426ac5"], + "segmentation_id": null, + "segmentation_type": null, + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } +}` + +const UpdateWithSecurityGroupsRequest = ` +{ + "port": { + "name": "port_with_updated_sg", + "security_groups": ["85cc3048-abc3-43cc-89b3-377341426ac5", "c0e1482e-2e3c-497e-8964-e4f818071700"] + } +}` + +const UpdateWithSecurityGroupsResponse = ` +{ + "port": { + "admin_state_up": true, + "allowed_address_pairs": [], + "description": "", + "device_id": "", + "device_owner": "", + "fixed_ips": [ + { + "ip_address": "192.168.2.40", + "subnet_id": "ab49eb24-667f-4a4e-9421-b4d915bff416" + } + ], + "id": "port-with-sg-id", + "mac_address": "fa:16:3e:b0:ca:f2", + "name": "port_with_updated_sg", + "network_id": "8f36b88a-443f-4d97-9751-34d34af9e782", + "security_groups": ["85cc3048-abc3-43cc-89b3-377341426ac5", "c0e1482e-2e3c-497e-8964-e4f818071700"], + "segmentation_id": null, + "segmentation_type": null, + "status": "PENDING_CREATE", + "tags": {}, + "tenant_id": "dcb2d589c0c646d0bad45c0cf9f90cf1" + } +}` diff --git a/v4/ecl/network/v2/ports/testing/request_test.go b/v4/ecl/network/v2/ports/testing/request_test.go index ee9cbdbf..fdf37ccb 100644 --- a/v4/ecl/network/v2/ports/testing/request_test.go +++ b/v4/ecl/network/v2/ports/testing/request_test.go @@ -219,3 +219,71 @@ func TestDeletePort(t *testing.T) { res := ports.Delete(fake.ServiceClient(), "ac57c5c9-aaf4-4ffc-b8b8-f1ef84656730") th.AssertNoErr(t, res.Err) } + +func TestCreatePortWithSecurityGroups(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateWithSecurityGroupsRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateWithSecurityGroupsResponse) + }) + + asu := true + securityGroups := []string{"85cc3048-abc3-43cc-89b3-377341426ac5"} + + options := &ports.CreateOpts{ + AdminStateUp: &asu, + FixedIPs: []ports.IP{{ + IPAddress: "192.168.2.40", + SubnetID: "ab49eb24-667f-4a4e-9421-b4d915bff416", + }}, + Name: "port_with_sg", + NetworkID: "8f36b88a-443f-4d97-9751-34d34af9e782", + SecurityGroups: &securityGroups, + TenantID: "dcb2d589c0c646d0bad45c0cf9f90cf1", + } + p, err := ports.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, "port_with_sg", p.Name) + th.CheckDeepEquals(t, securityGroups, p.SecurityGroups) +} + +func TestUpdatePortWithSecurityGroups(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/ports/port-with-sg-id", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateWithSecurityGroupsRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateWithSecurityGroupsResponse) + }) + + name := "port_with_updated_sg" + securityGroups := []string{"85cc3048-abc3-43cc-89b3-377341426ac5", "c0e1482e-2e3c-497e-8964-e4f818071700"} + + options := ports.UpdateOpts{ + Name: &name, + SecurityGroups: &securityGroups, + } + p, err := ports.Update(fake.ServiceClient(), "port-with-sg-id", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, name, p.Name) + th.CheckDeepEquals(t, securityGroups, p.SecurityGroups) +} diff --git a/v4/ecl/network/v2/security_group_rules/doc.go b/v4/ecl/network/v2/security_group_rules/doc.go new file mode 100644 index 00000000..cb002741 --- /dev/null +++ b/v4/ecl/network/v2/security_group_rules/doc.go @@ -0,0 +1,51 @@ +/* +Package security_group_rules contains functionality for working with ECL Security Group Rule resources. + +Security Group Rules define specific ingress and egress traffic rules for Security Groups. + +Example to List Security Group Rules + + listOpts := security_group_rules.ListOpts{ + SecurityGroupID: "security-group-id", + } + + allPages, err := security_group_rules.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allRules, err := security_group_rules.ExtractSecurityGroupRules(allPages) + if err != nil { + panic(err) + } + + for _, rule := range allRules { + fmt.Printf("%+v\n", rule) + } + +Example to Create a Security Group Rule + + createOpts := security_group_rules.CreateOpts{ + Direction: "ingress", + SecurityGroupID: "security-group-id", + Ethertype: "IPv4", + Protocol: "tcp", + PortRangeMin: &[]int{22}[0], + PortRangeMax: &[]int{22}[0], + RemoteIPPrefix: &[]string{"0.0.0.0/0"}[0], + } + + rule, err := security_group_rules.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Security Group Rule + + ruleID := "rule-id" + err := security_group_rules.Delete(networkClient, ruleID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package security_group_rules diff --git a/v4/ecl/network/v2/security_group_rules/requests.go b/v4/ecl/network/v2/security_group_rules/requests.go new file mode 100644 index 00000000..e8a737dc --- /dev/null +++ b/v4/ecl/network/v2/security_group_rules/requests.go @@ -0,0 +1,100 @@ +package security_group_rules + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSecurityGroupRuleListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the security group rule attributes you want to see returned. +type ListOpts struct { + Description string `q:"description"` + Direction string `q:"direction"` + Ethertype string `q:"ethertype"` + ID string `q:"id"` + PortRangeMax int `q:"port_range_max"` + PortRangeMin int `q:"port_range_min"` + Protocol string `q:"protocol"` + RemoteGroupID string `q:"remote_group_id"` + RemoteIPPrefix string `q:"remote_ip_prefix"` + SecurityGroupID string `q:"security_group_id"` + TenantID string `q:"tenant_id"` +} + +// ToSecurityGroupRuleListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSecurityGroupRuleListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// security group rules. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSecurityGroupRuleListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SecurityGroupRulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific security group rule based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecurityGroupRuleCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a security group rule. +type CreateOpts struct { + Description string `json:"description,omitempty"` + Direction string `json:"direction" required:"true"` + Ethertype string `json:"ethertype,omitempty"` + PortRangeMax *int `json:"port_range_max,omitempty"` + PortRangeMin *int `json:"port_range_min,omitempty"` + Protocol string `json:"protocol,omitempty"` + RemoteGroupID *string `json:"remote_group_id,omitempty"` + RemoteIPPrefix *string `json:"remote_ip_prefix,omitempty"` + SecurityGroupID string `json:"security_group_id" required:"true"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToSecurityGroupRuleCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSecurityGroupRuleCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "security_group_rule") +} + +// Create accepts a CreateOpts struct and creates a new security group rule. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecurityGroupRuleCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// Delete accepts a unique ID and deletes the security group rule associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} diff --git a/v4/ecl/network/v2/security_group_rules/results.go b/v4/ecl/network/v2/security_group_rules/results.go new file mode 100644 index 00000000..06106c21 --- /dev/null +++ b/v4/ecl/network/v2/security_group_rules/results.go @@ -0,0 +1,77 @@ +package security_group_rules + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*SecurityGroupRule, error) { + var s SecurityGroupRule + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "security_group_rule") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +// SecurityGroupRule represents a security group rule +type SecurityGroupRule struct { + Description string `json:"description"` + Direction string `json:"direction"` + Ethertype string `json:"ethertype"` + ID string `json:"id"` + PortRangeMax *int `json:"port_range_max"` + PortRangeMin *int `json:"port_range_min"` + Protocol string `json:"protocol"` + RemoteGroupID *string `json:"remote_group_id"` + RemoteIPPrefix *string `json:"remote_ip_prefix"` + SecurityGroupID string `json:"security_group_id"` + TenantID string `json:"tenant_id"` +} + +type SecurityGroupRulePage struct { + pagination.LinkedPageBase +} + +func (r SecurityGroupRulePage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"security_group_rules_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r SecurityGroupRulePage) IsEmpty() (bool, error) { + is, err := ExtractSecurityGroupRules(r) + return len(is) == 0, err +} + +func ExtractSecurityGroupRules(r pagination.Page) ([]SecurityGroupRule, error) { + var s []SecurityGroupRule + err := ExtractSecurityGroupRulesInto(r, &s) + return s, err +} + +func ExtractSecurityGroupRulesInto(r pagination.Page, v interface{}) error { + return r.(SecurityGroupRulePage).Result.ExtractIntoSlicePtr(v, "security_group_rules") +} diff --git a/v4/ecl/network/v2/security_group_rules/testing/fixtures.go b/v4/ecl/network/v2/security_group_rules/testing/fixtures.go new file mode 100644 index 00000000..b1896c6a --- /dev/null +++ b/v4/ecl/network/v2/security_group_rules/testing/fixtures.go @@ -0,0 +1,155 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/security_group_rules" +) + +const ListResponse = ` +{ + "security_group_rules": [ + { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "2bc0accf-312e-429a-956e-e4407625eb62", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + }, + { + "description": "Allow HTTPS", + "direction": "ingress", + "ethertype": "IPv4", + "id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "port_range_max": 443, + "port_range_min": 443, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "10.0.0.0/8", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } + ] +}` + +const GetResponse = ` +{ + "security_group_rule": { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "2bc0accf-312e-429a-956e-e4407625eb62", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +}` + +const CreateRequest = ` +{ + "security_group_rule": { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +}` + +const CreateResponse = ` +{ + "security_group_rule": { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "2bc0accf-312e-429a-956e-e4407625eb62", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +}` + +const CreateRequestWithRemoteGroup = ` +{ + "security_group_rule": { + "description": "Allow from same group", + "direction": "ingress", + "ethertype": "IPv4", + "protocol": "tcp", + "remote_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a" + } +}` + +const CreateResponseWithRemoteGroup = ` +{ + "security_group_rule": { + "description": "Allow from same group", + "direction": "ingress", + "ethertype": "IPv4", + "id": "rule-with-remote-group-id", + "port_range_max": null, + "port_range_min": null, + "protocol": "tcp", + "remote_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "remote_ip_prefix": null, + "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a", + "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550" + } +}` + +var port22 = 22 +var port443 = 443 + +var SecurityGroupRule1 = security_group_rules.SecurityGroupRule{ + Description: "Allow SSH", + Direction: "ingress", + Ethertype: "IPv4", + ID: "2bc0accf-312e-429a-956e-e4407625eb62", + PortRangeMax: &port22, + PortRangeMin: &port22, + Protocol: "tcp", + RemoteGroupID: nil, + RemoteIPPrefix: strPtr("0.0.0.0/0"), + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", +} + +var SecurityGroupRule2 = security_group_rules.SecurityGroupRule{ + Description: "Allow HTTPS", + Direction: "ingress", + Ethertype: "IPv4", + ID: "c0e1482e-2e3c-497e-8964-e4f818071700", + PortRangeMax: &port443, + PortRangeMin: &port443, + Protocol: "tcp", + RemoteGroupID: nil, + RemoteIPPrefix: strPtr("10.0.0.0/8"), + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", +} + +var ExpectedSecurityGroupRuleSlice = []security_group_rules.SecurityGroupRule{ + SecurityGroupRule1, + SecurityGroupRule2, +} + +func strPtr(s string) *string { + return &s +} diff --git a/v4/ecl/network/v2/security_group_rules/testing/request_test.go b/v4/ecl/network/v2/security_group_rules/testing/request_test.go new file mode 100644 index 00000000..555aaf80 --- /dev/null +++ b/v4/ecl/network/v2/security_group_rules/testing/request_test.go @@ -0,0 +1,182 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/security_group_rules" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListSecurityGroupRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + security_group_rules.List(client, security_group_rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := security_group_rules.ExtractSecurityGroupRules(page) + if err != nil { + t.Errorf("Failed to extract security group rules: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedSecurityGroupRuleSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetSecurityGroupRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/2bc0accf-312e-429a-956e-e4407625eb62", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + rule, err := security_group_rules.Get(fake.ServiceClient(), "2bc0accf-312e-429a-956e-e4407625eb62").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecurityGroupRule1, rule) +} + +func TestCreateSecurityGroupRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + portRangeMax := 22 + portRangeMin := 22 + remoteIPPrefix := "0.0.0.0/0" + + options := &security_group_rules.CreateOpts{ + Description: "Allow SSH", + Direction: "ingress", + Ethertype: "IPv4", + PortRangeMax: &portRangeMax, + PortRangeMin: &portRangeMin, + Protocol: "tcp", + RemoteIPPrefix: &remoteIPPrefix, + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + } + rule, err := security_group_rules.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, &SecurityGroupRule1, rule) +} + +func TestCreateSecurityGroupRuleWithRemoteGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequestWithRemoteGroup) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponseWithRemoteGroup) + }) + + remoteGroupID := "a7734e61-b545-452d-a3cd-0189cbd9747a" + + options := &security_group_rules.CreateOpts{ + Description: "Allow from same group", + Direction: "ingress", + Ethertype: "IPv4", + Protocol: "tcp", + RemoteGroupID: &remoteGroupID, + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + } + rule, err := security_group_rules.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + expected := &security_group_rules.SecurityGroupRule{ + Description: "Allow from same group", + Direction: "ingress", + Ethertype: "IPv4", + ID: "rule-with-remote-group-id", + PortRangeMax: nil, + PortRangeMin: nil, + Protocol: "tcp", + RemoteGroupID: &remoteGroupID, + RemoteIPPrefix: nil, + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + TenantID: "e4f50856753b4dc6afee5fa6b9b6c550", + } + th.AssertDeepEquals(t, expected, rule) +} + +func TestRequiredCreateOptsSecurityGroupRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + // Test missing direction + res := security_group_rules.Create(fake.ServiceClient(), security_group_rules.CreateOpts{ + SecurityGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + }) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + + // Test missing security_group_id + res = security_group_rules.Create(fake.ServiceClient(), security_group_rules.CreateOpts{ + Direction: "ingress", + }) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestDeleteSecurityGroupRule(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-group-rules/2bc0accf-312e-429a-956e-e4407625eb62", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := security_group_rules.Delete(fake.ServiceClient(), "2bc0accf-312e-429a-956e-e4407625eb62") + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/network/v2/security_group_rules/urls.go b/v4/ecl/network/v2/security_group_rules/urls.go new file mode 100644 index 00000000..772276fb --- /dev/null +++ b/v4/ecl/network/v2/security_group_rules/urls.go @@ -0,0 +1,27 @@ +package security_group_rules + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("security-group-rules", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("security-group-rules") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/v4/ecl/network/v2/security_groups/doc.go b/v4/ecl/network/v2/security_groups/doc.go new file mode 100644 index 00000000..ea2cd534 --- /dev/null +++ b/v4/ecl/network/v2/security_groups/doc.go @@ -0,0 +1,63 @@ +/* +Package security_groups contains functionality for working with ECL Security Group resources. + +Security Groups provide a way to define network access rules to control +inbound and outbound traffic to instances. + +Example to List Security Groups + + listOpts := security_groups.ListOpts{ + TenantID: "tenant-id", + } + + allPages, err := security_groups.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allSecurityGroups, err := security_groups.ExtractSecurityGroups(allPages) + if err != nil { + panic(err) + } + + for _, sg := range allSecurityGroups { + fmt.Printf("%+v\n", sg) + } + +Example to Create a Security Group + + createOpts := security_groups.CreateOpts{ + Name: "example-security-group", + Description: "Example security group", + } + + sg, err := security_groups.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Security Group + + securityGroupID := "security-group-id" + + name := "updated-name" + description := "updated description" + updateOpts := security_groups.UpdateOpts{ + Name: &name, + Description: &description, + } + + sg, err := security_groups.Update(networkClient, securityGroupID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Security Group + + securityGroupID := "security-group-id" + err := security_groups.Delete(networkClient, securityGroupID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package security_groups diff --git a/v4/ecl/network/v2/security_groups/requests.go b/v4/ecl/network/v2/security_groups/requests.go new file mode 100644 index 00000000..fc5f4b6d --- /dev/null +++ b/v4/ecl/network/v2/security_groups/requests.go @@ -0,0 +1,122 @@ +package security_groups + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSecurityGroupListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the security group attributes you want to see returned. +type ListOpts struct { + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` +} + +// ToSecurityGroupListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSecurityGroupListQuery() (string, error) { + q, err := eclcloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// security groups. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *eclcloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSecurityGroupListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SecurityGroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific security group based on its unique ID. +func Get(c *eclcloud.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(getURL(c, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecurityGroupCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a security group. +type CreateOpts struct { + Description string `json:"description,omitempty"` + Name string `json:"name,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + TenantID string `json:"tenant_id,omitempty"` +} + +// ToSecurityGroupCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSecurityGroupCreateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "security_group") +} + +// Create accepts a CreateOpts struct and creates a new security group using +// the values provided. This operation does not actually require a request +// body, i.e. the CreateOpts struct argument can be empty. +func Create(c *eclcloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecurityGroupCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Post(createURL(c), b, &r.Body, nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSecurityGroupUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options used to update a security group. +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` + Tags *map[string]string `json:"tags,omitempty"` +} + +// ToSecurityGroupUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSecurityGroupUpdateMap() (map[string]interface{}, error) { + return eclcloud.BuildRequestBody(opts, "security_group") +} + +// Update accepts a UpdateOpts struct and updates an existing security group +// using the values provided. +func Update(c *eclcloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSecurityGroupUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(updateURL(c, id), b, &r.Body, &eclcloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete accepts a unique ID and deletes the security group associated with it. +func Delete(c *eclcloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(deleteURL(c, id), nil) + return +} diff --git a/v4/ecl/network/v2/security_groups/results.go b/v4/ecl/network/v2/security_groups/results.go new file mode 100644 index 00000000..f2407f49 --- /dev/null +++ b/v4/ecl/network/v2/security_groups/results.go @@ -0,0 +1,92 @@ +package security_groups + +import ( + "github.com/nttcom/eclcloud/v4" + "github.com/nttcom/eclcloud/v4/pagination" +) + +type commonResult struct { + eclcloud.Result +} + +func (r commonResult) Extract() (*SecurityGroup, error) { + var s SecurityGroup + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "security_group") +} + +type CreateResult struct { + commonResult +} + +type GetResult struct { + commonResult +} + +type UpdateResult struct { + commonResult +} + +type DeleteResult struct { + eclcloud.ErrResult +} + +// SecurityGroupRule represents a rule within a security group +type SecurityGroupRule struct { + Description string `json:"description"` + Direction string `json:"direction"` + Ethertype string `json:"ethertype"` + ID string `json:"id"` + PortRangeMax *int `json:"port_range_max"` + PortRangeMin *int `json:"port_range_min"` + Protocol string `json:"protocol"` + RemoteGroupID *string `json:"remote_group_id"` + RemoteIPPrefix *string `json:"remote_ip_prefix"` + SecurityGroupID string `json:"security_group_id"` + TenantID string `json:"tenant_id"` +} + +// SecurityGroup represents a security group +type SecurityGroup struct { + Description string `json:"description"` + ID string `json:"id"` + Name string `json:"name"` + SecurityGroupRules []SecurityGroupRule `json:"security_group_rules"` + Status string `json:"status"` + Tags map[string]string `json:"tags"` + TenantID string `json:"tenant_id"` +} + +type SecurityGroupPage struct { + pagination.LinkedPageBase +} + +func (r SecurityGroupPage) NextPageURL() (string, error) { + var s struct { + Links []eclcloud.Link `json:"security_groups_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return eclcloud.ExtractNextURL(s.Links) +} + +func (r SecurityGroupPage) IsEmpty() (bool, error) { + is, err := ExtractSecurityGroups(r) + return len(is) == 0, err +} + +func ExtractSecurityGroups(r pagination.Page) ([]SecurityGroup, error) { + var s []SecurityGroup + err := ExtractSecurityGroupsInto(r, &s) + return s, err +} + +func ExtractSecurityGroupsInto(r pagination.Page, v interface{}) error { + return r.(SecurityGroupPage).Result.ExtractIntoSlicePtr(v, "security_groups") +} diff --git a/v4/ecl/network/v2/security_groups/testing/fixtures.go b/v4/ecl/network/v2/security_groups/testing/fixtures.go new file mode 100644 index 00000000..d12035f7 --- /dev/null +++ b/v4/ecl/network/v2/security_groups/testing/fixtures.go @@ -0,0 +1,185 @@ +package testing + +import ( + "github.com/nttcom/eclcloud/v4/ecl/network/v2/security_groups" +) + +const ListResponse = ` +{ + "security_groups": [ + { + "description": "default security group", + "id": "85cc3048-abc3-43cc-89b3-377341426ac5", + "name": "default", + "security_group_rules": [], + "status": "ACTIVE", + "tags": {}, + "tenant_id": "6f70656e737461636b20342065766572" + }, + { + "description": "Test security group", + "id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "name": "test-sg", + "security_group_rules": [ + { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "rule-id-1", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "tenant_id": "6f70656e737461636b20342065766572" + } + ], + "status": "ACTIVE", + "tags": { + "env": "test" + }, + "tenant_id": "6f70656e737461636b20342065766572" + } + ] +}` + +const GetResponse = ` +{ + "security_group": { + "description": "Test security group", + "id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "name": "test-sg", + "security_group_rules": [ + { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "rule-id-1", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "tenant_id": "6f70656e737461636b20342065766572" + } + ], + "status": "ACTIVE", + "tags": { + "env": "test" + }, + "tenant_id": "6f70656e737461636b20342065766572" + } +}` + +const CreateRequest = ` +{ + "security_group": { + "description": "Test security group", + "name": "test-sg", + "tags": { + "env": "test" + }, + "tenant_id": "6f70656e737461636b20342065766572" + } +}` + +const CreateResponse = ` +{ + "security_group": { + "description": "Test security group", + "id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "name": "test-sg", + "security_group_rules": [], + "status": "ACTIVE", + "tags": { + "env": "test" + }, + "tenant_id": "6f70656e737461636b20342065766572" + } +}` + +const UpdateRequest = ` +{ + "security_group": { + "description": "Updated security group", + "name": "updated-sg", + "tags": { + "env": "production" + } + } +}` + +const UpdateResponse = ` +{ + "security_group": { + "description": "Updated security group", + "id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "name": "updated-sg", + "security_group_rules": [ + { + "description": "Allow SSH", + "direction": "ingress", + "ethertype": "IPv4", + "id": "rule-id-1", + "port_range_max": 22, + "port_range_min": 22, + "protocol": "tcp", + "remote_group_id": null, + "remote_ip_prefix": "0.0.0.0/0", + "security_group_id": "c0e1482e-2e3c-497e-8964-e4f818071700", + "tenant_id": "6f70656e737461636b20342065766572" + } + ], + "status": "ACTIVE", + "tags": { + "env": "production" + }, + "tenant_id": "6f70656e737461636b20342065766572" + } +}` + +var port22 = 22 + +var SecurityGroup1 = security_groups.SecurityGroup{ + Description: "default security group", + ID: "85cc3048-abc3-43cc-89b3-377341426ac5", + Name: "default", + SecurityGroupRules: []security_groups.SecurityGroupRule{}, + Status: "ACTIVE", + Tags: map[string]string{}, + TenantID: "6f70656e737461636b20342065766572", +} + +var SecurityGroup2 = security_groups.SecurityGroup{ + Description: "Test security group", + ID: "c0e1482e-2e3c-497e-8964-e4f818071700", + Name: "test-sg", + SecurityGroupRules: []security_groups.SecurityGroupRule{ + { + Description: "Allow SSH", + Direction: "ingress", + Ethertype: "IPv4", + ID: "rule-id-1", + PortRangeMax: &port22, + PortRangeMin: &port22, + Protocol: "tcp", + RemoteGroupID: nil, + RemoteIPPrefix: strPtr("0.0.0.0/0"), + SecurityGroupID: "c0e1482e-2e3c-497e-8964-e4f818071700", + TenantID: "6f70656e737461636b20342065766572", + }, + }, + Status: "ACTIVE", + Tags: map[string]string{ + "env": "test", + }, + TenantID: "6f70656e737461636b20342065766572", +} + +var ExpectedSecurityGroupSlice = []security_groups.SecurityGroup{SecurityGroup1, SecurityGroup2} + +func strPtr(s string) *string { + return &s +} diff --git a/v4/ecl/network/v2/security_groups/testing/request_test.go b/v4/ecl/network/v2/security_groups/testing/request_test.go new file mode 100644 index 00000000..83b01d20 --- /dev/null +++ b/v4/ecl/network/v2/security_groups/testing/request_test.go @@ -0,0 +1,157 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/nttcom/eclcloud/v4/ecl/network/v2/common" + "github.com/nttcom/eclcloud/v4/ecl/network/v2/security_groups" + "github.com/nttcom/eclcloud/v4/pagination" + th "github.com/nttcom/eclcloud/v4/testhelper" +) + +func TestListSecurityGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ListResponse) + }) + + client := fake.ServiceClient() + count := 0 + + security_groups.List(client, security_groups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := security_groups.ExtractSecurityGroups(page) + if err != nil { + t.Errorf("Failed to extract security groups: %v", err) + return false, nil + } + + th.CheckDeepEquals(t, ExpectedSecurityGroupSlice, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestGetSecurityGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/c0e1482e-2e3c-497e-8964-e4f818071700", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, GetResponse) + }) + + sg, err := security_groups.Get(fake.ServiceClient(), "c0e1482e-2e3c-497e-8964-e4f818071700").Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, &SecurityGroup2, sg) +} + +func TestCreateSecurityGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, CreateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateResponse) + }) + + options := &security_groups.CreateOpts{ + Description: "Test security group", + Name: "test-sg", + Tags: map[string]string{ + "env": "test", + }, + TenantID: "6f70656e737461636b20342065766572", + } + sg, err := security_groups.Create(fake.ServiceClient(), options).Extract() + th.AssertNoErr(t, err) + + expected := &security_groups.SecurityGroup{ + Description: "Test security group", + ID: "c0e1482e-2e3c-497e-8964-e4f818071700", + Name: "test-sg", + SecurityGroupRules: []security_groups.SecurityGroupRule{}, + Status: "ACTIVE", + Tags: map[string]string{ + "env": "test", + }, + TenantID: "6f70656e737461636b20342065766572", + } + th.AssertDeepEquals(t, expected, sg) +} + +func TestUpdateSecurityGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/c0e1482e-2e3c-497e-8964-e4f818071700", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, UpdateRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateResponse) + }) + + description := "Updated security group" + name := "updated-sg" + tags := map[string]string{ + "env": "production", + } + + options := &security_groups.UpdateOpts{ + Description: &description, + Name: &name, + Tags: &tags, + } + sg, err := security_groups.Update(fake.ServiceClient(), "c0e1482e-2e3c-497e-8964-e4f818071700", options).Extract() + th.AssertNoErr(t, err) + + th.CheckEquals(t, description, sg.Description) + th.CheckEquals(t, name, sg.Name) + th.CheckDeepEquals(t, tags, sg.Tags) +} + +func TestDeleteSecurityGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/security-groups/c0e1482e-2e3c-497e-8964-e4f818071700", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := security_groups.Delete(fake.ServiceClient(), "c0e1482e-2e3c-497e-8964-e4f818071700") + th.AssertNoErr(t, res.Err) +} diff --git a/v4/ecl/network/v2/security_groups/urls.go b/v4/ecl/network/v2/security_groups/urls.go new file mode 100644 index 00000000..a1e79cb9 --- /dev/null +++ b/v4/ecl/network/v2/security_groups/urls.go @@ -0,0 +1,31 @@ +package security_groups + +import "github.com/nttcom/eclcloud/v4" + +func resourceURL(c *eclcloud.ServiceClient, id string) string { + return c.ServiceURL("security-groups", id) +} + +func rootURL(c *eclcloud.ServiceClient) string { + return c.ServiceURL("security-groups") +} + +func getURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *eclcloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *eclcloud.ServiceClient, id string) string { + return resourceURL(c, id) +}