Random changes for many files.
Signed-off-by: Pavel Kirilin <win10@list.ru>
13
.editorconfig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
16
.eslintrc.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'@nuxtjs/eslint-config-typescript',
|
||||||
|
'plugin:nuxt/recommended',
|
||||||
|
'prettier'
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
],
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {}
|
||||||
|
}
|
90
.gitignore
vendored
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Node template
|
||||||
|
# Logs
|
||||||
|
/logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# Nuxt generate
|
||||||
|
dist
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Service worker
|
||||||
|
sw.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Vim swap files
|
||||||
|
*.swp
|
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
3
Caddyfile
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
blog.local {
|
||||||
|
reverse_proxy /* http://localhost:3000
|
||||||
|
}
|
69
README.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# blog
|
||||||
|
|
||||||
|
## Build Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# install dependencies
|
||||||
|
$ yarn install
|
||||||
|
|
||||||
|
# serve with hot reload at localhost:3000
|
||||||
|
$ yarn dev
|
||||||
|
|
||||||
|
# build for production and launch server
|
||||||
|
$ yarn build
|
||||||
|
$ yarn start
|
||||||
|
|
||||||
|
# generate static project
|
||||||
|
$ yarn generate
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
|
||||||
|
|
||||||
|
## Special Directories
|
||||||
|
|
||||||
|
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
|
||||||
|
|
||||||
|
### `assets`
|
||||||
|
|
||||||
|
The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
|
||||||
|
|
||||||
|
### `components`
|
||||||
|
|
||||||
|
The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
|
||||||
|
|
||||||
|
### `layouts`
|
||||||
|
|
||||||
|
Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
|
||||||
|
|
||||||
|
|
||||||
|
### `pages`
|
||||||
|
|
||||||
|
This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
|
||||||
|
|
||||||
|
### `plugins`
|
||||||
|
|
||||||
|
The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
|
||||||
|
|
||||||
|
### `static`
|
||||||
|
|
||||||
|
This directory contains your static files. Each file inside this directory is mapped to `/`.
|
||||||
|
|
||||||
|
Example: `/static/robots.txt` is mapped as `/robots.txt`.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
|
||||||
|
|
||||||
|
### `store`
|
||||||
|
|
||||||
|
This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).
|
47
components/BottomNav.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<NuxtLink
|
||||||
|
v-if="prev"
|
||||||
|
:to="{ name: 'blog-slug', params: { slug: prev.slug } }"
|
||||||
|
class="font-bold text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{{ prev.title }}
|
||||||
|
</NuxtLink>
|
||||||
|
<span v-else> </span>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="next"
|
||||||
|
:to="{ name: 'blog-slug', params: { slug: next.slug } }"
|
||||||
|
class="font-bold hover:underline"
|
||||||
|
>
|
||||||
|
{{ next.title }}
|
||||||
|
</NuxtLink>
|
||||||
|
<span v-else> </span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from '@nuxtjs/composition-api'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup({ prev = null, next = null }) {
|
||||||
|
console.log(prev)
|
||||||
|
console.log(next)
|
||||||
|
return {
|
||||||
|
prev,
|
||||||
|
next,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// export default {
|
||||||
|
// props: {
|
||||||
|
// prev: {
|
||||||
|
// type: Object,
|
||||||
|
// default: () => null,
|
||||||
|
// },
|
||||||
|
// next: {
|
||||||
|
// type: Object,
|
||||||
|
// default: () => null,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
</script>
|
13
components/Footer.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<footer class="p-5">
|
||||||
|
<div class="content has-text-centered">
|
||||||
|
<p>
|
||||||
|
<strong>Dev blog</strong> by
|
||||||
|
<a href="https://github.com/s3rius/">@s3rius</a>.
|
||||||
|
Built with ❤️ and <a href="https://nuxtjs.org/">nuxt</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script></script>
|
162
content/ru/docker-envs.md
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
---
|
||||||
|
title: Разделение докера на среды.
|
||||||
|
description: Как работать с несколькими docker-compose.
|
||||||
|
position: 3
|
||||||
|
category: DevOps
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# Разделение докера на среды
|
||||||
|
|
||||||
|
Должен ли ты разделять среды докера в несколько `docker-compose` файлов?
|
||||||
|
Определенно! В некоторых случаях невозможно разобраться что разработчики хотели сделать или почему ничего не работает. Настройка раздельных сред может стать настоязей мешанино. В этой статье я покажу как настроить `docker-compose` и избежать миллиона проблем.
|
||||||
|
|
||||||
|
Как мы вообще можем разделить среды для локальной разработки и продовые?
|
||||||
|
Отет прост: Требуется декомпозировать проект и создать отдельные файлы под каждую из сред или даже сервисов.
|
||||||
|
|
||||||
|
Это нормально, если у тебя будет больше 2-ч `docker-compose` файлов.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
```
|
||||||
|
deploy
|
||||||
|
├── docker-compose.autotests.yml
|
||||||
|
├── docker-compose.db.yml
|
||||||
|
├── docker-compose.dev.yml
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── dockerfiles
|
||||||
|
│ ├── api.Dockerfile
|
||||||
|
│ └── front.Dockerfile
|
||||||
|
└── scripts
|
||||||
|
├── start-autotests.sh
|
||||||
|
├── start-backend.sh
|
||||||
|
└── start-migrations.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Как это работает?
|
||||||
|
Докер умеет работать с множеством `docker-compose` файлов одновременно.И мы можем использовать это для разделения сред.
|
||||||
|
|
||||||
|
Выглядит это следующим образом.
|
||||||
|
```bash
|
||||||
|
docker-compose \
|
||||||
|
-f "deploy/docker-compose.yml" \
|
||||||
|
-f "deploy/docker-compose.db.yml" \
|
||||||
|
-f "deploy/docker-compose.dev.yml" \
|
||||||
|
up
|
||||||
|
```
|
||||||
|
|
||||||
|
В каждом из этих файлов определен какой-то кусок конфигурации, который не пересекается. Например в `docker-compose.yml` определено приложение и некоторые необходимые сервисы, а в остальных файлах добавляются сервисы или меняются значения предыдущих.
|
||||||
|
|
||||||
|
Наверное, тут проще на примере пояснить.
|
||||||
|
|
||||||
|
Допустим у нас есть проект, у которого поднимается бекенд с параметрами, которые отличаются на проде и локально.
|
||||||
|
|
||||||
|
Для простоты создадим простецикий проект со следующей структурой.
|
||||||
|
|
||||||
|
```
|
||||||
|
proj
|
||||||
|
├── deploy
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ └── dockerfiles
|
||||||
|
│ └── script.Dockerfile
|
||||||
|
└── script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Для начала напишем скрипт, который будет в центре всего.
|
||||||
|
```python{}[script.pt]
|
||||||
|
from sys import argv # это аргуметы переданные в скрипт
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("; ".join(argv[1:])) # выводитна экран все аргументы программы
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
После того, как скрипт готов и отлажен давайте завернем его в докер,
|
||||||
|
создав `Dockerfile`.
|
||||||
|
|
||||||
|
```dockerfile{}[deploy/dockerfiles/script.Dockerfile]
|
||||||
|
from python:3.8 # asd
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY script.py /app/script.py
|
||||||
|
CMD python /app/script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Как вы видите, Dockerfile очень прост. Теперь добавим главный `docker-compose.yml`.
|
||||||
|
|
||||||
|
```yaml{}[deploy/docker-compose.yml]
|
||||||
|
---
|
||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
services:
|
||||||
|
script:
|
||||||
|
build: # Собираем приложение используя наш dockerfile.
|
||||||
|
dockerfile: ./deploy/dockerfiles/script.Dockerfile
|
||||||
|
context: .
|
||||||
|
# Запускаем его с командой для продового запуска.
|
||||||
|
command: python script.py this is prod
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь запустим всё это чудо.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
d-test docker-compose \
|
||||||
|
-f "./deploy/docker-compose.yml" \
|
||||||
|
--project-directory "." \
|
||||||
|
run --rm script
|
||||||
|
```
|
||||||
|
|
||||||
|
Вот что будет выведено на экран.
|
||||||
|
```log
|
||||||
|
Creating d-test_script_run ... done
|
||||||
|
this; is; prod
|
||||||
|
```
|
||||||
|
|
||||||
|
Как мы видим, на экран вывелось сообщение, которое мы указали в нашем `docker-compose.yml`
|
||||||
|
|
||||||
|
А теперь для локальной разработки мы не будем ничего менять в нашем файле композиции, а создадим новый рядом.
|
||||||
|
|
||||||
|
```yaml{}[deploy/docker-compose.dev.yml]
|
||||||
|
---
|
||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
services:
|
||||||
|
script:
|
||||||
|
command: python script.py this is dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь добавим ещё один файл в нашу команду запуска.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
d-test docker-compose \
|
||||||
|
-f "deploy/docker-compose.yml" \
|
||||||
|
-f "deploy/docker-compose.dev.yml" \
|
||||||
|
--project-directory "." \
|
||||||
|
run --rm script
|
||||||
|
```
|
||||||
|
|
||||||
|
Вот что будет выведено на экран:
|
||||||
|
```log
|
||||||
|
Creating d-test\_script\_run ... done
|
||||||
|
this; is; dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Как можно заметить, конфигурация запуска перезаписалась в порядке вызова.
|
||||||
|
|
||||||
|
То есть, каждый последующий файл композиции может добавлять сервисы и **частично** изменять конфигурацию предыдущих.
|
||||||
|
|
||||||
|
Итоговая структура проекта:
|
||||||
|
```
|
||||||
|
proj
|
||||||
|
├── deploy
|
||||||
|
│ ├── docker-compose.dev.yml
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ └── dockerfiles
|
||||||
|
│ └── script.Dockerfile
|
||||||
|
└── script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Где это применимо?
|
||||||
|
Ну, в любом проекте, сложнее того, который мы рассмотрели. Потому что в реальной жизни не всё так радужно и локальная версия приложения может отличаться не только параметрами запуска, но и целыми сервисами, которые требуются для локальной копии приложения.
|
132
content/ru/makefiles.md
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
title: Makefiles для чайников.
|
||||||
|
description: Автоматизируем по старинке.
|
||||||
|
position: 1
|
||||||
|
category: 'DevOps'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Makefiles для чайников
|
||||||
|
Если описывать в двух словах, то это просто описание команд для упрощения работы с проектом. Изначально делались для того, чтобы удобно компилировать всякие проекты на любых языках.
|
||||||
|
|
||||||
|
## Как они работают
|
||||||
|
Достаточно просто.
|
||||||
|
|
||||||
|
Вот пример `Makefile`:
|
||||||
|
|
||||||
|
```makefile{}[Makefile]
|
||||||
|
# Сгенерируем простой файл
|
||||||
|
test.gen:
|
||||||
|
@echo 'echo "Hi!"' > "test.gen"
|
||||||
|
|
||||||
|
# Команда для запуска файла test.gen
|
||||||
|
.PHONY: run
|
||||||
|
run: test.gen
|
||||||
|
sh "test.gen"
|
||||||
|
```
|
||||||
|
|
||||||
|
До двоеточий обазначены команды (tagets). Например тут это: `test.gen` и `run`.
|
||||||
|
Данные таргеты можно запускать через `make ${target}`.
|
||||||
|
|
||||||
|
Например, если ввести `make run` в папке с `Makefile`, то мы получим следующее:
|
||||||
|
```console
|
||||||
|
$ sh "test.gen"
|
||||||
|
Hi!
|
||||||
|
```
|
||||||
|
|
||||||
|
Как видно из выхлопа данной команды, у нас успешно запустился файл `test.gen`, хотя мы не запускали команду `make test.gen`. Что произошло? Давайте разбираться.
|
||||||
|
|
||||||
|
## Зависимости таргетов
|
||||||
|
|
||||||
|
На строчке объявления таргета `run` видно, что объявлен `test.gen`. Это зависимость данного таргета и она будет вызвана до того, как выполнится скрипт описываемого таргета. Таких зависимостей может быть много, перечисляются они чере пробел.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
```makefile{}[Makefile]
|
||||||
|
.PHONY: target1
|
||||||
|
target1:
|
||||||
|
echo "1"
|
||||||
|
|
||||||
|
.PHONY: target2
|
||||||
|
target2: target1
|
||||||
|
echo "2"
|
||||||
|
|
||||||
|
.PHONY: target3
|
||||||
|
target3: target1 target2
|
||||||
|
echo "memes"
|
||||||
|
```
|
||||||
|
|
||||||
|
При вызове `make target3` будет выведено:
|
||||||
|
```console
|
||||||
|
$ make target3
|
||||||
|
echo "1"
|
||||||
|
1
|
||||||
|
echo "2"
|
||||||
|
2
|
||||||
|
echo "memes"
|
||||||
|
memes
|
||||||
|
```
|
||||||
|
|
||||||
|
Как можно видеть, он построил граф зависимостей и не выполнил `target1` дважды.
|
||||||
|
|
||||||
|
## Сокрытие вывода команд
|
||||||
|
|
||||||
|
В предыдущем примере можно заметить, что он написал все команды в терминал. Для того, чтобы этого избежать следует добавить "@" в начало команды и она не будет напечатана.
|
||||||
|
|
||||||
|
```makefile{}[Makefile]
|
||||||
|
.PHONY: target1
|
||||||
|
target1:
|
||||||
|
@echo "1"
|
||||||
|
|
||||||
|
.PHONY: target2
|
||||||
|
target2: target1
|
||||||
|
@echo "2"
|
||||||
|
|
||||||
|
.PHONY: target3
|
||||||
|
target3: target1 target2
|
||||||
|
@echo "memes"
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь при вызое `make target3` будет показано следующее:
|
||||||
|
```cosole
|
||||||
|
$ make target3
|
||||||
|
1
|
||||||
|
2
|
||||||
|
memes
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Валидация сгенерированных файлов
|
||||||
|
Зачастую `Makefile` используют для компиляции С и зачастую требуется
|
||||||
|
собрать какую-либо часть проект и пропустить сборку этой части, если эта часть уже собрана.
|
||||||
|
Раскрою секрет, в Makefile это базовый функционал.
|
||||||
|
|
||||||
|
Давайте немного поменяем первый Makefile и запустим дважды.
|
||||||
|
```makefile{}[Makefile]
|
||||||
|
# Сгенерируем простой файл
|
||||||
|
test.gen:
|
||||||
|
echo 'echo "Hi!"' > "test.gen"
|
||||||
|
|
||||||
|
# Команда для запуска файла test.gen
|
||||||
|
.PHONY: run
|
||||||
|
run: test.gen
|
||||||
|
sh "test.gen"
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь вызываемая команда таргета `test.gen` выводится на экран.
|
||||||
|
|
||||||
|
Позапускаем.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make run
|
||||||
|
echo 'echo "Hi!"' > "test.gen" # Наша командабыла вызвана.
|
||||||
|
sh "test.gen"
|
||||||
|
Hi!
|
||||||
|
$ make run
|
||||||
|
sh "test.gen" # Наша команда не вызвана.
|
||||||
|
Hi!
|
||||||
|
```
|
||||||
|
|
||||||
|
Дело в том, что названия таргетов - названия файлов, которые должны сгенерировать эти самые таргеты.
|
||||||
|
То есть, в данном случае таргет `test.gen` должен сгенерировать файл `test.gen` по окончании выполнения. Если этот файл уже присутствует, то команда выполнена не будет. Именно поэтому у нас она не запустилась второй раз, так как в первый запуск был создан треубемый файл и его никто не удалял между запусками.
|
||||||
|
|
||||||
|
А что если я вот не хочу чтобы команда создавала и проверяла файлы?
|
||||||
|
Для этого пишут `.PHONY: ${target}`. Например у нас так объявлен таргет `run` и, даже если файл с названием `run` будет присутствовать в директории цель не будет выполняться.
|
584
content/ru/project-start.md
Normal file
@ -0,0 +1,584 @@
|
|||||||
|
---
|
||||||
|
title: Как стартовать проект
|
||||||
|
description: Лучший вариант начала проекта на питоне.
|
||||||
|
category: Python
|
||||||
|
position: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Как стартовать проект
|
||||||
|
|
||||||
|
В целом, вариантов несколько. Например, можно просто хранить несколько скриптов на github и быть довольным, можно дописать к ним `setup.py` и выложиться на pypi. Но лучший способ - структурированный проект с `poetry`.
|
||||||
|
|
||||||
|
Почему?
|
||||||
|
|
||||||
|
Особенностей у poetry много. Сейчас расскажу про то, как использовать и что делать.
|
||||||
|
|
||||||
|
Для начала создадим проект.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ poetry new new_proj
|
||||||
|
Created package new_proj in new_proj
|
||||||
|
$ cd new_proj
|
||||||
|
```
|
||||||
|
|
||||||
|
Данная команда сгенерирует следующую структуру:
|
||||||
|
```
|
||||||
|
new_proj
|
||||||
|
├── new_proj
|
||||||
|
│ └── __init__.py
|
||||||
|
├── pyproject.toml
|
||||||
|
├── README.rst
|
||||||
|
└── tests
|
||||||
|
├── __init__.py
|
||||||
|
└── test_new_proj.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Это наш новый проект с небольшими заготовками. В целом, тут нет ничего необычного. Весь код библиотеки/проекта будет в папке с названием проекта. `README.rst` - дефолтный README. `pyproject.toml` - мета-данные о проекте, такие как: зависимости, описание, дополнительные установочные опции и многое другое. Ты можешь прочитать побольше о `pyproject.toml` [тут](https://python-poetry.org/docs/pyproject/).
|
||||||
|
|
||||||
|
## Как работать с poetry
|
||||||
|
|
||||||
|
Вся информация есть в [официальной документации](https://python-poetry.org/docs) poetry. Тут я расскажу про основные моменты.
|
||||||
|
|
||||||
|
### Управление зависимостями
|
||||||
|
|
||||||
|
Чтобы добавить зависимость проекта достаточно выполнить
|
||||||
|
```console
|
||||||
|
$ poetry add ${dependency}
|
||||||
|
```
|
||||||
|
|
||||||
|
Данная команда найдет последнюю нужную версию и запишет её в `pyproject.toml` и в `poetry.lock`.
|
||||||
|
|
||||||
|
Для того, чтобы установить зависимость для разработки (линтеры например), достаточно добавить флаг `--dev`.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ poetry add ${dependency} --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Чтобы удалить зависимость достаточно просто `add` заменить на `remove`.
|
||||||
|
|
||||||
|
Примеры работы:
|
||||||
|
```console
|
||||||
|
$ poetry add loguru
|
||||||
|
Using version ^0.5.3 for loguru
|
||||||
|
|
||||||
|
Updating dependencies
|
||||||
|
Resolving dependencies... (0.1s)
|
||||||
|
|
||||||
|
Writing lock file
|
||||||
|
|
||||||
|
Package operations: 1 install, 0 updates, 0 removals
|
||||||
|
• Installing loguru (0.5.3)
|
||||||
|
$ poetry remove loguru
|
||||||
|
Updating dependencies
|
||||||
|
Resolving dependencies... (0.1s)
|
||||||
|
|
||||||
|
Writing lock file
|
||||||
|
|
||||||
|
Package operations: 0 installs, 0 updates, 1 removal
|
||||||
|
• Removing loguru (0.5.3)
|
||||||
|
```
|
||||||
|
### Выполнение команд
|
||||||
|
|
||||||
|
При использовании virtualenv для входа в оболочку всегда надо было вводить
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Однако с poetry это излишне. Он сам создает и менеджит виртуальные среды.
|
||||||
|
Чтобы выполнить одиночную команду можно вызвать `run`.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
```console
|
||||||
|
$ poetry install black --dev
|
||||||
|
Using version ^20.8b1 for black
|
||||||
|
• Installing appdirs (1.4.4)
|
||||||
|
• Installing click (7.1.2)
|
||||||
|
• Installing mypy-extensions (0.4.3)
|
||||||
|
• Installing pathspec (0.8.1)
|
||||||
|
• Installing regex (2020.11.13)
|
||||||
|
• Installing toml (0.10.2)
|
||||||
|
• Installing typed-ast (1.4.2)
|
||||||
|
• Installing typing-extensions (3.7.4.3)
|
||||||
|
• Installing black (20.8b1)
|
||||||
|
$ poetry run black .
|
||||||
|
reformatted new_proj/new_proj/__init__.py
|
||||||
|
reformatted new_proj/tests/test_new_proj.py
|
||||||
|
All done! ✨ 🍰 ✨
|
||||||
|
2 files reformatted, 1 file left unchanged.
|
||||||
|
```
|
||||||
|
|
||||||
|
Для выполнения нескольких комманд подряд, можно войти в shell. Это аналог `source venv/bin/activate`.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ poetry shell
|
||||||
|
Spawning shell within /home/s3rius/.cache/pypoetry/virtualenvs/new-proj-eutP4v0O-py3.9
|
||||||
|
$ black .
|
||||||
|
reformatted new_proj/new_proj/__init__.py
|
||||||
|
reformatted new_proj/tests/test_new_proj.py
|
||||||
|
All done! ✨ 🍰 ✨
|
||||||
|
2 files reformatted, 1 file left unchanged.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Версионирование
|
||||||
|
|
||||||
|
Менять версии пакета вручную больше не нужно.
|
||||||
|
|
||||||
|
У poetry есть кое что для тебя.
|
||||||
|
```console
|
||||||
|
$ poetry version patch
|
||||||
|
Bumping version from 0.1.0 to 0.1.1
|
||||||
|
$ poetry version preminor
|
||||||
|
Bumping version from 0.1.1 to 0.2.0-alpha.0
|
||||||
|
$ poetry version minor
|
||||||
|
Bumping version from 0.2.0-alpha.0 to 0.2.0
|
||||||
|
$ poetry version premajor
|
||||||
|
Bumping version from 0.2.0 to 1.0.0-alpha.0
|
||||||
|
$ poetry version major
|
||||||
|
Bumping version from 1.0.0-alpha.0 to 1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## pyproject.toml
|
||||||
|
|
||||||
|
Как было сказано ранее, данный файл содержит в себе мета-информацию пакета.
|
||||||
|
|
||||||
|
Пример `pyproject.toml`
|
||||||
|
```toml{}[pyproject.toml]
|
||||||
|
[tool.poetry]
|
||||||
|
name = "new_proj"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Test library for example"
|
||||||
|
readme = "README.rst"
|
||||||
|
homepage = "https://test_url.com/"
|
||||||
|
repository = "https://github.meme/"
|
||||||
|
authors = ["Pavel Kirilin <win10@list.ru>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.8"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
pytest = "^6.1"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
```
|
||||||
|
|
||||||
|
Для того чтобы устновить все зависимости требуется просто ввести
|
||||||
|
```console
|
||||||
|
$ poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
Данная команда создаст виртуальную среду сама и установит **все** зависимости. Включая dev-зависимости. Чтобы установить зависимости только для приложения можно добавить `--no-dev` ключ.
|
||||||
|
|
||||||
|
## Как паковать и публиковать на pypi
|
||||||
|
Всё очень просто.
|
||||||
|
Давайте добавим какую-нибудь функцию в проект.
|
||||||
|
|
||||||
|
```python{}[new_proj/main.py]
|
||||||
|
def ab_problem(a: int, b: int) -> int:
|
||||||
|
return a + b
|
||||||
|
```
|
||||||
|
|
||||||
|
```python{}[new_proj/__init.py]
|
||||||
|
from new_proj.main import ab_problem
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'ab_problem'
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь соберем проект.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ poetry build
|
||||||
|
Building new_proj (0.1.0)
|
||||||
|
- Building sdist
|
||||||
|
- Built new_proj-0.1.0.tar.gz
|
||||||
|
- Building wheel
|
||||||
|
- Built new_proj-0.1.0-py3-none-any.whl
|
||||||
|
```
|
||||||
|
|
||||||
|
Та-да. Это готовый к публикации на pypi пакет. Лежит он в папке dist.
|
||||||
|
|
||||||
|
Давайте проверим, что всё работает корректно.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ pip install "./dist/new_proj-0.1.0-py3-none-any.whl"
|
||||||
|
Processing ./dist/new_proj-0.1.0-py3-none-any.whl
|
||||||
|
Installing collected packages: new-proj
|
||||||
|
Successfully installed new-proj-0.1.0
|
||||||
|
|
||||||
|
$ python
|
||||||
|
Python 3.9.1 (default, Feb 1 2021, 04:02:33)
|
||||||
|
[GCC 10.2.0] on linux
|
||||||
|
Type "help", "copyright", "credits" or "license" for more information.
|
||||||
|
$ from new_proj import ab_problem
|
||||||
|
$ ab_problem(1,33)
|
||||||
|
34
|
||||||
|
```
|
||||||
|
|
||||||
|
Как можно видеть всё работает корректно и теперь мы можем использовать наш пакет.
|
||||||
|
|
||||||
|
Для публикации следует использовать:
|
||||||
|
```console
|
||||||
|
$ poetry publish -u "user" -p "password"
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробнее можно почитать [тут](https://python-poetry.org/docs/cli/#publish).
|
||||||
|
|
||||||
|
# Конфигурация проекта
|
||||||
|
Конечно, такого рода конфигурация проекта всё равно никуда не годиться.
|
||||||
|
Давайте настроим автоматический линтинг.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ poetry add \
|
||||||
|
$ flake8 \
|
||||||
|
$ black \
|
||||||
|
$ isort \
|
||||||
|
$ mypy \
|
||||||
|
$ pre-commit \
|
||||||
|
$ yesqa \
|
||||||
|
$ autoflake \
|
||||||
|
$ wemake-python-styleguide --dev
|
||||||
|
---> 100%
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь добавим конфигурационных файлов в корень проекта.
|
||||||
|
Это мои конфигурации, которые я настроил под себя, можешь менять их как хочешь.
|
||||||
|
|
||||||
|
`.mypy.ini` для настройки валидации типов.
|
||||||
|
```ini{}[.mypy.ini]
|
||||||
|
[mypy]
|
||||||
|
strict = True
|
||||||
|
ignore_missing_imports=True
|
||||||
|
allow_subclassing_any=True
|
||||||
|
allow_untyped_calls=True
|
||||||
|
pretty=True
|
||||||
|
show_error_codes=True
|
||||||
|
implicit_reexport=True
|
||||||
|
allow_untyped_decorators=True
|
||||||
|
```
|
||||||
|
|
||||||
|
`.isort.cfg` для конфигурации сортировки импортов.
|
||||||
|
```ini{}[.isort.cfg]
|
||||||
|
[isort]
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = true
|
||||||
|
use_parentheses = true
|
||||||
|
```
|
||||||
|
|
||||||
|
`.flake8` - конфигурация линтинга. Тут довольно много. Это игнорирование ненужных кодов ошибок, которые не особо-то и ошибки.
|
||||||
|
```ini{}[.flake8]
|
||||||
|
[flake8]
|
||||||
|
max-complexity = 6
|
||||||
|
inline-quotes = double
|
||||||
|
max-line-length = 88
|
||||||
|
extend-ignore = E203
|
||||||
|
docstring_style=sphinx
|
||||||
|
|
||||||
|
ignore =
|
||||||
|
; Found `f` string
|
||||||
|
WPS305,
|
||||||
|
; Missing docstring in public module
|
||||||
|
D100,
|
||||||
|
; Missing docstring in magic method
|
||||||
|
D105,
|
||||||
|
; Missing docstring in __init__
|
||||||
|
D107,
|
||||||
|
; Found class without a base class
|
||||||
|
WPS306,
|
||||||
|
; Missing docstring in public nested class
|
||||||
|
D106,
|
||||||
|
; First line should be in imperative mood
|
||||||
|
D401,
|
||||||
|
; Found `__init__.py` module with logic
|
||||||
|
WPS412,
|
||||||
|
; Found implicit string concatenation
|
||||||
|
WPS326,
|
||||||
|
; Found string constant over-use
|
||||||
|
WPS226,
|
||||||
|
; Found upper-case constant in a class
|
||||||
|
WPS115,
|
||||||
|
; Found nested function
|
||||||
|
WPS430,
|
||||||
|
; Found using `@staticmethod`
|
||||||
|
WPS602,
|
||||||
|
; Found method without arguments
|
||||||
|
WPS605,
|
||||||
|
; Found overused expression
|
||||||
|
WPS204,
|
||||||
|
; Found too many module members
|
||||||
|
WPS202,
|
||||||
|
; Found too high module cognitive complexity
|
||||||
|
WPS232,
|
||||||
|
; line break before binary operator
|
||||||
|
W503,
|
||||||
|
; Found module with too many imports
|
||||||
|
WPS201,
|
||||||
|
; Found vague import that may cause confusion: X
|
||||||
|
WPS347,
|
||||||
|
; Inline strong start-string without end-string.
|
||||||
|
RST210,
|
||||||
|
; subprocess call with shell=True seems safe, but may be changed in the future.
|
||||||
|
S602,
|
||||||
|
; Starting a process with a partial executable path.
|
||||||
|
S607,
|
||||||
|
; Consider possible security implications associated with subprocess module.
|
||||||
|
S404,
|
||||||
|
; Found nested class
|
||||||
|
WPS431,
|
||||||
|
; Found wrong module name
|
||||||
|
WPS100,
|
||||||
|
; Found too many methods
|
||||||
|
WPS214,
|
||||||
|
; Found too long ``try`` body
|
||||||
|
WPS229,
|
||||||
|
; Found function with too much cognitive complexity
|
||||||
|
WPS231,
|
||||||
|
|
||||||
|
; all init files
|
||||||
|
__init__.py:
|
||||||
|
; ignore not used imports
|
||||||
|
F401,
|
||||||
|
; ignore import with wildcard
|
||||||
|
F403,
|
||||||
|
; Found wrong metadata variable
|
||||||
|
WPS410,
|
||||||
|
|
||||||
|
per-file-ignores =
|
||||||
|
; all tests
|
||||||
|
test_*.py,tests.py,tests_*.py,*/tests/*:
|
||||||
|
; Use of assert detected
|
||||||
|
S101,
|
||||||
|
|
||||||
|
exclude =
|
||||||
|
./.git,
|
||||||
|
./venv,
|
||||||
|
./cached_venv,
|
||||||
|
./var,
|
||||||
|
```
|
||||||
|
|
||||||
|
`.pre-commit-config.yaml` - конфигураци хуков для запуска всех линтеров перед коммитом.
|
||||||
|
```yaml{}[.pre-commit-config.yaml]
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v2.4.0
|
||||||
|
hooks:
|
||||||
|
- id: check-ast
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: check-toml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
|
||||||
|
- repo: https://github.com/asottile/add-trailing-comma
|
||||||
|
rev: v2.1.0
|
||||||
|
hooks:
|
||||||
|
- id: add-trailing-comma
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
name: Format with Black
|
||||||
|
entry: black
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
|
||||||
|
- id: autoflake
|
||||||
|
name: autoflake
|
||||||
|
entry: autoflake
|
||||||
|
language: system
|
||||||
|
types: [ python ]
|
||||||
|
args: [ --in-place, --remove-all-unused-imports, --remove-duplicate-keys ]
|
||||||
|
|
||||||
|
- id: isort
|
||||||
|
name: isort
|
||||||
|
entry: isort
|
||||||
|
language: system
|
||||||
|
types: [ python ]
|
||||||
|
|
||||||
|
- id: flake8
|
||||||
|
name: Check with Flake8
|
||||||
|
entry: flake8
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
types: [ python ]
|
||||||
|
args: [--count, .]
|
||||||
|
|
||||||
|
- id: mypy
|
||||||
|
name: Validate types with MyPy
|
||||||
|
entry: mypy
|
||||||
|
language: system
|
||||||
|
types: [ python ]
|
||||||
|
|
||||||
|
- id: yesqa
|
||||||
|
name: Remove usless noqa
|
||||||
|
entry: yesqa
|
||||||
|
language: system
|
||||||
|
types: [ python ]
|
||||||
|
|
||||||
|
- id: pytest
|
||||||
|
name: pytest
|
||||||
|
entry: pytest
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
types: [ python ]
|
||||||
|
```
|
||||||
|
|
||||||
|
И не забываем про `.gitignore`. Его можно найти [тут](https://github.com/github/gitignore/blob/master/Python.gitignore).
|
||||||
|
|
||||||
|
ОН НЕОБХОДИМ ЧТОБЫ pre-commit РАБОТЛ МАКСИМАЛЬНО КОРРЕКТНО.
|
||||||
|
|
||||||
|
Теперь установим хуки в репозиторий.
|
||||||
|
```console
|
||||||
|
$ git init
|
||||||
|
Initialized empty Git repository in .git/
|
||||||
|
$ poetry shell
|
||||||
|
$ pre-commit install
|
||||||
|
pre-commit installed at .git/hooks/pre-commit
|
||||||
|
$ git commit
|
||||||
|
... # Упадет с кучей ошибок
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь поправим все возникшие проблемы.
|
||||||
|
|
||||||
|
Во первых исправим тесты.
|
||||||
|
|
||||||
|
В `tests/test_new_proj.py` напишем следующее:
|
||||||
|
```python{}[tests/test_new_proj.py]
|
||||||
|
from new_proj import ab_problem
|
||||||
|
|
||||||
|
|
||||||
|
def test_ab() -> None:
|
||||||
|
"""AB problecm success case."""
|
||||||
|
assert ab_problem(1, 2) == 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавим описания в `__init__` файлы.
|
||||||
|
|
||||||
|
```python{}[tests/__init__.py]
|
||||||
|
"""Tests for new_proj."""
|
||||||
|
```
|
||||||
|
|
||||||
|
```python{}[new_proj/__init__.py]
|
||||||
|
"""Project for solving ab problem."""
|
||||||
|
from new_proj.main import ab_problem
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ab_problem",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Пофиксим основной файл проекта.
|
||||||
|
|
||||||
|
```python{}[new_proj/main.py]
|
||||||
|
def ab_problem(first: int, second: int) -> int:
|
||||||
|
"""
|
||||||
|
Solve AB problem.
|
||||||
|
|
||||||
|
The function sums two integers.
|
||||||
|
|
||||||
|
:param first: a argument.
|
||||||
|
:param second: b argument.
|
||||||
|
:returns: sum.
|
||||||
|
"""
|
||||||
|
return first + second
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь вы можете сделать свой первый коммит.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ git commit
|
||||||
|
Check python ast................Passed
|
||||||
|
Trim Trailing Whitespace........Passed
|
||||||
|
Check Toml......................Passed
|
||||||
|
Fix End of Files................Passed
|
||||||
|
Add trailing commas.............Passed
|
||||||
|
Format with Black...............Passed
|
||||||
|
autoflake.......................Passed
|
||||||
|
isort...........................Passed
|
||||||
|
Check with Flake8...............Passed
|
||||||
|
Validate types with MyPy........Passed
|
||||||
|
Remove usless noqa..............Passed
|
||||||
|
pytest..........................Passed
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь ты знаешь как создавать шедевры.
|
||||||
|
# Создание CLI-приложения
|
||||||
|
А что если я хочу cli-приложение?
|
||||||
|
Ты не представляешь насколько это просто.
|
||||||
|
|
||||||
|
Пойдем модифицируем наш основной файл.
|
||||||
|
|
||||||
|
```python{}[new_proj/main.py]
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
"""
|
||||||
|
Parse CLI arguments.
|
||||||
|
|
||||||
|
:returns: parsed namespace.
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument("a", type=int)
|
||||||
|
parser.add_argument("b", type=int)
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def ab_problem(first: int, second: int) -> int:
|
||||||
|
"""
|
||||||
|
Solve AB problem.
|
||||||
|
|
||||||
|
The function sums two integers.
|
||||||
|
|
||||||
|
:param first: a argument.
|
||||||
|
:param second: b argument.
|
||||||
|
:returns: sum.
|
||||||
|
"""
|
||||||
|
return first + second
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Main function."""
|
||||||
|
args = parse_args()
|
||||||
|
print(ab_problem(args.a, args.b)) # noqa: WPS421
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь поправим pyproject.toml таким образом чтобы он создал cli для нашей функции.
|
||||||
|
|
||||||
|
Добавим следующую секцию куда-нибудь в `pyproject.toml`:
|
||||||
|
```toml
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
ab_solver = "new_proj.main:main"
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь нам доступна программа `ab_solver` внутри shell.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ poetry install
|
||||||
|
$ poetry shell
|
||||||
|
$ ab_solver 1 2
|
||||||
|
3
|
||||||
|
```
|
||||||
|
|
||||||
|
Хочешь установить? Пожалуйста.
|
||||||
|
```console
|
||||||
|
$ poetry build
|
||||||
|
Building new_proj (0.1.0)
|
||||||
|
- Building sdist
|
||||||
|
- Built new_proj-0.1.0.tar.gz
|
||||||
|
- Building wheel
|
||||||
|
- Built new_proj-0.1.0-py3-none-any.whl
|
||||||
|
$ pip install "./dist/new_proj-0.1.0-py3-none-any.whl"
|
||||||
|
Processing ./dist/new_proj-0.1.0-py3-none-any.whl
|
||||||
|
Installing collected packages: new-proj
|
||||||
|
Successfully installed new-proj-0.1.0
|
||||||
|
$ ab_solver 1 2
|
||||||
|
3
|
||||||
|
```
|
||||||
|
|
||||||
|
Если запаблишить проект, то у пользователя тоже установится ваша cli-программа.
|
461
content/ru/traefik.md
Normal file
@ -0,0 +1,461 @@
|
|||||||
|
---
|
||||||
|
title: Traefik - роутинг просто.
|
||||||
|
description: Изучите как использовать traefik.
|
||||||
|
position: 2
|
||||||
|
category: DevOps
|
||||||
|
---
|
||||||
|
|
||||||
|
# Traefik - роутинг просто
|
||||||
|
|
||||||
|
|
||||||
|
Сегодня я бы хотел рассказать о такой клёвой штуке, как [Traefik](https://traefik.io/traefik/). Не так давно я перевёл все сервисы своего сервера на traefik и это буквально сделало жизнь проще. Сегодня я расскажу вам зачем он нужен, как его настроить, и покажу на примере, как именно он помогает. По факту, это nginx на стероидах, в плане конфигурации, потому что в traefik за тебя сделано гораздо больше.
|
||||||
|
|
||||||
|
# Что такое traefik
|
||||||
|
|
||||||
|
Traefik - это система, которая позволяет настроить маппинг между доменными именами и конкретными приложениями. Допустим, у вас есть контейнер frontend-приложения, которое вы хотели бы разместить на домене `myapp.com`, и также у вас есть контейнер backend-приложения, которое вы бы хотели разместить на домене `api.mayapp.com`. [Traefik](https://traefik.io/traefik/) поможет вам это сделать без лишних файлов конфигурации.
|
||||||
|
|
||||||
|
## Сравнение с другими инструментами
|
||||||
|
|
||||||
|
Если бы вы решили сделать это через [Nginx](https://www.nginx.com/), то вы бы создали новые файлы конфигураций под каждый домен, в папочке с конфигурациями для всех доменов, положили куда-нибудь сертификат от домена `*myapp.com` и подключали бы его вручную в каждом файле доменов. А если бы вам надо было увеличить количество контейнеров отдельного приложения, вы бы указывали сервера в директиве `upstream` и перечисляли там адреса инстансов приложения вручную.
|
||||||
|
|
||||||
|
Любители [Apache HTTP Server](https://httpd.apache.org/) на этом моменте обычно берут дробовик и разносят себе голову хорошим зарядом дроби.
|
||||||
|
|
||||||
|
А господа, которые используют [Traefik](https://traefik.io/traefik/), просто дописывают лейблы контейнеру в `docker-compose.yml` и идут дальше дегустировать вино.
|
||||||
|
|
||||||
|
<b-message type="is-info" has-icon>
|
||||||
|
Если же ваше приложение построено на вебсокетах, то тут уже фанаты Nginx тянутся за дробовиком. Ну а для ценителей traefik ничего не меняется, ведь в нём встроена поддержка HTTP/2.0.
|
||||||
|
</b-message>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
В официальной документации зарисована следующая схема работы traefik:
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Как можно видеть по данному изображению, есть 5 основных составляющих traefik, а именно:
|
||||||
|
|
||||||
|
* entrypoints
|
||||||
|
* routers
|
||||||
|
* rules (часть routers)
|
||||||
|
* middlewares (чать routers)
|
||||||
|
* services
|
||||||
|
|
||||||
|
### Entrypoint
|
||||||
|
Являются основными слушателями входящих соединений. Через них проходит весь трафик. Если попробовать объяснить в двух словах: "Порт, на который должно прийти входящее соединение", - вполне себе неплохое объяснение. В данном туториале я создам 2 `entrypoint`, которые будут слушать на http и https.
|
||||||
|
|
||||||
|
### Routers, Rules и Middlewares
|
||||||
|
Роутер - связующее звено между `entrypoint` и сервисом, куда нужно направить трафик. Роутер хранит в себе информацию, куда направить входящий трафик, и
|
||||||
|
правила, по которым можно определить, пускать ли трафик дальше.
|
||||||
|
|
||||||
|
Rules - это и есть те самые правила, которые определяют, через какой роутер пустить трафик.
|
||||||
|
Допустим, у вас в конфиге есть следующие правила:
|
||||||
|
```
|
||||||
|
Host(`myapp.com`) && (PathPrefix(`/api`) || PathPrefix(`/memes`))
|
||||||
|
```
|
||||||
|
|
||||||
|
Это можно читать как "Этот роутер пустит в моё приложение, если запрос пришёл на хост `myapp.com` и путь запроса начинается с `/api` или `/memes`"
|
||||||
|
|
||||||
|
Довольно просто, не так ли?
|
||||||
|
|
||||||
|
Ну а middlewares - это некоторая логика, что нужно выполнить с запросом до того, как он попадёт в ваше приложение. В этой статье я не буду рассказывать про существующие `middlewares`, тут потребуется самостоятельное ознакомление с [документацией](https://doc.traefik.io/traefik/middlewares/overview/).
|
||||||
|
|
||||||
|
# Конфигурация
|
||||||
|
|
||||||
|
<b-message type="is-info" has-icon>
|
||||||
|
В данной статье я буду использовать Docker для конфигурации traefik.
|
||||||
|
Вы, конечно же, можете использовать локально установленную версию, всё будет работать, как часы.
|
||||||
|
</b-message>
|
||||||
|
|
||||||
|
|
||||||
|
Для нашего основного traefik на сервере мы создадим небольшой docker-compose.yml, файл конфига и папочку с сертификатами.
|
||||||
|
Структура будет следующая:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── certs
|
||||||
|
│ ├── local.key
|
||||||
|
│ └── local.pem
|
||||||
|
├── config.toml
|
||||||
|
└── docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь разберем каждый файл по порядку.
|
||||||
|
Вот основной docker-compose нашего traefik.
|
||||||
|
|
||||||
|
```yaml{}[docker-compose.yml]
|
||||||
|
---
|
||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik-proxy:
|
||||||
|
# The official v2.0 Traefik docker image
|
||||||
|
image: traefik:v2.4.8
|
||||||
|
container_name: traefik_main
|
||||||
|
command:
|
||||||
|
- --providers.docker=true
|
||||||
|
- --providers.docker.exposedbydefault=false
|
||||||
|
- --providers.docker.network=traefik-shared
|
||||||
|
- --providers.file.directory=/etc/traefik/dynamic
|
||||||
|
- --providers.file.watch=true
|
||||||
|
- --entrypoints.http.address=:80
|
||||||
|
- --entrypoints.https.address=:443
|
||||||
|
ports:
|
||||||
|
# The HTTP port
|
||||||
|
- "80:80"
|
||||||
|
# The HTTPS port
|
||||||
|
- "443:443"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- traefik-shared
|
||||||
|
environment:
|
||||||
|
MAIN_HOST: 192.168.1.89
|
||||||
|
volumes:
|
||||||
|
# So that Traefik can listen to the Docker events
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./config.toml:/etc/traefik/dynamic/traefik.toml
|
||||||
|
- ./certs:/etc/certs/
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-shared:
|
||||||
|
name: traefik-shared
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Вот какие параметры я передаю в `traefik-cli`.
|
||||||
|
|
||||||
|
| Параметр | Что делает |
|
||||||
|
|-------------------------------------------------|----------------------------------------------------------------------------------|
|
||||||
|
| `providers.docker=true` | Включает прослушивание докера на новые события и следит за лейблами контейнеров. |
|
||||||
|
| `providers.docker.exposedbydefault=false` | Отключает автоматическое создание роутеров ко всем контейнерам на сервере. |
|
||||||
|
| `providers.docker.network=traefik-shared` | Сеть докера, по которой будет выполнятся подключение к контейнерам. |
|
||||||
|
| `providers.file.directory=/etc/traefik/dynamic` | Папка с конфигурационным файлом. |
|
||||||
|
| `providers.file.watch=true` | Включает отслеживание изменений файла конфигурации. |
|
||||||
|
| `entrypoints.http.address=:80` | Создаёт entrypoint с названием http и слушает 80 порт. |
|
||||||
|
| `entrypoints.https.address=:443` | Создаёт entrypoint с названием https и слушает 443 порт. |
|
||||||
|
|
||||||
|
Конечно, вы всегда можете глянуть `traefik --help` и подобрать себе желаемые параметры.
|
||||||
|
|
||||||
|
Также из docker-compose файла видно, что я создал докер сеть `traefik-shared`, которую в дальнейшем буду использовать на всех контейнерах, которым требуется свой домен.
|
||||||
|
|
||||||
|
Далее следует папка с сертификатами. Для реальных доменов я использую cloudflare и скачиваю сертификаты и ключи с панели администратора. Также не забываю выставлять мод шифрования на strict.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Для генерации локальных сертификатов я использую тулу [mkcert](https://github.com/FiloSottile/mkcert).
|
||||||
|
|
||||||
|
Для любого локального домена я делаю что-то типа:
|
||||||
|
```bash
|
||||||
|
mkcert "*.local"
|
||||||
|
mv _wildcard.local-key.pem local.key
|
||||||
|
mv _wildcard.local.pem local.pem
|
||||||
|
```
|
||||||
|
И помещаю это в папочку `certs` рядом с `docker-compose.yml`.
|
||||||
|
|
||||||
|
После того как я создал все нужные сертефикаты для всех доменов, их надо указать в файле `config.toml` в папочке traefik.
|
||||||
|
|
||||||
|
Вот пример:
|
||||||
|
```toml{}[config.toml]
|
||||||
|
[tls.options]
|
||||||
|
[tls.options.default]
|
||||||
|
# Эта опция вообще для strict cloudflare tls encryption,
|
||||||
|
# Но я включаю её и на локальных доменах.
|
||||||
|
sniStrict = true
|
||||||
|
|
||||||
|
[[tls.certificates]]
|
||||||
|
# Я тут указываю /etc/certs, потому что в docker-compose
|
||||||
|
# у нас volume на эту папку.
|
||||||
|
certFile = "/etc/certs/local.pem"
|
||||||
|
keyFile = "/etc/certs/local.key"
|
||||||
|
```
|
||||||
|
|
||||||
|
<b-message type="is-info" has-icon>
|
||||||
|
Для добавления новых сертификатов в данный файл достаточно добавить:
|
||||||
|
</b-message>
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[tls.certificates]]
|
||||||
|
certFile = "/etc/certs/<certFile>"
|
||||||
|
keyFile = "/etc/certs/<keyFile>"
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого вы можете запускать traefik и наслаждаться доменами для ваших контейнеров.
|
||||||
|
|
||||||
|
# Запуск приложений
|
||||||
|
Теперь сконфигурируем приложение таким образом, чтобы к нему можно было обращаться через доменное имя.
|
||||||
|
|
||||||
|
Для примера возьмем мелкое приложение на nodejs со следующей структурой проекта:
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── Dockerfile
|
||||||
|
├── .dockerignore
|
||||||
|
├── index.js
|
||||||
|
├── package.json
|
||||||
|
└── yarn.lock
|
||||||
|
```
|
||||||
|
|
||||||
|
```js{}[index.js]
|
||||||
|
const express = require('express')
|
||||||
|
|
||||||
|
let req_count = 0;
|
||||||
|
const hostname = process.env.HOSTNAME;
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app = express();
|
||||||
|
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
console.log(`GET / ${hostname}`)
|
||||||
|
res.send({request_num: req_count++, host: hostname})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server is listening on port ${PORT}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```json{}[package.json]
|
||||||
|
{
|
||||||
|
"name": "express-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": "s3rius",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"runserver": "node index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.17.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```dockerfile{}[Dockerfile]
|
||||||
|
FROM node:16-alpine3.11
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json yarn.lock /app/
|
||||||
|
|
||||||
|
RUN yarn install
|
||||||
|
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
ENTRYPOINT ["yarn", "run"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml{}[docker-compose.yml]
|
||||||
|
---
|
||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
build: .
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.test_node.rule=Host(`test_node.local`)
|
||||||
|
- traefik.http.routers.test_node.entrypoints=http
|
||||||
|
- traefik.http.routers.test_node.service=node_test
|
||||||
|
- traefik.http.services.node_test.loadbalancer.server.port=3000
|
||||||
|
command: runserver
|
||||||
|
networks:
|
||||||
|
- traefik-shared
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-shared:
|
||||||
|
name: traefik-shared
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
<b-message type="is-info" has-icon>
|
||||||
|
Файл yarn.lock генерируется командой `yarn install`. Если у вас не установлен `yarn`, то ничего страшного. Просто это будет занимать чуть больше времени на сборку образа.
|
||||||
|
</b-message>
|
||||||
|
|
||||||
|
Как вы видите, приложение слушает на порт `3000` и отвечает свим hostname и количеством обработанных запросов. А в docker-compose.yml, в отличие от обычного проекта, появились labels.
|
||||||
|
|
||||||
|
| Лейбл | Что делает |
|
||||||
|
|---------------------------------------------------------------|---------------------------------------------------------------------------------------|
|
||||||
|
| ``traefik.enable=true`` | Включить поддержку роутинга через traefik |
|
||||||
|
| ``traefik.http.routers.<router>.rule=Host(`test_node.local`)`` | Поставить правило роутинга, если Host запроса равен test_node.local |
|
||||||
|
| ``traefik.http.routers.<router>.entrypoints=http`` | Слушать на entrypoint http (80 порт, это было объявлено в параметрах запуска traefik) |
|
||||||
|
| ``traefik.http.routers.<router>.service=<service>`` | Сервис, связанный с роутером test_node |
|
||||||
|
| ``traefik.http.services.<service>.loadbalancer.server.port=3000`` | Порт, куда направлять запросы в сервис node_test |
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
<b-message type="is-danger" has-icon>
|
||||||
|
|
||||||
|
Роутеры, сервисы и хосты явно создавать нигде не нужно.
|
||||||
|
Они сами создаются, когда вы их указываете.
|
||||||
|
|
||||||
|
Название сервиса и роутера могут совпадать.
|
||||||
|
|
||||||
|
Обратите внимание на косые кавычки при указании хоста! Это обязательно.
|
||||||
|
|
||||||
|
В объявлении labels могут быть использованы переменные среды. Например:
|
||||||
|
``traefik.http.routers.test_node.rule=Host(`${APP_HOST}`)``
|
||||||
|
|
||||||
|
</b-message>
|
||||||
|
|
||||||
|
Также можно видеть, что я подключил контейнер к сети, которую мы указывали в контейнере traefik. Здесь она помечена как external.
|
||||||
|
|
||||||
|
Теперь мы можем спокойно запустить наш сервис. Для этого воспользуемся следующей командой:
|
||||||
|
```bash
|
||||||
|
docker-compose up --build --scale server=3
|
||||||
|
```
|
||||||
|
|
||||||
|
В данной команде мы указали, что хотим поднять 3 инстанса нашего сервиса. Остальным пусть занимается traefik.
|
||||||
|
|
||||||
|
Теперь попробуем некоторое количество раз выполнить запрос на наш сервис.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ curl -H "Host: test_node.local" "http://localhost"
|
||||||
|
{"request_num":0,"host":"7417ac8fda92"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат должен быть примерно таким:
|
||||||
|
|
||||||
|
[](/images/traefik_imgs/curls.gif)
|
||||||
|
|
||||||
|
Как вы видите, traefik балансирует между контейнерами за нас. И я считаю, что это - победа.
|
||||||
|
|
||||||
|
## Подключение TLS и сертификатов
|
||||||
|
Тут всё не намного сложнее. Давайте немного поменяем лейблы нашего контейнера.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.test_node.rule=Host(`test_node.local`)
|
||||||
|
- traefik.http.routers.test_node.entrypoints=https
|
||||||
|
- traefik.http.routers.test_node.tls=true
|
||||||
|
- traefik.http.routers.test_node.service=node_test
|
||||||
|
- traefik.http.services.node_test.loadbalancer.server.port=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Как вы видите, я поменял entrypoints на `https`. Как вы можете помнить, это entrypoint, который слушает на 443 порт. И также я включил поддержку tls лейблом `traefik.http.routers.<router>.tls=true`
|
||||||
|
|
||||||
|
На данном этапе вам потребуется добавить свой хост в `/etc/hosts`, если вы используете нормальную систему. Но если вы всё же на windows, то вам потребуется добавить правило в `C:\Windows\System32\drivers\etc\hosts`.
|
||||||
|
|
||||||
|
И добавляем в конец файла запись:
|
||||||
|
```
|
||||||
|
127.0.0.1 test_node.local
|
||||||
|
```
|
||||||
|
|
||||||
|
И также вам потребуется cертификат на этот домен.
|
||||||
|
Для этого:
|
||||||
|
|
||||||
|
1. Создадим сертификат через `mkcert`, как упоминалось ранее;
|
||||||
|
2. Поместим ключ и сертификат в папку certs;
|
||||||
|
3. Добавим ключ и сертификат для вашего домена в config.toml (Формат указан выше).
|
||||||
|
|
||||||
|
Теперь вы можете обращаться к вашему приложению напрямую через локальное доменное имя.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ curl --insecure https://test_node.local
|
||||||
|
{"request_num":0,"host":"7417ac8fda92"}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Добавление локальных сервисов не из докера
|
||||||
|
|
||||||
|
Все те флаги, которые мы указываем в labels, вы также можете указать в файле конфигурации рядом с docker-compose.yml, указав конкретный ip адрес.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
```toml
|
||||||
|
[http.routers]
|
||||||
|
# Define a connection between requests and services
|
||||||
|
[http.routers.<router_name>]
|
||||||
|
rule = "Host(`my_app.local`)"
|
||||||
|
entrypoints = "https"
|
||||||
|
service = "<service_name>"
|
||||||
|
[http.routers.<router_name>.tls]
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
# Define how to reach an existing service on our infrastructure
|
||||||
|
[http.services.<service_name>.loadBalancer]
|
||||||
|
[[http.services.<service_name>.loadBalancer.servers]]
|
||||||
|
url = "http://192.168.1.89:8100"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Создание локального DNS
|
||||||
|
В данном пункте я бы хотел рассказать, как настроить свой DNS-сервер, чтобы ваши домены были доступны всем устройствам в локальной сети. Для этого я буду использовать dnsmasq. Пользователям винды он недоступен, поэтому советую развернуть маленький домашний сервер на линуксе.
|
||||||
|
|
||||||
|
Для этого установите `dnsmasq` и найдите и раскомментируйте, либо добавьте следующие строчки в файл `/etc/dnsmasq.conf`:
|
||||||
|
|
||||||
|
```conf{}[/etc/dnsmasq.conf]
|
||||||
|
# Never forward plain names (without a dot or domain part)
|
||||||
|
domain-needed
|
||||||
|
# Never forward addresses in the non-routed address spaces.
|
||||||
|
bogus-priv
|
||||||
|
address=/.local/192.168.1.89
|
||||||
|
address=/.<other_domain>/<your_local_ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
У меня traefik развернут на хосте `192.168.1.89`. У вас ip может отличаться.
|
||||||
|
Чтобы это узнать, посмотрите свой ip через роутер или выполните `ip addr`.
|
||||||
|
|
||||||
|
Вообще, `dnsmasq` парсит файл `/etc/hosts` и вы можете туда добавлять записи, типа:
|
||||||
|
```
|
||||||
|
192.168.1.1 mydomain.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Но так как я указал `address`, то это необязательно. `dnsmasq` и без явного указания поддоменов должен будет работать отлично.
|
||||||
|
|
||||||
|
Запустите `dnsmasq` в режиме сревиса:
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable dnsmasq.service
|
||||||
|
sudo systemctl start dnsmasq.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь пойдите в настройки вашего роутера и найдите DNS-сервера. Добавьте туда ваш ip. Также не забудьте сделать локальный ip вашего устройства статичным.
|
||||||
|
|
||||||
|
Для примера, в роутерах keenetic это можно сделать, зарегистрировав устройство в меню 'Список устройств' и нажав на галочку 'Постоянный IP-адрес'.
|
||||||
|
|
||||||
|
И добавьте свой DNS-сервер в список DNS серверов вашего роутера.
|
||||||
|

|
||||||
|
|
||||||
|
Готово. Вы можете попробовать зайти на свой домен с другого устройства в локальной сети, и это должно работать.
|
||||||
|
|
||||||
|
# Мониторинг работы traefik
|
||||||
|
|
||||||
|
Вообще, traefik имеет WEB интерфейс для отслеживания его работы.
|
||||||
|
Для того чтобы подключить его, давайте поменяем `docker-compose.yml`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
traefik-proxy:
|
||||||
|
# The official v2.0 Traefik docker image
|
||||||
|
image: traefik:v2.4.8
|
||||||
|
container_name: traefik_main
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.traefik_router.rule=Host(`traefik.local`)
|
||||||
|
- traefik.http.routers.traefik_router.service=api@internal
|
||||||
|
- traefik.http.routers.traefik_router.entrypoints=https
|
||||||
|
- traefik.http.routers.traefik_router.tls=true
|
||||||
|
|
||||||
|
command:
|
||||||
|
- --api.dashboard=true
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Я добавил ключ `--api.dashboard=true` и лейблы для роута до `traefik`.
|
||||||
|
Замечу, service определён заранее - это `api@internal`. Укажите его в `traefik.http.routers.<router_name>.service`.
|
||||||
|
|
||||||
|
Теперь вы можете зайти на `traefik.local` через свой браузер и увидеть конфигурацию traefik.
|
||||||
|
|
||||||
|
[](/images/traefik_imgs/traefik_web.png)
|
||||||
|
|
||||||
|
|
||||||
|
Разве это не круто?
|
12
jsconfig.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./*"],
|
||||||
|
"@/*": ["./*"],
|
||||||
|
"~~/*": ["./*"],
|
||||||
|
"@@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", ".nuxt", "dist"]
|
||||||
|
}
|
117
layouts/default.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page dark-mode">
|
||||||
|
<b-navbar type="is-primary w-100">
|
||||||
|
<template #brand>
|
||||||
|
<NuxtLink to="/" class="navbar-item">
|
||||||
|
<img src="/logo.png" alt="Logo" /> S3rius' dev blog
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
<template #end>
|
||||||
|
<div class="navbar-menu">
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
class="navbar-burger"
|
||||||
|
aria-label="menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="[category, pages] in categories"
|
||||||
|
:key="category"
|
||||||
|
class="is-hidden-desktop"
|
||||||
|
>
|
||||||
|
<p class="navbar-item is-inactive divider">
|
||||||
|
<span>{{ category }}</span>
|
||||||
|
</p>
|
||||||
|
<NuxtLink
|
||||||
|
class="navbar-item"
|
||||||
|
v-for="(item, key) of pages"
|
||||||
|
:to="item.slug"
|
||||||
|
:key="key"
|
||||||
|
exact-active-class="is-active"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</b-navbar>
|
||||||
|
|
||||||
|
<section class="main-content columns">
|
||||||
|
<aside class="column is-2 section is-hidden-touch">
|
||||||
|
<b-menu>
|
||||||
|
<div v-for="[category, pages] in categories" :key="category">
|
||||||
|
<p class="menu-label">{{ category }}</p>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li v-for="(item, key) of pages" :key="key">
|
||||||
|
<NuxtLink :to="item.slug" exact-active-class="is-active">
|
||||||
|
{{ item.title }}
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</b-menu>
|
||||||
|
</aside>
|
||||||
|
<div class="container column">
|
||||||
|
<Nuxt />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Footer></Footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent, useContext, ref } from '@nuxtjs/composition-api'
|
||||||
|
import Footer from '../components/Footer.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const { $content } = useContext()
|
||||||
|
const categories = ref([])
|
||||||
|
$content({ deep: true })
|
||||||
|
.only(['category', 'slug', 'title', 'position'])
|
||||||
|
.sortBy('createdAt', 'asc')
|
||||||
|
.fetch()
|
||||||
|
.then((pages) => {
|
||||||
|
let cats = new Map()
|
||||||
|
for (const page of pages) {
|
||||||
|
if (cats.get(page.category) === undefined) {
|
||||||
|
cats.set(page.category, [])
|
||||||
|
}
|
||||||
|
cats.get(page.category).push(page)
|
||||||
|
}
|
||||||
|
// cats.forEach((pages) => {
|
||||||
|
// pages.sort(
|
||||||
|
// (page1, page2) => (page1.position || 0) > (page2.position || 0)
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
categories.value = Array.from(cats)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
categories,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { Footer },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.divider {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
line-height: 0.1em;
|
||||||
|
margin: 10px 0 20px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
</style>
|
87
nuxt.config.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
export default {
|
||||||
|
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
|
||||||
|
ssr: false,
|
||||||
|
|
||||||
|
// Target: https://go.nuxtjs.dev/config-target
|
||||||
|
target: 'static',
|
||||||
|
|
||||||
|
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||||
|
head: {
|
||||||
|
title: "S3rius' dev blog",
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' },
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{ hid: 'description', name: 'description', content: '' },
|
||||||
|
{ name: 'format-detection', content: 'telephone=no' },
|
||||||
|
],
|
||||||
|
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Global CSS: https://go.nuxtjs.dev/config-css
|
||||||
|
css: [
|
||||||
|
'~/static/css/syntax-highlighter.scss',
|
||||||
|
'~/static/css/global-styles.scss',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
||||||
|
plugins: [],
|
||||||
|
|
||||||
|
// Auto import components: https://go.nuxtjs.dev/config-components
|
||||||
|
components: true,
|
||||||
|
|
||||||
|
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
|
||||||
|
buildModules: [
|
||||||
|
// https://go.nuxtjs.dev/typescript
|
||||||
|
'@nuxt/typescript-build',
|
||||||
|
// https://go.nuxtjs.dev/stylelint
|
||||||
|
'@nuxtjs/stylelint-module',
|
||||||
|
'@nuxtjs/composition-api/module',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Modules: https://go.nuxtjs.dev/config-modules
|
||||||
|
modules: [
|
||||||
|
// https://go.nuxtjs.dev/buefy
|
||||||
|
'nuxt-buefy',
|
||||||
|
// https://go.nuxtjs.dev/axios
|
||||||
|
'@nuxtjs/axios',
|
||||||
|
// https://go.nuxtjs.dev/pwa
|
||||||
|
'@nuxtjs/pwa',
|
||||||
|
// https://content.nuxtjs.org/
|
||||||
|
'@nuxt/content',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Axios module configuration: https://go.nuxtjs.dev/config-axios
|
||||||
|
axios: {},
|
||||||
|
|
||||||
|
// PWA module configuration: https://go.nuxtjs.dev/pwa
|
||||||
|
pwa: {
|
||||||
|
manifest: {
|
||||||
|
lang: 'en',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
fileName: 'logo.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Build Configuration: https://go.nuxtjs.dev/config-build
|
||||||
|
build: {},
|
||||||
|
|
||||||
|
content: {
|
||||||
|
markdown: {
|
||||||
|
remarkPlugins: [
|
||||||
|
'remark-squeeze-paragraphs',
|
||||||
|
'remark-slug',
|
||||||
|
'remark-external-links',
|
||||||
|
'remark-footnotes',
|
||||||
|
],
|
||||||
|
rehypePlugins: [
|
||||||
|
'rehype-sort-attribute-values',
|
||||||
|
'rehype-sort-attributes',
|
||||||
|
'rehype-raw',
|
||||||
|
],
|
||||||
|
prism: {
|
||||||
|
theme: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
47
package.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "blog",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxt",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"start": "nuxt start",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"lint:js": "eslint --ext \".js,.ts,.vue\" --ignore-path .gitignore .",
|
||||||
|
"lint:style": "stylelint \"**/*.{vue,css}\" --ignore-path .gitignore",
|
||||||
|
"lint": "yarn lint:js && yarn lint:style"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/rehype-prism": "^0.8.0",
|
||||||
|
"@nuxt/content": "^1.15.1",
|
||||||
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
"@nuxtjs/composition-api": "^0.31.0",
|
||||||
|
"@nuxtjs/pwa": "^3.3.5",
|
||||||
|
"clipboard": "^2.0.8",
|
||||||
|
"core-js": "^3.15.1",
|
||||||
|
"nuxt": "^2.15.7",
|
||||||
|
"nuxt-buefy": "^0.4.8",
|
||||||
|
"prism-themes": "^1.9.0",
|
||||||
|
"prismjs": "^1.25.0",
|
||||||
|
"remark-attr": "^0.11.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/eslint-parser": "^7.14.7",
|
||||||
|
"@nuxt/types": "^2.15.8",
|
||||||
|
"@nuxt/typescript-build": "^2.1.0",
|
||||||
|
"@nuxtjs/eslint-config-typescript": "^8.0.0",
|
||||||
|
"@nuxtjs/eslint-module": "^3.0.2",
|
||||||
|
"@nuxtjs/stylelint-module": "^4.0.0",
|
||||||
|
"eslint": "^7.29.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-nuxt": "^2.0.0",
|
||||||
|
"eslint-plugin-vue": "^7.12.1",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"sass": "^1.44.0",
|
||||||
|
"sass-loader": "10.1.1",
|
||||||
|
"stylelint": "^13.13.1",
|
||||||
|
"stylelint-config-prettier": "^8.0.2",
|
||||||
|
"stylelint-config-standard": "^22.0.0",
|
||||||
|
"stylelint-config-standard-scss": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
116
pages/_slug.vue
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<nuxt-content :document="page" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
useContext,
|
||||||
|
ref,
|
||||||
|
onMounted,
|
||||||
|
useStore,
|
||||||
|
} from '@nuxtjs/composition-api'
|
||||||
|
import Prism from '~/plugins/prism'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const page = ref({})
|
||||||
|
const { $content, params, error } = useContext()
|
||||||
|
const store = useStore()
|
||||||
|
// Finding current path's slug.
|
||||||
|
const slug = params.value.slug || 'index'
|
||||||
|
|
||||||
|
$content(store.state.lang, slug)
|
||||||
|
.fetch()
|
||||||
|
.then((doc) => {
|
||||||
|
page.value = doc
|
||||||
|
})
|
||||||
|
.catch((_) => {
|
||||||
|
error({ statusCode: 404, message: 'Page not found' })
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log(Prism.languages)
|
||||||
|
Prism.highlightAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@/static/css/global-styles.scss';
|
||||||
|
|
||||||
|
.nuxt-content {
|
||||||
|
.icon.icon-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin heading {
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: rgba(0, 0, 0, 0.54);
|
||||||
|
margin: 1em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@include heading;
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
@include heading;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
@include heading;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nuxt-content-highlight {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
.filename {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
border-top-left-radius: 0.3em;
|
||||||
|
border-top-right-radius: 0.3em;
|
||||||
|
padding-left: 1em;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 auto;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: $primary;
|
||||||
|
font: lighter;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename + pre {
|
||||||
|
margin-top: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
background-color: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: disc;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.5em !important;
|
||||||
|
margin-left: 1.25em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
7
pages/index.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<section class="section">
|
||||||
|
<div>a</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script></script>
|
24
plugins/prism.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import Prism from 'prismjs'
|
||||||
|
|
||||||
|
import 'prism-themes/themes/prism-dracula.min.css'
|
||||||
|
|
||||||
|
// Include the toolbar plugin: (optional)
|
||||||
|
import 'prismjs/plugins/toolbar/prism-toolbar'
|
||||||
|
|
||||||
|
// Include the line numbers plugin: (optional)
|
||||||
|
// import 'prismjs/plugins/line-numbers/prism-line-numbers'
|
||||||
|
// import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
|
||||||
|
|
||||||
|
// Include the line highlight plugin: (optional)
|
||||||
|
// import 'prismjs/plugins/line-highlight/prism-line-highlight'
|
||||||
|
// import 'prismjs/plugins/line-highlight/prism-line-highlight.css'
|
||||||
|
|
||||||
|
// Include some other plugins: (optional)
|
||||||
|
import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard'
|
||||||
|
// import 'prismjs/plugins/highlight-keywords/prism-highlight-keywords'
|
||||||
|
// import 'prismjs/plugins/show-language/prism-show-language'
|
||||||
|
import 'prismjs/components/prism-makefile'
|
||||||
|
// Set vue SFC to markdown
|
||||||
|
Prism.languages.vue = Prism.languages.markup
|
||||||
|
|
||||||
|
export default Prism
|
119
static/css/global-styles.scss
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// Import Bulma's core
|
||||||
|
@import "~bulma/sass/utilities/_all";
|
||||||
|
|
||||||
|
// Set your colors
|
||||||
|
$primary: #687cec;
|
||||||
|
$primary-light: findLightColor($primary);
|
||||||
|
$primary-dark: findDarkColor($primary);
|
||||||
|
$primary-invert: findColorInvert($primary);
|
||||||
|
$background: #fafdff;
|
||||||
|
$background-invert: findColorInvert($background);
|
||||||
|
|
||||||
|
// Lists and maps
|
||||||
|
$custom-colors: null !default;
|
||||||
|
$custom-shades: null !default;
|
||||||
|
|
||||||
|
// Setup $colors to use as bulma classes (e.g. 'is-twitter')
|
||||||
|
$colors: mergeColorMaps(
|
||||||
|
(
|
||||||
|
"white": (
|
||||||
|
$white,
|
||||||
|
$black,
|
||||||
|
),
|
||||||
|
"black": (
|
||||||
|
$black,
|
||||||
|
$white,
|
||||||
|
),
|
||||||
|
"light": (
|
||||||
|
$light,
|
||||||
|
$light-invert,
|
||||||
|
),
|
||||||
|
"dark": (
|
||||||
|
$dark,
|
||||||
|
$dark-invert,
|
||||||
|
),
|
||||||
|
"primary": (
|
||||||
|
$primary,
|
||||||
|
$primary-invert,
|
||||||
|
$primary-light,
|
||||||
|
$primary-dark,
|
||||||
|
),
|
||||||
|
"link": (
|
||||||
|
$link,
|
||||||
|
$link-invert,
|
||||||
|
$link-light,
|
||||||
|
$link-dark,
|
||||||
|
),
|
||||||
|
"info": (
|
||||||
|
$info,
|
||||||
|
$info-invert,
|
||||||
|
$info-light,
|
||||||
|
$info-dark,
|
||||||
|
),
|
||||||
|
"success": (
|
||||||
|
$success,
|
||||||
|
$success-invert,
|
||||||
|
$success-light,
|
||||||
|
$success-dark,
|
||||||
|
),
|
||||||
|
"warning": (
|
||||||
|
$warning,
|
||||||
|
$warning-invert,
|
||||||
|
$warning-light,
|
||||||
|
$warning-dark,
|
||||||
|
),
|
||||||
|
"danger": (
|
||||||
|
$danger,
|
||||||
|
$danger-invert,
|
||||||
|
$danger-light,
|
||||||
|
$danger-dark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
$custom-colors
|
||||||
|
);
|
||||||
|
|
||||||
|
// Links
|
||||||
|
$link: $primary;
|
||||||
|
$link-invert: $primary-invert;
|
||||||
|
$link-focus-border: $primary;
|
||||||
|
|
||||||
|
// Import Bulma and Buefy styles
|
||||||
|
@import "~bulma";
|
||||||
|
@import "~buefy/src/scss/buefy";
|
||||||
|
|
||||||
|
.bnumber {
|
||||||
|
align-items: center;
|
||||||
|
background-color: whitesmoke;
|
||||||
|
border-radius: 9999px;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
height: 2em;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
min-width: 2.5em;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
display: inherit;
|
||||||
|
background-color: none;
|
||||||
|
border: none;
|
||||||
|
min-width: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
background: none;
|
||||||
|
width: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: $background;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
max-width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
9
static/css/syntax-highlighter.scss
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.nuxt-content {
|
||||||
|
@apply break-words;
|
||||||
|
|
||||||
|
& .nuxt-content-highlight {
|
||||||
|
& > .filename {
|
||||||
|
@apply block bg-gray-700 text-gray-100 font-mono text-sm tracking-tight py-2 px-4 -mb-3 rounded-t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
static/favicon.ico
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
static/images/traefik_imgs/couldflare_certs.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
static/images/traefik_imgs/curls.gif
Normal file
After Width: | Height: | Size: 4.5 MiB |
BIN
static/images/traefik_imgs/keenetic_dns.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
static/images/traefik_imgs/logo.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
static/images/traefik_imgs/nginx_comparasion.png
Normal file
After Width: | Height: | Size: 463 KiB |
BIN
static/images/traefik_imgs/traefik-architecture.png
Normal file
After Width: | Height: | Size: 361 KiB |
BIN
static/images/traefik_imgs/traefik_web.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
static/logo.png
Normal file
After Width: | Height: | Size: 38 KiB |
10
store/README.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# STORE
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your Vuex Store files.
|
||||||
|
Vuex Store option is implemented in the Nuxt.js framework.
|
||||||
|
|
||||||
|
Creating a file in this directory automatically activates the option in the framework.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
|
15
store/index.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
interface State{
|
||||||
|
lang: String
|
||||||
|
}
|
||||||
|
|
||||||
|
export const state = () => {
|
||||||
|
return {
|
||||||
|
lang: "ru"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
set_lang (state: State, value: String) {
|
||||||
|
state.lang = value
|
||||||
|
},
|
||||||
|
}
|
9
stylelint.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
|
||||||
|
// add your custom config here
|
||||||
|
// https://stylelint.io/user-guide/configuration
|
||||||
|
rules: {
|
||||||
|
'at-rule-no-unknown': null,
|
||||||
|
'declaration-empty-line-before': null,
|
||||||
|
},
|
||||||
|
}
|
39
tsconfig.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2018",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"ESNext.AsyncIterable",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": [
|
||||||
|
"./*"
|
||||||
|
],
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"@nuxt/types",
|
||||||
|
"@nuxtjs/axios",
|
||||||
|
"@types/node",
|
||||||
|
"@nuxt/content",
|
||||||
|
"@nuxtjs/color-mode",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
".nuxt",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
4
types/vue-shim.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.vue" {
|
||||||
|
import Vue from 'vue'
|
||||||
|
export default Vue
|
||||||
|
}
|