Skip to content

Commit bc6501c

Browse files
committed
Initial commit
0 parents  commit bc6501c

File tree

7 files changed

+520
-0
lines changed

7 files changed

+520
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# NGINX Plus license files
2+
*.crt
3+
*.key
4+
5+
# Visual Studio Code settings
6+
.vscode

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
NGINX_PLUS_VERSION=1.13.7-2
2+
NGINX_IMAGE=nginxplus:$(NGINX_PLUS_VERSION)
3+
4+
docker-build:
5+
docker build --build-arg NGINX_PLUS_VERSION=$(NGINX_PLUS_VERSION)~stretch -t $(NGINX_IMAGE) docker
6+
7+
run-nginx-plus:
8+
docker run --rm -p 8080:8080 $(NGINX_IMAGE)
9+
10+
test:
11+
go test client/*
12+
go test tests/client_test.go

client/nginx_client.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package client
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"net/http"
8+
)
9+
10+
// NginxClient lets you add/remove servers to/from NGINX Plus via its upstream_conf API
11+
type NginxClient struct {
12+
upstreamConfEndpoint string
13+
statusEndpoint string
14+
}
15+
16+
type peers struct {
17+
Peers []peer
18+
}
19+
20+
type peer struct {
21+
ID int
22+
Server string
23+
}
24+
25+
// NewNginxClient creates an NginxClient.
26+
func NewNginxClient(upstreamConfEndpoint string, statusEndpoint string) (*NginxClient, error) {
27+
err := checkIfUpstreamConfIsAccessible(upstreamConfEndpoint)
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
err = checkIfStatusIsAccessible(statusEndpoint)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
client := &NginxClient{upstreamConfEndpoint: upstreamConfEndpoint, statusEndpoint: statusEndpoint}
38+
return client, nil
39+
}
40+
41+
func checkIfUpstreamConfIsAccessible(endpoint string) error {
42+
resp, err := http.Get(endpoint)
43+
if err != nil {
44+
return fmt.Errorf("upstream_conf endpoint %v is not accessible: %v", endpoint, err)
45+
}
46+
defer resp.Body.Close()
47+
48+
body, err := ioutil.ReadAll(resp.Body)
49+
if err != nil {
50+
return fmt.Errorf("upstream_conf endpoint %v is not accessible: %v", endpoint, err)
51+
}
52+
53+
if resp.StatusCode != http.StatusBadRequest {
54+
return fmt.Errorf("upstream_conf endpoint %v is not accessible: expected 400 response, got %v", endpoint, resp.StatusCode)
55+
}
56+
57+
bodyStr := string(body)
58+
expected := "missing \"upstream\" argument\n"
59+
if bodyStr != expected {
60+
return fmt.Errorf("upstream_conf endpoint %v is not accessible: expected %q body, got %q", endpoint, expected, bodyStr)
61+
}
62+
63+
return nil
64+
}
65+
66+
func checkIfStatusIsAccessible(endpoint string) error {
67+
resp, err := http.Get(endpoint)
68+
if err != nil {
69+
return fmt.Errorf("status endpoint is %v not accessible: %v", endpoint, err)
70+
}
71+
defer resp.Body.Close()
72+
73+
if resp.StatusCode != http.StatusOK {
74+
return fmt.Errorf("status endpoint is %v not accessible: expected 200 response, got %v", endpoint, resp.StatusCode)
75+
}
76+
77+
return nil
78+
}
79+
80+
// CheckIfUpstreamExists checks if the upstream exists in NGINX. If the upstream doesn't exist, it returns an error.
81+
func (client *NginxClient) CheckIfUpstreamExists(upstream string) error {
82+
_, err := client.getUpstreamPeers(upstream)
83+
return err
84+
}
85+
86+
func (client *NginxClient) getUpstreamPeers(upstream string) (*peers, error) {
87+
request := fmt.Sprintf("%v/upstreams/%v", client.statusEndpoint, upstream)
88+
89+
resp, err := http.Get(request)
90+
if err != nil {
91+
return nil, fmt.Errorf("Failed to connect to the status api to get upstream %v info: %v", upstream, err)
92+
}
93+
defer resp.Body.Close()
94+
95+
if resp.StatusCode == http.StatusNotFound {
96+
return nil, fmt.Errorf("Upstream %v is not found", upstream)
97+
}
98+
99+
body, err := ioutil.ReadAll(resp.Body)
100+
if err != nil {
101+
return nil, fmt.Errorf("Failed to read the response body with upstream %v info: %v", upstream, err)
102+
}
103+
var prs peers
104+
err = json.Unmarshal(body, &prs)
105+
if err != nil {
106+
return nil, fmt.Errorf("Error unmarshaling upstream %v: got %q response: %v", upstream, string(body), err)
107+
}
108+
109+
return &prs, nil
110+
}
111+
112+
// AddHTTPServer adds the server to the upstream.
113+
func (client *NginxClient) AddHTTPServer(upstream string, server string) error {
114+
id, err := client.getIDOfHTTPServer(upstream, server)
115+
116+
if err != nil {
117+
return fmt.Errorf("Failed to add %v server to %v upstream: %v", server, upstream, err)
118+
}
119+
if id != -1 {
120+
return fmt.Errorf("Failed to add %v server to %v upstream: server already exists", server, upstream)
121+
}
122+
123+
request := fmt.Sprintf("%v?upstream=%v&add=&server=%v", client.upstreamConfEndpoint, upstream, server)
124+
125+
resp, err := http.Get(request)
126+
if err != nil {
127+
return fmt.Errorf("Failed to add %v server to %v upstream: %v", server, upstream, err)
128+
}
129+
defer resp.Body.Close()
130+
131+
if resp.StatusCode != http.StatusOK {
132+
return fmt.Errorf("Failed to add %v server to %v upstream: expected 200 response, got %v", server, upstream, resp.StatusCode)
133+
}
134+
135+
return nil
136+
}
137+
138+
// DeleteHTTPServer the server from the upstream.
139+
func (client *NginxClient) DeleteHTTPServer(upstream string, server string) error {
140+
id, err := client.getIDOfHTTPServer(upstream, server)
141+
if err != nil {
142+
return fmt.Errorf("Failed to remove %v server from %v upstream: %v", server, upstream, err)
143+
}
144+
if id == -1 {
145+
return fmt.Errorf("Failed to remove %v server from %v upstream: server doesn't exists", server, upstream)
146+
}
147+
148+
request := fmt.Sprintf("%v?upstream=%v&remove=&id=%v", client.upstreamConfEndpoint, upstream, id)
149+
150+
resp, err := http.Get(request)
151+
if err != nil {
152+
return fmt.Errorf("Failed to remove %v server from %v upstream: %v", server, upstream, err)
153+
}
154+
defer resp.Body.Close()
155+
156+
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
157+
return fmt.Errorf("Failed to add %v server to %v upstream: expected 200 or 204 response, got %v", server, upstream, resp.StatusCode)
158+
}
159+
160+
return nil
161+
}
162+
163+
// UpdateHTTPServers updates the servers of the upstream.
164+
// Servers that are in the slice, but don't exist in NGINX will be added to NGINX.
165+
// Servers that aren't in the slice, but exist in NGINX, will be removed from NGINX.
166+
func (client *NginxClient) UpdateHTTPServers(upstream string, servers []string) ([]string, []string, error) {
167+
serversInNginx, err := client.GetHTTPServers(upstream)
168+
if err != nil {
169+
return nil, nil, fmt.Errorf("Failed to update servers of %v upstream: %v", upstream, err)
170+
}
171+
172+
toAdd, toDelete := determineUpdates(servers, serversInNginx)
173+
174+
for _, server := range toAdd {
175+
err := client.AddHTTPServer(upstream, server)
176+
if err != nil {
177+
return nil, nil, fmt.Errorf("Failed to update servers of %v upstream: %v", upstream, err)
178+
}
179+
}
180+
181+
for _, server := range toDelete {
182+
err := client.DeleteHTTPServer(upstream, server)
183+
if err != nil {
184+
return nil, nil, fmt.Errorf("Failed to update servers of %v upstream: %v", upstream, err)
185+
}
186+
}
187+
188+
return toAdd, toDelete, nil
189+
}
190+
191+
func determineUpdates(updatedServers []string, nginxServers []string) (toAdd []string, toRemove []string) {
192+
for _, server := range updatedServers {
193+
found := false
194+
for _, serverNGX := range nginxServers {
195+
if server == serverNGX {
196+
found = true
197+
break
198+
}
199+
}
200+
if !found {
201+
toAdd = append(toAdd, server)
202+
}
203+
}
204+
205+
for _, serverNGX := range nginxServers {
206+
found := false
207+
for _, server := range updatedServers {
208+
if serverNGX == server {
209+
found = true
210+
break
211+
}
212+
}
213+
if !found {
214+
toRemove = append(toRemove, serverNGX)
215+
}
216+
}
217+
218+
return
219+
}
220+
221+
// GetHTTPServers returns the servers of the upsteam from NGINX.
222+
func (client *NginxClient) GetHTTPServers(upstream string) ([]string, error) {
223+
peers, err := client.getUpstreamPeers(upstream)
224+
if err != nil {
225+
return nil, fmt.Errorf("Error getting servers of %v upstream: %v", upstream, err)
226+
}
227+
228+
var servers []string
229+
for _, peer := range peers.Peers {
230+
servers = append(servers, peer.Server)
231+
}
232+
233+
return servers, nil
234+
}
235+
236+
func (client *NginxClient) getIDOfHTTPServer(upstream string, name string) (int, error) {
237+
peers, err := client.getUpstreamPeers(upstream)
238+
if err != nil {
239+
return -1, fmt.Errorf("Error getting id of server %v of upstream %v: %v", name, upstream, err)
240+
}
241+
242+
for _, p := range peers.Peers {
243+
if p.Server == name {
244+
return p.ID, nil
245+
}
246+
}
247+
248+
return -1, nil
249+
}

client/nginx_client_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package client
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestDetermineUpdates(t *testing.T) {
9+
var tests = []struct {
10+
updated []string
11+
nginx []string
12+
expectedToAdd []string
13+
expectedToDelete []string
14+
}{{
15+
updated: []string{"10.0.0.3:80", "10.0.0.4:80"},
16+
nginx: []string{"10.0.0.1:80", "10.0.0.2:80"},
17+
expectedToAdd: []string{"10.0.0.3:80", "10.0.0.4:80"},
18+
expectedToDelete: []string{"10.0.0.1:80", "10.0.0.2:80"},
19+
}, {
20+
updated: []string{"10.0.0.2:80", "10.0.0.3:80", "10.0.0.4:80"},
21+
nginx: []string{"10.0.0.1:80", "10.0.0.2:80", "10.0.0.3:80"},
22+
expectedToAdd: []string{"10.0.0.4:80"},
23+
expectedToDelete: []string{"10.0.0.1:80"},
24+
}, {
25+
updated: []string{"10.0.0.1:80", "10.0.0.2:80", "10.0.0.3:80"},
26+
nginx: []string{"10.0.0.1:80", "10.0.0.2:80", "10.0.0.3:80"},
27+
}, {
28+
// empty values
29+
}}
30+
31+
for _, test := range tests {
32+
toAdd, toDelete := determineUpdates(test.updated, test.nginx)
33+
if !reflect.DeepEqual(toAdd, test.expectedToAdd) || !reflect.DeepEqual(toDelete, test.expectedToDelete) {
34+
t.Errorf("determiteUpdates(%v, %v) = (%v, %v)", test.updated, test.nginx, toAdd, toDelete)
35+
}
36+
}
37+
}

docker/Dockerfile

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
FROM debian:stretch-slim
2+
3+
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
4+
5+
ARG NGINX_PLUS_VERSION
6+
7+
# Download certificate and key from the customer portal (https://cs.nginx.com)
8+
# and copy to the build context
9+
COPY nginx-repo.crt /etc/ssl/nginx/
10+
COPY nginx-repo.key /etc/ssl/nginx/
11+
12+
# Make sure the certificate and key have correct permissions
13+
RUN chmod 644 /etc/ssl/nginx/*
14+
15+
# Install NGINX Plus
16+
RUN set -x \
17+
&& apt-get update \
18+
&& apt-get install --no-install-recommends --no-install-suggests -y apt-transport-https ca-certificates gnupg1 \
19+
&& \
20+
NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \
21+
found=''; \
22+
for server in \
23+
ha.pool.sks-keyservers.net \
24+
hkp://keyserver.ubuntu.com:80 \
25+
hkp://p80.pool.sks-keyservers.net:80 \
26+
pgp.mit.edu \
27+
; do \
28+
echo "Fetching GPG key $NGINX_GPGKEY from $server"; \
29+
apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \
30+
done; \
31+
test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \
32+
echo "Acquire::https::plus-pkgs.nginx.com::Verify-Peer \"true\";" >> /etc/apt/apt.conf.d/90nginx \
33+
&& echo "Acquire::https::plus-pkgs.nginx.com::Verify-Host \"true\";" >> /etc/apt/apt.conf.d/90nginx \
34+
&& echo "Acquire::https::plus-pkgs.nginx.com::SslCert \"/etc/ssl/nginx/nginx-repo.crt\";" >> /etc/apt/apt.conf.d/90nginx \
35+
&& echo "Acquire::https::plus-pkgs.nginx.com::SslKey \"/etc/ssl/nginx/nginx-repo.key\";" >> /etc/apt/apt.conf.d/90nginx \
36+
&& printf "deb https://plus-pkgs.nginx.com/debian stretch nginx-plus\n" > /etc/apt/sources.list.d/nginx-plus.list \
37+
&& apt-get update && apt-get install -y nginx-plus=${NGINX_PLUS_VERSION} \
38+
&& apt-get remove --purge --auto-remove -y gnupg1 \
39+
&& rm -rf /var/lib/apt/lists/* \
40+
&& rm -rf /etc/ssl/nginx \
41+
&& rm /etc/apt/apt.conf.d/90nginx /etc/apt/sources.list.d/nginx-plus.list
42+
43+
44+
# Forward request logs to Docker log collector
45+
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
46+
&& ln -sf /dev/stderr /var/log/nginx/error.log
47+
48+
EXPOSE 80
49+
50+
STOPSIGNAL SIGTERM
51+
52+
RUN rm -rf /etc/nginx/conf.d/*
53+
COPY test.conf /etc/nginx/conf.d/
54+
55+
CMD ["nginx", "-g", "daemon off;"]

docker/test.conf

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
upstream test {
2+
zone test 64k;
3+
}
4+
5+
server {
6+
listen 8080;
7+
8+
root /usr/share/nginx/html;
9+
10+
location = /status.html {
11+
}
12+
13+
location /status {
14+
status;
15+
}
16+
17+
location /upstream_conf {
18+
upstream_conf;
19+
}
20+
}

0 commit comments

Comments
 (0)