Skip to content

Commit 88ea99b

Browse files
author
isaac
committed
Add key-value support
Full support for key-value endpoints for both http and stream contexts.
1 parent 0eecde4 commit 88ea99b

File tree

3 files changed

+446
-0
lines changed

3 files changed

+446
-0
lines changed

client/nginx.go

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ const APIVersion = 2
1414

1515
const streamNotConfiguredCode = "StreamNotConfigured"
1616

17+
// Stream is a stream context parameter.
18+
const Stream = true
19+
20+
// HTTP is a HTTP context parameter.
21+
const HTTP = false
22+
1723
// NginxClient lets you access NGINX Plus API.
1824
type NginxClient struct {
1925
apiEndpoint string
@@ -806,3 +812,243 @@ func (client *NginxClient) getStreamUpstreams() (*StreamUpstreams, error) {
806812
}
807813
return &upstreams, nil
808814
}
815+
816+
// KeyValPairs are the key-value pairs stored in a zone.
817+
type KeyValPairs map[string]string
818+
819+
// KeyValPairsByZone are the KeyValPairs for all zones, by zone name.
820+
type KeyValPairsByZone map[string]KeyValPairs
821+
822+
// GetKeyValPairs fetches key/value pairs for a given HTTP zone.
823+
func (client *NginxClient) GetKeyValPairs(zone string) (KeyValPairs, error) {
824+
return client.getKeyValPairs(zone, HTTP)
825+
}
826+
827+
// GetStreamKeyValPairs fetches key/value pairs for a given Stream zone.
828+
func (client *NginxClient) GetStreamKeyValPairs(zone string) (KeyValPairs, error) {
829+
return client.getKeyValPairs(zone, Stream)
830+
}
831+
832+
func (client *NginxClient) getKeyValPairs(zone string, stream bool) (KeyValPairs, error) {
833+
base := "http"
834+
if stream {
835+
base = "stream"
836+
}
837+
if zone == "" {
838+
return nil, fmt.Errorf("zone required")
839+
}
840+
var keyValPairs KeyValPairs
841+
err := client.get(fmt.Sprintf("%v/keyvals/%v", base, zone), &keyValPairs)
842+
if err != nil {
843+
return nil, fmt.Errorf("failed to get keyvals for zone: %v/%v: %v", base, zone, err)
844+
}
845+
return keyValPairs, nil
846+
}
847+
848+
// GetAllKeyValPairs fetches all key/value pairs for all HTTP zones.
849+
func (client *NginxClient) GetAllKeyValPairs() (KeyValPairsByZone, error) {
850+
return client.getAllKeyValPairs(HTTP)
851+
}
852+
853+
// GetAllStreamKeyValPairs fetches all key/value pairs for all Stream zones.
854+
func (client *NginxClient) GetAllStreamKeyValPairs() (KeyValPairsByZone, error) {
855+
return client.getAllKeyValPairs(Stream)
856+
}
857+
858+
func (client *NginxClient) getAllKeyValPairs(stream bool) (KeyValPairsByZone, error) {
859+
base := "http"
860+
if stream {
861+
base = "stream"
862+
}
863+
var keyValPairsByZone KeyValPairsByZone
864+
err := client.get(fmt.Sprintf("%v/keyvals", base), &keyValPairsByZone)
865+
if err != nil {
866+
return nil, fmt.Errorf("failed to get keyvals for all %v zones: %v", base, err)
867+
}
868+
return keyValPairsByZone, nil
869+
}
870+
871+
// AddKeyValPair adds a new key/value pair to a given HTTP zone.
872+
func (client *NginxClient) AddKeyValPair(zone string, key string, val string) error {
873+
return client.addKeyValPair(zone, key, val, HTTP)
874+
}
875+
876+
// AddStreamKeyValPair adds a new key/value pair to a given Stream zone.
877+
func (client *NginxClient) AddStreamKeyValPair(zone string, key string, val string) error {
878+
return client.addKeyValPair(zone, key, val, Stream)
879+
}
880+
881+
func (client *NginxClient) addKeyValPair(zone string, key string, val string, stream bool) error {
882+
base := "http"
883+
if stream {
884+
base = "stream"
885+
}
886+
path := fmt.Sprintf("%v/keyvals", base)
887+
url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, APIVersion, path)
888+
if zone != "" {
889+
url = fmt.Sprintf("%v/%v", url, zone)
890+
} else {
891+
return fmt.Errorf("zone required")
892+
}
893+
894+
jsonInput, err := json.Marshal(KeyValPairs{key: val})
895+
if err != nil {
896+
return fmt.Errorf("failed to marshall input: %v", err)
897+
}
898+
899+
resp, err := client.httpClient.Post(url, "application/json", bytes.NewBuffer(jsonInput))
900+
if err != nil {
901+
return fmt.Errorf("failed to create post request: %v", err)
902+
}
903+
defer resp.Body.Close()
904+
905+
if resp.StatusCode != http.StatusCreated {
906+
return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf(
907+
"expected %v response, got %v",
908+
http.StatusCreated, resp.StatusCode))
909+
}
910+
return nil
911+
}
912+
913+
// ModifyKeyValPair modifies the value of an existing key in a given HTTP zone.
914+
func (client *NginxClient) ModifyKeyValPair(zone string, key string, val string) error {
915+
return client.modifyKeyValPair(zone, key, val, HTTP)
916+
}
917+
918+
// ModifyStreamKeyValPair modifies the value of an existing key in a given Stream zone.
919+
func (client *NginxClient) ModifyStreamKeyValPair(zone string, key string, val string) error {
920+
return client.modifyKeyValPair(zone, key, val, Stream)
921+
}
922+
923+
func (client *NginxClient) modifyKeyValPair(zone string, key string, val string, stream bool) error {
924+
base := "http"
925+
if stream {
926+
base = "stream"
927+
}
928+
path := fmt.Sprintf("%v/keyvals", base)
929+
url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, APIVersion, path)
930+
if zone != "" {
931+
url = fmt.Sprintf("%v/%v", url, zone)
932+
} else {
933+
return fmt.Errorf("zone required")
934+
}
935+
936+
jsonInput, err := json.Marshal(KeyValPairs{key: val})
937+
if err != nil {
938+
return fmt.Errorf("failed to marshall input: %v", err)
939+
}
940+
req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(jsonInput))
941+
if err != nil {
942+
return fmt.Errorf("failed to create a patch request: %v", err)
943+
}
944+
req.Header.Set("Content-Type", "application/json")
945+
946+
resp, err := client.httpClient.Do(req)
947+
if err != nil {
948+
return fmt.Errorf("failed to do patch request: %v", err)
949+
}
950+
defer resp.Body.Close()
951+
// We will consider ONLY 204 as success
952+
if resp.StatusCode != http.StatusNoContent {
953+
return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf(
954+
"expected %v response, got %v",
955+
http.StatusNoContent, resp.StatusCode))
956+
}
957+
return nil
958+
}
959+
960+
// DeleteKeyValuePair deletes the key/value pair for a given key.
961+
func (client *NginxClient) DeleteKeyValuePair(zone string, key string) error {
962+
return client.deleteKeyValuePair(zone, key, HTTP)
963+
}
964+
965+
// DeleteStreamKeyValuePair deletes the key/value pair for a given key.
966+
func (client *NginxClient) DeleteStreamKeyValuePair(zone string, key string) error {
967+
return client.deleteKeyValuePair(zone, key, Stream)
968+
}
969+
970+
// To delete a key/value pair you set the value to null via the API,
971+
// then NGINX+ will delete the key.
972+
func (client *NginxClient) deleteKeyValuePair(zone string, key string, stream bool) error {
973+
base := "http"
974+
if stream {
975+
base = "stream"
976+
}
977+
path := fmt.Sprintf("%v/keyvals", base)
978+
url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, APIVersion, path)
979+
if zone != "" {
980+
url = fmt.Sprintf("%v/%v", url, zone)
981+
} else {
982+
return fmt.Errorf("zone required")
983+
}
984+
985+
// map[string]string can't have a nil value so we use a different type here.
986+
keyval := make(map[string]interface{})
987+
keyval[key] = nil
988+
989+
jsonInput, err := json.Marshal(keyval)
990+
if err != nil {
991+
return fmt.Errorf("failed to marshall input: %v", err)
992+
}
993+
994+
req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(jsonInput))
995+
if err != nil {
996+
return fmt.Errorf("failed to create a patch request: %v", err)
997+
}
998+
req.Header.Set("Content-Type", "application/json")
999+
1000+
resp, err := client.httpClient.Do(req)
1001+
if err != nil {
1002+
return fmt.Errorf("failed to do patch request: %v", err)
1003+
}
1004+
defer resp.Body.Close()
1005+
1006+
// Expect status 204
1007+
if resp.StatusCode != http.StatusNoContent {
1008+
return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf(
1009+
"expected %v response, got %v",
1010+
http.StatusNoContent, resp.StatusCode))
1011+
}
1012+
return nil
1013+
}
1014+
1015+
// DeleteKeyValPairs deletes all the key-value pairs in a given HTTP zone.
1016+
func (client *NginxClient) DeleteKeyValPairs(zone string) error {
1017+
return client.deleteKeyValPairs(zone, HTTP)
1018+
}
1019+
1020+
// DeleteStreamKeyValPairs deletes all the key-value pairs in a given Stream zone.
1021+
func (client *NginxClient) DeleteStreamKeyValPairs(zone string) error {
1022+
return client.deleteKeyValPairs(zone, Stream)
1023+
}
1024+
1025+
func (client *NginxClient) deleteKeyValPairs(zone string, stream bool) error {
1026+
base := "http"
1027+
if stream {
1028+
base = "stream"
1029+
}
1030+
if zone == "" {
1031+
return fmt.Errorf("zone required")
1032+
}
1033+
path := fmt.Sprintf("%v/keyvals/%v", base, zone)
1034+
url := fmt.Sprintf("%v/%v/%v", client.apiEndpoint, APIVersion, path)
1035+
1036+
req, err := http.NewRequest(http.MethodDelete, url, nil)
1037+
if err != nil {
1038+
return fmt.Errorf("failed to create a delete request: %v", err)
1039+
}
1040+
1041+
resp, err := client.httpClient.Do(req)
1042+
if err != nil {
1043+
return fmt.Errorf("failed to do delete request: %v", err)
1044+
}
1045+
defer resp.Body.Close()
1046+
1047+
// expect status 204
1048+
if resp.StatusCode != http.StatusNoContent {
1049+
return createResponseMismatchError(resp.Body).Wrap(fmt.Sprintf(
1050+
"expected %v response, got %v",
1051+
http.StatusNoContent, resp.StatusCode))
1052+
}
1053+
return nil
1054+
}

docker/nginx.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@ http {
2828

2929
#gzip on;
3030

31+
keyval_zone zone=zone_one:32k;
32+
keyval $arg_text $text zone=zone_one;
33+
3134
include /etc/nginx/conf.d/*.conf;
3235
}
3336

3437
stream {
38+
keyval_zone zone=zone_one_stream:32k;
39+
keyval $hostname $text zone=zone_one_stream;
40+
3541
upstream stream_test {
3642
zone stream_test 64k;
3743
}

0 commit comments

Comments
 (0)