Skip to content

Instantly share code, notes, and snippets.

@bgautrea
Last active October 17, 2019 13:58
Show Gist options
  • Save bgautrea/80aaff7358aaf7f0cfd6fa731da123c4 to your computer and use it in GitHub Desktop.
Save bgautrea/80aaff7358aaf7f0cfd6fa731da123c4 to your computer and use it in GitHub Desktop.
FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
ENV NGINX_PLUS_VERSION 19-1~stretch
ARG IC_VERSION
# Download certificate and key from the customer portal (https://cs.nginx.com)
# and copy to the build context
COPY nginx-repo.crt /etc/ssl/nginx/
COPY nginx-repo.key /etc/ssl/nginx/
# Make sure the certificate and key have correct permissions
RUN chmod 644 /etc/ssl/nginx/*
# Install NGINX Plus
RUN set -x \
&& apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y apt-transport-https ca-certificates gnupg1 \
&& \
NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \
found=''; \
for server in \
ha.pool.sks-keyservers.net \
hkp://keyserver.ubuntu.com:80 \
hkp://p80.pool.sks-keyservers.net:80 \
pgp.mit.edu \
; do \
echo "Fetching GPG key $NGINX_GPGKEY from $server"; \
apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \
done; \
test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \
echo "Acquire::https::plus-pkgs.nginx.com::Verify-Peer \"true\";" >> /etc/apt/apt.conf.d/90nginx \
&& echo "Acquire::https::plus-pkgs.nginx.com::Verify-Host \"true\";" >> /etc/apt/apt.conf.d/90nginx \
&& echo "Acquire::https::plus-pkgs.nginx.com::SslCert \"/etc/ssl/nginx/nginx-repo.crt\";" >> /etc/apt/apt.conf.d/90nginx \
&& echo "Acquire::https::plus-pkgs.nginx.com::SslKey \"/etc/ssl/nginx/nginx-repo.key\";" >> /etc/apt/apt.conf.d/90nginx \
&& echo "Acquire::https::plus-pkgs.nginx.com::User-Agent \"k8s-ic-$IC_VERSION-apt\";" >> /etc/apt/apt.conf.d/90nginx \
&& printf "deb https://plus-pkgs.nginx.com/debian stretch nginx-plus\n" > /etc/apt/sources.list.d/nginx-plus.list \
&& apt-get update && apt-get install -y nginx-plus=${NGINX_PLUS_VERSION} \
&& apt-get install -y python3-pip \
&& pip3 install awscli --upgrade \
&& pip3 install boto3 --upgrade \
&& apt-get remove --purge --auto-remove -y gnupg1 \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /etc/ssl/nginx \
&& rm /etc/apt/apt.conf.d/90nginx /etc/apt/sources.list.d/nginx-plus.list
COPY secret.py /root/awssecret.py
COPY start.sh /usr/local/bin/start.sh
RUN chmod 744 /usr/local/bin/start.sh \
&& mkdir /root/.aws
COPY config /root/.aws/
# forward nginx access and error logs to stdout and stderr of the ingress
# controller process
RUN ln -sf /proc/1/fd/1 /var/log/nginx/access.log \
&& ln -sf /proc/1/fd/1 /var/log/nginx/stream-access.log \
&& ln -sf /proc/1/fd/2 /var/log/nginx/error.log
EXPOSE 80 443
COPY nginx-ingress internal/configs/version1/nginx-plus.ingress.tmpl internal/configs/version1/nginx-plus.tmpl internal/configs/version2/nginx-plus.virtualserver.tmpl /
RUN rm /etc/nginx/conf.d/* \
&& mkdir -p /etc/nginx/secrets
# Uncomment the line below if you would like to add the default.pem to the image
# and use it as a certificate and key for the default server
# ADD default.pem /etc/nginx/secrets/default
#ENTRYPOINT ["/nginx-ingress"]
ENTRYPOINT ["/usr/local/bin/start.sh"]
kind: ConfigMap
apiVersion: v1
metadata:
name: nginx-config
namespace: nginx-ingress
data:
enable-debug: "True"
main-template: |
user nginx;
worker_processes {{.WorkerProcesses}};
{{- if .WorkerRlimitNofile}}
worker_rlimit_nofile {{.WorkerRlimitNofile}};{{end}}
{{- if .WorkerCPUAffinity}}
worker_cpu_affinity {{.WorkerCPUAffinity}};{{end}}
{{- if .WorkerShutdownTimeout}}
worker_shutdown_timeout {{.WorkerShutdownTimeout}};{{end}}
daemon off;
error_log /var/log/nginx/error.log {{.ErrorLogLevel}};
pid /var/run/nginx.pid;
{{- if .MainSnippets}}
{{range $value := .MainSnippets}}
{{$value}}{{end}}
{{- end}}
events {
worker_connections {{.WorkerConnections}};
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
{{- if .HTTPSnippets}}
{{range $value := .HTTPSnippets}}
{{$value}}{{end}}
{{- end}}
{{if .LogFormat -}}
log_format main '{{.LogFormat}}';
{{- else -}}
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
{{- end}}
{{if .AccessLogOff}}
access_log off;
{{else}}
access_log /var/log/nginx/access.log main;
{{end}}
sendfile on;
#tcp_nopush on;
keepalive_timeout {{.KeepaliveTimeout}};
keepalive_requests {{.KeepaliveRequests}};
#gzip on;
server_names_hash_max_size {{.ServerNamesHashMaxSize}};
{{if .ServerNamesHashBucketSize}}server_names_hash_bucket_size {{.ServerNamesHashBucketSize}};{{end}}
variables_hash_bucket_size {{.VariablesHashBucketSize}};
variables_hash_max_size {{.VariablesHashMaxSize}};
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
{{if .SSLProtocols}}ssl_protocols {{.SSLProtocols}};{{end}}
{{if .SSLCiphers}}ssl_ciphers "{{.SSLCiphers}}";{{end}}
{{if .SSLPreferServerCiphers}}ssl_prefer_server_ciphers on;{{end}}
{{if .SSLDHParam}}ssl_dhparam {{.SSLDHParam}};{{end}}
{{if .ResolverAddresses}}
resolver {{range $resolver := .ResolverAddresses}}{{$resolver}}{{end}}{{if .ResolverValid}} valid={{.ResolverValid}}{{end}}{{if not .ResolverIPV6}} ipv6=off{{end}};
{{if .ResolverTimeout}}resolver_timeout {{.ResolverTimeout}};{{end}}
{{end}}
server {
listen 80 default_server{{if .ProxyProtocol}} proxy_protocol{{end}};
listen 443 ssl default_server{{if .HTTP2}} http2{{end}}{{if .ProxyProtocol}} proxy_protocol{{end}};
ssl_verify_client on;
ssl_client_certificate /etc/nginx/secrets/client;
ssl_certificate /etc/nginx/secrets/default;
ssl_certificate_key /etc/nginx/secrets/default.key;
server_name _;
server_tokens "{{.ServerTokens}}";
access_log off;
{{if .HealthStatus}}
location /nginx-health {
default_type text/plain;
return 200 "healthy\n";
}
{{end}}
location / {
return 404;
}
}
{{- if .NginxStatus}}
# NGINX Plus APIs
server {
listen {{.NginxStatusPort}};
root /usr/share/nginx/html;
access_log off;
location = /dashboard.html {
}
{{range $value := .NginxStatusAllowCIDRs}}
allow {{$value}};{{end}}
deny all;
location /api {
api write=off;
}
}
{{- end}}
# NGINX Plus API over unix socket
server {
listen unix:/var/run/nginx-plus-api.sock;
access_log off;
# $config_version_mismatch is defined in /etc/nginx/config-version.conf
location /configVersionCheck {
if ($config_version_mismatch) {
return 503;
}
return 200;
}
location /api {
api write=on;
}
}
include /etc/nginx/config-version.conf;
include /etc/nginx/conf.d/*.conf;
}
stream {
{{if .StreamLogFormat -}}
log_format stream-main '{{.StreamLogFormat}}';
{{- else -}}
log_format stream-main '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time';
{{- end}}
access_log /var/log/nginx/stream-access.log stream-main;
{{range $value := .StreamSnippets}}
{{$value}}{{end}}
}
ingress-template: |
# configuration for {{.Ingress.Namespace}}/{{.Ingress.Name}}
{{range $upstream := .Upstreams}}
upstream {{$upstream.Name}} {
zone {{$upstream.Name}} 256k;
{{if $upstream.LBMethod }}{{$upstream.LBMethod}};{{end}}
{{range $server := $upstream.UpstreamServers}}
server {{$server.Address}}:{{$server.Port}} max_fails={{$server.MaxFails}} fail_timeout={{$server.FailTimeout}}
{{- if $server.SlowStart}} slow_start={{$server.SlowStart}}{{end}}{{if $server.Resolve}} resolve{{end}};{{end}}
{{if $upstream.StickyCookie}}
sticky cookie {{$upstream.StickyCookie}};
{{end}}
{{if $.Keepalive}}keepalive {{$.Keepalive}};{{end}}
{{- if $upstream.UpstreamServers -}}
{{- if $upstream.Queue}}
queue {{$upstream.Queue}} timeout={{$upstream.QueueTimeout}}s;
{{- end -}}
{{- end}}
}
{{- end}}
{{range $server := .Servers}}
server {
{{if not $server.GRPCOnly}}
{{range $port := $server.Ports}}
listen {{$port}}{{if $server.ProxyProtocol}} proxy_protocol{{end}};
{{- end}}
{{end}}
{{if $server.SSL}}
{{- range $port := $server.SSLPorts}}
listen {{$port}} ssl{{if $server.HTTP2}} http2{{end}}{{if $server.ProxyProtocol}} proxy_protocol{{end}};
{{- end}}
ssl_certificate {{$server.SSLCertificate}};
ssl_certificate_key {{$server.SSLCertificateKey}};
{{$ssl_client_cert := index $.Ingress.Annotations "custom.nginx.org/ssl-client-cert"}}
{{$ssl_verify := index $.Ingress.Annotations "custom.nginx.org/ssl-verify"}}
{{if eq $ssl_verify "True"}}
ssl_verify_client on;
ssl_client_certificate {{$ssl_client_cert}};
{{end}}
{{if $server.SSLCiphers}}
ssl_ciphers {{$server.SSLCiphers}};
{{end}}
{{end}}
{{range $setRealIPFrom := $server.SetRealIPFrom}}
set_real_ip_from {{$setRealIPFrom}};{{end}}
{{if $server.RealIPHeader}}real_ip_header {{$server.RealIPHeader}};{{end}}
{{if $server.RealIPRecursive}}real_ip_recursive on;{{end}}
server_tokens "{{$server.ServerTokens}}";
{{if index $.Ingress.Annotations "custom.nginx.org/extra-name"}}
{{$extra_name := index $.Ingress.Annotations "custom.nginx.org/extra-name"}}
server_name {{$server.Name}} {{$extra_name}};
{{else}}
server_name {{$server.Name}};
{{end}}
status_zone {{$server.StatusZone}};
{{if not $server.GRPCOnly}}
{{range $proxyHideHeader := $server.ProxyHideHeaders}}
proxy_hide_header {{$proxyHideHeader}};{{end}}
{{range $proxyPassHeader := $server.ProxyPassHeaders}}
proxy_pass_header {{$proxyPassHeader}};{{end}}
{{end}}
{{if $server.SSL}}
{{if not $server.GRPCOnly}}
{{- if $server.HSTS}}
set $hsts_header_val "";
proxy_hide_header Strict-Transport-Security;
{{- if $server.HSTSBehindProxy}}
if ($http_x_forwarded_proto = 'https') {
{{else}}
if ($https = on) {
{{- end}}
set $hsts_header_val "max-age={{$server.HSTSMaxAge}}; {{if $server.HSTSIncludeSubdomains}}includeSubDomains; {{end}}preload";
}
add_header Strict-Transport-Security "$hsts_header_val" always;
{{end}}
{{- if $server.SSLRedirect}}
if ($scheme = http) {
return 301 https://$host:{{index $server.SSLPorts 0}}$request_uri;
}
{{- end}}
{{end}}
{{- end}}
{{- if $server.RedirectToHTTPS}}
if ($http_x_forwarded_proto = 'http') {
return 301 https://$host$request_uri;
}
{{- end}}
{{with $jwt := $server.JWTAuth}}
auth_jwt_key_file {{$jwt.Key}};
auth_jwt "{{.Realm}}"{{if $jwt.Token}} token={{$jwt.Token}}{{end}};
{{- if $jwt.RedirectLocationName}}
error_page 401 {{$jwt.RedirectLocationName}};
{{end}}
{{end}}
{{- if $server.ServerSnippets}}
{{range $value := $server.ServerSnippets}}
{{$value}}{{end}}
{{- end}}
{{- range $healthCheck := $server.HealthChecks}}
location @hc-{{$healthCheck.UpstreamName}} {
{{- range $name, $header := $healthCheck.Headers}}
proxy_set_header {{$name}} "{{$header}}";
{{- end }}
proxy_connect_timeout {{$healthCheck.TimeoutSeconds}}s;
proxy_read_timeout {{$healthCheck.TimeoutSeconds}}s;
proxy_send_timeout {{$healthCheck.TimeoutSeconds}}s;
proxy_pass {{$healthCheck.Scheme}}://{{$healthCheck.UpstreamName}};
health_check {{if $healthCheck.Mandatory}}mandatory {{end}}uri={{$healthCheck.URI}} interval=
{{- $healthCheck.Interval}}s fails={{$healthCheck.Fails}} passes={{$healthCheck.Passes}};
}
{{end -}}
{{- range $location := $server.JWTRedirectLocations}}
location {{$location.Name}} {
internal;
return 302 {{$location.LoginURL}};
}
{{end -}}
{{range $location := $server.Locations}}
location {{$location.Path}} {
{{with $location.MinionIngress}}
# location for minion {{$location.MinionIngress.Namespace}}/{{$location.MinionIngress.Name}}
{{end}}
{{if $location.GRPC}}
{{if not $server.GRPCOnly}}
error_page 400 @grpcerror400;
error_page 401 @grpcerror401;
error_page 403 @grpcerror403;
error_page 404 @grpcerror404;
error_page 405 @grpcerror405;
error_page 408 @grpcerror408;
error_page 414 @grpcerror414;
error_page 426 @grpcerror426;
error_page 500 @grpcerror500;
error_page 501 @grpcerror501;
error_page 502 @grpcerror502;
error_page 503 @grpcerror503;
error_page 504 @grpcerror504;
{{end}}
{{- if $location.LocationSnippets}}
{{range $value := $location.LocationSnippets}}
{{$value}}{{end}}
{{- end}}
{{with $jwt := $location.JWTAuth}}
auth_jwt_key_file {{$jwt.Key}};
auth_jwt "{{.Realm}}"{{if $jwt.Token}} token={{$jwt.Token}}{{end}};
{{end}}
grpc_connect_timeout {{$location.ProxyConnectTimeout}};
grpc_read_timeout {{$location.ProxyReadTimeout}};
grpc_set_header Host $host;
grpc_set_header X-Real-IP $remote_addr;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
grpc_set_header X-Forwarded-Host $host;
grpc_set_header X-Forwarded-Port $server_port;
grpc_set_header X-Forwarded-Proto $scheme;
{{- if $location.ProxyBufferSize}}
grpc_buffer_size {{$location.ProxyBufferSize}};
{{- end}}
{{if $location.SSL}}
grpc_pass grpcs://{{$location.Upstream.Name}}
{{else}}
grpc_pass grpc://{{$location.Upstream.Name}};
{{end}}
{{else}}
proxy_http_version 1.1;
{{if $location.Websocket}}
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
{{- else}}
{{- if $.Keepalive}}proxy_set_header Connection "";{{end}}
{{- end}}
{{- if $location.LocationSnippets}}
{{range $value := $location.LocationSnippets}}
{{$value}}{{end}}
{{- end}}
{{ with $jwt := $location.JWTAuth }}
auth_jwt_key_file {{$jwt.Key}};
auth_jwt "{{.Realm}}"{{if $jwt.Token}} token={{$jwt.Token}}{{end}};
{{if $jwt.RedirectLocationName}}
error_page 401 {{$jwt.RedirectLocationName}};
{{end}}
{{end}}
proxy_connect_timeout {{$location.ProxyConnectTimeout}};
proxy_read_timeout {{$location.ProxyReadTimeout}};
client_max_body_size {{$location.ClientMaxBodySize}};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto {{if $server.RedirectToHTTPS}}https{{else}}$scheme{{end}};
proxy_buffering {{if $location.ProxyBuffering}}on{{else}}off{{end}};
{{- if $location.ProxyBuffers}}
proxy_buffers {{$location.ProxyBuffers}};
{{- end}}
{{- if $location.ProxyBufferSize}}
proxy_buffer_size {{$location.ProxyBufferSize}};
{{- end}}
{{- if $location.ProxyMaxTempFileSize}}
proxy_max_temp_file_size {{$location.ProxyMaxTempFileSize}};
{{- end}}
{{if $location.SSL}}
{{- if index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-mtls"}}
{{- $proxy_ssl_mtls := index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-mtls"}}
{{- if eq $proxy_ssl_mtls "True"}}
{{- $client_cert := index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-client-certificate"}}
{{- $client_key := index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-client-certificate-key"}}
proxy_ssl_certificate {{$client_cert}};
proxy_ssl_certificate_key {{$client_key}};
{{- end}}
{{- end}}
{{- if index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-verify"}}
{{- $proxy_ssl_verify := index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-verify"}}
{{- if eq $proxy_ssl_verify "True"}}
{{- $trusted_cert := index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-trusted-certificate"}}
proxy_ssl_verify on;
proxy_ssl_trusted_certificate {{$trusted_cert}};
{{- if index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-session-reuse"}}
{{- $proxy_ssl_session_reuse := index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-session-reuse"}}
{{- if eq $proxy_ssl_session_reuse "True"}}
proxy_ssl_session_reuse on;
{{- end}}
{{- end}}
{{- if index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-verify-depth"}}
{{- $proxy_ssl_verify_depth := index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-verify-depth"}}
proxy_ssl_verify_depth {{$proxy_ssl_verify_depth}};
{{- end}}
{{- if index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-name"}}
{{- $proxy_ssl_name := index $.Ingress.Annotations "custom.nginx.org/proxy-ssl-name"}}
proxy_ssl_server_name on;
proxy_ssl_name {{$proxy_ssl_name}};
{{- end}}
{{- end}}
{{- end}}
{{- with $location.MinionIngress}}
{{- if index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-mtls"}}
{{- $proxy_ssl_mtls := index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-mtls"}}
{{- if eq $proxy_ssl_mtls "True"}}
{{- $client_cert := index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-client-certificate"}}
{{- $client_key := index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-client-certificate-key"}}
proxy_ssl_certificate {{$client_cert}};
proxy_ssl_certificate_key {{$client_key}};
{{- end}}
{{- end}}
{{- if index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-verify"}}
{{- $proxy_ssl_verify := index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-verify"}}
{{- if eq $proxy_ssl_verify "True"}}
{{- $trusted_cert := index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-trusted-certificate"}}
proxy_ssl_verify on;
proxy_ssl_trusted_certificate {{$trusted_cert}};
{{- if index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-session-reuse"}}
{{- $proxy_ssl_session_reuse := index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-session-reuse"}}
{{- if eq $proxy_ssl_session_reuse "True"}}
proxy_ssl_session_reuse on;
{{- end}}
{{- end}}
{{- if index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-verify-depth"}}
{{- $proxy_ssl_verify_depth := index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-verify-depth"}}
proxy_ssl_verify_depth {{$proxy_ssl_verify_depth}};
{{- end}}
{{- if index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-name"}}
{{- $proxy_ssl_name := index $location.MinionIngress.Annotations "custom.nginx.org/proxy-ssl-name"}}
proxy_ssl_name {{$proxy_ssl_name}};
{{- end}}
{{- end}}
{{- end}}
{{- end}}
proxy_pass https://{{$location.Upstream.Name}}{{$location.Rewrite}};
{{else}}
proxy_pass http://{{$location.Upstream.Name}}{{$location.Rewrite}};
{{end}}
{{end}}
}{{end}}
{{if $server.GRPCOnly}}
error_page 400 @grpcerror400;
error_page 401 @grpcerror401;
error_page 403 @grpcerror403;
error_page 404 @grpcerror404;
error_page 405 @grpcerror405;
error_page 408 @grpcerror408;
error_page 414 @grpcerror414;
error_page 426 @grpcerror426;
error_page 500 @grpcerror500;
error_page 501 @grpcerror501;
error_page 502 @grpcerror502;
error_page 503 @grpcerror503;
error_page 504 @grpcerror504;
{{end}}
{{if $server.HTTP2}}
location @grpcerror400 { default_type application/grpc; return 400 "\n"; }
location @grpcerror401 { default_type application/grpc; return 401 "\n"; }
location @grpcerror403 { default_type application/grpc; return 403 "\n"; }
location @grpcerror404 { default_type application/grpc; return 404 "\n"; }
location @grpcerror405 { default_type application/grpc; return 405 "\n"; }
location @grpcerror408 { default_type application/grpc; return 408 "\n"; }
location @grpcerror414 { default_type application/grpc; return 414 "\n"; }
location @grpcerror426 { default_type application/grpc; return 426 "\n"; }
location @grpcerror500 { default_type application/grpc; return 500 "\n"; }
location @grpcerror501 { default_type application/grpc; return 501 "\n"; }
location @grpcerror502 { default_type application/grpc; return 502 "\n"; }
location @grpcerror503 { default_type application/grpc; return 503 "\n"; }
location @grpcerror504 { default_type application/grpc; return 504 "\n"; }
{{end}}
}{{end}}
#!/bin/bash
python3 /root/awssecret.py > /etc/nginx/secrets/default.key
/nginx-ingress $@
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment