Saya membuat repositori PyPI saya dengan kebenaran dan S3. Pada Nginx

Dalam artikel ini saya ingin berkongsi pengalaman saya dengan NJS, jurubahasa JavaScript untuk Nginx yang dibangunkan oleh Nginx Inc, menerangkan keupayaan utamanya menggunakan contoh sebenar. NJS ialah subset JavaScript yang membolehkan anda memanjangkan fungsi Nginx. Kepada soalan kenapa jurubahasa sendiri??? Dmitry Volyntsev menjawab secara terperinci. Ringkasnya: NJS adalah cara nginx, dan JavaScript lebih progresif, "asli" dan tanpa GC, tidak seperti Lua.

Suatu masa dahulu…

Pada tugas terakhir saya, saya mewarisi gitlab dengan beberapa saluran paip CI/CD beraneka ragam dengan docker-compose, dind dan hidangan lain, yang telah dipindahkan ke rel kaniko. Imej yang sebelum ini digunakan dalam CI telah dialihkan dalam bentuk asalnya. Mereka berfungsi dengan betul sehingga hari apabila IP gitlab kami berubah dan CI bertukar menjadi labu. Masalahnya ialah salah satu imej docker yang mengambil bahagian dalam CI mempunyai git, yang menarik modul Python melalui ssh. Untuk ssh anda memerlukan kunci peribadi dan... ia berada dalam imej bersama-sama dengan known_hosts. Dan mana-mana CI gagal dengan ralat pengesahan utama disebabkan oleh ketidakpadanan antara IP sebenar dan yang dinyatakan dalam known_hosts. Imej baharu telah dipasang dengan pantas daripada Dockfiles sedia ada dan pilihan itu telah ditambah StrictHostKeyChecking no. Tetapi rasa buruk itu kekal dan terdapat keinginan untuk memindahkan libs ke repositori PyPI peribadi. Bonus tambahan, selepas bertukar kepada PyPI persendirian, ialah saluran paip yang lebih mudah dan penerangan biasa tentang requirements.txt

Pilihan telah dibuat, Tuan-tuan!

Kami menjalankan segala-galanya dalam awan dan Kubernetes, dan pada akhirnya kami ingin mendapatkan perkhidmatan kecil yang merupakan bekas tanpa negara dengan storan luaran. Oleh kerana kami menggunakan S3, keutamaan diberikan kepadanya. Dan, jika boleh, dengan pengesahan dalam gitlab (anda boleh menambahnya sendiri jika perlu).

Carian pantas menghasilkan beberapa hasil: s3pypi, pypicloud dan pilihan dengan penciptaan "manual" fail html untuk lobak. Pilihan terakhir hilang dengan sendirinya.

s3pypi: Ini adalah cli untuk menggunakan pengehosan S3. Kami memuat naik fail, menjana html dan memuat naiknya ke baldi yang sama. Sesuai untuk kegunaan rumah.

pypicloud: Ia kelihatan seperti projek yang menarik, tetapi selepas membaca dokumentasi saya kecewa. Walaupun dokumentasi yang baik dan keupayaan untuk berkembang untuk memenuhi keperluan anda, sebenarnya ia ternyata berlebihan dan sukar untuk dikonfigurasikan. Membetulkan kod agar sesuai dengan tugas anda, mengikut anggaran pada masa itu, akan mengambil masa 3-5 hari. Perkhidmatan ini juga memerlukan pangkalan data. Kami meninggalkannya sekiranya kami tidak menemui apa-apa lagi.

Carian yang lebih mendalam menghasilkan modul untuk Nginx, ngx_aws_auth. Hasil ujiannya ialah XML dipaparkan dalam pelayar, yang menunjukkan kandungan baldi S3. Komit terakhir pada masa pencarian adalah setahun yang lalu. Repositori itu kelihatan terbiar.

Dengan pergi ke sumber dan membaca PEP-503 Saya menyedari bahawa XML boleh ditukar kepada HTML dengan cepat dan diberikan kepada pip. Selepas googling sedikit lagi tentang Nginx dan S3, saya terjumpa satu contoh pengesahan dalam S3 yang ditulis dalam JS untuk Nginx. Itulah cara saya bertemu NJS.

Mengambil contoh ini sebagai asas, sejam kemudian saya melihat dalam pelayar saya XML yang sama seperti semasa menggunakan modul ngx_aws_auth, tetapi semuanya telah ditulis dalam JS.

Saya sangat menyukai penyelesaian nginx. Pertama, dokumentasi yang baik dan banyak contoh, kedua, kami mendapat semua kebaikan Nginx untuk bekerja dengan fail (di luar kotak), ketiga, sesiapa yang tahu cara menulis konfigurasi untuk Nginx akan dapat mengetahui apa itu. Minimalisme juga merupakan kelebihan bagi saya, berbanding Python atau Go (jika ditulis dari awal), apatah lagi nexus.

TL;DR Selepas 2 hari, versi ujian PyPi telah digunakan dalam CI.

Bagaimana ia berfungsi?

Modul dimuatkan ke dalam Nginx ngx_http_js_module, disertakan dalam imej docker rasmi. Kami mengimport skrip kami menggunakan arahan js_importkepada konfigurasi Nginx. Fungsi dipanggil oleh arahan js_content. Arahan digunakan untuk menetapkan pembolehubah js_set, yang mengambil sebagai hujah hanya fungsi yang diterangkan dalam skrip. Tetapi kita boleh melaksanakan subqueries dalam NJS hanya menggunakan Nginx, bukan sebarang XMLHttpRequest. Untuk melakukan ini, lokasi yang sepadan mesti ditambahkan pada konfigurasi Nginx. Dan skrip mesti menerangkan subpermintaan ke lokasi ini. Untuk dapat mengakses fungsi daripada konfigurasi Nginx, nama fungsi mesti dieksport dalam skrip itu sendiri 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}

Apabila diminta dalam penyemak imbas http://localhost:8080/ kita masuk location /di mana arahan js_content memanggil fungsi request diterangkan dalam skrip kami script.js. Sebaliknya, dalam fungsi request subquery dibuat untuk location = /sub-query, dengan kaedah (dalam contoh semasa GET) yang diperoleh daripada hujah (r), diluluskan secara tersirat apabila fungsi ini dipanggil. Respons subpermintaan akan diproses dalam fungsi call_back.

Mencuba S3

Untuk membuat permintaan kepada storan S3 peribadi, kami memerlukan:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Daripada kaedah http yang digunakan, tarikh/masa semasa, S3_NAME dan URI, jenis rentetan tertentu dijana, yang ditandatangani (HMAC_SHA1) menggunakan SECRET_KEY. Seterusnya adalah baris seperti AWS $ACCESS_KEY:$HASH, boleh digunakan dalam pengepala kebenaran. Tarikh/masa yang sama yang digunakan untuk menjana rentetan dalam langkah sebelumnya mesti ditambahkan pada pengepala X-amz-date. Dalam kod ia kelihatan seperti ini:

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(Contoh keizinan AWS Sign v2, ditukar kepada status tidak digunakan)

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 + 'nnnn';

  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}

Sedikit penerangan tentang _subrequest_uri: ini adalah pembolehubah yang, bergantung pada uri awal, membentuk permintaan kepada S3. Jika anda perlu mendapatkan kandungan "root", maka anda perlu membuat permintaan uri yang menunjukkan pembatas delimiter, yang akan mengembalikan senarai semua elemen xml CommonPrefixes, sepadan dengan direktori (dalam kes PyPI, senarai semua pakej). Jika anda perlu mendapatkan senarai kandungan dalam direktori tertentu (senarai semua versi pakej), maka permintaan uri mesti mengandungi medan awalan dengan nama direktori (pakej) semestinya berakhir dengan garis miring /. Jika tidak, perlanggaran mungkin berlaku apabila meminta kandungan direktori, contohnya. Terdapat direktori aiohttp-request dan aiohttp-requests dan jika permintaan ditentukan /?prefix=aiohttp-request, maka respons akan mengandungi kandungan kedua-dua direktori. Sekiranya terdapat garis miring di hujungnya, /?prefix=aiohttp-request/, maka respons akan mengandungi hanya direktori yang diperlukan. Dan jika kita meminta fail, maka uri yang dihasilkan tidak sepatutnya berbeza dari yang asal.

Simpan dan mulakan semula Nginx. Dalam penyemak imbas kami memasukkan alamat Nginx kami, hasil permintaan akan menjadi XML, sebagai contoh:

Senarai direktori

<?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>

Daripada senarai direktori anda hanya memerlukan elemen CommonPrefixes.

Dengan menambahkan direktori yang kami perlukan pada alamat kami dalam penyemak imbas, kami juga akan menerima kandungannya dalam bentuk XML:

Senarai fail dalam direktori

<?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>

Daripada senarai fail kami hanya akan mengambil elemen Key.

Yang tinggal hanyalah menghuraikan XML yang terhasil dan menghantarnya sebagai HTML, setelah terlebih dahulu menggantikan pengepala Jenis Kandungan dengan teks/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="/ms/${reValue.groups.v}">${a_text}</a>`);
    }
  }

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

Mencuba PyPI

Kami menyemak bahawa tiada apa-apa yang rosak di mana-mana pada pakej yang diketahui berfungsi.

# Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ для тСстов Π½ΠΎΠ²ΠΎΠ΅ ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅
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

Kami ulangi dengan libs kami.

# Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ для тСстов Π½ΠΎΠ²ΠΎΠ΅ ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅
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

Dalam CI, mencipta dan memuatkan pakej kelihatan seperti ini:

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}"

Pengesahan

Dalam Gitlab adalah mungkin untuk menggunakan JWT untuk pengesahan/kebenaran perkhidmatan luaran. Menggunakan arahan auth_request dalam Nginx, kami akan mengubah hala data pengesahan ke subpermintaan yang mengandungi panggilan fungsi dalam skrip. Skrip akan membuat subpermintaan lain kepada url Gitlab dan jika data pengesahan dinyatakan dengan betul, maka Gitlab akan mengembalikan kod 200 dan muat naik/muat turun pakej akan dibenarkan. Mengapa tidak menggunakan satu subquery dan segera menghantar data ke Gitlab? Kerana itu kita perlu mengedit fail konfigurasi Nginx setiap kali kita membuat sebarang perubahan dalam kebenaran, dan ini adalah tugas yang agak membosankan. Selain itu, jika Kubernetes menggunakan dasar sistem fail akar baca sahaja, maka ini menambahkan lagi kerumitan apabila menggantikan nginx.conf melalui peta konfigurasi. Dan menjadi sangat mustahil untuk mengkonfigurasi Nginx melalui peta konfigurasi sambil pada masa yang sama menggunakan dasar yang melarang sambungan volum (pvc) dan sistem fail akar baca sahaja (ini juga berlaku).

Menggunakan perantaraan NJS, kami mendapat peluang untuk menukar parameter yang ditentukan dalam konfigurasi nginx menggunakan pembolehubah persekitaran dan melakukan beberapa semakan dalam skrip (contohnya, URL yang dinyatakan secara salah).

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}

Kemungkinan besar persoalannya sedang berkembang: -Mengapa tidak menggunakan modul siap pakai? Semuanya telah dilakukan di sana! Sebagai contoh, var AWS = require('aws-sdk') dan tidak perlu menulis "basikal" dengan pengesahan S3!

Mari kita beralih kepada kontra

Bagi saya, ketidakupayaan untuk mengimport modul JS luaran menjadi ciri yang tidak menyenangkan, tetapi dijangka. Diterangkan dalam contoh di atas memerlukan('crypto') ialah modul terbina dalam dan hanya memerlukan kerja untuk mereka. Juga tiada cara untuk menggunakan semula kod daripada skrip dan anda perlu menyalin dan menampalnya ke dalam fail yang berbeza. Saya berharap suatu hari nanti fungsi ini akan dilaksanakan.

Mampatan juga mesti dilumpuhkan untuk projek semasa dalam Nginx gzip off;

Kerana tiada modul gzip dalam NJS dan adalah mustahil untuk menyambungkannya; oleh itu, tiada cara untuk berfungsi dengan data termampat. Benar, ini sebenarnya bukan tolak untuk kes ini. Tidak banyak teks, dan fail yang dipindahkan sudah dimampatkan dan pemampatan tambahan tidak akan banyak membantu mereka. Selain itu, ini bukan perkhidmatan yang sarat atau kritikal sehingga anda perlu bersusah payah menyampaikan kandungan beberapa milisaat lebih cepat.

Menyahpepijat skrip mengambil masa yang lama dan hanya boleh dilakukan melalui "cetakan" dalam error.log. Bergantung pada maklumat tahap pengelogan yang ditetapkan, amaran atau ralat, adalah mungkin untuk menggunakan 3 kaedah r.log, r.warn, r.error masing-masing. Saya cuba menyahpepijat beberapa skrip dalam Chrome (v8) atau alat konsol njs, tetapi tidak semuanya boleh disemak di sana. Apabila menyahpepijat kod, alias ujian berfungsi, sejarah kelihatan seperti ini:

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

dan mungkin terdapat beratus-ratus urutan sedemikian.

Menulis kod menggunakan subkueri dan pembolehubah untuk mereka bertukar menjadi kusut kusut. Kadangkala anda mula tergesa-gesa mengelilingi tetingkap IDE yang berbeza cuba memikirkan urutan tindakan kod anda. Ia tidak sukar, tetapi kadang-kadang ia sangat menjengkelkan.

Tiada sokongan penuh untuk ES6.

Mungkin terdapat beberapa kekurangan lain, tetapi saya tidak menemui apa-apa lagi. Kongsi maklumat jika anda mempunyai pengalaman negatif menggunakan NJS.

Kesimpulan

NJS ialah penterjemah sumber terbuka ringan yang membolehkan anda melaksanakan pelbagai skrip JavaScript dalam Nginx. Semasa pembangunannya, perhatian besar diberikan kepada prestasi. Sudah tentu, masih banyak yang hilang, tetapi projek itu sedang dibangunkan oleh pasukan kecil dan mereka secara aktif menambah ciri baharu dan membetulkan pepijat. Saya berharap suatu hari nanti NJS akan membolehkan anda menyambungkan modul luaran, yang akan menjadikan fungsi Nginx hampir tidak terhad. Tetapi terdapat NGINX Plus dan kemungkinan besar tiada ciri!

Repositori dengan kod penuh untuk artikel

njs-pypi dengan sokongan AWS Sign v4

Penerangan tentang arahan modul ngx_http_js_module

Repositori rasmi NJS ΠΈ dokumentasi

Contoh penggunaan NJS daripada Dmitry Volintsev

njs - skrip JavaScript asli dalam nginx / Ucapan oleh Dmitry Volnyev di Saint HighLoad++ 2019

NJS dalam pengeluaran / Ucapan Vasily Soshnikov di HighLoad++ 2019

Menandatangani dan Mengesahkan Permintaan REST dalam AWS

Sumber: www.habr.com