Hice mi repositorio de PyPI con autorización y S3. En Nginx

En este artículo quiero compartir mi experiencia de trabajo con NJS, un intérprete de JavaScript para Nginx desarrollado por Nginx Inc, describiendo sus principales características usando un ejemplo real. NJS es un subconjunto de JavaScript que amplía la funcionalidad de Nginx. Cuando se le preguntó por qué poseer intérprete ??? Dmitry Volyntsev respondió en detalle. En resumen: NJS es nginx-way y JavaScript es más progresivo, "nativo" y sin GC, a diferencia de Lua.

Hace mucho tiempo ...

En mi último trabajo, heredé gitlab con una serie de canalizaciones CI / CD heterogéneas con docker-compose, dind y otras delicias que se transfirieron a los rieles kaniko. Las imágenes que se usaban anteriormente en CI se han movido en su forma original. Funcionaron bien hasta el día en que nuestro gitlab cambió su IP y CI se convirtió en una calabaza. El problema era que una de las imágenes de la ventana acoplable que participaba en el CI contenía git, que extraía módulos de Python a través de ssh. Ssh necesita una clave privada y ... estaba en la imagen junto con known_hosts. Y cualquier CI no pudo verificar la clave debido a una falta de coincidencia entre la IP real y la especificada en known_hosts. Se creó rápidamente una nueva imagen a partir de los Dockfiles existentes y se agregó una opciónStrictHostKeyChecking no... Pero el regusto desagradable permaneció y hubo un deseo de transferirlo a un repositorio privado de PyPI. Una ventaja adicional, después de cambiar a una PyPI privada, se convirtió en una canalización más simple y una descripción normal de los requisitos.txt

¡La elección está hecha, señores!

Hicimos girar todo en las nubes y Kubernetes y, como resultado, queríamos obtener un pequeño servicio que fuera un contenedor sin estado con almacenamiento externo. Bueno, dado que usamos S3, entonces era la prioridad. Y, si es posible, con autenticación en gitlab (puede agregarlo usted mismo si es necesario).

Una búsqueda rápida arrojó varios resultados para s3pypi, pypicloud y una opción para generar archivos html "manualmente" para el nabo. La última opción desapareció por sí sola.

s3pypi: este es el cli para usar el alojamiento S3. Subimos archivos, generamos html y llenamos el mismo depósito. Apto para uso doméstico.

pypicloud: , . , . , , 3-5 . . , .

Nginx, ngx_aws_auth. XML , S3. , , . .

PEP-503 , XML HTML pip. Nginx S3 S3 JS Nginx. NJS.

 ,  XML,  ngx_aws_auth, JS.

nginx . - , - Nginx ( ), - Nginx, . , Python Go ( ), nexus.

TL;DR 2 PyPi CI.

?

Nginx ngx_http_js_module, docker-. c js_import Nginx.  js_content. js_set, . NJS Nginx,  XMLHttpRequest. Nginx . (subrequest) .  Nginx, export default.

nginx.conf

