From 299a879708b050e2f1f125810f8e8fcb50b7e8e5 Mon Sep 17 00:00:00 2001 From: isaac Date: Mon, 10 Dec 2018 11:17:11 +0000 Subject: [PATCH] Add key-value support Full support for key-value endpoints for both http and stream contexts. --- client/nginx.go | 246 +++++++++++++++++++++++++++++++++++++++++++ docker/nginx.conf | 6 ++ tests/client_test.go | 194 ++++++++++++++++++++++++++++++++++ 3 files changed, 446 insertions(+) diff --git a/client/nginx.go b/client/nginx.go index 0f9678b..a2b2dc3 100644 --- a/client/nginx.go +++ b/client/nginx.go @@ -14,6 +14,12 @@ const APIVersion = 2 const streamNotConfiguredCode = "StreamNotConfigured" +// Stream is a stream context parameter. +const Stream = true + +// HTTP is a HTTP context parameter. +const HTTP = false + // NginxClient lets you access NGINX Plus API. type NginxClient struct { apiEndpoint string @@ -806,3 +812,243 @@ func (client *NginxClient) getStreamUpstreams() (*StreamUpstreams, error) { } return &upstreams, nil } + +// KeyValPairs are the key-value pairs stored in a zone. +type KeyValPairs map[string]string + +// KeyValPairsByZone are the KeyValPairs for all zones, by zone name. +type KeyValPairsByZone map[string]KeyValPairs + +// GetKeyValPairs fetches key/value pairs for a given HTTP zone. +func (client *NginxClient) GetKeyValPairs(zone string) (KeyValPairs, error) { + return client.getKeyValPairs(zone, HTTP) +} + +// GetStreamKeyValPairs fetches key/value pairs for a given Stream zone. +func (client *NginxClient) GetStreamKeyValPairs(zone string) (KeyValPairs, error) { + return client.getKeyValPairs(zone, Stream) +} + +func (client *NginxClient) getKeyValPairs(zone string, stream bool) (KeyValPairs, error) { + base := "http" + if stream { + base = "stream" + } + if zone == "" { + return nil, fmt.Errorf("zone required") + } + var keyValPairs KeyValPairs + err := client.get(fmt.Sprintf("%v/keyvals/%v", base, zone), &keyValPairs) + if err != nil { + return nil, fmt.Errorf("failed to get keyvals for zone: %v/%v: %v", base, zone, err) + } + return keyValPairs, nil +} + +// GetAllKeyValPairs fetches all key/value pairs for all HTTP zones. +func (client *NginxClient) GetAllKeyValPairs() (KeyValPairsByZone, error) { + return client.getAllKeyValPairs(HTTP) +} + +// GetAllStreamKeyValPairs fetches all key/value pairs for all Stream zones. +func (client *NginxClient) GetAllStreamKeyValPairs() (KeyValPairsByZone, error) { + return client.getAllKeyValPairs(Stream) +} + +func (client *NginxClient) getAllKeyValPairs(stream bool) (KeyValPairsByZone, error) { + base := "http" + if stream { + base = "stream" + } + var keyValPairsByZone KeyValPairsByZone + err := client.get(fmt.Sprintf("%v/keyvals", base), &keyValPairsByZone) + if err != nil { + return nil, fmt.Errorf("failed to get keyvals for all %v zones: %v", base, err) + } + return keyValPairsByZone, nil +} + +// AddKeyValPair adds a new key/value pair to a given HTTP zone. +func (client *NginxClient) AddKeyValPair(zone string, key string, val string) error { + return client.addKeyValPair(zone, key, val, HTTP) +} + +// AddStreamKeyValPair adds a new key/value pair to a given Stream zone. +func (client *NginxClient) AddStreamKeyValPair(zone string, key string, val string) error { + return client.addKeyValPair(zone, key, val, Stream) +} + +func (client *NginxClient) addKeyValPair(zone string, key string, val string, stream bool) error { + base := "http" + if stream { + base = "stream" + } + path := fmt.Sprintf("%v/keyvals", base) + url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, APIVersion, path) + if zone != "" { + url = fmt.Sprintf("%v/%v", url, zone) + } else { + return fmt.Errorf("zone required") + } + + jsonInput, err := json.Marshal(KeyValPairs{key: val}) + if err != nil { + return fmt.Errorf("failed to marshall input: %v", err) + } + + resp, err := client.httpClient.Post(url, "application/json", bytes.NewBuffer(jsonInput)) + if err != nil { + return fmt.Errorf("failed to create post request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf( + "expected %v response, got %v", + http.StatusCreated, resp.StatusCode)) + } + return nil +} + +// ModifyKeyValPair modifies the value of an existing key in a given HTTP zone. +func (client *NginxClient) ModifyKeyValPair(zone string, key string, val string) error { + return client.modifyKeyValPair(zone, key, val, HTTP) +} + +// ModifyStreamKeyValPair modifies the value of an existing key in a given Stream zone. +func (client *NginxClient) ModifyStreamKeyValPair(zone string, key string, val string) error { + return client.modifyKeyValPair(zone, key, val, Stream) +} + +func (client *NginxClient) modifyKeyValPair(zone string, key string, val string, stream bool) error { + base := "http" + if stream { + base = "stream" + } + path := fmt.Sprintf("%v/keyvals", base) + url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, APIVersion, path) + if zone != "" { + url = fmt.Sprintf("%v/%v", url, zone) + } else { + return fmt.Errorf("zone required") + } + + jsonInput, err := json.Marshal(KeyValPairs{key: val}) + if err != nil { + return fmt.Errorf("failed to marshall input: %v", err) + } + req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(jsonInput)) + if err != nil { + return fmt.Errorf("failed to create a patch request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := client.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to do patch request: %v", err) + } + defer resp.Body.Close() + // We will consider ONLY 204 as success + if resp.StatusCode != http.StatusNoContent { + return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf( + "expected %v response, got %v", + http.StatusNoContent, resp.StatusCode)) + } + return nil +} + +// DeleteKeyValuePair deletes the key/value pair for a key in a given HTTP zone. +func (client *NginxClient) DeleteKeyValuePair(zone string, key string) error { + return client.deleteKeyValuePair(zone, key, HTTP) +} + +// DeleteStreamKeyValuePair deletes the key/value pair for a key in a given Stream zone. +func (client *NginxClient) DeleteStreamKeyValuePair(zone string, key string) error { + return client.deleteKeyValuePair(zone, key, Stream) +} + +// To delete a key/value pair you set the value to null via the API, +// then NGINX+ will delete the key. +func (client *NginxClient) deleteKeyValuePair(zone string, key string, stream bool) error { + base := "http" + if stream { + base = "stream" + } + path := fmt.Sprintf("%v/keyvals", base) + url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, APIVersion, path) + if zone != "" { + url = fmt.Sprintf("%v/%v", url, zone) + } else { + return fmt.Errorf("zone required") + } + + // map[string]string can't have a nil value so we use a different type here. + keyval := make(map[string]interface{}) + keyval[key] = nil + + jsonInput, err := json.Marshal(keyval) + if err != nil { + return fmt.Errorf("failed to marshall input: %v", err) + } + + req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(jsonInput)) + if err != nil { + return fmt.Errorf("failed to create a patch request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := client.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to do patch request: %v", err) + } + defer resp.Body.Close() + + // Expect status 204 + if resp.StatusCode != http.StatusNoContent { + return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf( + "expected %v response, got %v", + http.StatusNoContent, resp.StatusCode)) + } + return nil +} + +// DeleteKeyValPairs deletes all the key-value pairs in a given HTTP zone. +func (client *NginxClient) DeleteKeyValPairs(zone string) error { + return client.deleteKeyValPairs(zone, HTTP) +} + +// DeleteStreamKeyValPairs deletes all the key-value pairs in a given Stream zone. +func (client *NginxClient) DeleteStreamKeyValPairs(zone string) error { + return client.deleteKeyValPairs(zone, Stream) +} + +func (client *NginxClient) deleteKeyValPairs(zone string, stream bool) error { + base := "http" + if stream { + base = "stream" + } + if zone == "" { + return fmt.Errorf("zone required") + } + path := fmt.Sprintf("%v/keyvals/%v", base, zone) + url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, APIVersion, path) + + req, err := http.NewRequest(http.MethodDelete, url, nil) + if err != nil { + return fmt.Errorf("failed to create a delete request: %v", err) + } + + resp, err := client.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to do delete request: %v", err) + } + defer resp.Body.Close() + + // expect status 204 + if resp.StatusCode != http.StatusNoContent { + return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf( + "expected %v response, got %v", + http.StatusNoContent, resp.StatusCode)) + } + return nil +} diff --git a/docker/nginx.conf b/docker/nginx.conf index 4bba7aa..5e61055 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -28,10 +28,16 @@ http { #gzip on; + keyval_zone zone=zone_one:32k; + keyval $arg_text $text zone=zone_one; + include /etc/nginx/conf.d/*.conf; } stream { + keyval_zone zone=zone_one_stream:32k; + keyval $hostname $text zone=zone_one_stream; + upstream stream_test { zone stream_test 64k; } diff --git a/tests/client_test.go b/tests/client_test.go index 35dcb14..4457d06 100644 --- a/tests/client_test.go +++ b/tests/client_test.go @@ -549,6 +549,200 @@ func TestStreamStats(t *testing.T) { t.Errorf("Couldn't remove stream servers: %v", err) } } +func TestKeyValue(t *testing.T) { + zoneName := "zone_one" + httpClient := &http.Client{} + c, err := client.NewNginxClient(httpClient, "http://127.0.0.1:8080/api") + if err != nil { + t.Fatalf("Error connecting to nginx: %v", err) + } + + err = c.AddKeyValPair(zoneName, "key1", "val1") + if err != nil { + t.Errorf("Couldn't set keyvals: %v", err) + } + + var keyValPairs client.KeyValPairs + keyValPairs, err = c.GetKeyValPairs(zoneName) + if err != nil { + t.Errorf("Couldn't get keyvals for zone: %v, err: %v", zoneName, err) + } + expectedKeyValPairs := client.KeyValPairs{ + "key1": "val1", + } + if !reflect.DeepEqual(expectedKeyValPairs, keyValPairs) { + t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairs, keyValPairs) + } + + keyValuPairsByZone, err := c.GetAllKeyValPairs() + if err != nil { + t.Errorf("Couldn't get keyvals, %v", err) + } + expectedKeyValPairsByZone := client.KeyValPairsByZone{ + zoneName: expectedKeyValPairs, + } + if !reflect.DeepEqual(expectedKeyValPairsByZone, keyValuPairsByZone) { + t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairsByZone, keyValuPairsByZone) + } + + // modify keyval + expectedKeyValPairs["key1"] = "valModified1" + err = c.ModifyKeyValPair(zoneName, "key1", "valModified1") + if err != nil { + t.Errorf("couldn't set keyval: %v", err) + } + + keyValPairs, err = c.GetKeyValPairs(zoneName) + if err != nil { + t.Errorf("couldn't get keyval: %v", err) + } + if !reflect.DeepEqual(expectedKeyValPairs, keyValPairs) { + t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairs, keyValPairs) + } + + // error expected + err = c.AddKeyValPair(zoneName, "key1", "valModified1") + if err == nil { + t.Errorf("adding same key/val should result in error") + } + + err = c.AddKeyValPair(zoneName, "key2", "val2") + if err != nil { + t.Errorf("error adding another key/val pair: %v", err) + } + + err = c.DeleteKeyValuePair(zoneName, "key1") + if err != nil { + t.Errorf("error deleting key") + } + + expectedKeyValPairs2 := client.KeyValPairs{ + "key2": "val2", + } + keyValPairs, err = c.GetKeyValPairs(zoneName) + if err != nil { + t.Errorf("couldn't get keyval: %v", err) + } + if !reflect.DeepEqual(keyValPairs, expectedKeyValPairs2) { + t.Errorf("didn't delete key1 %+v", keyValPairs) + } + + err = c.DeleteKeyValPairs(zoneName) + if err != nil { + t.Errorf("couldn't delete all: %v", err) + } + + keyValPairs, err = c.GetKeyValPairs(zoneName) + if err != nil { + t.Errorf("couldn't get keyval: %v", err) + } + if len(keyValPairs) > 0 { + t.Errorf("zone should be empty after bulk delete") + } + + // error expected + err = c.ModifyKeyValPair(zoneName, "key1", "val1") + if err == nil { + t.Errorf("modifying nonexistent key/val should result in error") + } +} + +func TestKeyValueStream(t *testing.T) { + zoneName := "zone_one_stream" + httpClient := &http.Client{} + c, err := client.NewNginxClient(httpClient, "http://127.0.0.1:8080/api") + if err != nil { + t.Fatalf("Error connecting to nginx: %v", err) + } + + err = c.AddStreamKeyValPair(zoneName, "key1", "val1") + if err != nil { + t.Errorf("Couldn't set keyvals: %v", err) + } + + keyValPairs, err := c.GetStreamKeyValPairs(zoneName) + if err != nil { + t.Errorf("Couldn't get keyvals for zone: %v, err: %v", zoneName, err) + } + expectedKeyValPairs := client.KeyValPairs{ + "key1": "val1", + } + if !reflect.DeepEqual(expectedKeyValPairs, keyValPairs) { + t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairs, keyValPairs) + } + + keyValPairsByZone, err := c.GetAllStreamKeyValPairs() + if err != nil { + t.Errorf("Couldn't get keyvals, %v", err) + } + expectedKeyValuePairsByZone := client.KeyValPairsByZone{ + zoneName: expectedKeyValPairs, + } + if !reflect.DeepEqual(expectedKeyValuePairsByZone, keyValPairsByZone) { + t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValuePairsByZone, keyValPairsByZone) + } + + // modify keyval + expectedKeyValPairs["key1"] = "valModified1" + err = c.ModifyStreamKeyValPair(zoneName, "key1", "valModified1") + if err != nil { + t.Errorf("couldn't set keyval: %v", err) + } + + keyValPairs, err = c.GetStreamKeyValPairs(zoneName) + if err != nil { + t.Errorf("couldn't get keyval: %v", err) + } + if !reflect.DeepEqual(expectedKeyValPairs, keyValPairs) { + t.Errorf("maps are not equal. expected: %+v, got: %+v", expectedKeyValPairs, keyValPairs) + } + + // error expected + err = c.AddStreamKeyValPair(zoneName, "key1", "valModified1") + if err == nil { + t.Errorf("adding same key/val should result in error") + } + + err = c.AddStreamKeyValPair(zoneName, "key2", "val2") + if err != nil { + t.Errorf("error adding another key/val pair: %v", err) + } + + err = c.DeleteStreamKeyValuePair(zoneName, "key1") + if err != nil { + t.Errorf("error deleting key") + } + + keyValPairs, err = c.GetStreamKeyValPairs(zoneName) + if err != nil { + t.Errorf("couldn't get keyval: %v", err) + } + expectedKeyValPairs2 := client.KeyValPairs{ + "key2": "val2", + } + if !reflect.DeepEqual(keyValPairs, expectedKeyValPairs2) { + t.Errorf("didn't delete key1 %+v", keyValPairs) + } + + err = c.DeleteStreamKeyValPairs(zoneName) + if err != nil { + t.Errorf("couldn't delete all: %v", err) + } + + keyValPairs, err = c.GetStreamKeyValPairs(zoneName) + if err != nil { + t.Errorf("couldn't get keyval: %v", err) + } + if len(keyValPairs) > 0 { + t.Errorf("zone should be empty after bulk delete") + } + + // error expected + err = c.ModifyStreamKeyValPair(zoneName, "key1", "valModified") + if err == nil { + t.Errorf("modifying nonexistent key/val should result in error") + } +} func compareUpstreamServers(x []client.UpstreamServer, y []client.UpstreamServer) bool { var xServers []string