dangogh closed pull request #1977: Added CRUD for /profiles URL: https://github.com/apache/incubator-trafficcontrol/pull/1977
This is a PR merged from a forked repository. As GitHub hides the original diff on merge, it is displayed below for the sake of provenance: As this is a foreign pull request (from a fork), the diff is supplied below (as it won't show otherwise due to GitHub magic): diff --git a/CHANGELOG.md b/CHANGELOG.md index 4034914f2..ed6278c5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added - Per-DeliveryService Routing Names: you can now choose a Delivery Service's Routing Name (rather than a hardcoded "tr" or "edge" name). This might require a few pre-upgrade steps detailed [here](http://traffic-control-cdn.readthedocs.io/en/latest/admin/traffic_ops/migration_from_20_to_22.html#per-deliveryservice-routing-names) +- Golang Proxy Endpoints (R=REST endpoints for GET, POST, PUT, DELETE) + - /api/1.3/asns (R) + - /api/1.3/cdn (R) + - /api/1.3/cdns/capacity + - /api/1.3/cdns/configs + - /api/1.3/cdns/dnsseckeys + - /api/1.3/cdns/domain + - /api/1.3/cdns/monitoring + - /api/1.3/cdns/health + - /api/1.3/cdns/routing + - /api/1.3/deliveryservice_requests (R) + - /api/1.3/division (R) + - /api/1.3/hwinfo + - /api/1.3/parameter (R) + - /api/1.3/phys_location (R) + - /api/1.3/ping + - /api/1.3/region (R) + - /api/1.3/server (R) + - /api/1.3/servers/checks + - /api/1.3/servers/details + - /api/1.3/servers/status + - /api/1.3/servers/totals + - /api/1.3/statuses (R) + - /api/1.3/system/info + - /api/1.3/types (R) ### Changed - Reformatted this CHANGELOG file to the keep-a-changelog format diff --git a/lib/go-tc/profiles.go b/lib/go-tc/profiles.go index 929bd8f64..bf4f02010 100644 --- a/lib/go-tc/profiles.go +++ b/lib/go-tc/profiles.go @@ -19,15 +19,31 @@ package tc * under the License. */ -// ProfileResponse ... +// ProfilesResponse ... type ProfilesResponse struct { Response []Profile `json:"response"` } // Profile ... type Profile struct { - ID int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - LastUpdated string `json:"lastUpdated"` + ID int `json:"id" db:"id"` + LastUpdated TimeNoMod `json:"lastUpdated"` + Name string `json:"name"` + Description string `json:"description"` + CDNName string `json:"cdnName"` + CDNID int `json:"cdn"` + RoutingDisabled bool `json:"routingDisabled"` + Type string `json:"type"` +} + +// ProfileNullable ... +type ProfileNullable struct { + ID *int `json:"id" db:"id"` + LastUpdated *TimeNoMod `json:"lastUpdated" db:"last_updated"` + Name *string `json:"name" db:"name"` + Description *string `json:"description" db:"description"` + CDNName *string `json:"cdnName" db:"cdn_name"` + CDNID *int `json:"cdn" db:"cdn"` + RoutingDisabled *bool `json:"routingDisabled" db:"routing_disabled"` + Type *string `json:"type" db:"type"` } diff --git a/traffic_ops/client/type.go b/traffic_ops/client/type.go index 4f58295db..f8617b3c2 100644 --- a/traffic_ops/client/type.go +++ b/traffic_ops/client/type.go @@ -43,7 +43,7 @@ func (to *Session) GetTypes(useInTable ...string) ([]tc.Type, ReqInf, error) { } defer resp.Body.Close() - var data tc.TypeResponse + var data tc.TypesResponse if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return nil, reqInf, err } diff --git a/traffic_ops/client/v13/profile.go b/traffic_ops/client/v13/profile.go new file mode 100644 index 000000000..bbcc7995d --- /dev/null +++ b/traffic_ops/client/v13/profile.go @@ -0,0 +1,133 @@ +/* + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v13 + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + + "github.com/apache/incubator-trafficcontrol/lib/go-tc" +) + +const ( + API_v13_Profiles = "/api/1.3/profiles" +) + +// Create a Profile +func (to *Session) CreateProfile(pl tc.Profile) (tc.Alerts, ReqInf, error) { + + var remoteAddr net.Addr + reqBody, err := json.Marshal(pl) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return tc.Alerts{}, reqInf, err + } + resp, remoteAddr, err := to.request(http.MethodPost, API_v13_Profiles, reqBody) + if err != nil { + return tc.Alerts{}, reqInf, err + } + defer resp.Body.Close() + var alerts tc.Alerts + err = json.NewDecoder(resp.Body).Decode(&alerts) + return alerts, reqInf, nil +} + +// Update a Profile by ID +func (to *Session) UpdateProfileByID(id int, pl tc.Profile) (tc.Alerts, ReqInf, error) { + + var remoteAddr net.Addr + reqBody, err := json.Marshal(pl) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return tc.Alerts{}, reqInf, err + } + route := fmt.Sprintf("%s/%d", API_v13_Profiles, id) + resp, remoteAddr, err := to.request(http.MethodPut, route, reqBody) + if err != nil { + return tc.Alerts{}, reqInf, err + } + defer resp.Body.Close() + var alerts tc.Alerts + err = json.NewDecoder(resp.Body).Decode(&alerts) + return alerts, reqInf, nil +} + +// Returns a list of Profiles +func (to *Session) GetProfiles() ([]tc.Profile, ReqInf, error) { + resp, remoteAddr, err := to.request(http.MethodGet, API_v13_Profiles, nil) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return nil, reqInf, err + } + defer resp.Body.Close() + + var data tc.ProfilesResponse + err = json.NewDecoder(resp.Body).Decode(&data) + return data.Response, reqInf, nil +} + +// GET a Profile by the Profile ID +func (to *Session) GetProfileByID(id int) ([]tc.Profile, ReqInf, error) { + route := fmt.Sprintf("%s/%d", API_v13_Profiles, id) + resp, remoteAddr, err := to.request(http.MethodGet, route, nil) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return nil, reqInf, err + } + defer resp.Body.Close() + + var data tc.ProfilesResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, reqInf, err + } + + return data.Response, reqInf, nil +} + +// GET a Profile by the Profile name +func (to *Session) GetProfileByName(name string) ([]tc.Profile, ReqInf, error) { + URI := API_v13_Profiles + "?name=" + url.QueryEscape(name) + resp, remoteAddr, err := to.request(http.MethodGet, URI, nil) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return nil, reqInf, err + } + defer resp.Body.Close() + + var data tc.ProfilesResponse + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, reqInf, err + } + + return data.Response, reqInf, nil +} + +// DELETE a Profile by ID +func (to *Session) DeleteProfileByID(id int) (tc.Alerts, ReqInf, error) { + URI := fmt.Sprintf("%s/%d", API_v13_Profiles, id) + resp, remoteAddr, err := to.request(http.MethodDelete, URI, nil) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return tc.Alerts{}, reqInf, err + } + defer resp.Body.Close() + var alerts tc.Alerts + err = json.NewDecoder(resp.Body).Decode(&alerts) + return alerts, reqInf, nil +} diff --git a/traffic_ops/testing/api/phys_location_test.go b/traffic_ops/testing/api/phys_location_test.go deleted file mode 100644 index ef21730c4..000000000 --- a/traffic_ops/testing/api/phys_location_test.go +++ /dev/null @@ -1,107 +0,0 @@ -/* - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package api - -import ( - "testing" - - "github.com/apache/incubator-trafficcontrol/lib/go-log" - "github.com/apache/incubator-trafficcontrol/lib/go-tc" -) - -func TestPhysLocations(t *testing.T) { - - CreateTestPhysLocations(t) - //UpdateTestPhysLocations(t) - //GetTestPhysLocations(t) - //DeleteTestPhysLocations(t) - -} - -func CreateTestPhysLocations(t *testing.T) { - for _, physLocation := range testData.PhysLocations { - resp, _, err := TOSession.CreatePhysLocation(physLocation) - log.Debugln("Response: ", resp) - if err != nil { - t.Errorf("could not CREATE physLocation: %v\n", err) - } - } -} - -func UpdateTestPhysLocations(t *testing.T) { - - firstPhysLocation := testData.PhysLocations[0] - // Retrieve the PhysLocation by physLocation so we can get the id for the Update - resp, _, err := TOSession.GetPhysLocationByName(firstPhysLocation.Name) - if err != nil { - t.Errorf("cannot GET PhysLocation by physLocation: %v - %v\n", firstPhysLocation.Name, err) - } - remotePhysLocation := resp[0] - expectedPhysLocationName := "UPDATED" - remotePhysLocation.Name = expectedPhysLocation - var alert tc.Alerts - alert, _, err = TOSession.UpdatePhysLocationByID(remotePhysLocation.ID, remotePhysLocation) - if err != nil { - t.Errorf("cannot UPDATE PhysLocation by id: %v - %v\n", err, alert) - } - - // Retrieve the PhysLocation to check physLocation got updated - resp, _, err = TOSession.GetPhysLocationByID(remotePhysLocation.ID) - if err != nil { - t.Errorf("cannot GET PhysLocation by physLocation: %v - %v\n", firstPhysLocation.Name, err) - } - respPhysLocation := resp[0] - if respPhysLocation.Name != expectedPhysLocation { - t.Errorf("results do not match actual: %s, expected: %s\n", respPhysLocation.Name, expectedPhysLocation) - } - -} - -func GetTestPhysLocations(t *testing.T) { - for _, physLocation := range testData.PhysLocations { - resp, _, err := TOSession.GetPhysLocationByName(physLocation.Name) - if err != nil { - t.Errorf("cannot GET PhysLocation by physLocation: %v - %v\n", err, resp) - } - } -} - -func DeleteTestPhysLocations(t *testing.T) { - - physLocation := testData.PhysLocations[1] - - // Retrieve the PhysLocation by name so we can get the id - resp, _, err := TOSession.GetPhysLocationByName(physLocation.Name) - if err != nil { - t.Errorf("cannot GET PhysLocation by name: %v - %v\n", physLocation.Name, err) - } - respPhysLocation := resp[0] - - delResp, _, err := TOSession.DeletePhysLocationByID(respPhysLocation.ID) - if err != nil { - t.Errorf("cannot DELETE PhysLocation by physLocation: %v - %v\n", err, delResp) - } - - // Retrieve the PhysLocation to see if it got deleted - physLocationResp, _, err := TOSession.GetPhysLocationByName(physLocation.Name) - if err != nil { - t.Errorf("error deleting PhysLocation: %s\n", err.Error()) - } - if len(physLocationResp) > 0 { - t.Errorf("expected PhysLocation: %s to be deleted\n", physLocation.Name) - } - -} diff --git a/traffic_ops/testing/api/v13/cdns_test.go b/traffic_ops/testing/api/v13/cdns_test.go index 2aba83b34..2c9ab4c2d 100644 --- a/traffic_ops/testing/api/v13/cdns_test.go +++ b/traffic_ops/testing/api/v13/cdns_test.go @@ -49,11 +49,11 @@ func UpdateTestCDNs(t *testing.T) { // Retrieve the CDN by name so we can get the id for the Update resp, _, err := TOSession.GetCDNByName(firstCDN.Name) if err != nil { - t.Errorf("cannot GET CDN by name: %v - %v\n", firstCDN.Name, err) + t.Errorf("cannot GET CDN by name: '%s' %v - %v\n", firstCDN.Name, err) } remoteCDN := resp[0] - expectedCDNName := "testCdn1" - remoteCDN.Name = expectedCDNName + expectedCDNDomain := "domain2" + remoteCDN.DomainName = expectedCDNDomain var alert tc.Alerts alert, _, err = TOSession.UpdateCDNByID(remoteCDN.ID, remoteCDN) if err != nil { @@ -63,11 +63,11 @@ func UpdateTestCDNs(t *testing.T) { // Retrieve the CDN to check CDN name got updated resp, _, err = TOSession.GetCDNByID(remoteCDN.ID) if err != nil { - t.Errorf("cannot GET CDN by name: %v - %v\n", firstCDN.Name, err) + t.Errorf("cannot GET CDN by name: '$%s' %v - %v\n", firstCDN.Name, err) } respCDN := resp[0] - if respCDN.Name != expectedCDNName { - t.Errorf("results do not match actual: %s, expected: %s\n", respCDN.Name, expectedCDNName) + if respCDN.DomainName != expectedCDNDomain { + t.Errorf("results do not match actual: %s, expected: %s\n", respCDN.DomainName, expectedCDNDomain) } } @@ -84,25 +84,28 @@ func GetTestCDNs(t *testing.T) { func DeleteTestCDNs(t *testing.T) { - cdn := testData.CDNs[1] - // Retrieve the CDN by name so we can get the id for the Update - resp, _, err := TOSession.GetCDNByName(cdn.Name) - if err != nil { - t.Errorf("cannot GET CDN by name: %v - %v\n", cdn.Name, err) - } - respCDN := resp[0] - - delResp, _, err := TOSession.DeleteCDNByID(respCDN.ID) - if err != nil { - t.Errorf("cannot DELETE CDN by name: %v - %v\n", err, delResp) - } - - // Retrieve the CDN to see if it got deleted - cdns, _, err := TOSession.GetCDNByName(cdn.Name) - if err != nil { - t.Errorf("error deleting CDN name: %s\n", err.Error()) - } - if len(cdns) > 0 { - t.Errorf("expected CDN name: %s to be deleted\n", cdn.Name) + for _, cdn := range testData.CDNs { + // Retrieve the CDN by name so we can get the id for the Update + resp, _, err := TOSession.GetCDNByName(cdn.Name) + if err != nil { + t.Errorf("cannot GET CDN by name: %v - %v\n", cdn.Name, err) + } + if len(resp) > 0 { + respCDN := resp[0] + + _, _, err := TOSession.DeleteCDNByID(respCDN.ID) + if err != nil { + t.Errorf("cannot DELETE CDN by name: '%s' %v\n", respCDN.Name, err) + } + + // Retrieve the CDN to see if it got deleted + cdns, _, err := TOSession.GetCDNByName(cdn.Name) + if err != nil { + t.Errorf("error deleting CDN name: %s\n", err.Error()) + } + if len(cdns) > 0 { + t.Errorf("expected CDN name: %s to be deleted\n", cdn.Name) + } + } } } diff --git a/traffic_ops/testing/api/v13/profiles_test.go b/traffic_ops/testing/api/v13/profiles_test.go new file mode 100644 index 000000000..c7fa48ba4 --- /dev/null +++ b/traffic_ops/testing/api/v13/profiles_test.go @@ -0,0 +1,121 @@ +/* + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v13 + +import ( + "testing" + + "github.com/apache/incubator-trafficcontrol/lib/go-log" + tc "github.com/apache/incubator-trafficcontrol/lib/go-tc" +) + +func TestProfiles(t *testing.T) { + + CreateTestCDNs(t) + CreateTestTypes(t) + CreateTestProfiles(t) + UpdateTestProfiles(t) + GetTestProfiles(t) + DeleteTestProfiles(t) + DeleteTestTypes(t) + DeleteTestCDNs(t) + +} + +func CreateTestProfiles(t *testing.T) { + + for _, pr := range testData.Profiles { + cdns, _, err := TOSession.GetCDNByName(pr.CDNName) + respCDN := cdns[0] + pr.CDNID = respCDN.ID + + resp, _, err := TOSession.CreateProfile(pr) + + log.Debugln("Response: ", resp) + if err != nil { + t.Errorf("could not CREATE profiles with name: %s %v\n", pr.Name, err) + } + } + +} + +func UpdateTestProfiles(t *testing.T) { + + firstProfile := testData.Profiles[0] + // Retrieve the Profile by name so we can get the id for the Update + resp, _, err := TOSession.GetProfileByName(firstProfile.Name) + if err != nil { + t.Errorf("cannot GET Profile by name: %v - %v\n", firstProfile.Name, err) + } + remoteProfile := resp[0] + expectedProfileDesc := "UPDATED" + remoteProfile.Description = expectedProfileDesc + var alert tc.Alerts + alert, _, err = TOSession.UpdateProfileByID(remoteProfile.ID, remoteProfile) + if err != nil { + t.Errorf("cannot UPDATE Profile by id: %v - %v\n", err, alert) + } + + // Retrieve the Profile to check Profile name got updated + resp, _, err = TOSession.GetProfileByID(remoteProfile.ID) + if err != nil { + t.Errorf("cannot GET Profile by name: %v - %v\n", firstProfile.Name, err) + } + respProfile := resp[0] + if respProfile.Description != expectedProfileDesc { + t.Errorf("results do not match actual: %s, expected: %s\n", respProfile.Description, expectedProfileDesc) + } + +} + +func GetTestProfiles(t *testing.T) { + + for _, pr := range testData.Profiles { + resp, _, err := TOSession.GetProfileByName(pr.Name) + if err != nil { + t.Errorf("cannot GET Profile by name: %v - %v\n", err, resp) + } + } +} + +func DeleteTestProfiles(t *testing.T) { + + for _, pr := range testData.Profiles { + // Retrieve the Profile by name so we can get the id for the Update + resp, _, err := TOSession.GetProfileByName(pr.Name) + if err != nil { + t.Errorf("cannot GET Profile by name: %v - %v\n", pr.Name, err) + } + if len(resp) > 0 { + respProfile := resp[0] + + delResp, _, err := TOSession.DeleteProfileByID(respProfile.ID) + if err != nil { + t.Errorf("cannot DELETE Profile by name: %v - %v\n", err, delResp) + } + //time.Sleep(1 * time.Second) + + // Retrieve the Profile to see if it got deleted + prs, _, err := TOSession.GetProfileByName(pr.Name) + if err != nil { + t.Errorf("error deleting Profile name: %s\n", err.Error()) + } + if len(prs) > 0 { + t.Errorf("expected Profile Name: %s to be deleted\n", pr.Name) + } + } + } +} diff --git a/traffic_ops/testing/api/v13/tc-fixtures.json b/traffic_ops/testing/api/v13/tc-fixtures.json index 9b2e76cde..783b0fc33 100644 --- a/traffic_ops/testing/api/v13/tc-fixtures.json +++ b/traffic_ops/testing/api/v13/tc-fixtures.json @@ -531,6 +531,56 @@ "zip": "30301" } ], + "profiles": [ + { + "cdnName": "cdn1", + "description": "edge description", + "last_updated": "2018-03-02T17:27:11.796899+00:00", + "name": "EDGE1", + "routing_disabled": true, + "type": "ATS_PROFILE" + }, + { + "cdnName": "cdn1", + "description": "mid description", + "last_updated": "2018-03-02T17:27:11.80173+00:00", + "name": "MID1", + "routing_disabled": false, + "type": "ATS_PROFILE" + }, + { + "cdnName": "cdn1", + "description": "cdn1 description", + "last_updated": "2018-03-02T17:27:11.80452+00:00", + "name": "CCR1", + "routing_disabled": false, + "type": "TR_PROFILE" + }, + { + "cdnName": "cdn2", + "description": "cdn2 description", + "last_updated": "2018-03-02T17:27:11.807948+00:00", + "name": "CCR2", + "routing_disabled": false, + "type": "TR_PROFILE" + }, + { + "cdnName": "cdn1", + "description": "rascal description", + "last_updated": "2018-03-02T17:27:11.813052+00:00", + "name": "RASCAL1", + "routing_disabled": false, + "type": "TM_PROFILE" + }, + { + "cdnName": "cdn2", + "description": "edge2 description", + "last_updated": "2018-03-02T17:27:11.818418+00:00", + "name": "EDGE2", + "routing_disabled": false, + "type": "ATS_PROFILE" + } + ], "regions": [ { "divisionName": "division1", diff --git a/traffic_ops/testing/api/v13/todb.go b/traffic_ops/testing/api/v13/todb.go index 19f26d51c..53e2fa6f3 100644 --- a/traffic_ops/testing/api/v13/todb.go +++ b/traffic_ops/testing/api/v13/todb.go @@ -55,12 +55,6 @@ func SetupTestData(*sql.DB) error { os.Exit(1) } - err = SetupCDNs(db) - if err != nil { - fmt.Printf("\nError setting up cdns %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err) - os.Exit(1) - } - err = SetupRoles(db) if err != nil { fmt.Printf("\nError setting up roles %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err) @@ -92,12 +86,6 @@ func SetupTestData(*sql.DB) error { } /* - err = SetupProfiles(db) - if err != nil { - fmt.Printf("\nError setting up profile %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err) - os.Exit(1) - } - err = SetupProfileParameters(db) if err != nil { fmt.Printf("\nError setting up parameter %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, err) @@ -240,21 +228,6 @@ INSERT INTO tenant (id, name, active, parent_id, last_updated) VALUES (4, 'child return nil } -// SetupCDNs ... -func SetupCDNs(db *sql.DB) error { - - sqlStmt := ` -INSERT INTO cdn (id, name, last_updated, dnssec_enabled, domain_name) VALUES (100, 'cdn5', '2018-01-19 21:19:31.588795', false, 'cdn1.kabletown.net'); -INSERT INTO cdn (id, name, last_updated, dnssec_enabled, domain_name) VALUES (200, 'cdn6', '2018-01-19 21:19:31.591457', false, 'cdn2.kabletown.net'); -INSERT INTO cdn (id, name, last_updated, dnssec_enabled, domain_name) VALUES (300, 'cdn7', '2018-01-19 21:19:31.592700', false, 'cdn3.kabletown.net'); -` - err := execSQL(db, sqlStmt, "cdn") - if err != nil { - return fmt.Errorf("exec failed %v", err) - } - return nil -} - // SetupStatuses ... func SetupStatuses(db *sql.DB) error { @@ -334,27 +307,6 @@ INSERT INTO server (id, host_name, domain_name, tcp_port, xmpp_id, xmpp_passwd, return nil } -// SetupProfiles ... -func SetupProfiles(db *sql.DB) error { - - sqlStmt := ` -INSERT INTO profile (id, name, description, last_updated, type, cdn, routing_disabled) VALUES (100, 'EDGE1', 'edge description', '2018-01-19 19:01:21.512005', 'ATS_PROFILE', 100, false); -INSERT INTO profile (id, name, description, last_updated, type, cdn, routing_disabled) VALUES (200, 'MID1', 'mid description', '2018-01-19 19:01:21.517781', 'ATS_PROFILE', 100, false); -INSERT INTO profile (id, name, description, last_updated, type, cdn, routing_disabled) VALUES (300, 'CCR1', 'ccr description', '2018-01-19 19:01:21.521121', 'TR_PROFILE', 100, false); -INSERT INTO profile (id, name, description, last_updated, type, cdn, routing_disabled) VALUES (301, 'CCR2', 'ccr description', '2018-01-19 19:01:21.524584', 'TR_PROFILE', 200, false); -INSERT INTO profile (id, name, description, last_updated, type, cdn, routing_disabled) VALUES (500, 'RIAK1', 'riak description', '2018-01-19 19:01:21.528911', 'RIAK_PROFILE', 100, false); -INSERT INTO profile (id, name, description, last_updated, type, cdn, routing_disabled) VALUES (600, 'RASCAL1', 'rascal description', '2018-01-19 19:01:21.532539', 'TM_PROFILE', 100, false); -INSERT INTO profile (id, name, description, last_updated, type, cdn, routing_disabled) VALUES (700, 'RASCAL2', 'rascal2 description', '2018-01-19 19:01:21.536447', 'TM_PROFILE', 200, false); -INSERT INTO profile (id, name, description, last_updated, type, cdn, routing_disabled) VALUES (8, 'MISC', 'misc profile description', '2018-01-19 19:01:21.539022', 'UNK_PROFILE', null, false); -INSERT INTO profile (id, name, description, last_updated, type, cdn, routing_disabled) VALUES (900, 'EDGE2', 'edge description', '2018-01-19 19:01:21.541300', 'ATS_PROFILE', 200, false); -` - err := execSQL(db, sqlStmt, "profile") - if err != nil { - return fmt.Errorf("exec failed %v", err) - } - return nil -} - // SetupProfileParameters ... func SetupProfileParameters(db *sql.DB) error { diff --git a/traffic_ops/testing/api/v13/traffic_control.go b/traffic_ops/testing/api/v13/traffic_control.go index 70c8d2818..6d3400f57 100644 --- a/traffic_ops/testing/api/v13/traffic_control.go +++ b/traffic_ops/testing/api/v13/traffic_control.go @@ -25,6 +25,7 @@ type TrafficControl struct { DeliveryServiceRequests []tcapi.DeliveryServiceRequest `json:"deliveryServiceRequests"` DeliveryServices []tcapi.DeliveryService `json:"deliveryservices"` Divisions []tcapi.Division `json:"divisions"` + Profiles []tcapi.Profile `json:"profiles"` Parameters []tcapi.Parameter `json:"parameters"` PhysLocations []tcapi.PhysLocation `json:"physLocations"` Regions []tcapi.Region `json:"regions"` diff --git a/traffic_ops/traffic_ops_golang/profile/profiles.go b/traffic_ops/traffic_ops_golang/profile/profiles.go new file mode 100644 index 000000000..e4903bd08 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/profile/profiles.go @@ -0,0 +1,353 @@ +package profile + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import ( + "errors" + "fmt" + "strconv" + + "github.com/apache/incubator-trafficcontrol/lib/go-log" + "github.com/apache/incubator-trafficcontrol/lib/go-tc" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tovalidate" + validation "github.com/go-ozzo/ozzo-validation" + "github.com/jmoiron/sqlx" + "github.com/lib/pq" +) + +//we need a type alias to define functions on +type TOProfile tc.ProfileNullable + +//the refType is passed into the handlers where a copy of its type is used to decode the json. +var refType = TOProfile(tc.ProfileNullable{}) + +func GetRefType() *TOProfile { + return &refType +} + +//Implementation of the Identifier, Validator interface functions +func (prof TOProfile) GetID() (int, bool) { + if prof.ID == nil { + return 0, false + } + return *prof.ID, true +} + +func (prof *TOProfile) GetAuditName() string { + if prof.Name != nil { + return *prof.Name + } + if prof.ID != nil { + return strconv.Itoa(*prof.ID) + } + return "unknown" +} + +func (prof *TOProfile) GetType() string { + return "profile" +} + +func (prof *TOProfile) SetID(i int) { + prof.ID = &i +} + +func (prof *TOProfile) Validate(db *sqlx.DB) []error { + errs := validation.Errors{ + "name": validation.Validate(prof.Name, validation.Required), + "description": validation.Validate(prof.Description, validation.Required), + "cdn": validation.Validate(prof.CDNID, validation.Required), + "type": validation.Validate(prof.Type, validation.Required), + } + if errs != nil { + return tovalidate.ToErrors(errs) + } + return nil +} + +func (prof *TOProfile) Read(db *sqlx.DB, parameters map[string]string, user auth.CurrentUser) ([]interface{}, []error, tc.ApiErrorType) { + var rows *sqlx.Rows + + // Query Parameters to Database Query column mappings + // see the fields mapped in the SQL query + queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{ + "name": dbhelpers.WhereColumnInfo{"prof.name", nil}, + "id": dbhelpers.WhereColumnInfo{"prof.id", api.IsInt}, + } + where, orderBy, queryValues, errs := dbhelpers.BuildWhereAndOrderBy(parameters, queryParamsToQueryCols) + if len(errs) > 0 { + return nil, errs, tc.DataConflictError + } + + query := selectQuery() + where + orderBy + log.Debugln("Query is ", query) + + rows, err := db.NamedQuery(query, queryValues) + if err != nil { + log.Errorf("Error querying Profile: %v", err) + return nil, []error{tc.DBError}, tc.SystemError + } + defer rows.Close() + + profiles := []interface{}{} + for rows.Next() { + var p tc.ProfileNullable + if err = rows.StructScan(&p); err != nil { + log.Errorf("error parsing Profile rows: %v", err) + return nil, []error{tc.DBError}, tc.SystemError + } + profiles = append(profiles, p) + } + + return profiles, []error{}, tc.NoError + +} + +func selectQuery() string { + + query := `SELECT +prof.description, +prof.id, +prof.last_updated, +prof.name, +prof.routing_disabled, +prof.type, +c.id as cdn, +c.name as cdn_name +FROM profile prof +JOIN cdn c ON prof.cdn = c.id` + + return query +} + +//The TOProfile implementation of the Updater interface +//all implementations of Updater should use transactions and return the proper errorType +//ParsePQUniqueConstraintError is used to determine if a profile with conflicting values exists +//if so, it will return an errorType of DataConflict and the type should be appended to the +//generic error message returned +func (prof *TOProfile) Update(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) { + rollbackTransaction := true + tx, err := db.Beginx() + defer func() { + if tx == nil || !rollbackTransaction { + return + } + err := tx.Rollback() + if err != nil { + log.Errorln(errors.New("rolling back transaction: " + err.Error())) + } + }() + + if err != nil { + log.Error.Printf("could not begin transaction: %v", err) + return tc.DBError, tc.SystemError + } + log.Debugf("about to run exec query: %s with profile: %++v", updateQuery(), prof) + resultRows, err := tx.NamedQuery(updateQuery(), prof) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok { + err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr) + if eType == tc.DataConflictError { + return errors.New("a profile with " + err.Error()), eType + } + return err, eType + } + log.Errorf("received error: %++v from update execution", err) + return tc.DBError, tc.SystemError + } + defer resultRows.Close() + + var lastUpdated tc.TimeNoMod + rowsAffected := 0 + for resultRows.Next() { + rowsAffected++ + if err := resultRows.Scan(&lastUpdated); err != nil { + log.Error.Printf("could not scan lastUpdated from insert: %s\n", err) + return tc.DBError, tc.SystemError + } + } + log.Debugf("lastUpdated: %++v", lastUpdated) + prof.LastUpdated = &lastUpdated + if rowsAffected != 1 { + if rowsAffected < 1 { + return errors.New("no profile found with this id"), tc.DataMissingError + } + return fmt.Errorf("this update affected too many rows: %d", rowsAffected), tc.SystemError + } + err = tx.Commit() + if err != nil { + log.Errorln("Could not commit transaction: ", err) + return tc.DBError, tc.SystemError + } + rollbackTransaction = false + return nil, tc.NoError +} + +//The TOProfile implementation of the Creator interface +//all implementations of Creator should use transactions and return the proper errorType +//ParsePQUniqueConstraintError is used to determine if a profile with conflicting values exists +//if so, it will return an errorType of DataConflict and the type should be appended to the +//generic error message returned +//The insert sql returns the id and lastUpdated values of the newly inserted profile and have +//to be added to the struct +func (prof *TOProfile) Create(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) { + rollbackTransaction := true + tx, err := db.Beginx() + defer func() { + if tx == nil || !rollbackTransaction { + return + } + err := tx.Rollback() + if err != nil { + log.Errorln(errors.New("rolling back transaction: " + err.Error())) + } + }() + + if err != nil { + log.Error.Printf("could not begin transaction: %v", err) + return tc.DBError, tc.SystemError + } + q := insertQuery() + fmt.Printf("q ---> %v\n", q) + resultRows, err := tx.NamedQuery(insertQuery(), prof) + if err != nil { + if pqErr, ok := err.(*pq.Error); ok { + err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr) + if eType == tc.DataConflictError { + return errors.New("a profile with " + err.Error()), eType + } + return err, eType + } + log.Errorf("received non pq error: %++v from create execution", err) + return tc.DBError, tc.SystemError + } + defer resultRows.Close() + + var id int + var lastUpdated tc.TimeNoMod + rowsAffected := 0 + for resultRows.Next() { + rowsAffected++ + if err := resultRows.Scan(&id, &lastUpdated); err != nil { + log.Error.Printf("could not scan id from insert: %s\n", err) + return tc.DBError, tc.SystemError + } + } + if rowsAffected == 0 { + err = errors.New("no profile was inserted, no id was returned") + log.Errorln(err) + return tc.DBError, tc.SystemError + } + if rowsAffected > 1 { + err = errors.New("too many ids returned from profile insert") + log.Errorln(err) + return tc.DBError, tc.SystemError + } + + prof.SetID(id) + prof.LastUpdated = &lastUpdated + err = tx.Commit() + if err != nil { + log.Errorln("Could not commit transaction: ", err) + return tc.DBError, tc.SystemError + } + rollbackTransaction = false + return nil, tc.NoError +} + +//The Profile implementation of the Deleter interface +//all implementations of Deleter should use transactions and return the proper errorType +func (prof *TOProfile) Delete(db *sqlx.DB, user auth.CurrentUser) (error, tc.ApiErrorType) { + rollbackTransaction := true + tx, err := db.Beginx() + defer func() { + if tx == nil || !rollbackTransaction { + return + } + err := tx.Rollback() + if err != nil { + log.Errorln(errors.New("rolling back transaction: " + err.Error())) + } + }() + + if err != nil { + log.Error.Printf("could not begin transaction: %v", err) + return tc.DBError, tc.SystemError + } + log.Debugf("about to run exec query: %s with profile: %++v", deleteQuery(), prof) + result, err := tx.NamedExec(deleteQuery(), prof) + if err != nil { + log.Errorf("received error: %++v from delete execution", err) + return tc.DBError, tc.SystemError + } + rowsAffected, err := result.RowsAffected() + if err != nil { + return tc.DBError, tc.SystemError + } + if rowsAffected < 1 { + return errors.New("no profile with that id found"), tc.DataMissingError + } + if rowsAffected > 1 { + return fmt.Errorf("this create affected too many rows: %d", rowsAffected), tc.SystemError + } + + err = tx.Commit() + if err != nil { + log.Errorln("Could not commit transaction: ", err) + return tc.DBError, tc.SystemError + } + rollbackTransaction = false + return nil, tc.NoError +} + +func updateQuery() string { + query := `UPDATE +profile SET +cdn=:cdn, +description=:description, +name=:name, +routing_disabled=:routing_disabled, +type=:type +WHERE id=:id RETURNING last_updated` + return query +} + +func insertQuery() string { + query := `INSERT INTO profile ( +cdn, +description, +name, +routing_disabled, +type) VALUES ( +:cdn, +:description, +:name, +:routing_disabled, +:type) RETURNING id,last_updated` + return query +} + +func deleteQuery() string { + query := `DELETE FROM profile +WHERE id=:id` + return query +} diff --git a/traffic_ops/traffic_ops_golang/profile/profiles_test.go b/traffic_ops/traffic_ops_golang/profile/profiles_test.go new file mode 100644 index 000000000..9bee2b54d --- /dev/null +++ b/traffic_ops/traffic_ops_golang/profile/profiles_test.go @@ -0,0 +1,144 @@ +package profile + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import ( + "errors" + "reflect" + "testing" + "time" + + "github.com/apache/incubator-trafficcontrol/lib/go-tc" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/test" + "github.com/jmoiron/sqlx" + + sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" +) + +func getTestProfiles() []tc.ProfileNullable { + profiles := []tc.ProfileNullable{} + + lastUpdated := tc.TimeNoMod{} + lastUpdated.Scan(time.Now()) + ID := 1 + name := "profile1" + description := "desc1" + pt := "TR_PROFILE" + cdnID := 1 + cdnName := "cdn1" + rd := true + + testCase := tc.ProfileNullable{ + ID: &ID, + LastUpdated: &lastUpdated, + Name: &name, + Description: &description, + CDNName: &cdnName, + CDNID: &cdnID, + RoutingDisabled: &rd, + Type: &pt, + } + profiles = append(profiles, testCase) + + testCase2 := testCase + name = "profile2" + testCase2.Name = &name + profiles = append(profiles, testCase2) + + return profiles +} + +func TestGetProfiles(t *testing.T) { + mockDB, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + defer mockDB.Close() + + db := sqlx.NewDb(mockDB, "sqlmock") + defer db.Close() + + testCase := getTestProfiles() + cols := test.ColsFromStructByTag("db", tc.ProfileNullable{}) + rows := sqlmock.NewRows(cols) + + for _, ts := range testCase { + rows = rows.AddRow( + ts.ID, + ts.LastUpdated, + ts.Name, + ts.Description, + ts.CDNName, + ts.CDNID, + ts.RoutingDisabled, + ts.Type, + ) + } + mock.ExpectQuery("SELECT").WillReturnRows(rows) + v := map[string]string{"name": "1"} + + profiles, errs, _ := refType.Read(db, v, auth.CurrentUser{}) + if len(errs) > 0 { + t.Errorf("profile.Read expected: no errors, actual: %v", errs) + } + + if len(profiles) != 2 { + t.Errorf("profile.Read expected: len(profiles) == 2, actual: %v", len(profiles)) + } + +} + +func TestInterfaces(t *testing.T) { + var i interface{} + i = &TOProfile{} + + if _, ok := i.(api.Creator); !ok { + t.Errorf("Profile must be Creator") + } + if _, ok := i.(api.Reader); !ok { + t.Errorf("Profile must be Reader") + } + if _, ok := i.(api.Updater); !ok { + t.Errorf("Profile must be Updater") + } + if _, ok := i.(api.Deleter); !ok { + t.Errorf("Profile must be Deleter") + } + if _, ok := i.(api.Identifier); !ok { + t.Errorf("Profile must be Identifier") + } +} + +func TestValidate(t *testing.T) { + p := TOProfile{} + errs := test.SortErrors(p.Validate(nil)) + expected := test.SortErrors([]error{ + errors.New("'cdn' cannot be blank"), + errors.New("'description' cannot be blank"), + errors.New("'name' cannot be blank"), + errors.New("'type' cannot be blank"), + }) + + if !reflect.DeepEqual(expected, errs) { + t.Errorf("expected %++v, got %++v", expected, errs) + } +} diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go index 31e1b4967..80e557f20 100644 --- a/traffic_ops/traffic_ops_golang/routes.go +++ b/traffic_ops/traffic_ops_golang/routes.go @@ -38,6 +38,7 @@ import ( "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/parameter" "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/physlocation" "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/ping" + "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/profile" "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/region" "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/server" "github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/status" @@ -170,6 +171,13 @@ func Routes(d ServerData) ([]Route, http.Handler, error) { {1.2, http.MethodPost, `servers/{id}/deliveryservices$`, server.AssignDeliveryServicesToServerHandler(d.DB), auth.PrivLevelOperations, Authenticated, nil}, {1.2, http.MethodGet, `servers/{host_name}/update_status$`, server.GetServerUpdateStatusHandler(d.DB), auth.PrivLevelReadOnly, Authenticated, nil}, + //Profiles + {1.3, http.MethodGet, `profiles/?(\.json)?$`, api.ReadHandler(profile.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil}, + {1.3, http.MethodGet, `profiles/{id}$`, api.ReadHandler(profile.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil}, + {1.3, http.MethodPut, `profiles/{id}$`, api.UpdateHandler(profile.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil}, + {1.3, http.MethodPost, `profiles/?$`, api.CreateHandler(profile.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil}, + {1.3, http.MethodDelete, `profiles/{id}$`, api.DeleteHandler(profile.GetRefType(), d.DB), auth.PrivLevelOperations, Authenticated, nil}, + //SSLKeys deliveryservice endpoints here that are marked marked as '-wip' need to have tenancy checks added {1.2, http.MethodGet, `deliveryservices-wip/xmlId/{xmlID}/sslkeys$`, getDeliveryServiceSSLKeysByXMLIDHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil}, {1.2, http.MethodGet, `deliveryservices-wip/hostname/{hostName}/sslkeys$`, getDeliveryServiceSSLKeysByHostNameHandler(d.DB, d.Config), auth.PrivLevelAdmin, Authenticated, nil}, ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: [email protected] With regards, Apache Git Services