load_module modules/ngx_http_js_module.so;
http {
  js_import   imported_name  from script.js;

server {
  listen 8080;
  ...
  location = /sub-query {
    internal;

    proxy_pass http://upstream;
  }

  location / {
    js_content imported_name.request;
  }
}

script.js

function request(r) {
  function call_back(resp) {
    // handler's code
    r.return(resp.status, resp.responseBody);
  }

  r.subrequest('/sub-query', { method: r.method }, call_back);
}

export default {request}

http://localhost:8080/ location / js_content request script.js. request location = /sub-query, ( GET) (r), . call_back.

 S3

S3-, :

ACCESS_KEY

SECRET_KEY

S3_BUCKET

http-, /, S3_NAME URI , (HMAC_SHA1)  SECRET_KEY. , AWS $ACCESS_KEY:$HASH, . /, ,   X-amz-date.  :

nginx.conf

load_module modules/ngx_http_js_module.so;
http {
  js_import   s3      from     s3.js;

  js_set      $s3_datetime     s3.date_now;
  js_set      $s3_auth         s3.s3_sign;

server {
  listen 8080;
  ...
  location ~* /s3-query/(?<s3_path>.*) {
    internal;

    proxy_set_header    X-amz-date     $s3_datetime;
    proxy_set_header    Authorization  $s3_auth;

    proxy_pass          $s3_endpoint/$s3_path;
  }

  location ~ "^/(?<prefix>[\w-]*)[/]?(?<postfix>[\w-\.]*)$" {
    js_content s3.request;
  }
}

s3.js( AWS Sign v2, deprecated)

var crypt = require('crypto');

var s3_bucket = process.env.S3_BUCKET;
var s3_access_key = process.env.S3_ACCESS_KEY;
var s3_secret_key = process.env.S3_SECRET_KEY;
var _datetime = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, '');

function date_now() {
  return _datetime
}

function s3_sign(r) {
  var s2s = r.method + '\n\n\n\n';

  s2s += `x-amz-date:${date_now()}\n`;
  s2s += '/' + s3_bucket;
  s2s += r.uri.endsWith('/') ? '/' : r.variables.s3_path;

  return `AWS ${s3_access_key}:${crypt.createHmac('sha1', s3_secret_key).update(s2s).digest('base64')}`;
}

function request(r) {
  var v = r.variables;

  function call_back(resp) {
    r.return(resp.status, resp.responseBody);
  }

  var _subrequest_uri = r.uri;
  if (r.uri === '/') {
    // root
    _subrequest_uri = '/?delimiter=/';

  } else if (v.prefix !== '' && v.postfix === '') {
    // directory
    var slash = v.prefix.endsWith('/') ? '' : '/';
    _subrequest_uri = '/?prefix=' + v.prefix + slash;
  }

  r.subrequest(`/s3-query${_subrequest_uri}`, { method: r.method }, call_back);
}

export default {request, s3_sign, date_now}

_subrequest_uri: uri S3. «», uri- delimiter, xml- CommonPrefixes, ( PyPI, ). ( ), uri-   prefix () /. , . aiohttp-request  aiohttp-requests /?prefix=aiohttp-request, . , /?prefix=aiohttp-request/, . , uri .

, Nginx. Nginx, XML, :

<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>myback-space</Name>
  <Prefix></Prefix>
  <Marker></Marker>
  <MaxKeys>10000</MaxKeys>
  <Delimiter>/</Delimiter>
  <IsTruncated>false</IsTruncated>
  <CommonPrefixes>
    <Prefix>new/</Prefix>
  </CommonPrefixes>
  <CommonPrefixes>
    <Prefix>old/</Prefix>
  </CommonPrefixes>
</ListBucketResult>

 CommonPrefixes.

, , , XML:

<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name> myback-space</Name>
  <Prefix>old/</Prefix>
  <Marker></Marker>
  <MaxKeys>10000</MaxKeys>
  <Delimiter></Delimiter>
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>old/giphy.mp4</Key>
    <LastModified>2020-08-21T20:27:46.000Z</LastModified>
    <ETag>&#34;00000000000000000000000000000000-1&#34;</ETag>
    <Size>1350084</Size>
    <Owner>
      <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
      <DisplayName></DisplayName>
    </Owner>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
  <Contents>
    <Key>old/hsd-k8s.jpg</Key>
    <LastModified>2020-08-31T16:40:01.000Z</LastModified>
    <ETag>&#34;b2d76df4aeb4493c5456366748218093&#34;</ETag>
    <Size>93183</Size>
    <Owner>
      <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>
      <DisplayName></DisplayName>
    </Owner>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
</ListBucketResult>

 Key.

XML HTML, Content-Type text/html.

function request(r) {
  var v = r.variables;

  function call_back(resp) {
    var body = resp.responseBody;

    if (r.method !== 'PUT' && resp.status < 400 && v.postfix === '') {
      r.headersOut['Content-Type'] = "text/html; charset=utf-8";
      body = toHTML(body);
    }

    r.return(resp.status, body);
  }
  
  var _subrequest_uri = r.uri;
  ...
}

function toHTML(xml_str) {
  var keysMap = {
    'CommonPrefixes': 'Prefix',
    'Contents': 'Key',
  };

  var pattern = `<k>(?<v>.*?)<\/k>`;
  var out = [];

  for(var group_key in keysMap) {
    var reS;
    var reGroup = new RegExp(pattern.replace(/k/g, group_key), 'g');

    while(reS = reGroup.exec(xml_str)) {
      var data = new RegExp(pattern.replace(/k/g, keysMap[group_key]), 'g');
      var reValue = data.exec(reS);
      var a_text = '';

      if (group_key === 'CommonPrefixes') {
        a_text = reValue.groups.v.replace(/\//g, '');
      } else {
        a_text = reValue.groups.v.split('/').slice(-1);
      }

      out.push(`<a href="/${reValue.groups.v}">${a_text}</a>`);
    }
  }

  return '<html><body>\n' + out.join('</br>\n') + '\n</html></body>'
}

PyPI

, .

#     
python3 -m venv venv
. ./venv/bin/activate

#   .
pip download aiohttp

#    
for wheel in *.whl; do curl -T $wheel http://localhost:8080/${wheel%%-*}/$wheel; done

rm -f *.whl

#    
pip install aiohttp -i http://localhost:8080

.

#     
python3 -m venv venv
. ./venv/bin/activate

pip install setuptools wheel
python setup.py bdist_wheel
for wheel in dist/*.whl; do curl -T $wheel http://localhost:8080/${wheel%%-*}/$wheel; done

pip install our_pkg --extra-index-url http://localhost:8080

CI, :

pip install setuptools wheel
python setup.py bdist_wheel

curl -sSfT dist/*.whl -u "gitlab-ci-token:${CI_JOB_TOKEN}" "https://pypi.our-domain.com/${CI_PROJECT_NAME}"

Gitlab JWT / . auth_request Nginx, . url Gitlab- , Gitlab 200 / . Gitlab? , Nginx , - , . , Kubernetes read-only root filesystem, nginx.conf configmap. Nginx configmap    (pvc)  read-only root filesystem ( ).

NJS, nginx - (, URL).

nginx.conf

location = /auth-provider {
  internal;

  proxy_pass $auth_url;
}

location = /auth {
  internal;

  proxy_set_header Content-Length "";
  proxy_pass_request_body off;
  js_content auth.auth;
}

location ~ "^/(?<prefix>[\w-]*)[/]?(?<postfix>[\w-\.]*)$" {
  auth_request /auth;

  js_content s3.request;
}

s3.js

var env = process.env;
var env_bool = new RegExp(/[Tt]rue|[Yy]es|[Oo]n|[TtYy]|1/);
var auth_disabled  = env_bool.test(env.DISABLE_AUTH);
var gitlab_url = env.AUTH_URL;

function url() {
  return `${gitlab_url}/jwt/auth?service=container_registry`
}

function auth(r) {
  if (auth_disabled) {
    r.return(202, '{"auth": "disabled"}');
    return null
  }

  r.subrequest('/auth-provider',
                {method: 'GET', body: ''},
                function(res) {
                  r.return(res.status, "");
                });
}

export default {auth, url}

: - ? ! ,  var AWS = require('aws-sdk')   "" S3-!

, JS-, , . require('crypto'), build-in- require . - . , - .

Nginx gzip off;

, gzip- NJS , . , . , . , .

«» error.log. info, warn error 3 r.log, r.warn, r.error . Chrome (v8) njs, . , , history :

docker-compose restart nginx
curl localhost:8080/
docker-compose logs --tail 10 nginx

.

, . IDE . , .

ES6.

- , . NJS.

NJS - open-source , Nginx JavaScript. . , . , - NJS , Nginx . NGINX Plus - !

njs-pypi AWS Sign v4

ngx_http_js_module

NJS

Ejemplos de uso de NJS de Dmitry Volyntsev

njs - scripting JavaScript nativo en nginx / discurso de Dmitry Volnyev en Saint HighLoad ++ 2019

NJS en producción / discurso de Vasily Soshnikov en HighLoad ++ 2019

Firma y autenticación de solicitudes REST en AWS




All Articles