Alex Takes Notes

Добрый день! Я делаю заметки о технологиях, с которыми имею дело. Возможно, они пригодятся и Вам!

Обновления

28.01.2025 Для разминки я решил быстро написать игру “угадай число” из 2-ой главы книги Rust Book, и вдруг оказалось, что библиотеку rand() переделали в новой её версии. Я внёс новые имена вызовов в статью.

14.01.2025 Я в несколько заходов пару дней возился, чтоб настроить возможность коммитить с 1 системы сразу в github и gitlab с помощью двух разных ssh-сертов. Оказалось, это не самое тривиальное дело, потому как у git нет ключа-указателя на нужный сертификат и имя пользователя. По итогам у меня получилась важная добавка к статье про git

09.01.2025 Новый год я начинаю постепенно, с замены инструмента отслеживания изменений в коде и запуска перекомпиляции. Ранее с задачей справлялся cargo watch, но автор проекта написал, что более не будет развивать его. Поэтому я опробовал и переехал на bacon, и добавил заметку о его использовании.

30.12.2024 Для анализа данных о системе из прошлой статьи нужно отфильтровать всё лишнее с помощью регулярных выражений.

26.12.2024 В качестве пробного приложения я пробую container runtime. Такое приложение сразу порождает кучу практичных вопросов. Например, вопрос подходящего ядра Linux. Чтобы узнать его версию, нужно быстро опросить систему, для этого я добавил кусочек библиотеки sysinfo. Есть и другой способ узнать версию ядра и тип ОС: uname -a && cat /etc/issue. Для этих целей можно запустить команду в STDIN поток ОС.

25.12.2024 Сегодня день посвящён работе с ошибками в Rust. У меня ранее уже была статья об этом, пора в её неё добавить деталей, а также разобрать специализированную библиотеку Anyhow для упрощения работы с ошибками.

24.12.2024 Механизм логгирования и трассировки позволяет отслеживать работу приложения максимально плотно. Есть несколько инструментов для сбора логов, но наиболее эффективный из них tokio tracing, так как корректно обрабатывает трассировку потоков многопоточных и многоканальных приложений. Разбор работы с ним теперь в статье про tracing. Общий посыл в том, что можно писать лог с функций любым логгером, и это вполне подходит - до тех пор, пока не начать писать многопоточное, многоканальное приложение. Как только появляется вязанка потоков, каждый из которых создаёт события, типовой логгер начинает сбоить, т.к лог события с одного потока перекрывает лог с другого потока. И вот, чтобы корректно с этим работать, логгер уже не пишет просто события, а создаёт “промежуток потоковой трассы событий”, т.е набор событий в контексте работы каждого потока, при этом запись этих событий должна быть как неблокирующее поток вывода действие, и отправляться оно должно необязательно в файл, а есть поддержка сразу отправки в SIEM, анализатор типа Datadog и т.д. в режиме дозаписи (rolling). Т.е логгер одновременно делает трассировку, отдачу телеметрии - с заворачиванием в стандарт OpenTelemetry по желанию, и многопоточную передачу этих данных в один или несколько приёмников: одновременно в файл с ротацией, SIEM, облако и т.д. Круто же!

23.12.2024 Долго я не понимал прелести работы библиотеки Rust clap по работе с аргументами командной строки. А потом как понял! Всему виной громоздкие документация и примеры. Начинать надо с малого, что я и сделал в своей статье про clap.

21.12.2024 Наконец-то я залезаю в многопоточность приложений. Тема непростая, интересная. Я добавил статью про работу с процессорными потоками в Rust, буду дополнять её далее.

19.12.2024 Немного обновил я данные там и сям:

02.12.2024 Наконец-то, массовый апдейт сайта! После обновления движка Hugo у меня были проблемы с занесением сюда новых заметок, но теперь это в прошлом.

06.08.2024 Я делаю новый подход к снаряду с Kubernetes. Попутно я заметил, что не сделал заметок по пространствам имён - исправляю этот недочёт.

24.04.2024 Недавно я разбирался с хабом для умного дома на базе OpenWRT и озадачился кросс-компиляцией утилит под него. А следом у меня возник вопрос о кросс-компиляции проектов Rust. Оказалось, для этого есть модуль Cross, который через контейнер Docker/Podman +Wine способен скомпилировать код на моём макбуке под Windows. Заметку об этом я добавил по ссылке.

30.03.2024 Множество мелких добавок-правок не попадают в новости, но что-то я периодически дописываю на портале. Из недавнего-интересного - чтение файлов в Rust. Чтение в строку, через буфер, в вектор из байт для бинарных файлов. В перспективе я думаю разобрать парсинг спец-форматов: XML, JSON, YAML, TOML.
03.02.2024 Ещё один элемент стиля Rust - повсеместное использование замыканий в самых разных хитрых комбинациях. Это сокращает количество кода, при этом сохраняет скорость выполнения. Прицепом я добавил в тему словарей инициализацию со значениями. В Python аналог defaultdict был полезной штукой, местный аналог в Rust тоже нужно было записать.

Subsections of Alex Takes Notes

Development

Find out how to create and organize your content quickly and intuitively.

  • Git

    Using Git repository

  • Rust

    Rust language notes

Subsections of Development

Git

Configuring Git

git config --global user.name "spider_net" # username init, seen in commits
git config --global user.email "test@gmail.com" # Init of user e-mail

SSH одновременно с GitHub и Gitlab

Для работы с одной системы с тем и другим репозиторием по SSH нужно создать ключи SSH под оба, а также прописать в файле конфигурации профили подключения:

Host github.com
  IdentityFile ~/.ssh/id_rsa
  IdentitiesOnly yes

Host gitlab.com
  IdentityFile ~/.ssh/id_ecdsa

Для проверки работоспособности конфигурации можно подключиться с помощью ssh напрямую:

ssh -vvvv git@github.com

Для избежания ошибок следует:

  • В имени ключа не использовать знак тире -
  • Сначала клонировать удалённый репозиторий с помощью git clone, далее локально его обновить и синхронизировать с помощью git push origin main

Cloning Repository

git clone https://github.com/drupal/drupal.git drupal7

Initialising Local & Remote Repo

git init # local repo init

git status # check status

git branch -m master main # change master branch to main, for GitLab

git add * # add all files to repo, .gitignore file lets you hide junk files, password files from syncing

git commit -m "Hello world"

git config --global user.email "Ivan@example.com"
git config --global user.name "Ivan the Terrible"

git log

git remote add origin git@gitlab.com:<name of account>/<project name>.git

git push origin main

Cloning Remote Repo on Amazon

Certificate file needed in .ssh folder. Also a config file .ssh/config is needed:

Host git-codecommit.us-east-1.amazonaws.com
   User Your-SSH-Key-ID, such as APKAEIBAERJR2EXAMPLE
   IdentityFile Your-Private-Key-File, such as ~/.ssh/codecommit_rsa or     ~/.ssh/id_rsa

Git commands to clone repo:

git clone ssh://<SSHKEY-ID>@git-codecommit.<REGION>.amazonaws.com/v1/repos/<REPO-NAME>

# Example:

git clone ssh://ABCDOIJKHJGYTHJG7@git-codecommit.eu-central-1.amazonaws.com/v1/repos/myrepo

Branching

Create new branch: git branch new-branch Pointer does not automatically switch to new branch, it is left on main branch, so you need to switch manually: git checkout new-branch

Merging Branches

  • Switch to branch for merging
  • Merge
  • Check pointers point to one place OR merge the other branch with the current one
git commit
git checkout main
git commit
git merge new-branch

Change of branches to make it look as if the functionality was developed step-by-step, not in parallel.

git rebase master
git checkout master
git rebase bugFix

Pointer

HEAD is the pointer to current project state. By default HEAD points to current branch. You can go back by commits or by direct commit hash: git checkout C1

You can use syntax “^K” for going up 1 level (where K is the route number to choose if there are more than 1 parent. By default, K=1) and “~N” for N steps up:

git checkout master^
# OR
git checkout bugFix^^
# OR
git checkout bugFix~2
# same route on schema from picture
# or build a sequence
git checkout bugFix^~1

Chaning main branch to several commits backwards:

git branch -f master HEAD~3
# -f means force - direct remap of branch for commit

Undo Actions

For 1-person local repo do reset. For multi-person remote repo, do revert.

git reset HEAD~1
# OR
git revert HEAD

reset reset revert revert In c2’ state are the changes, which cancel state C2

Moving commits

Chosen commits are moved upon current commit using cherry-pick command. Used when it is known what specific commits to move: git cherry-pick C2 C4 git rebase -i HEAD~4 --aboveAll

Small commit modifications

You can modify a commit, dividing it into two with same parent: git commit --amend

Tagging commits

Marking milestones is done with tags. They block the commit from changes, moving etc.:

git tag v1 <hash> # if hash is not provided, then current commit is tagged

Tags serve as anchors in tree of commits. To define your position against the nearest anchor, use command:

git describe <ref> # if <ref> is not provided, current commit will be described

Naming

Remote branches have a naming convention: <remote name>/<branch name>

Main remote i called origin. So master branch is origin/main. When doing commit in local branch, the system is put into detached HEAD mode:

git checkout origin/master
git commit

Fetch data from remote repository

When data is downloaded from remote repo, the origin/main branch is updated to reflect the new commits: git fetch

Only commits non-present locally are downloaded. No local state of files is changed after download. The local main status is unchanged. To change, a merge must be done: git fetch + git merge = git pull git pull

Publishing local repository to remote

Publishing syncs commits at remote repo and local repo (main and origin/main point to the same commit): git push

If the remote repo has changed by someone by the time you need to push there, it means that your feature is based on an old commit. So Git will not let you push. Before push, a fetch must be made to sync the changes, than a rebase or merge to update the main branch, and then a push:

git fetch + git rebase origin/main + git push OR git fetch + git merge origin/main + git push = git pull –rebase git pull --rebase

Changing main and origin/main

You can change that another branch will be main for the remote repo: `git checkout -b side origin/main

Push arguments

You can specify what branch to push: git push origin main

origin = remote repo, main = branch to take commit from.

It does not matter where the HEAD is at this moment. You can specify where to push commits using git push origin <source>:<destination> notation: git push origin side^2:master

If push is made to a non-existent branch on remote repo, Git will create this branch: git push origin main:newBranch

Fetch arguments

Same as push, but the other wat around. Go to foo branch on remote repo, download commits from there to local branch origin/foo: git fetch origin foo

Rust

Articles in Section

Аргументы за/против Rust

Плюсы Rust:

Минусы:

  • Его сложно учить, много синтаксиса
  • Перестроение мозгов: дуализм результата (enums Result), метки lifecycle, traits
  • Язык молодой, экосистема беднее чем у Python, тем более чем у C++, сообщество меньше

Сравнения

Golang, хороший-плохой-злой: https://habr.com/ru/companies/vk/articles/353790/

Видеокурсы

Задачи по программированию

Rust Tools

External links:

rustup component add rustfmt 
rustup component add rust-src
  • Install Bacon (formerly Cargo-watch) to rerun code upon save:
cargo install --locked bacon
#cargo install cargo-watch
git clone https://github.com/AstroNvim/AstroNvim ~/.config/nvim
nvim +PackerSync

# After install use commands:
:LspInstall rust -> rust-analyzer
:TSInstall rust
  • Neovide GUI upgrade on Astro Vim
git clone https://github.com/neovide/neovide
cd neovide
cargo build --release
  • EVCXR or iRUST REPL
cargo install evcxr_repl
cargo install irust
cargo install --locked bacon
bacon test # run from project folder 

VSCode Extensions for Rust

  • CodeLLDB
  • Dependi (бывш crates) - сообщает, если пакеты устарели

VSCode Settings

(For CodeLLDB) Allow breakpoints everywhere: "debug.allowBreakpointsEverywhere": true cargo check: поменять check на clippy: "rust-analyzer.check.command": "clippy"

Rust Prelude

Rust has a Prelude - a set of libraries included in every project. See current libs included in Prelude

Allow Unused

Turn off noise about unused objects while learning with a crate attribute:

#![allow(unused)] // silence unused warnings while learning
fn main() {}

User input

std::io::stdin library is used to get user input from standard input stream. Not included in Prelude:

use std:io

let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("Failed to load");

.expect handles Err variant of read_line function, crashing the program with the defined error message.

Subsections of Rust

Cargo

Use Cargo for managing projects.

cargo new test_project // create project with binary file src/main.rs
// OR
cargo new test_project --lib // create project with library file src/lib.rs 

cd test_project

Source code is in src folder.

cargo build # build project for debug, do not run
cargo run # build & run project
cargo check # fast compiling
cargo build --release # slow build with optimizations for speed of release version

Documentation of methods & traits used in code can be compiled an opened in browser with Cargo command:

cargo doc --open

Cargo Examples

Create “examples” folder beside the “src” folder. Create a file named, for example, “variables.rs” and place some test code in the folder. Then in the project root folder run:

cargo run --example variables

(new) Bacon - интерактивный перезапуск компиляции

This will compile+build the code in examples folder, file “variables.rs”. Very convenient to try test different stuff. For live development do:

bacon run -- -q # отслеживать текущий проект
bacon run -- -q --example <файл> # отслеживать файл в папке examples

-q - убрать вывод деталей компиляции

(old) Cargo Watch - интерактивный перезапуск компиляции

This will compile+build the code in examples folder, file “variables.rs”. Very convenient to try test different stuff. For live development do:

cargo watch -q -c -x 'run -q --example variables'

-x - rerun upon code change -c - clear terminal each time -q - quiet mode ❗Проект cargo watch заморожен, более не развивается.

Panic Response

В ответ на панику, по умолчанию программа разматывает стек (unwinding) - проходит по стеку и вычищает данные всех функций. Для уменьшения размера можно просто отключать программу без очистки - abort. Для этого в файле Cargo.toml надо добавить в разделы [profile]:

[profile.release]
panic = 'abort'

Cargo Clippy linter

Example of Clippy config:

cargo clippy --fix -- \
-W clippy::pedantic \
-W clippy::nursery \
-W clippy::unwrap_used \
-W clippy::expect_used

Clippy has a markdown book:

cargo install mdbook
# Run from top level of your rust-clippy directory:
mdbook serve book --open
# Goto http://localhost:3000 to see the book

Important Cargo libs

  • Tokio - async runtime
  • Eyre & color-eyre - type signatures, error report handling
  • Tracing - collect diagnostic info
  • Reqwest - HTTP requests library, async
  • Rayon - data parallelism library, without data races
  • Clap - commandline passing library
  • SQLX - compile-checked SQL, async
  • Chrono - time/date library
  • EGUI - web-gui @60FPS, runs in Webassembly
  • Yew.rs - web-library like React.js

Subsections of Cargo

Cargo Offline

Cargo OFFLINE

Для создания локального сервера можно скачать все пакеты Cargo с помощью проекта Panamax.

cargo install --locked panamax
panamax init ~/test/my-mirror

Нужно зайти в папку my-mirror, проверить настройки в файле mirror.toml. И далее запустить синхронизацию:

panamax sync ~/test/my-mirror

Далее, можно публиковать зеркало по веб через встроенный сервер (по порту 8080):

panamax serve ~/test/my-mirror

На индекс странице сервера будет справка по подключению Rust клиентов к зеркалу. В частности, посредством создания файла настроек ~/.cargo/config :

[source.my-mirror]
registry = "http://panamax.local/crates.io-index"
[source.crates-io]
replace-with = "my-mirror"

Cargo Workspaces

Cargo Workspaces

Объединение кода в 1 проект. В Cargo.toml нужно объявить его структуру:

[package]
name = "ardan_workspace"
version = "0.1.0"
edition = "2021"

[dependencies]

[workspace]
members = [ 
  "session1/hello_world",
  "session1/variables",
]

После этого можно добавлять подпрограммы в members, через cargo new <подрограмма>.

Создание и подключение библиотеки в Workspace

Создание библиотеки вместо бинарника - `cargo new –lib examplelib

Прописать в файле cargo.toml у бинарной программы в workspace зависимость:

<..>

[dependencies]
examplelib = { path = "../examplelib"}

Далее, в код бинарной программы включить функции из библиотеки:

use examplelib::function01; // фукнкция должна быть публичной (pub fn)

Libraries

Articles in Section

Library Unit-tests

Примеры:

pub fn greet_user(name: &str) -> String {
    format!("Hello {name}")
}

pub fn login(username: &str, password: &str) -> bool {
    username == "admin" && password == "P@ssw0rd"
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_greet_user() {
        assert_eq!("Hello Alex", greet_user("Alex"));
    }

    #[test]
    fn test_login() {
        assert!(login("admin", "P@ssw0rd"));
    }
}

Запуск тестов - командой cargo test

Subsections of Libraries

Anyhow Error Handler

Библиотека работы с ошибками - https://docs.rs/anyhow/latest/anyhow/

  • Использует единый тип Error для всех ошибок;
  • Добавляет контекст к ошибкам;
  • Поддержка backtrace для debug;

Установка

cargo add anyhow

Использование

Anyhow создаёт алиас наподобие Result<T> = Result<T, Box<dyn Error>>, чтобы скрыть тип ошибок и сделать его универсальным.

// ---- Без Anyhow
fn string_error() -> Result<(), String> {
    Ok(())
}
fn io_error() -> Result<(), std::io::Error> {
    Ok(())
}
fn any_error() -> Result<(), Box<dyn Error>> {
    string_error()?;
    io_error()?;
    Ok(())
}
// ---- С Anyhow:
use anyhow::Result;

fn string_error() -> Result<()> {
    Ok(())
}
fn io_error() -> Result<()> {
    Ok(())
}
fn any_error() -> Result<()> {
    string_error()?;
    io_error()?;
    Ok(())
}

Пример неудачного чтения файла:

use anyhow::{Context, Result};

fn read_config_file(path: &str) -> Result<String> {
    std::fs::read_to_string(path).with_context(|| format!("Failed to read file {}", path))
}

fn main() -> Result<()> {
    let config_content = read_config_file("conf.txt")?;

    println!("Config content:\n{:?}", config_content);

    Ok(())
}
  • Result<T> становится алиасом к Result<T, anyhow::Error>;
  • Context, with_context() позволяет добавить подробности к ошибке, в случае неуспеха функции чтения read_to_string();
  • Оператор ? выносит ошибку вверх, при этом авто-конвертирует её тип в anyhow::Error.

Замена Box<dyn> с контекстом

Возьмём пример, в котором чтение файла std::fs::read_to_string() (может быть неудачным), далее дешифровка его контента с помощью base64 decode() (может не получиться) в цепочку байт, из которой формируется строка String::from_utf8() (может не получиться). Все эти три потенциальных ошибки имеют разный тип.

Один способ все три их принять на одну функцию, это с помощью Box<dyn std::error::Error>>, потому что все 3 ошибки применяют std::error::Error.

use base64::{self, engine, Engine};

fn decode() -> Result<(), Box<dyn std::error::Error>> {
    let input = std::fs::read_to_string("input")?;
    for line in input.lines() {
        let bytes = engine::general_purpose::STANDARD.decode(line)?;
        println!("{}", String::from_utf8(bytes)?);
    }
    Ok(())
}

Подход рабочий, но при срабатывании одной из трёх ошибок, судить о происхождении проблемы можно будет лишь по сообщению внутри ошибки.

В случае применения Anyhow, можно заменить им Box<dyn>, при этом сразу добавить контекстные сообщения, которое поможет понять место:

use anyhow::Context;
use base64::{self, engine, Engine};

fn decode() -> Result<(), anyhow::Error> {
    let input = std::fs::read_to_string("input")
    .context("Failed to read file")?; // контекст 1
    for line in input.lines() {
        let bytes = engine::general_purpose::STANDARD
            .decode(line)
            .context("Failed to decode the input")?; // контекст 2
        println!(
            "{}",
            String::from_utf8(bytes)
            .context("Failed to cenvert bytes")? // контекст 3
        );
    }
    Ok(())

clap CLI

Установка

Установить вместе с допом derive, чтобы добавлять clap как признак (trait) у структур данных.

cargo add clap -F derive

Как использовать

Добавка clap позволяет парсить аргументы командной строки, а также добавляет ключи “-h” и “–help” для помощи без кода:

use clap::Parser;

#[derive(Parser, Debug)]
struct Args {
    arg1: String,
    arg2: usize,
}

fn main() {
    let args = Args::parse();
    println!("{:?}", args)
}

При запуске данная программа требует 2 аргумента, притом второй обязательно числом.

Добавление описаний

Имя программы и версия вносятся отдельным признаком. Доп. поля описания вносятся с помощью спец. комментариев ///:

use clap::Parser;

#[derive(Parser, Debug)]
#[command(author = "Author Name", version, about)]
/// A very simple CLI parser
struct Args {
    /// Text argument option
    arg1: String,
    /// Number argument option
    arg2: usize,
}

fn main() {
    let args = Args::parse();
    println!("{:?}", args)
}

Добавка флагов

Флаги добавляем с помощью аннотации #[arg(short, long)] для короткого и длинного именования флага. Если у 2-х флагов одинаковая первая буква, можно указать вручную их короткую версию. Короткая версия не может быть String, можно только 1 char.

<..>
struct Args {
    #[arg(short = 'a', long)]
    /// Text argument option
    arg1: String,
    
    #[arg(short = 'A', long)]
    /// Number argument option
    arg2: usize,
}
<..>

Необязательные флаги

Для отметки аргумента как необязательного достаточно указать его тип как Option<тип> и в скобках исходный тип данных:

struct Args {
    #[arg(short = 'a', long)]
    /// Text argument option
    arg1: String,
    
    #[arg(short = 'A', long)]
    /// Number argument option
    arg2: Option<usize>,
}

Такой подход потребует обработать ситуацию, когда в arg2 ничего нет. Вместо так делать, можно указать значение по умолчанию:

struct Args {
    #[arg(short = 'a', long)]
    /// Text argument option
    arg1: String,
    
    #[arg(default_value_t=usize::MAX, short = 'A', long)]
    /// Number argument option
    arg2: usize,
}

Теперь arg2 по умолчанию будет равен максимальному числу usize, если не указано иное.

Валидация введённых значений

В случае аргумента-строки есть возможность ввести пустую строку из пробелов " ". Для исключения таких вариантов, вводится функция валидации и её вызов:

use clap::Parser;

#[derive(Parser, Debug)]
#[command(author = "Author Name", version, about)]
/// A very simple CLI parser
struct Args {
    #[arg(value_parser = validate_argument_name, short = 'a', long)]
    /// Text argument option
    arg1: String,
    #[arg(default_value_t=usize::MAX, short = 'A', long)]
    /// Number argument option
    arg2: usize,
}

fn validate_argument_name(name: &str) -> Result<String, String> {
    if name.trim().len() != name.len() {
        Err(String::from(
            "строка не должна начинаться или заканчиваться пробелами",
        ))
    } else {
        Ok(name.to_string())
    }
}

fn main() {
    let args = Args::parse();
    println!("{:?}", args)
}

Теперь при попытке вызвать программу tiny-clapper -- -a " " будет показана ошибка валидации.

Command CLI

Библиотека позволяет запускать команды в ОС.

Варианты использования

  • spawn — runs the program and returns a value with details
  • output — runs the program and returns the output
  • status — runs the program and returns the exit code

Использование

Output - просто и быстро

    let output = Command::new("cat")
        .arg("/etc/issue")
        .output()
        .with_context(|| "ls command failed")?;

    println!("{}", output.status);
    println!("{}", String::from_utf8_lossy(&output.stdout));
    println!("{}", String::from_utf8_lossy(&output.stderr));

❗Ограничение - можно вызывать только существующие объекты, нельзя добавлять свой текст.

spawn - гибкий ввод

Самый гибкий вариант, позволяющий делать свой ввод, а не только существующие команды и файлы-папки, это через spawn:

    let mut child = Command::new("cat") // команда
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;

    let stdin = child.stdin.as_mut()?;
    stdin.write_all(b"Hello Rust!\n")?; // текст к команде, /n обязателен
    let output = child.wait_with_output()?;
    
    for i in output.stdout.iter() { // цикл на случай многострочного вывода
        print!("{}", *i as char);
    }
    Ok(())

❗Ограничение - можно подавать на вход текст лишь тем командам, которые требуют сразу указать вводный текст. При этом ряд команд делают паузу перед потреблением текста на вход, с такими свой ввод работать не будет это относится и к фильтрации через pipe = | grep <...> и аналоги.

Pipe (nightly) - полный ввод (не проверенный способ)

https://doc.rust-lang.org/std/pipe/fn.pipe.html Для использования нужен nightly вариант Rust

rustup install nightly
rustup default nightly

Использование:

#![feature(anonymous_pipe)] // только в Rust Nightly
use std::pipe

let text = "| grep file".as_bytes();

// Запускаем саму команду
let child = Command::new("ls")
    .arg("/Users/test")
    .stdin({
        // Нельзя отправить просто строку в команду
        // Нужно создать файловый дескриптор (как в обычном stdin "pipe")
        // Поэтому создаём пару pipes тут
        let (reader, mut writer) = std::pipe::pipe().unwrap();

        // Пишем строку в одну pipe
        writer.write_all(text).unwrap();

        // далее превращаем вторую для передачи в команду сразу при spawn. 
        Stdio::from(reader)
    })
    .spawn()?;

itertools

External link: https://docs.rs/itertools/latest/itertools/

Working with iterators

Sorting() a string of letters (with rev() - reverse order)

use itertools::Itertools;

let text = "Hello world";
let text_sorted = text.chars().sorted().rev().collect::<String>();
// rev() - Iterate the iterable in reverse
   
println!("Text: {}, Sorted Text: {}", text, text_sorted); 
// Text: Hello world, Sorted Text: wroollledH 

Counts() подсчёт количества одинаковых элементов в Array

use itertools::Itertools;

let number_list = [1,12,3,1,5,2,7,8,7,8,2,3,12,7,7];
let mode = number_list.iter().counts(); // Itertools::counts() 
// возвращает Hashmap, где ключи взяты из массива, значения - частота
for (key, value) in &mode {  
    println!("Число {key} встречается {value} раз");  
 } 

rand

Generating random numbers

External links:

Using rand lib example:

use rand::Rng;

fn main() {
let secret_of_type = rand::rng().random::<u32>();
let secret = rand::rng().random_range(1..=100);

println!("Random nuber of type u32: {secret_of_type}");
println!("Random nuber from 1 to 100: {}", secret); }

В старой версии библиотеки применялся признак gen(), который переименовали в связи с добавлением gen() в Rust 2024.

regex

Внешние ссылки:

Установка

cargo add regex

REGEX Выражения

Один символ

.             any character except new line (includes new line with s flag)
\d            digit (\p{Nd})
\D            not digit
\pX           Unicode character class identified by a one-letter name
\p{Greek}     Unicode character class (general category or script)
\PX           Negated Unicode character class identified by a one-letter name
\P{Greek}     negated Unicode character class (general category or script)

Классы символов

[xyz]         A character class matching either x, y or z (union).
[^xyz]        A character class matching any character except x, y and z.
[a-z]         A character class matching any character in range a-z.
[[:alpha:]]   ASCII character class ([A-Za-z])
[[:^alpha:]]  Negated ASCII character class ([^A-Za-z])
[x[^xyz]]     Nested/grouping character class (matching any character except y and z)
[a-y&&xyz]    Intersection (matching x or y)
[0-9&&[^4]]   Subtraction using intersection and negation (matching 0-9 except 4)
[0-9--4]      Direct subtraction (matching 0-9 except 4)
[a-g~~b-h]    Symmetric difference (matching `a` and `h` only)
[\[\]]        Escaping in character classes (matching [ or ])

Совмещения символов

xy    concatenation (x followed by y)
x|y   alternation (x or y, prefer x)

Повторы символов

x*        zero or more of x (greedy)
x+        one or more of x (greedy)
x?        zero or one of x (greedy)
x*?       zero or more of x (ungreedy/lazy)
x+?       one or more of x (ungreedy/lazy)
x??       zero or one of x (ungreedy/lazy)
x{n,m}    at least n x and at most m x (greedy)
x{n,}     at least n x (greedy)
x{n}      exactly n x
x{n,m}?   at least n x and at most m x (ungreedy/lazy)
x{n,}?    at least n x (ungreedy/lazy)
x{n}?     exactly n x

Пустые символы

^     the beginning of text (or start-of-line with multi-line mode)
$     the end of text (or end-of-line with multi-line mode)
\A    only the beginning of text (even with multi-line mode enabled)
\z    only the end of text (even with multi-line mode enabled)
\b    a Unicode word boundary (\w on one side and \W, \A, or \z on other)
\B    not a Unicode word boundary

Группировка и флаги

(exp)          numbered capture group (indexed by opening parenthesis)
(?P<name>exp)  named (also numbered) capture group (names must be alpha-numeric)
(?<name>exp)   named (also numbered) capture group (names must be alpha-numeric)
(?:exp)        non-capturing group
(?flags)       set flags within current group
(?flags:exp)   set flags for exp (non-capturing)

Спец-символы

\*          literal *, works for any punctuation character: \.+*?()|[]{}^$
\a          bell (\x07)
\f          form feed (\x0C)
\t          horizontal tab
\n          new line
\r          carriage return
\v          vertical tab (\x0B)
\123        octal character code (up to three digits) (when enabled)
\x7F        hex character code (exactly two digits)
\x{10FFFF}  any hex character code corresponding to a Unicode code point
\u007F      hex character code (exactly four digits)
\u{7F}      any hex character code corresponding to a Unicode code point
\U0000007F  hex character code (exactly eight digits)
\U{7F}      any hex character code corresponding to a Unicode code point

Типы символов по ASCII

[[:alnum:]]    alphanumeric ([0-9A-Za-z])
[[:alpha:]]    alphabetic ([A-Za-z])
[[:ascii:]]    ASCII ([\x00-\x7F])
[[:blank:]]    blank ([\t ])
[[:cntrl:]]    control ([\x00-\x1F\x7F])
[[:digit:]]    digits ([0-9])
[[:graph:]]    graphical ([!-~])
[[:lower:]]    lower case ([a-z])
[[:print:]]    printable ([ -~])
[[:punct:]]    punctuation ([!-/:-@\[-`{-~])
[[:space:]]    whitespace ([\t\n\v\f\r ])
[[:upper:]]    upper case ([A-Z])
[[:word:]]     word characters ([0-9A-Za-z_])
[[:xdigit:]]   hex digit ([0-9A-Fa-f])

Использование REGEX

Сверка с is_match()

use regex::Regex;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let input = "hello, world!";
    let pattern = Regex::new(r"hello, (world|universe)!")?;
    
    let is_match = pattern.is_match(input);
    println!("Is match: {}", is_match); // true
    Ok(())
}

Для простого выяснения факта совпадения лучше всего пользоваться is_match как самым быстрым способом.

Поиск первого совпадения

    let pattern = regex::Regex::new(r"hello, (world|universe)!")?;
    let input = "hello, world!";
    let first_match = pattern.find(input);

    println!("First match: {}", first_match.unwrap().as_str());

Первое совпадение будет иметь тип Option<match>, а в случае отсутствия совпадений = None.

Поиск всех совпадений

    let pattern = regex::Regex::new(r"hello, (world|universe)!")?;
    let input = "hello, world! hello, universe!";
    let matches: Vec<_> = pattern.find_iter(input).collect(); // find_iter()
    matches.iter().for_each(|i| println!("{}", i.as_str()));
    // matches = Vec<match> и содержит все совпадения

serde

Installing

Add serde framework with Derive feature to use it in structures and functions. Also add a separate serde_json lib for converting into specifically JSON:

cargo add serde -F derive
cargo add serde_json

Usage

Add serde, then mark the structures with Serialise, Deserialise traits and use serde_json for serialising:

use serde::{Deserialize, Serialize};

#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub enum LoginRole {
    Admin,
    User,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub username: String,
    pub password: String,
    pub role: LoginRole,
}

pub fn get_default_users() -> HashMap<String, User> {
    let mut users = HashMap::new();
    users.insert(
        "admin".to_string(),
        User::new("admin", "password", LoginRole::Admin),
    );
    users.insert(
        "bob".to_string(),
        User::new("bob", "password", LoginRole::User),
    );
    users
}

pub fn get_users() -> HashMap<String, User> {
    let users_path = Path::new("users.json");
    if users_path.exists() {
        // Load the file!
        let users_json = std::fs::read_to_string(users_path).unwrap();
        let users: HashMap<String, User> = serde_json::from_str(&users_json).unwrap();
        users
    } else {
        // Create a file and return it
        let users = get_default_users();
        let users_json = serde_json::to_string(&users).unwrap();
        std::fs::write(users_path, users_json).unwrap();
        users
    }

sysinfo

Библиотека для изучения параметров системы и системных ресурсов. https://docs.rs/sysinfo/latest/sysinfo/

Установка

cargo add sysinfo

Использование

use sysinfo::{RefreshKind, System};

fn main() {
    let _sys = System::new_with_specifics(RefreshKind::nothing());
    println!("System name:             {:?}", System::name());
    println!("System kernel version:   {:?}", System::kernel_version());
    println!("System OS version:       {:?}", System::os_version());
    println!("System host name:        {:?}", System::host_name());
}

С помощью RefreshKind::nothing() отключаются все динамические вещи, такие как остаток свободной памяти, загрузка ЦПУ, сеть и так далее, что драматически ускоряет опрос системы.

Можно частично опрашивать разные системные ресурсы, по заданным интервалам.

Tokio tracing

Внешние ссылки:

Наблюдаемость / Observability

Наблюдаемость позволяет понять систему извне, задавая вопросы о ней, при этом не зная ее внутреннего устройства. Кроме того, она позволяет легко устранять неполадки и решать новые проблемы, то есть «неизвестные неизвестные». Она также поможет вам ответить на вопрос «почему что-то происходит?».

Чтобы задать эти вопросы о вашей системе, приложение должно содержать полный набор инструментов. То есть код приложения должен отправлять сигналы, такие как трассировки, метрики и журналы. Приложение правильно инструментировано, когда разработчикам не нужно добавлять дополнительные инструменты для устранения неполадок, потому что у них есть вся необходимая информация.

Терминология

  • Событие журнала (Log event/message) - событие, произошедшее в конкретный момент времени;
  • Промежуток (Span record) - запись потока исполнения в системе за период времени. Он также выполняет функции контекста для событий журнала и родителя для под-промежутков;
  • Трасса (trace) - полная запись потока исполнения в системе от получения запроса до отправки ответа. Это по сути промежуток-родитель, или корневой промежуток;
  • Подписчик (subscriber) - реализует способ сбора данных трассы, например, запись их в стандартный вывод;
  • Контекст трассировки (Tracing Context): набор значений, которые будут передаваться между службами

Установка

cargo add tracing
cargo add tracing-subscriber -F json

Использование

Инициализация

Простая инициализация и отслеживание событий:

use tracing::info;

fn main() {
    // Установка глобального сборщика по конфигурации
    tracing_subscriber::fmt::init();

    let number_of_yaks = 3;

    // новое событие, вне промежутков
    info!(number_of_yaks, "preparing to shave yaks");
}

Ручная инициализация свойств подписчика для форматирования лога:

fn setup_tracing() {
    let subscriber = tracing_subscriber::fmt()
        .json() // нужно cargo add tracing-subscriber -F json
        .with_max_level(tracing::Level::TRACE) // МАХ уровень логирования
        .compact() // компактный лог
        .with_file(true) // показывать файл-исходник
        .with_line_number(true) // показать номера строк кода
        .with_thread_ids(true) // показать ID потока с событием
        .with_target(false) // не показывать цель (модуль) события
        .finish();

    tracing::subscriber::set_global_default(subscriber).unwrap();

    tracing::info!("Starting up");
    tracing::warn!("Are you sure this is a good idea?");
    tracing::error!("This is an error!");
}

Трассировка в синхронном режиме

use tracing::{instrument, warn};

#[instrument]
fn sync_tracing() {
    warn!("event 1");
    sync_tracing_sub();
}

#[instrument]
fn sync_tracing_sub() {
    warn!("event 2");
}

fn main() {
    setup_tracing();
    sync_tracing();
}

Макрос #[instrument] автоматически создаёт промежутки (spans) для функций, а подписчик (subscriber) настроен выводить промежутки в stdout.

Трассировка потоков в асинхронном режиме

use tracing_subscriber::fmt::format::FmtSpan;

#[tracing::instrument] // инструмент следит за временем работы
async fn hello() {
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}

#[tokio::main] // cargo add tokio -F time,macros,rt-multi-thread
async fn main() -> anyhow::Result<()> {
    let subscriber = tracing_subscriber::fmt()
        .json()
        .compact()
        .with_file(true)
        .with_line_number(true)
        .with_thread_ids(true)
        .with_target(false)
        .with_span_events(FmtSpan::ENTER | FmtSpan::CLOSE) // вход и выход
        .finish();                                   // потока отслеживать

    tracing::subscriber::set_global_default(subscriber).unwrap();

    hello().await;
    Ok(())
}

В итоге получаем лог работы потока с временем задержки:

2024-12-24T14:30:17.378906Z  INFO ThreadId(01) hello: src/main.rs:3: {"message":"enter"} {}
2024-12-24T14:30:18.383596Z  INFO ThreadId(01) hello: src/main.rs:3: {"message":"enter"} {}
2024-12-24T14:30:18.383653Z  INFO ThreadId(01) hello: src/main.rs:3: {"message":"enter"} {}
2024-12-24T14:30:18.383675Z  INFO ThreadId(01) hello: src/main.rs:3: {"message":"close","time.busy":"179µs","time.idle":"1.00s"} {}

Запись журнала в хранилище

Внешние ссылки:

Для записи журнала применяется отдельный модуль tracing-appender:

cargo add tracing-appender

У него много функций не только записи в облачные службы типа Datadog, но и создание журнала с дозаписью (минутной, часовой, дневной), а также запись как неблокирующее действие во время многопоточного исполнения.

Пример инициализации неблокирующей записи журнала в файл (лучше всего как JSON), одновременно вывод на экран и организация часового журнала с дозаписью (rolling):

use tracing::{instrument, warn};

// тянем std::io::Write признак в режиме неблокирования
use tracing_subscriber::fmt::writer::MakeWriterExt; 

fn setup_tracing() {
    // инициализация файла с дозаписью
    let logfile = tracing_appender::rolling::hourly("/some/directory", "app-log");
    // уровень записи при логировании = INFO
    let stdout = std::io::stdout.with_max_level(tracing::Level::INFO);

    let subscriber = tracing_subscriber::fmt()
        .with_max_level(tracing::Level::TRACE)
        .json()
        .compact()
        .with_file(true)
        .with_line_number(true)
        .with_thread_ids(true)
        .with_target(false)
        .with_writer(stdout.and(logfile)) // обязательно указать запись тут
        .finish();                        // в файл и в stdout консоль
    tracing::subscriber::set_global_default(subscriber).unwrap();
}

#[instrument]
fn sync_tracing() {
    warn!("event 1");
    sync_tracing_sub();
}

#[instrument]
fn sync_tracing_sub() {
    warn!("event 2");
}

fn main() {
    setup_tracing();
    sync_tracing();
}

CLI Arguments

ARGS

Аргументы командной строки можно захватить с помощью методов args()+collect() библиотеки env. Нужно поместить их в строковый вектор.

use std::env;  
  
fn main() {  
    let args: Vec<String> = env::args().collect();  
  
    for arg in args.iter() {  
        println!("{}", arg);  
    } }

Первым параметров всегда идёт имя самой программы.

Tip

Продвинутую обработку аргументов CLI удобно делать с помощью библиотеки Clap

Closures

Closure

  • Замыкания можно присвоить переменным и вызывать;
  • В отличие от функций, замыкания могут использовать переменные вне своего блока.
(0..3).for_each(|x| {println!("map i = {}", x * 2);});

Map и Filter

.map(<closure>) передаёт владение элементами итератора замыканию, чтобы их можно было трансформировать в другие элементы, которые далее возвращает замыкание.

.filter(<closure>) возвращает оригинальные элементы, когда предикат замыкания возвращает true. Таким образом, отдавать владение элементами замыканию нельзя, и нужно передавать по ссылке.

let a = (0..3).map(|x| x * 2);
for i in a {
    println!("map i = {}", i);
}

let a = (0..3).filter(|&x| x % 2 == 0);
for i in a {
    println!("filter i = {}", i);
}

Вложенные замыкания map()

    let a = (0..=3).map(|x| x * 2).map(|y| y - 1);
    // первая итерация map(): 2, 4, 6
    // вторая итерация map(): 1, 3, 5
    for i in a {
        println!("{i}");
    }

Fold

Каждая итерация fold принимает 2 аргумента:

  • Исходное значение счётчика;
  • Замыкание, которое тоже принимает 2 аргумента: счётчик и объект. Замыкание возвращает счётчик.

fold используется для превращения множества в 1 значение. Пример для получения суммы всех чётных чисел:

pub fn main() {
    let even_sum = (1..=10).fold(0, |acc, num| if num % 2 == 0 { acc + num } else { acc });
    println!("{even_sum:?}");
}

fold можно часто заменить другими методами. Например, код выше можно заменить на отбор всех чётных числе с помощью filter и их сложение с помощью sum:

(0..=10).filter(|n| *n % 2 == 0).sum()

Наравне с sum также популярны product и collect.

Счётчик у fold необязательно числовой, можно использовать, например, String:

pub fn giant_grunts(initial: char) -> String {
    ["Bee", "Fee", "Gee", "Fi", "Hi", "Fo", "Mo", "Fum", "Tum"].iter().fold(
        String::new(),
        |acc, grunt| if grunt.starts_with(initial) { acc + grunt } else { acc },
    )}

pub fn main() {
    let song = giant_grunts('F');
    println!("{song:?}"); // "FeeFiFoFum" 
}

All

Замыкание all возвращает True, если все элементы в замыкании соответствуют условию.

let a: Vec<i32> = vec![1, 2, 3, 4];
print!("{}\n", a.into_iter().all(|x| x > 1)); // false

Для пустого вектора замыкание all вернёт True:

let a: Vec<i32> = vec![];
print!("{}\n", a.into_iter().all(|x| x > 1)); // true

Цикл через замыкание vs for

use std::collections::HashMap;
pub fn main() {
    let num_vec = vec![1, 2, 1, 3, 5, 2, 1, 4, 6];
    let mut number_count: HashMap<i32, i32> = HashMap::new();
    for key in num_vec {
        *number_count.entry(key).or_default() += 1;
    }
    /* for (k, v) in number_count {
        print!("{} -> {}; ", k, v);
    } */
    
    number_count.iter().for_each(|(k, v)| {
        print!("{} -> {}; ", k, v);
    }); //цикл через замыкание итератора
}

Constants

Константы

Это глобальные по области действия значения, неизменяемые. При объявлении нужно всегда сразу указывать их тип.

const MAX_LIMIT: u8 = 15;  // тип u8 обязательно надо указать
  
fn main() {  
    for i in 1..MAX_LIMIT {  
        println!("{}", i);  
    }  }

Cross-compiling

Проверка системы

Сделать новый проект, добавить в него библиотеку cargo add current_platform. Далее создаём и запускаем код проверки среды компиляции и исполнения:

use current_platform::{COMPILED_ON, CURRENT_PLATFORM};

fn main() {
    println!("Run from {}! Compiled on {}.", CURRENT_PLATFORM, COMPILED_ON);
}
  • Посмотреть текущую ОС компиляции: rustc -vV
  • Посмотреть список ОС для кросс-компиляции: rustc --print target-list Формат списка имеет поля <arch><sub>-<vendor>-<sys>-<env>, например, x86_64-apple-darwin => macOS на чипе Intel.

Настройка кросс-компилятора

Нужно установить cross: cargo install cross установит его по пути $HOME/.cargo/bin

Далее, нужно установить вариант Docker в системе:

  • На macOS => Docker Desktop;
  • На Linux => Podman

Запуск кода на компиляцию под ОС Windows: cross run --target x86_64-pc-windows-gnu => в папке target/x86_64-pc-windows-gnu/debug получаем EXE-файл с результатом. ❗Компиляция проходит через WINE.

Проверка среды компиляции

Cross поддерживает тестирование других платформ. Добавка проверки:

mod tests {
    use current_platform::{COMPILED_ON, CURRENT_PLATFORM};

    #[test]
    fn test_compiled_on_equals_current_platform() {
        assert_eq!(COMPILED_ON, CURRENT_PLATFORM);
    } }

Запустить проверку локально: cargo test Запустить проверку с кросс-компиляцией: cross test --target x86_64-pc-windows-gnu На Linux/macOS проверка пройдёт, а вот при компиляции под Windows - нет: `test tests::test_compiled_on_equals_current_platform … FAILED

Добавка платформенно-специфичного кода

Можно вписать код, который запустится только на определённой ОС, например, только на Windows:

use current_platform::{COMPILED_ON, CURRENT_PLATFORM};

#[cfg(target_os="windows")]
fn windows_only() {
    println!("This will only print on Windows!");
}

fn main() {
    println!("Run from {}! Compiled on {}.", CURRENT_PLATFORM, COMPILED_ON);
    #[cfg(target_os="windows")]
    {
        windows_only();
    }
}

Enums

Перечисления

Тип перечислений позволяет организовать выбор из нескольких вариантов в пределах логического множества.

enum Money {  
    Rub,  
    Kop  
}

match

Аналог switch в других языках, однако, круче: его проверка не сводится к bool, а также реакция на каждое действие может быть блоком:

fn main() {  
    let m = Money::Kop;  
    println!("Я нашёл кошелёк, а там {}p",match_value_in_kop(m));  
}  
  
fn match_value_in_kop(money: Money) -> u8 {  
    match money {  
        Money::Rub => 100,  
        Money::Kop => {  
            println!("Счастливая копейка!");  
            1  
        }  }  }

Проверка условия и запуск соответствующего метода:

struct State {
    color: (u8, u8, u8),
    position: Point,
    quit: bool,
}

impl State {
    fn change_color(&mut self, color: (u8, u8, u8)) {
        self.color = color; }

    fn quit(&mut self) {
        self.quit = true; }

    fn echo(&self, s: String) {
        println!("{}", s); }

    fn move_position(&mut self, p: Point) {
        self.position = p; }

    fn process(&mut self, message: Message) {
        match message { // проверка и запуск одного из методов
            Message::Quit => self.quit(),
            Message::Echo(x) => self.echo(x),
            Message::ChangeColor(x, y, z) => self.change_color((x, y, z)),
            Message::Move(x) => self.move_position(x),
        } } }

if let

В случае, когда выбор сводится к тому, что мы сравниваем 1 вариант с заданным паттерном и далее запускаем код при успехе, а в случае неравенства ничего не делаем, можно вместо match применять более короткую конструкцию if-let:

    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {}", max),
        _ => (), //  другие варианты ничего не возвращают
    }

Превращается в:

    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {}", max);
    }

Применение if-let - это синтаксический сахар, укорачивает код, однако, лишает нас проверки наличия обработчиков на все варианты возвращаемых значений как в конструкции match.

Сравнение величин

Для сравнения значений в переменных есть метод std::cmp, который возвращает объект типа enum Ordering с вариантами:

use std::cmp::Ordering;
use std:io;

fn main() {
let secret_number = 42;
let mut guess = String::new();
io::stdin()
.read_line(&guess)
.expect("Read line failed!");
  
let guess :i32 = guess.trim().parse()
  .expect("Error! Non-number value entered.");

match guess.cmp(&secret_number) {
  Ordering::Greater => println!("Number too big"),
  Ordering::Less => println!("Number too small"),
  Ordering::Equal => println("Found exact number!")
 }
}

Error Handling

Внешние ссылки:

Ошибки без восстановления

Ряд ошибок приводит к вылету приложения. Также можно вручную вызвать вылет командой panic!:

fn main() {
    panic!("Battery critically low! Shutting down to prevent data loss.");
}

При этом некоторое время тратится на закрытие приложения, очистку стека и данных. Можно переложить это на ОС, введя настройку в Cargo.toml:

[profile.release]
panic = 'abort'

Подготовка примера

Допустим, мы берём вектор из строк-чисел, складываем их и возвращаем сумму как строку:

fn sum_str_vec (strs: Vec<String>) -> String {  
    let mut accum = 0i32;  
    for s in strs {  
        accum += to_int(&s); // to_int = заготовка, см. ниже реализацию 
    }  
    return accum.to_string();  
}  
  
fn main() {  
    let v = vec![String::from("3"), String::from("4")]; // Правильный ввод 
    let total = sum_str_vec(v);  
    println!("Total equals: {:?}", total);  
  
    let v = vec![String::from("3"), String::from("abc")]; // Неправильный ввод 
    let total = sum_str_vec(v);  
    println!("Total equals: {:?}", total);  
}

Для конвертации строки в числа, нужно реализовать функцию to_int в соответствии со стратегиями обработки ошибочного ввода. Конвертацию мы делаем функцией parse(), которая возвращает тип Result<T,E>, где T - значение, E - код ошибки.

Стратегия 1 - паника

В случае неверного ввода, программа полностью останавливается в панике. Метод unwrap() у типа Result<T,E> убирает проверки на ошибки и есть договор с компилятором о том, что ошибки в этом месте быть не может. Если она есть, программа падает с паникой:

fn to_int(s: &str) -> i32 {  
    s.parse().unwrap() } 

Стратегия 2 - паника с указанием причины

В случае неверного ввода, программа сообщает фразу, заданную автором, далее полностью останавливается в панике. Метод expect() аналогичен unwrap(), но выводит сообщение:

fn to_int(s: &str) -> i32 {  
    s.parse().expect("Error converting from string") } 

Стратегия 3 - обработать то, что возможно обработать

Можно сконвертировать и прибавить к результату те строки, которые позволяют это сделать, проигнорировать остальные. Метод unwrap_or() позволяет указать возвращаемое значение в случае ошибки:

fn to_int(s: &str) -> i32 {  
    s.parse().unwrap_or(0) } // при вводе "abc" вернётся 0, сумма = "3" 

Более предпочтительный вариант использовать закрытие unwrap_or_else(), так как метод unwrap_or() будет вызван ДО того как будет отработана основная команда, ВНЕ ЗАВИСИМОСТИ от того, будет ли её результат Some или None. Это потеря производительности, а также потенциальные глюки при использовании внутри unwrap_or() сложных выражений. Закрытие unwrap_or_else() будет вызвано только в случае None, иначе же эта ветка не обрабатывается:

fn to_int(s: &str) -> i32 {  
    s.parse().unwrap_or_else(|_| 0) }

Стратегия 4 - решение принимает вызывающая функция

Вместо возврата числа, возвращаем тип Option<число> - в зависимости от успешности функции, в нём будет либо само число, либо None:

fn to_int(s: &str) -> Option<i32> {  
    s.parse().ok() // ok() конвертирует Result<T,E> в Option<T> 

И тогда вызывающая функция должна обработать результат:

fn sum_str_vec (strs: Vec<String>) -> String {  
    let mut accum = 0i32;  
    for s in strs {  
        accum += match to_int(&s) {  
            Some(v) => v,  
            None => {  
                println!("Error converting a value, skipped");  
                0  // вернётся 0 +в лог пойдёт запись о пропуске
            }, }  }  
    return accum.to_string();  
}

Более короткий вариант записи через if let:

fn sum_str_vec (strs: Vec<String>) -> String {  
    let mut accum = 0i32;  
    for s in strs {  
        if let Some(val) = to_int(&s) {  
            accum += val;  
        } else { println!("Error converting a value, skipped"); }  
    }  
    return accum.to_string();  
}

Тип Option<T> также имеет метод unwrap_or(), отсюда ещё вариант записи:

fn sum_str_vec (strs: Vec<String>) -> String {  
    let mut accum = 0i32;  
    for s in strs {  
        accum += to_int(&s).unwrap_or(0); // раскрываем Option<T> 
    }  
    return accum.to_string();  
}

Стратегия 5 - в случае проблем, передать всё в основную программу

Вместо передачи значения из функции, в случае каких-либо проблем, мы возвращаем None:

fn sum_str_vec (strs: Vec<String>) -> Option<String> {  
    let mut accum = 0i32;  
    for s in strs {  
        accum += to_int(&s)?;  // в случае None, ? передаёт его далее на выход
    }                         
    Some(accum.to_string())    // на выход пойдёт значение или None
}

Стратегия 6 - передать всё в основную программу с объяснением ошибки

Мы возвращаем проблему в основную программу с объясением проблемы. Для этого заводим структуру под ошибку, и передаём уже не объект Option<T>, а объект Result<T,E>, где E = SummationError. Для такого объекта есть метод ok_or(), который либо передаёт значение, либо передаёт ошибку нужного типа:

#[derive(Debug)]  
struct SummationError;

fn sum_str_vec (strs: Vec<String>) -> Result<String, SummationError> {  
    let mut accum = 0i32;  
    for s in strs {  
        accum += to_int(&s).ok_or(SummationError)?;  
    }  
    Ok(accum.to_string())  
}

Вместо выдумывать свой собственный тип и конвертировать вывод метода parse() из Result<T,E> в Option<T>, а потом обратно, можно сразу протащить ошибку в объекте Result<T,E> в главную программу:

use std::num::ParseIntError;  // тип ошибки берём из библиотеки
  
fn to_int(s: &str) -> Result<i32, ParseIntError> {  
    s.parse()  // parse возвращает просто Result<T,E>
}  
  
fn sum_str_vec (strs: Vec<String>) -> Result<String, ParseIntError> {  
    let mut accum = 0i32;  
    for s in strs {  
        accum += to_int(&s)?; }  // ? передаёт ошибку нужного типа далее
    Ok(accum.to_string())  
}

Однако, мы хотим скрыть подробности работы и ошибки от главной программы и передать ей ошибку в понятном виде, без разъяснения деталей её возникновения. Для этого можно сделать трансляцию ошибки из библиотечной в собственный тип, и далее передать методом map_err():

use std::num::ParseIntError;  
  
#[derive(Debug)]  
struct SummationError;  
  
fn to_int(s: &str) -> Result<i32, ParseIntError> {  
    s.parse()  
}  
  
fn sum_str_vec (strs: Vec<String>) -> Result<String, SummationError> {  
    let mut accum = 0i32;  
    for s in strs {  
        accum += to_int(&s).map_err(|_| SummationError)?; // конвертация ошибки  
    }                                                     // перед передачей
    Ok(accum.to_string())  
}

Где можно использовать оператор ?

Оператор ? можно использовать только в функциях для возврата совместимых значений типа Result<T,E>, Option<T> или иных данных со свойством FromResidual. Для работы такого возврата в заголовке функции должен быть прописан возврат нужного типа данных.
При использовании ? на выражении типа Result<T,E> или Option<T>, ошибка Err(e) или None будет возвращена рано из функции, а в случае успеха - выражение вернёт результат, и функция продолжит работу.

Пример функции, которая возвращает последний символ 1ой строки текста:

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
} // lines() возвращает итератор на текст
  // next() берёт первую строку текста. Если текст пустой - сразу возвращаем None

Библиотеки работы с ошибками

Files

Получить список файлов

Функция read_dir:

use std::fs;
fn main() {
    let paths = fs::read_dir("./").unwrap();

    for path in paths {
        println!("{}", path.unwrap().path().display())
    }
}

Получить список файлов с их абсолютным путём в системе

use std::fs;
use std::process;

fn main() {
    let paths = fs::read_dir("./").unwrap();

    for path in paths {
        let pathbuf = path.unwrap().path();
        println!("{:?}", fs::canonicalize(&pathbuf));
    }

    println!("\nPID = {}\n", process::id());
}

Чтение текста из файлов в строку

Для чтения файлов, нужно сначала добавить несколько методов из библиотек: FIle - открытие файлов и Read - чтение, который входит в prelude.

se std::fs::File;         // импорт File
use std::io::prelude::*;  // импорт Read
  
fn main() {  
    // читаемый файл находится в корне проекта.
    let mut file = File::open("access.log.10").expect("File not opened!");  
  
    let mut contents = String::new(); // закачать файл в переменную-строку
    file.read_to_string(&mut contents).expect("Cannot read file!");  
  
    println!("File contents: \n\n{}",contents);  
}

Чтение в вектор из байтов (vector of bytes)

Чтение файла в память целиком как вектора байтов - для бинарных файлов, либо для частого обращения к содержимому:

use std::io::Read;
use std::{env, fs, io, str};

fn main() -> io::Result<()> {
    let mut file = fs::File::open("test_file.txt")?;
    let mut contents = Vec::new();
    file.read_to_end(&mut contents);
    println!("File contents: {:?}", contents); // вывод байт

    let text = match str::from_utf8(&contents) { // перевод в строку UTF8
        Ok(v) => v,
        Err(e) => panic!("Invalid UTF-8: {e}"),
    };
    println!("Result: {text}"); // вывод строкой
    Ok(())
}

Чтение текста из файла в буфер по строчкам

Нужно добавить к библиотеке File также библиотеку организации буфера чтения, а также обработать ошибки открытия файла и чтения.

use std::io::{BufRead, BufReader};
use std::{env, fs, io};

fn main() -> io::Result<()> {
    let file = fs::File::open("textfile.txt")?;
    let reader = BufReader::new(file);
    
    for line in reader.lines() { // io::BufRead::lines()
        let line = line?;
        println!("{line}");
    }
    Ok(())

 Запись в файлы

use std::fs::File;  
use std::io::prelude::*;  
  
fn main() {  
    let mut file = File::create("report.log").expect("File not created!");  
  
    file.write_all(b"report.log");  
}

Flow Control

IF-ELSE

В Rust есть управление потоком программы через конструкции IF, ELSE IF, ELSE:

let test_number = 6;

if test_number % 4 == 0 {
println!("Divisible by 4");
} else if test_number % 3 == 0 { // Проверка останавливается на первом 
println!("Divisible by 3");      // выполнимом условии, дальнейшие проверки
} else if test_number % 2 == 0 { // пропускаются.
println!("Divisible by 2");
} else {
println!("Number is not divisible by 4, 3 or 2");
}

Конструкция IF является выражением (expression), поэтому можно делать присваивание:

let condition = true;

let number = if condition { "aa" } else { "ab" }; // присваивание результата IF
println!("Number is {number}");

LOOPS

Три варианта организовать цикл: через операторы loop, while, for.

LOOP - организация вечных циклов. Конструкция LOOP является выражением (expression), поэтому можно делать присваивание.

    let mut counter = 0;
    let result = loop {        
        counter += 1;
        if counter == 10 {
            break counter * 2; // выход из вечного цикла
        }
    }; // ";" нужно, т.к. было выражение
    println!("The result is {result}");

Если делать вложенные циклы, то можно помечать их меткой, чтобы выходить с break на нужный уровень.

    let mut count = 0;
    'counting_up: loop {            // метка внешнего цикла
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;  // goto метка
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");

WHILE - цикл с условием. Можно реализовать через loop, но с while сильно короче.

    let mut number = 10;
    while number != 0 {
        println!("{number}!");
        number -= 1;
    }
    println!("ЗАПУСК!");

FOR - цикл по множествам элементов. В том числе можно задать подмножество чисел.

for i in (1..10).rev() { // .rev() - выдача подмножества в обратную сторону
println!("Value: {i}");
}
println!("ЗАПУСК!");

Generics

Usage

Generics нужны для того, чтобы не повторять один и тот-же код несколько раз для разных типов данных. Это один из трёх основных способов экономии на повторении кода наравне с макросами и интерфейсами traits. Пример: нужно сортировать 2 набора данных - массив чисел, массив символов.

fn largest_i32(n_list: &[i32]) -> &i32 {
    let mut largest_num = &n_list[0];
    for i in n_list {
        if largest_num < i {
            largest_num = i;
        }
    }
    largest_num
}

fn largest_char(n_list: &[char]) -> &char {
    let mut largest_char = &n_list[0];
    for i in n_list {
        if largest_char < i {
            largest_char = i;
        }
    }
    largest_char

Вместо того, чтобы писать 2 почти идентичные функции под 2 типа данных, можно объединить оба типа в 1 функцию, указав “неопределённый тип”.

Note

Нужно учесть, что не все типы данных имеют возможность сравнения объектов между собой, возможность выстроить их в порядок (Order). Поэтому в примере ниже надо дополнить тип в заголовке интерфейсом-свойством (trait) порядка.

fn largest_universal<T: std::cmp::PartialOrd>(list: &[T]) -> &T {  
    // <T> = неопределённый тип, со свойством упорядочивания PartialOrd
    let mut largest = &list[0];  
  
    for item in list {  
        if largest < item {  
            largest = item;  
        }  
    }  
    largest  
}

fn main() {  
    let num_list = vec![11, 6, 33, 56, 13];//упорядочить числа 
    println!("Largest number: {}", largest_universal(&num_list));  
  
    let char_list = vec!['y','m','a','q'];//упорядочить символы в той же функции  
    println!("Largest char: {}", largest_universal(&char_list));
}

Структуры с неопределёнными типами

Можно создавать структуры, тип данных которых заранее неопределён. Причём в одной структуре можно сделать несколько разных типов.

struct Point<T,U> {  // <T> и <U> - 2 разных типа
    x: T,  
    y: U,  
}

let integer = Point{x:5,y:6};  // в оба поля пишем числа типа i32
let float_int = Point{x:1,y:4.2}; // в поля пишем разные типы i32 и f32

Аналогично неопределённые типы можно делать в перечислениях:

enum Option<T> {
    Some(T), // <T> - возможное значение любого типа, которое может быть или нет
    None,
}

enum Result<T, E> {
    Ok(T), // T - тип результата успешной операции
    Err(E), // Е - тип ошибки
}

Методы со структурами с неопределёнными типами

Можно прописать методы над данными неопределённого типа. Важно в заголовке метода указывать при этом неопределённый тип, чтобы помочь компилятору отделить его от обычного типа данных. Можно при этом сделать реализацию для конкретных типов данных, например, расчёт расстояния до точки от центра координат будет работать только для чисел с плавающей точкой. Для других типов данных метод расчёта работать не будет:

struct Point<T> {  
    x: T,  
    y: T,  
}  
  
impl<T> Point<T> {  // указываем неопределённый тип в заголовке
    fn show_x(&self) -> &T {  // метод возвращает поле данных
        &self.x  
    } }

impl Point<f32> {  // указываем конкретный тип float в методе, 
// чтобы только для него реализовать расчёт. Для не-float метод не будет работать
    fn distance_from_origin(&self) -> f32 {  
        (self.x.powi(2) + self.y.powi(2)).sqrt()  
    } }
  
fn main() {  
    let p = Point{x:5,y:6};  
    println!("P.x = {}",p.show_x()); // вызов метода для экземпляра p
}
Tip

Код с неопределёнными типами не замедляет производительность, потому что компилятор проходит и подставляет конкретные типы везде, где видит generics (“monomorphization” of code).

Hashmaps

Hashmap<K, V>

Это изменяемая структура словарь (“dictionary” в Python), которая хранит пары “ключ->значение”. В Rust Prelude она не входит, макроса создания не имеет. Поэтому нужно указывать библиотеку явно и явно создавать структуру.

use std::collections::HashMap;  
  
fn main() {  
    let mut scores = HashMap::new();  
    scores.insert(String::from("Alpha"),1);  
    scores.insert(String::from("Beta"),2);  
    scores.insert(String::from("Gamma"),3); 
}

Все ключи Hashmap должны быть уникальны и одного типа, все значения должны быть одного типа.

Warning

Значения с кучи типа String перемещаются (move) в Hashmap, который становится их владельцем.

Взятие значения по ключу из Hashmap с помощью get нужно сопровождать проверкой - есть ли в памяти запрашиваемые ключ и значение:

let name = String::from("Gamma");  
if let Some(letter_num) = scores.get(&name) {  
    println!("{}",letter_num);  
} else { println!("Value not in HashMap!"); }

Итерация по Hashmap похожа на итерацию по вектору:

for (key, value) in &scores {  
    println!("{key} -> {value}"); }

Обновление Hashmap

Есть ряд стратегий обновления значений в Hashmap:

  • Перезаписывать всегда
scores.insert(String::from("Gamma"),3); // вставка дважды значений по одному 
scores.insert(String::from("Gamma"),6); // ключу сохранит последнее значение
  • Записывать значение, если у ключа его нет:
scores.entry(String::from("Delta")).or_insert(4); // entry проверяет наличие
// значения, or_insert возвращает mut ссылку на него, либо записывает новое 
// значение и возвращает mut ссылку на это значение.
  • Записывать значение, если ключа нет. Если же у ключа есть значение, модифицировать его:
use std::collections::HashMap;

let mut map: HashMap<&str, u32> = HashMap::new();

map.entry("poneyland") // первое добавление
   .and_modify(|e| { *e += 1 })
   .or_insert(42);     // добавит ключ "poneyland: 42"
assert_eq!(map["poneyland"], 42);

map.entry("poneyland") // второе добавление найдёт ключ со значением
   .and_modify(|e| { *e += 1 }) // и модифицирует его
   .or_insert(42);
assert_eq!(map["poneyland"], 43);
  • Записывать значение, если ключа нет, в виде результата функции. Эта функция получает ссылку на значение ключа key, который передаётся в .entry(key):
use std::collections::HashMap;

let mut map: HashMap<&str, usize> = HashMap::new();
map
  .entry("poneyland")
  .or_insert_with_key(|key| key.chars().count());

assert_eq!(map["poneyland"], 9);
  • Поднимать старое значение ключа, проверять его перед перезаписью:
let text = "hello world wonderful world";  
let mut map = HashMap::new();  
  
for word in text.split_whitespace() {  
    let count = map.entry(word).or_insert(0);  
    *count += 1;  
}  

println!("{:?}",map); // {"wonderful": 1, "hello": 1, "world": 2}

Инициализация HashMap со значениями по-умолчанию

Поведение аналогично типу defaultdict в Python. Заполнять ключами HashMap:

  • в случае отсутствия ключа, создавать его со значением по-умолчанию (0);
  • в случае присутствия ключа, добавлять к значению +1.
use std::collections::HashMap;
pub fn main() {
    let num_vec = vec![1, 2, 1, 3, 5, 2, 1, 4, 6];
    let mut number_count: HashMap<i32, i32> = HashMap::new();
    for key in num_vec {
        *number_count.entry(key).or_default() += 1;
    }
    for (k, v) in number_count {
        print!("{} -> {}; ", k, v);
    }
}
// 4 -> 1; 1 -> 3; 2 -> 2; 6 -> 1; 5 -> 1; 3 -> 1;

Modules Structure

External link: https://doc.rust-lang.org/stable/book/ch07-02-defining-modules-to-control-scope-and-privacy.html

Правила работы с модулями

  • Crate Root. При компиляции crate, компилятор ищет корень: src/lib.rs для библиотеки, либо src/main.rs для запускаемого файла (binary);
  • Модули. При декларировании модуля в crate root, например, mod test, компилятор будет искать его код в одном из мест:
    • Сразу после объявления в том же файле (inline);
    • В файле src/test.rs;
    • В файл src/test/mod.rs - старый стиль, поддерживается, но не рекомендуется.
  • Подмодули. В любом файле, кроме crate root, можно объявить подмодуль, например, mod automa. Компилятор будет искать код подмодуля в одном из мест:
    • Сразу после объявления в том же файле (inline);
    • В файле src/test/automa.rs;
    • В файле src/test/automa/mod.rs - *старый стиль, поддерживается, но не рекомендуется.
  • Путь до кода. Когда модуль часть crate, можно обращаться к его коду из любого места этой crate в соответствии с правилами privacy и указывая путь до кода. Например, путь до типа robot в подмодуле automa будет crate::test::automa::robot;
  • Private/public. Код модуля скрыт от родительских модулей по умолчанию. Для его публикации нужно объявлять его с pub mod вместо mod;
  • Use. Ключевое слово use нужно для сокращения написания пути до кода: use crate::test::automa::robot позволяет далее писать просто robot для обращения к данным этого типа.
Note

Нужно лишь единожды внести внешний файл с помощью mod в дереве модулей, чтобы он стал частью проекта, и чтобы другие файлы могли на него ссылаться. В каждом файле с помощью mod не надо ссылаться, mod - это НЕ include из Python и других языков программирования.

Пример структуры

my_crate
├── Cargo.lock
├── Cargo.toml
└── src
    ├── test
    │   └── automa.rs
    ├── test.rs
    └── main.rs

Вложенные модули

Для примера, возьмём библиотеку, обеспечивающую работу ресторана. Ресторан делится на части front - обслуживание посетителей, и back - кухня, мойка, бухгалтерия.

mod front {
    mod hosting {
        fn add_to_waitlist() {}
        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}
        fn serve_order() {}
        fn take_payment() {}
    }
}

Пример обращения к функции вложенного модуля

Объявление модуля публичным с помощью pub не делает его функции публичными. Нужно указывать публичность каждой функции по отдельности:

mod front_of_house {
    pub mod hosting { // модуль публичен, чтобы к нему обращаться
        pub fn add_to_waitlist() {} // функция явно публична
        // несмотря на публичность модуля, к функции обратиться нельзя
        // если она непублична
    }
}

pub fn eat_at_restaurant() {
    // Абсолютный путь через корень - ключевое слово crate
    crate::front_of_house::hosting::add_to_waitlist();

    // Относительный путь
    front_of_house::hosting::add_to_waitlist();
}

Обращние к функции выше уровнем

Относительный вызов функции можно сделать через super (аналог “..” в файловой системе):

fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order(); // вызов функции в родительском модуле
    }
    fn cook_order() {}
}

Обращение к структурам и перечислениям

Поля структур приватны по умолчанию. Обозначение структуры публичной с pub не делает её поля публичными - каждое поле нужно делать публичным по отдельности.

mod back_of_house {
    pub struct Breakfast {      // структура обозначена как публичная
        pub toast: String,      // поле обозначено публичным
        seasonal_fruit: String, // поле осталось приватным
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
    } } } }

pub fn eat_at_restaurant() {
    // Обращение к функции. Без функции к структуре с приватным полем 
    // не получится обратиться:
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // запись в публичное поле:
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // если раскомменировать строку далее, будет ошибка компиляции:
    // meal.seasonal_fruit = String::from("blueberries");
}

Поля перечислений публичны по умолчанию. Достатоно сделать само перечисление публичным pub enum, чтобы видеть все его поля.

mod back_of_house {
    pub enum Appetizer { // обозначаем перечисление публичным
        Soup,
        Salad,
    } }

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Обращение к объектам под другим именем

С помощью as можно создать ярлык на объект в строке с use. Особенно это удобно в случае, когда нужно обратиться к одинаковым по имени объектам в разных модулях:

use std::fmt::Result;
use std::io::Result as IoResult; // IoResult - ярлык на тип Result в модуле io

fn function1() -> Result {
    // ... }

fn function2() -> IoResult<()> {
    // ... }

Ре-экспорт объектов

При обращении к объекту с помощью use, сам ярлык этот становится приватным - через него могут обращаться только функции текущего scope. Для того, чтобы из других модулей функции могли тоже обратиться через этот ярлык, нужно сделать его публичным:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    } }

pub use crate::front_of_house::hosting; // ре-экспорт объекта

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist(); // обращение к функции через ярлык
}

Работа с внешними библиотеками

Внешние библиотеки включаются в файл Cargo.toml. Далее, публичные объекты из них заносятся в scope с помощью use.

# Cargo.toml
rand = "0.8.5"
use rand::Rng;
fn main() {
    let secret_number = rand::thread_rng().gen_range(1..=100);
}

Если нужно внести несколько объектов из одной библиотеки, то можно сократить количество use:

//use std::cmp::Ordering;
//use std::io;
use std::{cmp::Ordering, io}; // список объектов от общего корня

//use std::io;
//use std::io::Write;
use std::io{self, Write}; // включение самого общего корня в scope

use std::collections::*; // включение всех публичных объектов по пути
Warning

Следует быть осторожным с оператором glob - *, так как про внесённые с его помощью объекты сложно сказать, где именно они были определены.

Ownership and References

Ownership

Объявленная переменная, обеспеченная памятью кучи (heap) - общей памятью (не стека!) всегда имеет владельца. При передаче такой переменной в другую переменную, либо в функцию, происходит перемещение указателя на переменную = смена владельца. После перемещения, нельзя обращаться к исходной переменной.

let s1 = String::from("hello"); // строка в куче создана из литералов в стеке
let s2 = s1;                    // перемещение
println!("{}, world!", s1);     // ошибка! Вызов перемещённой переменной

Решения:

  • Можно сделать явный клон переменной со значением;
    let s1 = String::from("hello");
    let s2 = s1.clone();                  // полный клон. Медленно и затратно,
    println!("s1 = {}, s2 = {}", s1, s2); // но нет передачи владения
  • Передавать ссылку на указатель. Ссылка на указатель - ‘&’, раскрыть ссылку на указатель - ‘*’.
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);      // передача ссылки на указатель
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize { // приём ссылки на указатель
    s.len()
}

References

Для внесения изменений по ссылке на указатель, нужно указать это явно через ‘mut’.

fn main() {
    let mut s = String::with_capacity(32); // объявить размер блока данных заранее, чтобы потом не довыделять при закидывании данных в строку = быстрее
    change(&mut s); // передача изменяемой ссылки
}

fn change(some_string: &mut String) { // приём изменяемой ссылки на указатель
    some_string.push_str("hello, world");
}
Tip

Правила:

  1. В области жизни может быть лишь одна изменяемая ссылка на указатель (нельзя одновременно нескольким потокам писать в одну область памяти);
  2. Если есть изменяемая ссылка на указатель переменной, не может быть неизменяемых ссылок на указатель этой же переменной (иначе можно перезаписать данные в процессе их же чтения);
  3. Если ссылка на указатель переменной неизменяемая, можно делать сколько угодно неизменяемых ссылок на указатель (можно вместе читать одни и те же данные);
  4. Конец жизни ссылки определяется её последним использованием. Можно объявлять новую ссылку на указатель, если последняя изменяемая ссылка по ходу программы более не вызывается.
let mut s = String::from("hello");
{
    let r1 = &mut s;
} // r1 вышла из области жизни, поэтому можно объявить новую ссылку на указатель.
    let r2 = &mut s;

Regular Expressions

Внешняя ссылка: https://docs.rs/regex/latest/regex/

Справка по символам

1 символ

.             any character except new line (includes new line with s flag)
[0-9]         any ASCII digit
\d            digit (\p{Nd})
\D            not digit
\pX           Unicode character class identified by a one-letter name
\p{Greek}     Unicode character class (general category or script)
\PX           Negated Unicode character class identified by a one-letter name
\P{Greek}     negated Unicode character class (general category or script)

Классы символов

[xyz]         A character class matching either x, y or z (union).
[^xyz]        A character class matching any character except x, y and z.
[a-z]         A character class matching any character in range a-z.
[[:alpha:]]   ASCII character class ([A-Za-z])
[[:^alpha:]]  Negated ASCII character class ([^A-Za-z])
[x[^xyz]]     Nested/grouping character class (matching any character except y and z)
[a-y&&xyz]    Intersection (matching x or y)
[0-9&&[^4]]   Subtraction using intersection and negation (matching 0-9 except 4)
[0-9--4]      Direct subtraction (matching 0-9 except 4)
[a-g~~b-h]    Symmetric difference (matching `a` and `h` only)
[\[\]]        Escaping in character classes (matching [ or ])
[a&&b]        An empty character class matching nothing

Повторы символов

x*        zero or more of x (greedy)
x+        one or more of x (greedy)
x?        zero or one of x (greedy)
x*?       zero or more of x (ungreedy/lazy)
x+?       one or more of x (ungreedy/lazy)
x??       zero or one of x (ungreedy/lazy)
x{n,m}    at least n x and at most m x (greedy)
x{n,}     at least n x (greedy)
x{n}      exactly n x
x{n,m}?   at least n x and at most m x (ungreedy/lazy)
x{n,}?    at least n x (ungreedy/lazy)
x{n}?     exactly n x

Примеры

Замена всех совпадений в строке

Задача заменить строки, разделённые - или _ на CamelCase. При этом если строка начинается с заглавной буквы, то первое слово новой строки тоже с неё начинается:

"the-stealth-warrior" => "theStealthWarrior"
"The_Stealth_Warrior" => "TheStealthWarrior"
"The_Stealth-Warrior" => "TheStealthWarrior"

Решение:

use regex::Regex;
fn to_camel_case(text: &str) -> String {
    let before = text;
    let re = Regex::new(r"(-|_)(?P<neme>[A-z])").unwrap();
    let after = re.replace_all(before,|captures: &regex::Captures| {
        captures[2].to_uppercase()
    });
    return after.to_string();
}

Strings

Статья по ссылкам на память в Rust

String

Тип данных с владельцем. Имеет изменяемый размер, неизвестный в момент компиляции. Представляет собой векторную структуру:

pub struct String {
    vec: Vec<u8>;
}

Поскольку структура содержит Vec, это значит, что есть указатель на массив памяти, размер строки size структуры и ёмкость capacity (сколько можно добавить к строке перед дополнительным выделением памяти под строку).

&String

Ссылка на String. Не имеет владельца, размер фиксирован, известен в момент компиляции.

fn change(mystring: &mut String) {
    if !mystring.ends_with("s") {
        mystring.push_str("s");   // добавляем "s" в конец исходной строки
    }

str

Набор символов (литералов), размещённых на стеке. Не имеет владельца, размер фиксирован, известен в момент компиляции. Можно превращать str в String через признак from:

let text = String::from("TEST"); // "TEST" :str

&str

Ссылка на часть, slice от String (куча), str (стек) или статической константы. Не имеет владельца, размер фиксирован, известен в момент компиляции.

  • &String можно неявно превращать в &str;
  • &str нельзя неявно превращать в &String.
fn main() {
    let s = "hello_world";
    let mut mut_string = String::from("hello");
    success(&mutable_string);
    fail(s);
}

fn success(data: &str) { // неявный перевод &String -> &str
    println!("{}", data);
}

fn fail(data: &String) { // ОШИБКА - expected &String, but found &str
    println!("{}", data);
}
Warning

Пока существует &str её в области жизни нельзя менять содержимое памяти, на которое она ссылается, даже владельцем строки.

Примеры применения

Строковые константы

const CONST_STRING: &'static str = "a constant string"; 

Изменение строк

При наличии String, нужно передать ссылку &mut String для изменения:

fn main() {
 let mut mutable_string = String::from("hello ");
 do_mutation(&mut mutable_string);
 println!("{}", mutables_string); // hello world!
}

fn do_mutation(input: &mut String) {
 input.push_str("world!");
}

Строки с владением

Получение String с передачей владения нужно при получении строки от функции, передача в поток (thread):

fn main() {
    let s = "hello_world";
    println!("{}", do_upper(s)); // HELLO_WORLD
}

fn do_upper(input: &str) -> String { // возврат String
    input.to_ascii_uppercase()
}

Отображение части строки

Передавать владельца не нужно, передаём в &str:

let s = String::from("Hello World!");
let word = first_word(&s);
println!("The first word is: {}", word);
}

fn first_word(s: &String) -> &str { // передача строки по ссылке
let word_count = s.as_bytes();

for (i, &item) in word_count.iter().enumerate() {
    if item == b' ' {
    return &s[..i]; // возврат части строки как &str
    }
}
&s[..]  // обязательно указать возвращаемое значение, если условие в цикле выше ничего не вернёт (например, строка не содержит пробелов = вернуть всю строку)

Можно пройти по строке итератором chars() и его методами взятия N-го символа nth() спереди или nth_back() сзади:

let person_name = String::from("Alice");  
println!("The last character of string is: {}", match person_name.chars().nth_back(0) {  // ищем 1-ый символ с конца строки
        Some(i) => i.to_string(),  // если находим - превращаем в строку
        None => "Nothing found!".to_string(),  // не находим = сообщаем
    });  

Вывод строк

  • Макрос println! позволяет вывести строку в поток stdout;
// println!("Hello there!\n"); 
// раскрывается в такой код:
use std::io::{self, Write};
io::stdout().lock().write_all(b"Hello there!\n").unwrap();
  • Макрос format! позволяет сформировать строку и вернуть из функции;
  • Метод len() выдаёт длину строки;
  • Метод is_empty() проверят, что строка непустая;
  • Метод contains() ищет одну строку в другой строке;
  • Метод replace(from,to) заменяет часть строки на другую и выдаёт результат;
  • Метод splt_whitespace() позволяет делить строку на части по пробелам;
  • Метод push_str() позволяет добавить текст к строке (строка должна быть mut).
fn main() {    
    let mut a = String::from("Wonderful RUST World");  
        println!("Hello{}!", output_string(&a));  // вывод строки  
        println!("String is empty? {}", a.is_empty());  
        println!("String length: {}", a.len());  
        println!("Does string contain 'Hello'? {}", a.contains("Hello")); 
        println!("{}",a.replace("RUST","Python")); // Wonderful Python World
  
        for i in a.split_whitespace() {  
            println!("{}", i);  
        }  
      
     a.push_str(" And let's go!");  
     println!("{}",a);
}    
    
fn output_string(t: &String) -> String {  
    format!(", {}",t)   // возврат сформированной строки  
}

Повтор части строки n раз

Новый подход использует std::repeat

fn main() {
    let repeated = "Repeat".repeat(4);
    println!("{}", repeated); // RepeatRepeatRepeatRepeat
} 

Старый вариант через итератор - позволяет бесконечно отдавать любое значение (как generic):

use std::iter;

fn main() {
    let repeated: String = iter::repeat("Repeat").take(4).collect();
    println!("{}", repeated);
}

Структуры

Если структуре надо владеть своими данными - использовать String. Если нет, можно использовать &str, но нужно указать время жизни (lifetime), чтобы структура не пережила взятую ей строку:

struct Owned {
    bla: String,
}

struct Borrowed<'a> {
    bla: &'a str,
}

fn main() {
    let o = Owned {
        bla: String::from("bla"),
    };
    let b = create_something(&o.bla);
}

fn create_something(other_bla: &str) -> Borrowed {
    let b = Borrowed { bla: other_bla };
    b // при возврате Borrowed, переменная всё ещё в области действия!
}

Разделение строки на подстроки

Можно делить с помощью метода split(). В том числе можно делить по нескольким символам разом:

    let text = String::from("the_stealth-warrior");
    let parts = text2.split(['-', '_']);
    for part in parts {
        println!("{}", part);

Первая буква в строке

Чтобы проверить или изменить 1-ую букву в строке (в том числе иероглиф или иной вариант алфавита), нужно строку переделать в вектор из букв:

    let char_vec: Vec<char> = text.chars().collect();
    if char_vec[0].is_lowercase() { .. }

Гласные / согласные буквы

Проверку нужно написать в виде функции:

fn is_vowel(c: char) -> bool {  
    c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||  
    c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U' }

let text = String::from("Aria");

Разворот слов

Дана строка с пробелами между словами. Необходимо развернуть слова в строке наоборот, при этом сохранить пробелы.

fn reverse_words_split(str: &str) -> String {  
    str.to_string()
    .split(" ") // при разделении split() множественные пробелы сохраняются
    .map(|x| x.chars().rev().collect::<String>()) // разворот слов+сбор в строку
    .collect::<Vec<String>>().                    // сбор всего в вектор
    .join(" ")                                    // превращение вектора в строку
}

fn main() {  
    let word: &str = "The   quick brown fox jumps over the lazy dog.";  
    println!("{}",reverse_words_split(&word));  
}

// ehT   kciuq nworb xof spmuj revo eht yzal .god

Нахождение закономерностей в структурах со строками

В примере мы передаём вектор из строк. Далее, анализируем его по частям:

fn likes(names: &[&str]) -> String {
    match names {
        [] => "no one likes this".to_string(),
        [a] => format!("{} likes this", a),
        [a, b] => format!("{} and {} like this", a, b),
        [a, b, c] => format!("{}, {} and {} like this", a, b, c),
        [a, b, other @ ..] => format!("{}, {} and {} others like this", a, b, other.len()),
    }
}

Удаление пробелов в строке String

Use split(' '), filter out empty entries then re-join by space:

s.trim()
    .split(' ')
    .filter(|s| !s.is_empty())
    .collect::<Vec<_>>()
    .join(" ")

// Using itertools:
use itertools::Itertools;
s.trim().split(' ').filter(|s| !s.is_empty()).join(" ")

// Using split_whitespace, allocating a vector & string
pub fn trim_whitespace_v1(s: &str) -> String {
    let words: Vec<_> = s.split_whitespace().collect();
    words.join(" ")
}

Популярные строковые методы

Structures

Struct Data Type

Struct - комплексный изменяемый тип данных, размещается в куче (heap), содержит внутри себя разные типы данных. Он похож на кортеж (tuple), однако типы данных должны иметь явные именования.

struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64, // запятая в конце обязательна
}

Можно передать struct в функцию или вернуть из функции:

fn main() {
// создаём изменяяемый объект по структуре данных выше
let mut user1 = create_user(String::from("john@doe.com"), String::from("testuser")); 

println!("User Email is {}", user1.email);
user1.email = String::from("Parker@doe.com");
println!("User Email is {}", user1.email);
}

fn create_user(email: String, username: String) -> User { // возврат из функции
User {
active: true,
username,  
email,
// имена полей имеют с входными переменными, и можно не писать username: username, email: email.
sign_in_count: 1,
} // return заменяется отсутствием знака ";"" как обычно 
}

Updating Structs

Если нужно создать новую структуру по подобию старой, и большая часть полей у них похожи, то можно использовать синтаксический сахар:

let user2 = User {
email: String::from("another@example.com"), // задать новое значение поля
..user1 // взять остальные атрибуты из user1. Идёт последней записью
};

Tuple structs

Структуры такого вида похожи на кортежи, но имеют имя структуры и тип. Нужны, когда нужно выделить кортеж отдельным типом, либо когда надо дать общее имя кортежу. При этом отдельные поля не имеют имён.

struct Color (i32, i32, i32);
struct Point (i32, i32, i32);

fn main() {
let red = Color(255,0,0);
let origin = Point(0, 0, 0);

Переменные red и origin разных типов. Функции, которые берут Color как параметр, не возьмут Point, несмотря на одинаковые типы внутри. Каждая структура = свой собственный тип. Разбор таких структур на элементы аналогичен кортежам.

let (x,y,z) = (origin.0,origin.1,origin.2);

Unit-like structs

Структуры без полей аналогичны кортежам без полей, только с именем.

struct TestTrait;

fn main() {
 test = TestTrait;
}

Такие структуры нужны для задания признаков (traits), когда в самой структуре данные хранить не нужно.

Структурные признаки

Можно выводить информацию о содержимом полей структуры для анализа кода. Для этого нужно добавить над структурой пометку debug:

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
  let scale = 2;
  let rect = Rectangle {
  width: dbg!(20 * scale), // вывод поля структуры. dbg! возвращает назад
  height: 10,              // взятое значение, с ним далее можно работать
  };
  println!("Rectangle content: {:?}",rect); // вывод содержимого структуры
  dbg!(&rect); // ещё вариант вывода - в поток stderr. Функция dbg! 
               // забирает владение структурой, поэтому передача по ссылке
}

Структурные методы

Можно добавлять функции как методы, привязанные к структурам. Это позволяет организовать код более чётко - по объектам и действиям над ними.

struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {  // impl определяет блок методов структуры Rectangle
fn area(&self, scale) -> u32 { // 1-ый параметр всегда self = структура
 self.width * self.height * scale // тело функции и возврат значения
} }

fn main() {
  let rect = Rectangle {
  width: 20,
  height: 10,
  };
  println!("Rectangle area is {}", rect.area(2)); // вызов метода
}

Как и везде, для внесения изменений в объект структуры, в блоке методов можно объявить &mut self, а для перемещения владения - просто self. Это нужно изредка при превращении self в другой вид объекта, с целью запретить вызов предыдущей версии объекта. Блоков impl может быть несколько.

Асоциированные функции

В блоке методов impl можно добавлять функции, которые первым параметром не берут саму структуру self. Такие функции не являются методами и часто служат для создания новых версий объекта.

fn square(side: u32) -> Self { // Self - алиас для типа данных Rectangle
  Self {
    width: side,
    height: side,
} } }

fn main() {
  let sq = Rectangle::square(10); // вызов асоциированной функции через ::
  println!("Square created from {:?}",sq);
}

Создание типа данных с проверками

Вместо проверять введённые данные на корректность внутри функций, можно объявить собственный тип данных, содержащий в себе все необходимые проверки. Например, объявим число от 1 до 100 для игры, где надо угадать число:

pub struct Guess { // объявили тип данных (публичный)
    value: i32,    // внутри число (приватное)
}

impl Guess {
    pub fn new(value: i32) -> Guess { // метод new проверяет значение
        if value < 1 || value > 100 { // на заданные границы 1-100
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }
        Guess { value } // возврат нового типа данных
    }

    pub fn value(&self) -> i32 { // метод getter для получения значения value
        self.value               // он нужен, тк напрямую видеть value нельзя
    }                            // Это приватная переменная в структуре.
}

Threads

Создание

Потоки - объект ОС, способ разделения работы ПО. На обработку потоков могут назначаться разные ядра, а могут и не назначаться. Потоки выгодно использовать тогда, когда они дают выигрыш во времени больше, чем время на их создание (на x86 процессорах = ~9000 наносек, на ARM процессорах = ~27000 наносек). Обычно, это интенсивные по вычислениям приложения. Для интенсивным по вводу-выводу приложений следует использовать async/await вместо потоков.

Пример создания:

fn hello_thread() {
    println!("Hello from thread")
}

fn main() {
    println!("Hello from the MAIN thread");

    let thread_handle = std::thread::spawn(hello_thread);
    thread_handle.join().unwrap(); // нужно соединить новый поток с главным
    // потоком программы, иначе он может не успеть вернуть данные до
    // завершения главного потока программы
}

Потоки являются владельцами данных, поэтому нужно передавать им данные перемещением, чтобы они были живы к моменту запуска потока:

fn do_math(i: u32) -> u32 {
    let mut n = i + 1;
    for _ in 0..10 {
        n *= 2;
    }
    n
}

fn main() {
    println!("Hello from the MAIN thread");

    let mut thread_handles = Vec::new(); // вектор указателей потоков
    for i in 0..10 {
        let thread_handle = std::thread::spawn(move || do_math(i));
        thread_handles.push(thread_handle); // добавить поток к вектор
    }
    for h in thread_handles.into_iter() {
        println!("{}", h.join().unwrap()); // соединение потоков с главным.
    }                                // и вывод результата каждого потока. 

Разделение задачи на потоки

Простой вариант - поделить вектор со значениями на куски (chunks), и под обработку каждого куска сделать отдельный поток, после чего собрать потоки вместе:

fn main() {
    const N_THREADS: usize = 8;
    let to_add = (0..5000).collect::<Vec<u32>>(); // вектор от 0 до 4999
    let mut thread_handles = Vec::new(); // вектор указателей потоков
    let chunks = to_add.chunks(N_THREADS); // размер кусков разбиения

    for chunk in chunks {
        let my_chunk = chunk.to_owned(); // обход borrow checker/lifetime
        thread_handles.push(std::thread::spawn(move || my_chunk.iter().sum::<u32>())); // создание потоков с принадлежащими им данными
    }

    // суммирование потоков-кусков в одно число
    let mut sum: u32 = 0;
    for handle in thread_handles {
        sum += handle.join().unwrap()
    }
    println!("Sum is {sum}");
}

Traits

Инициализация типажа

Типаж нужен для организации интерфейса: он задаёт ограничения-особенности поведения для переменных или структур с неопределёнными (generic) переменными. Мы отделяем объявление типажа от его реализации. При объявлении типажа можно оставить обязательную реализацию на потом, либо вписать реализацию функций в типаже по-умолчанию:

// типаж задаёт метод и ограничения по входным/выходным типам
trait LandVehicle {  
    fn LandDrive(&self) -> String; }  

// типаж задаёт методы плюс их реализация по умолчанию
trait WaterVehicle {  
    fn WaterDrive(&self) { println!("Default float"); }  
}

Применение типажей к структурам данных

Во время применения, если реализация по умолчанию была задана, то можно её переделать под конкретную структуру, либо использовать эту реализацию:

struct Sedan {}  
struct Rocketship {}  

// типаж LandVehicle не имеет реализации по умолчанию, реализуем тут
impl LandVehicle for Sedan {  
    fn LandDrive(&self) -> String { format!("Car zoom-zoom!") } }

// типаж WaterVehicle имеет выше реализацию по умолчанию, используем её
impl WaterVehicle for Rocketship {}

Объединение типажей

При объединении, создаётся ярлык (alias). При этом каждый входящий в него типаж нужно отдельно применить к структуре данных. При этом можно также использовать реализацию определённых в типаже методов по умолчанию, либо написать свою.

// создание ярлыка
trait AmphibiousVehicle: LandVehicle + WaterVehicle {}

// применение типажей к структуре
impl AmphibiousVehicle for Carrier {}  
impl LandVehicle for Carrier {  
    fn LandDrive(&self) -> String { format!("Use air thrust to travel on land") }  
}  
impl WaterVehicle for Carrier {}

Вызов методов экземпляра структуры определённого типажа

fn main() {  
    let toyota_camry = Sedan {};  
    println!("{}",toyota_camry.LandDrive());
  
    let rs = Rocketship {};  
    rs.WaterDrive();  
  
    let project_x = Carrier {};  
    println!("{}",project_x.LandDrive());  
    project_x.WaterDrive();  
}

Variables and constants

Note

When in doubt on variable type: just use i32 for everything! i32 is the default in Rust, and the fastest, even on x64 architecture.

Scalar Types

Unsigned Signed
u8 i8
u16 i16
u32 i32
u64 i64
u128 i128
usize isize
Floating point vars: f32, f64.

You can declare number variables with _ sign in any place for convenience:

let x: i32 = 1_000_000;
let (mut missiles: i32, ready: u32) = (8, 5); // 2 vars tuple assign in 1 line

You can also add the var type to the number (convenient with _ when using generics):

let x = 1000_u32;
let y = 3.14_f32;

Converting

String2Int:

let n_str = "123456789".to_string();
let n_int = n_str.parse::<i32>().unwrap();

Char2Int:

let letter = 'a';
println!("{}", letter as u32 - 96); // = 97-96 = 1
let i = 97u8; // только с u8 разрешено делать 'as char'
println!("Value: {}", i as char);

Boolean type

bool type can be true or false. Non-integer - do NOT try to use arithmetic on these. But you can cast them:

true as u8;
false as u8;

Mutablitity

By default, variables in Rust are immutable. To make a variable mutable, special “mut” identifier must be placed. Rust compiler may infer the variable type from the type of value.

let x = 5; // immutable variable, type i32 guessed by Rust as default for numbers.

let mut x = 5; // mutable variable

Shadowing

Variable names can be reused. This is not mutability, because shadowing always re-creates the variable from scratch. Previous variable value may be used:

let x = 5; 
let x = x + 1; // new variable created with value = 6

Constants

Constant values are always immutable and available in the scope they were created in throughout the whole program. Type of constant must always be defined. Constants may contain results of operations. They are evaluated by Rust compiler. List of evaluations: https://doc.rust-lang.org/stable/reference/const_eval.html

const ONE_DAY_IN_SECONDS: u32 = 24 * 60 * 60; // type u32 MUST be defined

let phrase = "Hello World";
println!("Before: {phrase}"); // Before: Hello World

let phrase = phrase.len();
println!("After: {phrase}"); // After: 11

Compound variables

Tuple

Compound type, immutable, consists of different types.

let tup: (u32, f32, i32) = (10, 1.2, -32);
let (x,y,z) = tup; // tuple deconstructing into variables
let a1 = tup.0;
let a2 = tup.1; // another way to deconstruct values

Deconstructing tuples is very useful when a function returns a tuple:

let (left, right) = slice.split_at(middle);
let (_, right) = slice.split_at(middle); // use '_' to throw away part of return

Array

Compound type, mutable, all values of the same type.

let ar: [i32;5] = [1,2,3,4,5]; 
// array data is allocated on the stack rather than the heap
// [i32;5] - type of values and number of elements
let first = ar[0]; 
let second = ar[1]; // accessing array elements

Подсчёт одинаковых элементов в массиве с помощью itertools…

Vectors

Vectors

Вектор - множество данных одного типа, количество которых можно изменять: добавлять и удалять элементы. Нужен, когда:

  • требуется собрать набор элементов для обработки в других местах;
  • нужно выставить элементы в определённом порядке, с добавлением новых элементов в конец;
  • нужно реализовать стэк;
  • нужен массив изменяемой величины и расположенный в куче.

Методы

// Задание пустого вектора:
// let mut a test_vector: Vec<i32> = Vec::new();  

// Задание вектора со значениями через макрос:
let mut test_vector = vec![1, 2, 3, 4];  

test_vector.push(42);  // добавить число 42 в конец mut вектора
test_vector.remove(0);  // удалить первый элемент =1
  
for i in &mut test_vector {  // пройти вектор как итератор для вывода
*i += 1; // изменять значения при их получении требует делать '*' dereference
println!("{i}"); }

println!("Vector length: {}", test_vector.len()); // количество элементов

Получение элемента вектора

Элемент можно получить с помощью индекса, либо с помощью метода get:

let mut test_vector = vec![1,2,3,4,5];  
  
println!("Third element of vector is: {}", &test_vector[2]);  // индекс
  
let third: Option<&i32> = test_vector.get(2);  // метод get
match third {  
    Some(third) => println!("Third element of vector is: {}", third),  
    None => println!("There is no third element")  
}

Разница в способах в реакции на попытку взять несуществующий элемент за пределами вектора. Взятие через индекс приведёт к панике и остановке программы. Взятие с помощью get сопровождается проверкой и обработкой ошибки.

Хранение элементов разных типов в векторе

Rust нужно заранее знать при компиляции, сколько нужно выделять памяти под каждый элемент. Если известны заранее все типы для хранения, то можно использовать промежуточный enum:

#[derive(Debug)]  
enum SpreadSheet {  
    Int(i32),  
    Float(f64),  
    Text(String)  
}  
  
fn main() {  
    let row = vec![  
      SpreadSheet::Int(42),  
      SpreadSheet::Float(3.14),  
      SpreadSheet::Text(String::from("red"))  
    ];  
  
    for i in row {  
        println!("{:?}",i);  
    }  }

Vector со строками String

let mut v: Vec<String> = Vec::new();

Пустой вектор с нулевыми строками можно создать через Default размером до 32 элементов (Rust 1.47):

let v: [String; 32] = Default::default();

Вектор большего размера можно создать через контейнер Vec:

let mut v: Vec<String> = vec![String::new(); 100];

Вектор с заданными строками можно инициализировать либо с помощью метода to_string(), либо через определение макроса:

macro_rules! vec_of_strings {
    ($($x:expr),*) => (vec![$($x.to_string()),*]);
}

fn main()
{
    let a = vec_of_strings!["a", "b", "c"];
    let b = vec!["a".to_string(), "b".to_string(), "c".to_string()];
    assert!(a==b); // True
}

Соединение вектора со строками в строку (Join):

result_vec.join(" "); // указывается разделитель для соединения
// в старых версиях Rust <1.3 применяют метод .connect();

Сортировка

let number_vector = vec!(1,12,3,1,5);   
number_vector.sort(); // 1,1,3,5,12

Способы реверс-сортировки

Смена элементов при сравнении:

number_vector.sort_by(|a,b| b.cmp(a));

Сортировка, потом реверс:

number_vector.sort();
number_vector.reverse();

Обёртка Reverse с экземпляром Ord:

use std::cmp::Reverse;
number_vector.sort_by_key(|w| Reverse(*w));

Если вернуть Reverse со ссылкой и без *, это приведёт к проблеме с временем жизни.

Получение вектора из итератора

    let collected_iterator: Vec<i32> = (0..10).collect();
    println!("Collected (0..10) into: {:?}", collected_iterator);
    // Collected (0..10) into: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Конвертация

Конвертация из массива array в vector:

let number_list = [1,12,3,1,5,2];  
let number_vector = number_list.to_vec(); // перевод array[i32] -> vector<i32>

Вариант через итератор:

let a = [10, 20, 30, 40]; 
let v: Vec<i32> = a.iter().map(|&e| e as i32).collect(); 

Вектор из байтов vector of bytes в строку String:

use std::str;

fn main() {
    let buf = &[0x41u8, 0x41u8, 0x42u8]; // vector of bytes
    let s = match str::from_utf8(buf) {
        Ok(v) => v,
        Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
    };
    println!("result: {}", s);
}

Subsections of Operations

Alpine Linux

Installation on ESXi

External Link

  • Enable Community repo in file /etc/apk/repositories
  • Install: apk add --update open-vm-tools
  • Enable at boot: rc-service open-vm-tools start rc-update add open-vm-tools

Network Config

External Link In file: /etc/network/interfaces

DHCP: iface eth0 inet dhcp

STATIC:

iface eth0 inet static
        address 192.168.1.150/24
        gateway 192.168.1.1

DNS

In file: /etc/resolv.conf

nameserver 8.8.8.8
nameserver 8.8.4.4

Configue CGROUPS for k8s

External Link rc-update add cgroups

Bugfix:

echo "cgroup /sys/fs/cgroup cgroup defaults 0 0" >> /etc/fstab

cat > /etc/cgconfig.conf <<END
mount {
  cpuacct = /cgroup/cpuacct;
  memory = /cgroup/memory;
  devices = /cgroup/devices;
  freezer = /cgroup/freezer;
  net_cls = /cgroup/net_cls;
  blkio = /cgroup/blkio;
  cpuset = /cgroup/cpuset;
  cpu = /cgroup/cpu;
}
END

sed -i '/^default_kernel_opts/s/=.*$/="quiet rootfstype=ext4 cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory"/' /etc/update-extlinux.conf
update-extlinux
apk add --no-cache iptables curl

Ansible

Articles in section

Installation

Do not install Ansible from local OS repo - it is usually older in version there. Use latest Python pip repo to install a fresh version.

sudo apt install python3-distutils
wget http://bootstrap.pypa.io/get-pip.py
sudo python3 get-pip.py
sudo pip3 install ansible
ansible --version
Tip

Ansible will be the latest version supporting current Python in OS. So to get latest Ansible , Python must be updated as well!

Warning

Do not update the default Python in OS - it is used by system services, which may break!

Build Python:

sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev curl libbz2-dev

wget https://www.python.org/ftp/python/3.9.15/Python-3.9.15.tgz

tar -xf Python-3.9.15.tgz

cd Python-3.9.15
./configure --enable-optimizations

make -j 8

Installing Python:

  • Best way is to do checkinstall, create a .deb file to share with the team.
  • Use altinstall parameter to install Python in an alternative path. This is simpler & better than using Python virtualenv.
sudo make altinstall # now Python 3.9x is installed on separate path, while Python 3.7 in OS is unchanged

sudo python3.9 -m pip install --upgrade pip

sudo python3.9 -m pip install ansible

ansible --version # now ansible is the latest version

Architecture

graph LR
I[(Inventory)] --> R1(Role01);
M(Modules) --> R1;
I[(Inventory)] --> R2(Role02);
M(Modules) --> R2;
I[(Inventory)] --> RN(Role N);
M(Modules) --> RN;
R1 -->|include role| P(Playbook);
R2 -->|include role| P(Playbook);
RN -->|include role| P(Playbook);
P --> C(Ansible Config);
C --> Py(Python);
Py-->|SSH|Client01;
Py-->|SSH|Client02;
Py-->|SSH|Client_N;

Folder Structure

video.yml - main program:

---
- import_playbook: playbooks/db.yml
- import_playbook: playbooks/front.yml
- import_playbook: playbooks/zoneminder.yml
...

front.yml - role example:

---
- name: front-end play
  hosts: all
  gather_facts: yes
  become: yes

  tasks:
    - name: include role apache
      include_role:
        name: apache

    - name: include role php
      include_role:
        name: php
...

apache / tasks / main.yml - tasks example:

---
- name: install apache
  apt:
  name: apache2

- name: Enable service apache2
  ansible.builtin.systemd:
    name: apache2
    enabled: yes
    masked: no

- name: Make sure apache2 is running
  ansible.builtin.systemd:
    state: started
    name: apache2
...

Role structure

Directories inside a role:

- defaults - variable values by default
-- main.yml

- vars - variables defined by role (for other roles)
-- main.yml

- tasks - jobs to be completed
-- main.yml

- handlers - actions to be taken after checks
-- main.yml

- files - static files to be copied into client machine
- templates

Subsections of Ansible

Inventory

Initialization

Create inventory file with IP addresses. Check computers in inventory by doing a ping:

ansible all -i inventory -u vagrant -m ping -k
# -i - <inventory file>
# -u - <username>
# -m - <select module>
# -k - <interactive password prompt>
ansible all -i inventory -u admin -m ping -k -vvv # debug mode with verbosity 3

Inventory examples:

[db]
vm ansible_host=192.168.1.98 ansible_user=aagern

[app]
vm ansible_host=192.168.1.98 ansible_user=aagern

[front]
vm ansible_host=192.168.1.98 ansible_user=aagern

[all:vars]
ansible_ssh_extra_args="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
ansible_python_interpreter=/usr/bin/python3
web1 ansible_ssh_host=192.168.33.20
db1 ansible_ssh_host=192.168.33.30

[webservers]
web1

[dbservers]
db1

[datacenter:children]
webservers
dbservers

[datacenter:vars]
ansible_ssh_user=vagrant
ansible_ssh_pass=vagrant

Configuration

Order of precedence for the config file:

  1. $ANSIBLE_CONFIG env variable  (first place to be searched)
  2. ./ansible.cfg - local file in the working directory
  3. ~/.ansible.cfg - user home directory
  4. /etc/ansible/ansible.cfg - global config (last place to be searched)

First config file found wins, Ansible stops looking for other config files.

Environment override

Specify: $ANSIBLE_

Override settings on the fly:

$ export ANSIBLE_FORKS=10

[defaults] forks - how many parallel processes does Ansible handle. Default=5, production recommended=20

host_key_checking - check host key before sending commands. Default=True (for production), for dev/test systems =False (easy control)

log_path - Ansible logging. Default=Null, produstion recommended - set path all Ansible users can write to.

Target patterns

Patterns to choose hosts/groups:

  • OR pattern: group1:group2

Example:

ansible webservers:dbservers -i inventory -m service -a "name=iptables state=stopped" --sudo
  • NOT pattern: :!group2
  • Wildcard pattern: web.lab.local*
  • REGEX pattern: ~web[0-9]+
  • AND pattern: group1:&group2 - apply to hosts BOTH in group1 and group2 (intersection)
  • Complex patterns

Complex pattern example: webservers:&production:!python3 # apply to web servers in production but not in the Python3 group

Modules

Help on modules

ansible-doc -l # all installed modules list
ansible-doc <module-name> # module man
ansible-doc -s <module-name> # playbook snippets code examples on module

Module examples

Copy module

  • Copies a file from the local box to a remote system - useful for copying config files to remote system
  • can do backups
  • can do remote validation

Fetch module

  • Copy a file from remote system to local box
  • validate file using md5 checksum

Setup module

  • Gather info and facts on remote system
  • Inventory analysis of the system
ansible -i inventory web1 -m setup # gather all available system info
ansible -i inventory web1 -m setup -a "filter=ansible_eth*" # gather info on NICs
ansible -i inventory all -m setup --tree ./setup # form an inventory of files in /setup/ folder with info on targeted systems

Apt module (for Debian systems) / Yum module (RedHat systems)

  • Install, update, delete packages
  • Can update entire system Example for Yum:
ansible webservers -i inventory -m yum -a "name=httpd state=present" -u vagrant --sudo
# name - name of package (Apache)
# present - if package is not there, install. If it is there - do nothing and report "changed: false" (idempotence test)

Service module

  • Start/stop/restart services
  • Set autostart option for services
ansible webservers -i inventory -m service -a "name=httpd state=started enabled=yes" -u vagrant --sudo

# name - name of service (Apache)
# state = started/stopped - make idempotence test and change if necessary
# enabled = yes/no - autostart on system boot

Playbooks

General tasks usage

# Tasks in a playbook are executed top down. Tasks use modules.
tasks:
  - name: Name the task for readability
    module: parameters=go_here

# Example:
  - name: Deploy Apache Configuration File
    copy: src=./ansible/files/configuration/httpd.conf

          dest=/etc/httpd/conf/

Playbook execution: ansible-playbook my_playbook.yml

Playbook example:

---
# -------- Global play declaration
- hosts: webservers    
  ## ----- Variables per play
  vars:
    git_repo: https://github.com/repo.git
    http_port: 8081
    db_name: wordpress

  ## ------------------------
  ### ---- Declare user to run tasks
  sudo: yes
  sudo_user: wordpress_user

  ### ------------------------------
  gather_facts: no # dont't gather facts with SETUP module (default gathers facts - expensive in time)
  remote_user: root
  tasks:

# --------------------------------
  - name: Install Apache
    yum: name=httpd state=present
  - name: Start Apache
    service: name=httpd state=started

Including files

Use “- include” and “- include_vars” directives to include playbook files:

tasks:
- include: wordpress.yaml
  vars:
    sitename: My Awesome Site
    
- include: reverse_proxy.yaml
- include_vars: variables.yaml

Register task output

Use the output of one task for another task:

tasks:
- shell: /usr/bin/whoami
  register: username

- file: path=/home/myfile.txt
  owner: {{ username }}

Debugging tasks with Debug module

Add screen output and print content of variables:

tasks:
  - debug: msg="This host is {{ inventory_hostname }} during execution"

  - shell: /usr/bin/whoami
    register: username
    
  - debug: var=username

Input during playbook execution

Promt user during execution:

- hosts: web1
  vars_prompt:
  - name: "sitename"
    prompt: "What is the new site name?"

  tasks:
    - debug: var=username

Playbook handlers

A handler can be informed to execute a task (restart service) only if state=changed.

  • Run after all tasks
  • Run only once no matter how many times they are called

Handlers syntax is the same as tasks syntax:

  tasks:
  - name: Copy Apache Config files
  - copy: src=./files/httpd.conf
          dest=/etc/httpd/config/

    notify:
      - Apache Restart

  handlers:
  - name: Apache Restart
    service: name=httpd state=restarted

“When” condition

Commence a task if condition is True:

tasks:
- name: Install Httpd Package
  apt: name=httpd state=present
  when: ansible_os_family == "RedHat"

- name: Install Apache2 Package
  yum: name=apache2 state=present
  when: ansible_os_family == "Debian"

Check output of previous task as condition to run next task:

tasks:
- name: Stop iptables now
  service: name=iptables state=stopped
  register: result
  ignore_errors: yes   # supress default stop on error

- debug: msg="Failure!"
  when: result|failed  # Debug message will only be shown if task has failed
                       # other conditions are "result|success", "result|skipped"

Checking variables with WHEN condition

Bring variable check to BOOL check:

- name: "test"
  hosts: local
  vars_prompt:
    - name: "os_type"
      prompt: "What OS? (centos or ubuntu)"
      default: "centos"
      private: no
  vars:
    - is_ubuntu: "{{os_type == 'ubuntu'}}"
    - is_debian: "{{os_type == 'debian'}}"
  tasks:
    - debug: msg="this shows the conditional if variable equals ubuntu"
      when: is_ubuntu|bool
      
    - debug: msg="this shows the conditional if variable equals centos"
      when: is_centos|bool

eBPF

Статьи в разделе

eBPF File monitoring

History

Early approach: periodic scanning of the file-system and comparing the expected state with the actual state. Limitations:

  • can only be used to detect modifications and not reads to files;
  • unreliable because a modification can go undetected if the file is modified, then returned back to its original state before the scanning occurs:
    • A quick attacker can read/modify target file, and clean up their tracks before the periodic scan.

Later approach: in-kernel inotify:

  • addresses unreliability - executed inline with the operation;
  • no way to associate or filter operations using the execution context (e.g., pid or cgroup) of the process doing the operation -> no way to filter events based on which Kubernetes workload performed the file access;
  • lack of flexibility in the actions taken when a file is accessed:
    • When a monitored file is accessed,  it will send an event to user-space and it’s up to the user-space agent to do the rest:
      • Example: when monitoring the directory /private/data, the sequence of operations would be:
  1. Agent adds /private into the directories to be watched
  2. Application creates /private/data directory
  3. inotify sends an event to the agent that a directory /private/data was created
  4. Agent adds /private/data to the directories to be watched

If a file was created and/or accessed in /private/data/* between steps 2 and 4, there will be no inotify event for the access prior to it being added to the watch list.

  • Possible to modify the kernel to add execution context to inotify events:
    • long process, might take years until a new kernel reaches production.

eBPF allows FIM implementation to correlate file access events with execution context such as process information (e.g., credentials) and its cloud native identity (e.g., k8s workload), perform inline updates to its internal state to avoid races, as well as implement inline enforcement on file operations.

Path-based FIM with eBPF

Install eBPF hooks (kprobes, specifically) to track file operations and implement File Integrity Monitoring (FIM).

  • install these hooks into system calls: the open system call to determine when a file is opened and for what access (read or write):
    • Hooking into system calls, however, might lead to time-of-check to time-of-use (TOCTOU) issues: the memory location with the pathname to be accessed belongs to user-space, and user-space can change it after the hook runs, but before the pathname is used to perform the actual open in-kernel operation:

Hooking into a (kernel) function that happens after the path is copied from user-space to kernel-space avoids this problem since the hook operates on memory that the user-space application cannot change. Hence, instead of a system call we will install a hook into a security_function. Specifically, we will hook into the security_file_permission function which is called on every file access (there is also security_file_open which is executed whenever a file is opened). Information about the process and its parent such as binary, arguments, credentials, and others. In cloud-native environments, the events also contain information about the container and the pod that this process belongs to.

If no filtering is applied, we get a file-all policy: generate events for every file access:

  • Many file accesses happen in a system at any point in time, and monitoring all of them is not a good practice because generating an event for each one incurs significant overhead;
  • file-all policy does not inform users about what file was actually accessed;
  •  We create a second version of the policy where sensitive ssh server private keys are monitored:

Tetragon policy with filtering of SSH keys:

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "sensivite-files"
spec:
  kprobes:
  - call: "security_file_permission"
	syscall: false
	args:
	- index: 0
  	   type: "file" #(struct file *) получаем путь
	selectors:
	- matchArgs: 	 
  	- index: 0
    	operator: "Equal"
    	values:
    	- "/etc/shadow"
    	- "/etc/sudoers"
    	- "/etc/ssh/ssh_host_ecdsa_key"
  • Filtering in-kernel & deciding at the eBPF hook whether the event is of interest to the user or not, means that no pointless events will be generated and processed by the agent;
  • The alternative is to do filtering in user-space tends to induce significant overhead for events that happen very frequently in a system (such as file access). For more details, see Tetragon’s 1.0 release blog post.

 * security_file_permission - the eBPF hook is called on every file access in the system  * Use security_file_open and have the eBPF hook be executed whenever a file is opened. However, it means that if a file is already opened before the hook is installed, the hook will not be called and certain accesses may be missed  * Hooks into other functions such as security_file_truncate or security_file_ioctl for other operations;

eBPF lets you do observability and do inline enforcement by stopping an operation from happening by modifying the request.

Example of denying /usr/bin.cat ssh files access:

      matchBinaries:
      - operator: "In"
        values:
        - "/usr/bin/cat"
      matchActions:
      - action: Override
        argError: -1

It is impossible to do proper enforcement without in-kernel filtering, because by the time the event has reached user-space it is already too late if the operation has already executed.

Path FIM limitations

  • Paths are taken from from struct file arguments of functions such as security_file_open;
  • The same file can have multiple names in a Linux system:
    • If a policy monitors /etc/ssh/ssh_host_rsa_key but the same underlying file is accessed via a different name, the access will go unnoticed;
    • Same file can have multiple names are hard links, bind mounts, and chroot.
  • Hard link to the file /etc/ssh/ssh_host_rsa_key named, for example, /mykey accesses via /mykey will not be caught by policies such as file-ssh-keys:
    • Creating hard links requires appropriate permissions (when  fs.protected_hardlinks  is set to 1, creating a link requires certain permissions on the target file);
    • bind mount requires CAP_SYS_ADMIN;
    • chroot requires CAP_CHROOT.
  • We need the ability to monitor file accesses regardless of the name with which the file is accessed.

inode-based FIM with eBPF

An inode number uniquely identifies an underlying file in a single filesystem. Example:

# stat /etc/ssh/ssh_host_ecdsa_key | grep Inode
Device: 259,2   Inode: 36176340	Links: 1
# ln /etc/ssh/ssh_host_ecdsa_key /key
# stat /key | grep Inode
Device: 259,2   Inode: 36176340	Links: 2
# touch /key2
# mount --bind /etc/ssh/ssh_host_ecdsa_key /key2
# stat /key2 | grep Inode
Device: 259,2   Inode: 36176340	Links: 2

Диаграмма работы сканера

sequenceDiagram
autonumber
actor U as User
participant A as Агент
participant S as Сканнер
participant F as Файл
participant B as Программа eBPF
participant I as Карта inodes
U->>A: политика
activate A
activate U
A->>S: шаблон
S->>F: получить inode
activate F
activate S
F-->>S: inode
S->>I: обновить список inodes
deactivate S
activate I
F->>B: событие
deactivate F
activate B
loop Синк в ядре
B->>I: запрос списка inodes
I-->>B: список inodes
end
deactivate I
B-->>A: событие
deactivate B
A-->>U: уведомление
deactivate A
deactivate U

Subsections of eBPF

Tetragon Filemon

File access traces with Tetragon

Tracing policies can be added to Tetragon through YAML configuration files that extend Tetragon’s base execution tracing capabilities. These policies perform filtering in kernel to ensure only interesting events are published to userspace from the BPF programs running in kernel. This ensures overhead remains low even on busy systems.

https://tetragon.io/docs/getting-started/file-events/

Tetragon 1.0: Kubernetes Security Observability & Runtime Enforcement with eBPF

https://isovalent.com/blog/post/tetragon-release-10/#observability-benchmarks-understanding-tetragon-performance

File Monitoring with eBPF and Tetragon (Part 1)

https://isovalent.com/blog/post/file-monitoring-with-ebpf-and-tetragon-part-1/

Grafana Loki

Installation

Create dirs for Loki:

cd ~
mkdir promtail
mkdir loki
mkdir grafana

Create a Docker-Compose file

vim docker-compose.yml

version: "3"

networks:
loki:

services:
loki:
image: grafana/loki:2.4.0
volumes:
- /home/aagern/loki:/etc/loki
ports:
- "3101:3100"
restart: unless-stopped
command: -config.file=/etc/loki/loki-local-config.yaml
networks:
- loki

promtail:
image: grafana/promtail:2.4.0
volumes:
- /var/log:/var/log
- /home/aagern/promtail:/etc/promtail
restart: unless-stopped
command: -config.file=/etc/loki/promtail-local-config.yaml
networks:
- loki

grafana:
image: grafana/grafana:latest
user: "1000"
volumes:
- /home/aagern/grafana:/var/lib/grafana
ports:
- "3000:3000"
restart: unless-stopped
networks:
- loki
Note

The config files MUST be in the inner container directory!

Loki Configs

wget https://raw.githubusercontent.com/grafana/loki/master/cmd/loki/loki-local-config.yaml
 
wget https://raw.githubusercontent.com/grafana/loki/main/clients/cmd/promtail/promtail-local-config.yaml

Promtail config

server:
http_listen_port: 9080
grpc_listen_port: 0
 
positions:
filename: /tmp/positions.yaml
 
clients:
- url: http://loki:3101/loki/api/v1/push
 
# Local machine logs
scrape_configs:
- job_name: local
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*log
 
#scrape_configs:
#- job_name: docker
# pipeline_stages:
# - docker: {}
# static_configs:
# - labels:
# job: docker
# path: /var/lib/docker/containers/*/*-json.log

Create the containers

cd ~
sudo docker-compose up -d --force-recreate
sudo docker ps

Data Sources

Go to Grafana :3000 First login/password = admin/admin

Add New Data Source… → Loki
http://loki:3100

Explore {job=varlogs} |= “Network”

Docker logs

scrape_configs:
- job_name: docker
 pipeline_stages:
  - docker: {}
  static_configs:
   - labels:
   job: docker
   path: /var/lib/docker/containers/*/*-json.log

Install a Docker Driver https://grafana.com/docs/loki/latest/clients/docker-driver/

docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
sudo docker plugin ls

Check config: https://grafana.com/docs/loki/latest/clients/docker-driver/configuration/

{
"debug" : true,
"log-driver": "loki",
"log-opts": {
"loki-url": "http://localhost:3100/loki/api/v1/push",
"loki-batch-size": "400"
}
}
  • Config: vim /etc/docker/daemon.json
  • Restart Docker: sudo systemctl restart docker

Kubernetes

Статьи в разделе

Master Node

Мастер нода представляет службы управления (Control Plane)

  • Кластер Etcd ведёт запись всех нод, размещённых на них контейнерах, запись иных данных - это СУБД для Kubernetes, заточенная на согласованности данных и их доступности;
  • Kube-scheduler: команды создания и переноса контейнеров на worker nodes. Считает число ресурсов на нодах и подбирает размещение pods на нодах в соответствии с профилем потребляемых ресурсов;
  • Kube API Server: служба обмена сообщениями в кластере k8s. Аутентификация отправителя сообщения, валидирует отправителя сообщений, регистрирует сообщения по интерфейсу API в базу Etcd; Это единственный компонент, который общается напрямую с Etcd;
  • Kube Controller Manager: содержит службы контроля Node Controller (следит за доступностью нод), Replication Controller (отслеживание распространения копий контейнеров в рамках группы репликации по нодам).

Worker Nodes

Ноды-работники размещают у себя контейнеры через Container Runtime Interface (CRI) с поддержкой containerd (через него Docker, с версии k8s 1.24+) и Rocket:

  • Для приёма команд и передачи статистики по рабочей ноде используется kubelet, служба управления нодой;
Note

Kubeadm не устанавливает автоматически Kubelet-ы. Они всегда ставятся вручную на worker nodes.

  • Для связи с нодой применяется служба Kube-proxy. Создаёт правила проброса потоков данных от служб к pods, на которых они размещены. Один из способов - создание правил iptables;

crictl

Проверка и решение проблем с рабочими нодами. В отличие от утилит Docker, crictl понимает pods.

crictl images # список образов
circtl ps -a # список контейнеров
crictl exec -i -t 288023742....aaabb392849 ls # запуск команды в контейнере
crictl logs 288023742....aaabb392849 # посмотреть лог контейнера
crictl pods 

IDE

Для написания YAML-файлов хорошо подходит редактор с плагином, понимающим k8s. Пример: VSCode + Red Hat YAML plugin

В свойствах плагина найти пункт Yaml: Schemas -> Edit in settings.json Добавить в конфиг:

{
    "yaml.schemas": {
        
    "kubernetes": "*.yaml"
    },
    "redhat.telemetry.enabled": true
}

Это позволит все файлы YAML редактировать с учётом полей, принятых для k8s.

Subsections of Kubernetes

Controller Manager

Node Controller

Отслеживает состояние нод (через kube-apiserver):

  • Отслеживает статус;
  • Реагирует на проблемы;
  • Node Monitor Period = 5 секунд на опрос всех нод через kube-apiserver;
  • Node Monitor Grace Period = 40 секунд время ожидания, если нода не отвечает - отметить её как Unreachable;
  • POD Eviction Timeout = 5 минут на ожидание возврата ноды, иначе перенос pod с этой ноды на другие ноды.

Replication Controller

Отслеживает множества replica sets, и что нужное число pods всегда доступны в каждом replica set.

Другие подсистемы

  • Deployment Controller
  • Namespace Controller
  • Job Controller
  • PV-Protection Controller
  • Endpoint Controller
  • CronJob
  • Service Account COntroller
  • Stateful-Set
  • Replica set
  • и т.д.

ETCD

Особенности

  • Служба Etcd слушает на TCP2379;
  • Клиент - etcdctl;
  •  По умолчанию, etcdctl использует API v2. Переключать на API v3 нужно явно. Команды в API v2 и v3 отличаются:
# ETCD API v2.x
./etcdctl set key1 value1
./etcdctl get key1
./etcdctl --version # важно увидеть версию API (2), от этого команды зависят

# ETCD API v3.x
./etcdctl version # в API 3.x параметр version => команда, набирать без "--"
./etcdctl put key1 value1
./etcdctl get key1
./etcdctl get / --prefix -keys-only # вывести ключи в БД, без значений 
export ETCDCTL_API=3 # поменять версию API на 3.х

Для аутентификации клиента ETCDCTL на ETCD API Server нужно указывать также сертификат:

--cacert /etc/kubernetes/pki/etcd/ca.crt     
--cert /etc/kubernetes/pki/etcd/server.crt     
--key /etc/kubernetes/pki/etcd/server.key

Отсюда полноценная команда-запрос клиента к ETCD будет выглядет вот так:

kubectl exec etcd-master -n kube-system -- sh -c "ETCDCTL_API=3 etcdctl get / --prefix --keys-only --limit=10 --cacert /etc/kubernetes/pki/etcd/ca.crt --cert /etc/kubernetes/pki/etcd/server.crt  --key /etc/kubernetes/pki/etcd/server.key"

Ingress

Ingress Controller

Объект служит в роли прокси и балансировщика L7.

Создание минимального ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: 
  name: minimal-ingress-controller
spec:
  replicas: 1
  selector:
    matchLabels:
      name: nginx-ingress
  template:
    metadata:
      labels:
        name: nginx-ingress
    spec: 
      containers:
        - name: nginx-ingress-controller
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.21.0
          args: 
            - /nginx-ingress-controller
            - --configmap=$ (POD_NAMESPACE)/nginx_configuraruin
          env:            # nginx требует 2 переменные для конфигурации
            - name: POD_NAME
              valueFrom: 
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom: 
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
            - name: http
              containerPort: 80
            - name: https
              containerPort: 443

Для конфигурации ingress на nginx также нужен ConfigMap. В него закладывается конфигурация nginx, которая в обычном варианте nginx как reverse-proxy вписывалась в config самого nginx:

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-configuration

Также необходимо создать Service, который публикует ingress вовне:

apiVersion: v1
kind: Service
metadata: 
  name: nginx-ingress
spec: 
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    targetPort: 443
    protocol: TCP
    name: https
  selector:
    name: nginx-ingress

И нужен Service Account для аутентификации:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nginx-ingress-serviceaccounts

Ingress Resource

Набор правил ingress называются Ingress Resource.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-rules
spec:
  backend:
    serviceName: app-service
    servicePort: 80

Определение нахождения правил ingress resource:

kubectl get ingress -A # найти Ingress Resource среди Namespaces 

Редактирование правил ingress resource:

kubectl edit ingress <имя ingress resource> -n <namespace>

Nginx rewrite rules

Настройка rewrite-target нужна, чтобы правильно транслировать сетевой путь.

# Без rewrite-target:
http://<ingress-service>:<ingress-port>/watch --> http://<watch-service>:<port>/path

# С rewrite-target типа replace("/path","/"):
http://<ingress-service>:<ingress-port>/watch --> http://<watch-service>:<port>/

Для включения правил rewrite, нужно добавить в манифест annotations:

1. apiVersion: extensions/v1beta1
2. kind: Ingress
3. metadata:
4.   name: test-ingress
5.   namespace: critical-space
6.   annotations:               # применение rewrite правил
7.     nginx.ingress.kubernetes.io/rewrite-target: /
8. spec:
9.   rules:
10.   - http:
11.       paths:
12.       - path: /pay
13.         backend:
14.           serviceName: pay-service
15.           servicePort: 8282

Jobs & CronJobs

Jobs

Это объект для выполнения однократных служебных задач.

apiVersion: batch/v1
kind: Job
metadata:
  name: my-job
spec:
  completions: 3 # сколько Pod-ов запускать под задачу
  parallelism: 3 # запускать Pod-ы не последовательно, а сразу пачками по 3
  # если 1 из 3 Pod завершится с ошибкой, k8s будет 1 оставшийся перезапускать,    # пока тот не закончит работу корректно
  template:
    spec:
      containers:
      - name: job-container
        image: busybox
        command: [ "/run/job" ]
      restartPolicy: Never

Команды для работы с Jobs:

kubectl create -f <имя job.yaml>
kubectl get jobs
kubectl logs <имя Pod с Job> # вывод результата
kubectl delete job <имя Pod>

CronJobs

Объект для создания периодической задачи:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: my-cronjob
spec:
  schedule: "*/1 * * * *" # работает как Cron в Linux, см ниже Cron Parameters
  jobTemplate: # ниже описание spec обычного Job
    spec:
      completions: 3
      parallelism: 3 
      template:
        spec:
          containers:
          - name: job-container
            image: busybox
            command: [ "/run/job" ]
          restartPolicy: Never

Cron parameters:

# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
# │ │ │ │ │                                   7 is also Sunday on some systems)
# │ │ │ │ │
# │ │ │ │ │
# * * * * * <command to execute>

Kube-apiserver

Описание kube-apiserver

  • При работе с кластером k8s через инстумент kubectl, по сути работа идёт именно с kube-apiserver;
  • Kube-apiserver забирает данные из БД Etcd и возвращает их пользователю kubectl.

Работа kube-apiserver по созданию pod:

  1. Аутентификация пользователя;
  2. Валидация запроса о создании pod;
  3. Получение данных;
  4. Обновление Etcd;
  5. kube-scheduler мониторит kube-apiserver, узнаёт о появлении pod без привязки к worker-node. Он находит node, куда положить pod и сообщает в kube-apiserver;
  6. kube-apiserver обновляет данные в Etcd;
  7. kube-apiserver передаёт данные на kubelet выбранного worker-node;
  8. Kubelet создаёт на своей worker-node нужный pod и раскатывает там образ контейнера через CRE.

Настройки

  • Если k8s ставился через kubeadm, то они находятся в /etc/kubernetes/manifests/kube-apiserver.yaml
  • Если k8s ставился вручную, то они находятся в /etc/systemd/system/kube-apiserver.service

Kubernetes Security

Kubernetes Security

Link: https://habr.com/ru/companies/vk/articles/730158/

Корректная конфигурация кластера k8s

  • K8s CIS Benchmark => kube-bench, проверка конфигов узлов кластера;
  • Ограничить сетевой доступ к k8s API (white lists, VPN);
  • Каждый пользователь имеет уникальный ID при доступе к k8s API; после развертывания кластера никто не должен в нем аутентифицироваться как system:masters:
    • если кто-то украдет у меня этот kubeconfig, придется перевыпускать все сертификаты в кластере, потому что отозвать доступ у группы system:masters без этого невозможно.
  • Настроить RBAC. Например, некий оператор в кластере получает список всех подов. В этом случае не нужно выдавать ему возможность видеть все секреты и тем более редактировать их.

Сканирование образов

  • Разбор уязвимостей: не каждая из них ведёт к эксплуатации;
  • Использовать общие базовые образы - при нахождении в таких уязов, их моно заменить и перезапустить CI/CD;
  • Уменьшение числа зависимостей; Инструменты debug не должны оказываться в образах для prod;
  • Собираем артефакт отдельно и копируем в итоговый контейнер, который будет использоваться в кластере.

Сетевая безопасность

  • CNI с сетевыми политиками; Если stage + prod в 1 кластере, разрабы попадают в stage, а из него - в prod, если нет сетевой изоляции; “политики как код” - развёртываются вместе с приложениями из Git-репозитория; Внедрять политики со старта, пока всё просто;
  • Ingress + egress политики настроены для всех компонентов кластера;
  • Все соединения должны быть явно разрешены.

Контроль над запускаемыми приложениями

  • Pod Security Admisson закрывает базовые потребности
  • Pod Security Standards запрещают пускать поды от root, использовать host network и т.д.
  • Если этого недостаточно, тогда:
  • Точки контроля:
    • Запрет на использование образов с DockerHub;
    • Обязательно указывать priorityClass;

Аудит и регистрация событий

  • Самый простой из коробки - аудит Kubernetes API;
  • Логирование в файл или stdout, отправка в SIEM и анализ.
  • Часть команд (пример: crictl exec) не отобразятся в логе Kubernetes API. Нужен сквозной аудит хостовая ОС + k8s узел. Например, с помощью Falco:
    • системные вызовы на хосте;
    • аудит-лог от Kubernetes API с помощтю Webhook backend;
    • проверка потока событий с помощю сконфигурированных правил;
    • отправка алертов в случае нарушений правил.
  • Технология eBPF в ядре - для событий хоста + контейнеров одновременно.

Расширение защиты

  • Аутентификация и авторизация
  • Аудит RBAC
  • Управление секретами
  • Защита цепочек поставок:
    • Запрет запуска образов с уязами;
    • Запуск только подписанных образов (cosign).
  • Режим обучения для создания политик;
  • Авто-реагирование на события аудита.

История ИБ K8s

  • 2016 - на kubelet не было механизма аутентификации. Надо было через SSH-Tunneling защищать. 120 вариаций k8s в 2024. В 1 до сих пор осталась эта проблема;
  • --insecure-port=8080 - даёт cluster-admin без авторизации. Можно нацелить kubectl на него и получить все права. Убрали только в 1.20; Облачный пров повесил этот порт на container network, клиенты не могли его выключить;
  • Irrevocable credentials. Нет поддержки по удалению клиентских сертификатов; Заказчик даёт аудитору k8s файл такого серта, из группы system:masters, и далее в течение года он действителен; irrevocable secrets - вплоть до 1.25, никогда не протухают, убиваются только вместе с service account. Они рано или поздно утекают - в git repo, в тикете к поддержке и т.д. Взамен пришли expiring token requests;
  • RBAC появился не сразу. Было --authorization-mode=AlwaysAllow. В 1 вариации k8s до сих пор так осталось.
  • Helm 2 + служба Tiller (у которой по gRPC TCP44134 нет аутентфикации, и он обычно cluster-admin), которую можно найти по DNS и компрометировать кластер.

Что сегодня

  • До сих пор можно выдавать client certs, а также long-lived tokens - до 1 года; По-умолчанию нигде (кроме OpenShift) вот это всё не включено сразу:
  • Pod Security Admission GA 1.25
  • Validating Admission Policy GA 1.30
  • Внешние опции (Kyverno, OPA и т.д.)

“Unpatchable 4”

CVE-2020-8554

Перехват трафика в multi-tenant кластерах. Кроме тех, где Cillium работает БЕЗ kube-proxy (потому что этот баг зависит от kube-proxy); Либо с помощью Kyverno и т.д. заблокировать создание clusterIP на внешних service with external ip.

CVE-2020-8561

Server-Side-Forgery (SSRF). ValidatingAdmissionWebhook + Remote Debug Level = Debug (MAX)

CVE-2020-8562

SSRF + Time-of-Check-Time-of-Use (TOCTOU) K8s API = HTTP-Proxy по сути своей. Но в нём есть hardcoded список адресов IP, куда он не проксирует (например, localhost). При наличии подконтрольного DNS, можно кидать к нему запрос на подключение на случайный IP, а DNS транслирует его в сторону API-сервера как localhost.

Лечится с помощью службы konnectivity, защищающей k8s Control Plane от запросов от Pod Network. https://kubernetes.io/docs/tasks/extend-kubernetes/setup-konnectivity/ https://stackoverflow.com/questions/61706923/what-is-the-konnectivity-service-for-kubernetes

CVE-2021-25740

Multi-tenant environment. Load-balancer allows requests from endpoint and passes commands to other namespace.

Kaspersky Container Security

Скрипт для скачивания продукта (версия 1.2.2) локально:

#!/bin/bash 
 
# Securely obtain credentials (replace with your actual method) 
read -s -p "Docker Password: " docker_password 
docker login repo.kcs.kaspersky.com -u <USER_LOGIN> -p "$docker_password" 
 
images=( 
  "repo.kcs.kaspersky.com/images/services/clickhouse:v1.2.2-without-ssl" 
  "repo.kcs.kaspersky.com/images/services/event-broker:v1.2.2" 
  "repo.kcs.kaspersky.com/images/services/image-handler:v1.2.2" 
  "repo.kcs.kaspersky.com/images/services/panel/nginx:v1.2.2" 
  "repo.kcs.kaspersky.com/images/services/scanner-server:v1.2.2" 
  "repo.kcs.kaspersky.com/images/services/licenses:v1.2.2" 
  "repo.kcs.kaspersky.com/images/services/middleware:v1.2.2" 
  "repo.kcs.kaspersky.com/images/initer:v1.2.2" 
  "repo.kcs.kaspersky.com/images/node-agent:v1.2.2" 
  "repo.kcs.kaspersky.com/images/kube-agent:v1.2.2" 
  "repo.kcs.kaspersky.com/images/updates:v1.2" 
  "repo.kcs.kaspersky.com/images/scanner:v1.2.2-with-db" 
  "repo.kcs.kaspersky.com/images/external/minio:2023.9.30" 
  "repo.kcs.kaspersky.com/images/external/nats:2.9.17" 
) 
 
for image in "${images[@]}"; do 
  docker pull "$image" || exit 1  # Exit on failure 
done 
 
echo "All images pulled successfully."

docker save $(docker images --format '{{.Repository}}:{{.Tag}}') -o KCS122.tar

<USER_LOGIN> и пароль необходимо получить у представителя компании.

Namespaces

Create

Use manifest to create:

apiVersion: v1
kind: Namespace
metadata:
  name: dev

Commands:

kubectl create -f namespace-dev.yaml
kubectl create namespace dev

Pods in Namespace

To place pods in selected namespace add it to their manifest:

apiVersion: v1
kind: Pod       
metadata: 
  namespace: dev
  name: myapp-pod
spec:
  containers:
    - name: nginx-container
      image: nginx

Viewing Namespace Contents

Write the target namespace to get pods from:

kubectl get pods --namesapce=dev

Changing namespace in the current context of kubectl to dev:

kubectl config set-context $(kubectl config current-context) --namespace=dev 

List pods in all namespaces:

kubectl get pods --all-namespaces

Pod Commands & Configs

Commands & Arguments

Команды и аргументы команд, которые срабатывают при запуске контейнера.

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
  labels:
    app: test_app
    env: productinon
spec:
  containers:
    - name: nginx-container
      image: nginx
      command: [ "python3" ]
      args: [ "app-test.py" ]

## Вариант 2
      command: 
        - "python3"
        - "app-test.py"

## Вариант 3
      command: [ "python3", "app-test.py" ]

Заменять команды, аргументы, метки и т.д. нельзя. Однако, можно вызвать ошибку, потом пересоздать Pod на лету из сохранённого промежуточного файла:

$ kubectl edit pod nginx-container # отредактировал поле command

error: pods "nginx-container" is invalid
A copy of your changes has been stored to "/tmp/kubectl-edit-1395347318.yaml"
error: Edit cancelled, no valid changes were saved.

$ kubectl replace --force -f /tmp/kubectl-edit-1395347318.yaml

Environmental Variables

Переменные среды задаются как список, похожим образом с командами.

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
  labels:
    app: test_app
    env: productinon
spec:
  containers:
    - name: nginx-container
      image: nginx
      env:
        - name: APP_COLOR 
          value: green

ConfigMap

Отдельный объект, который содержит переменные среды. Можно получить их список через kubectl get configmaps

apiVersion: v1
kind: ConfigMap
metadata: 
  name: mydb
data:
  APP_COLOR: blue
  APP_MODE: testdev

Императивный способ создания ConfigMap

kubectl create configmap \ 
        <имя конфига> --from-literal=<ключ>=<значение> \
                      --from-literal=APP_USER=testuser

kubectl create configmap \
        <имя конфига> --from-file=<путь до файла>
                    # --from-file=app_config.properties

Ссылка на ConfigMap в описании Pod

Ссылка производится по именам ConfigMap в виде списка:

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
  labels:
    app: test_app
    env: productinon
spec:
  containers:
    - name: nginx-container
      image: nginx
      envFrom:
        - configMapRef: 
            name: mydb
            
# Вариант взять только конкретную переменную: 
      env: 
        - name: APP_COLOR 
          valueFrom: 
            configMapKeyRef:
              name: mydb
              key: APP_COLOR

Secrets

Секреты - это ConfigMap, значения которых кодируются по base64. Можно получить их список через:

kubectl get secrets # список секретов
kubectl describe secret <имя секрета> # не отображает значения
kubectl get secret <имя секрета> -o yaml # отображает значения в файле 

Декларативное описание

apiVersion: v1
kind: Secret
metadata: 
  name: mydb
data:
  APP_PWD: dmVyeXNlY3JldA== # base64 Encode
  APP_TOKEN: dGVzdGRldg==

Императивный способ создания Secret

kubectl create secret generic \ 
        <имя конфига> --from-literal=<ключ>=<значение> \
                      --from-literal=APP_USER=testuser

kubectl create secret generic \
        <имя конфига> --from-file=<путь до файла>
                    # --from-file=app_config.properties

Ссылка на Secret в описании Pod

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
  labels:
    app: test_app
    env: productinon
spec:
  containers:
    - name: nginx-container
      image: nginx
      envFrom:
        - secretRef: 
            name: mydb

# Вариант взять только конкретное значение: 
      env: 
        - name: APP_COLOR 
          valueFrom: 
            secretKeyRef:
              name: mydb
              key: APP_PWD
              
# Вариант смонтировать как файлы (каждый пароль - отдельный файл)
      volumes:
      - name: app-secret-volume
        secret: 
          secretName: app-secret

Service Accounts

Специальные учётные записи для доступа к k8s. При создании вместе с ними создаётся объект secret.

  • С версии k8s 1.22 объект Secret имеет время жизни;
  • С версии k8s 1.24 секрет не создаётся на автомате, нужно его отдельно создать:
kubectl create serviceaccount dashboard-sa
kubectl create token dashboard-sa # с k8s 1.24+ необходимо создать токен, у которого время жизни (по умолчанию) =1 час с момента создания

Pods

Articles

PODs

Pod - наименьшая сущность в k8s. Обычно, pod = контейнер по принципу 1:1. Однако, можно несколько контейнеров разместить в 1 pod, при условии, что они функционально разные. Обычно это главный контейнер приложения и вспомогательные контейнеры, которые с ним связаны.

В обычном Docker, если развернуть множество копий “контейнер приложения” + “вспомогательный контейнер”, то нужно будет иметь карту взаимосвязей между ними всеми. Более того, в случае выхода из строя контейнера с приложением, нужно будет вручную удалять сопутствующий вспомогательный контейнер. От этого всего избавляют pod-ы, в рамках которых всё размещается, обеспечивается внутренняя связность, и далее k8s размножает готовые копии pod-ов в рамках кластера.

Pod-ы добавляют функционал к контейнерам:

  • Labels and annotations
  • Restart policies
  • Probes (startup probes, readiness probes, liveness probes, and potentially more)
  • Affinity and anti-affinity rules
  • Termination control
  • Security policies
  • Resource requests and limits

Работа с pod-ами ведётся с помощью API или инструмента kubectl:

kubectl run nginx --image nginx # образ nginx будет скачан с DockerHub
kubectl get pods # список всех pod-ов и их статусов
kubectl get pods --selector app=App1 # отфильтровать вывод по заданному label

Создание Pod через файл YAML

Создадим pod-definition.yml:

apiVersion: v1
kind: Pod       
metadata: 
  name: myapp-pod
  labels:
    app: myapp
    type: front-end
spec:
  containers:
    - name: nginx-container
      image: nginx

Далее создаём pod командой:

kubectl create -f pod-definition.yml
kubectl get pods

Посмотреть доступные поля, подробную информацию о поле у pod:

kubectl explain pods --recursive
kubectl explain pod.spec.restartPolicy

Посмотреть конкретное поле у всех pod, например, образ image, из которого он сделан:

kubectl get pods -o jsonpath={.items[*].spec.containers[*].image}

Можно у работающего Pod получить спецификацию в YAML, из которой он сделан:

kubectl get pod <имя pod> -o yaml > pod-definition.yaml

Удалить Pod

kubectl delete pod <имя Pod> --now 

Зайти внутрь Pod и выполнить команды:

kubectl exec -it <имя pod> -- /bin/sh

Обновить Pod

В конфигурацию pod можно добавить период обновления (например, 30 секунд) и установить “imagePullPolicy: “Always”. Удалить Pod с помощью kubectl delete pod pod_name. Новый контейнер будет создан на последней версии образа, после чего старый контейнер будет удалён.

spec:
  terminationGracePeriodSeconds: 30
  containers:
  - name: my_container
    image: my_image:latest
    imagePullPolicy: "Always"

Есть вариант “дёргать” за Deployment, вызывая тем самым обновление:

kubectl patch deployment <имя deployment> -p \
  '{"spec":{"template":{"spec":{"terminationGracePeriodSeconds":31}}}}'

Выполнение задач в Pod

Если необходимо, чтобы Pod поработал и выключился, без перезапуска, то необходимо поменять его restartPolicy, которая по умолчанию стоит в Always - то есть перезапуск всегда по завершении работы.

spec:
  containers:
  - name: my_container
    image: my_image:latest
  restartPolicy: Never # ещё вариант OnFailure

Императивные команды

В отличие от декларативных, такие команды позволяют быстро решить однократную задачу.

kubectl run nginx --image=nginx --dry-run=client -o yaml # --dry-run=client - не создаёт объект, сообщает о возможности его создания

kubectl run httpd --image=httpd:alpine --port=80 --expose=true # создать Pod из образа httpd:alpine и к нему сразу создать ClusterIP с публикацией порта

Multi-Container PODs

Несколько контейнеров в 1 POD делят один адрес IP (между собой они общаются через адаптер localhost), хранилище. Есть несколько типовых сценариев:

  • Sidecar pattern - самый популярный случай, один контейнер отрабатывает задачу (например, выгрузки данных на веб-сайт), а другой решает вспомогательную задачу (например, синхронизация данных для последующей выгрузки);
  • Init pattern - перед запуском контейнера с основным ПО сначала стартует вспомогательный контейнер, который производит настройку окружения;
  • Adapter pattern - ПО в основном контейнере обрабатывает данные, а вспомогательный контейнер передаёт эти данные в другое приложение в понятном ему формате. Например, система SIEM не понимает формат логов приложения, и вспомогательный модуль парсит и транслирует логи в понятный для SIEM формат;
  • Ambassador pattern - ПО в основном контейнере отрабатывает задачи, а вспомогательный контейнер вызывает через API внешние системы, чтобы собрать с них данные для обработки, либо передать данные в эти системы.

PODы стартуют атомарно - только после успешного старта всех контейнеров POD считается запущенным. Частичный запуск не допускается. POD целиком всеми контейнерами размещается на одной ноде worker.

apiVersion: v1
kind: Pod        
metadata: 
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
    - name: nginx-container
      image: nginx
      ports:
        - containerPort: 8080
    - name: log-agent
      image: log-agent

InitContainer - не живёт постоянно, а выполняется ДО загрузки остальных контейнеров в Pod, поэтому его инициализация - в отдельной секции:

apiVersion: v1
kind: Pod        
metadata: 
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
    - name: myapp-container
      image: nginx
      ports:
        - containerPort: 8080
  initContainers:
    - name: init-service
      image: busybox
      command: [ 'sh', '-c', 'git clone <some repo to be used by app>' ]

Если таких InitContainer несколько, они будут выполняться последовательно один за другим. Если любой InitContainer не сможет выполниться успешно, k8s будет перезапускать Pod, пока InitContainer успешно не завершится.

Ручное распределение (manual scheduling)

Если в кластере нет распределения, можно указать вручную параметр nodeName:

apiVersion: v1
kind: Pod       
metadata: 
  name: myapp-pod
spec:
  containers:
    - name: nginx-container
      image: nginx
  nodeName: node01

Без указания этого параметра в отсутствии распределения Pod будет висеть как Pending. K8s не даст указать этот параметр на лету, после добавления Pod надо заменить kubectl replace --force -f nginx-pod.yaml

Subsections of Pods

Readiness Probes

Readiness Probes

Определение, что ПО в контейнер действительно запустилось успешно и готово принимать данные пользователей, можно провести по-разному, добавив в манифест раздел spec -> containers поле readinessProbe.

  • Для проверки HTTP сервера:

    readinessProbe:
      httpGet:
        path: /api/ready
        port: 8080
      initialDelaySeconds: 10 # предусматриваем 10 сек задержку на старте
      periodSeconds: 5 # повторяем проверку спустя 5 секунд
      failureThreshold: 8 # повторяем проверку 8 раз (по умолчанию 3)
  • Для проверки открытого порта (например, у СУБД):

    readinessProbe:
      tcpSocket:
        port: 3306
  • Для проверки с помощью своей команды:

    readinessProbe:
      exec:
        command: 
          - cat
          - /app/is_ready

Liveness Probes

Периодическое определение, работает ли ПО в контейнере - для случаев, когда падение ПО не приводит к его вылету и закрытию контейнера.

  • Для проверки HTTP сервера:

    livenessProbe:
      httpGet:
        path: /api/health_status
        port: 8080
      initialDelaySeconds: 10 # предусматриваем 10 сек задержку на старте
      periodSeconds: 5 # повторяем проверку спустя 5 секунд
      failureThreshold: 8 # повторяем проверку 8 раз (по умолчанию 3)
  • Для проверки открытого порта (например, у СУБД):

    livenessProbe:
      tcpSocket:
        port: 3306
  • Для проверки с помощью своей команды:

    livenessProbe:
      exec:
        command: [ "cat", "/app/is_working" ]

Container Logging

Для получения логов с Pod:

kubectl logs {-f} <pod name> <container name> # -f = tail

kubectl logs -f myapp logger # пример, в случае нескольких контейнеров в Pod выбран контейнер logger 

Resources

Requests

Запрос контейнеров на гарантированные ресурсы.

apiVersion: v1
kind: Pod
metadata:
  name: myapp
  labels:
    name: myapp
spec:
  containers:
  - name: mywebapp
    image: nginx
    resources:
      requests:
        memory: "4Gi" # 4 гибибайта
        cpu: 2 # минималка 0.1 CPU

Limits

Указание ограничений. При переходе лимита по CPU скорость для Pod замедляется (throttling). При переходе лимита по RAM происходит убийство Pod с ошибкой OOM Error (Out-Of-Memory).

apiVersion: v1
kind: Pod
metadata:
  name: myapp
  labels:
    name: myapp
spec:
  containers:
  - name: mywebapp
    image: nginx
    resources:
      requests:
        memory: "2Gi"
        cpu: 2
      limits:
        memory: "4Gi"
        cpu: 2

Security Contexts

Security Contexts

В описании Pod можно указать ID пользователя, который запускает контейнеры, а также описать его возможности (capabilities).

  • Если контекст безопасности определён на уровне Pod, он действует для всех входящих в него контейнеров;
  • Если контекст безопасности определён на уровне Pod и на уровне контейнера, то настройки контейнера приоритетны перед настройками Pod.

Уровень Pod:

apiVersion: v1
kind: Pod
metadata:
  name: web-pod
spec:
  securityContext:
    runAsUser: 1001
  containers:
    - name: ubuntu
      image: ubuntu

Уровень контейнера:

apiVersion: v1
kind: Pod
metadata:
  name: web-pod
spec:
  containers:
    - name: ubuntu
      image: ubuntu
      securityContext:
        runAsUser: 1001
        capabilities:   
          add: ["MAC_ADMIN"]
# возможности можно определить ТОЛЬКО на уровне контейнера

Selectors and Affinity

Node Selectors

Добавить пометки к node можно командой:

kubectl label nodes <node-name> <label-key>=<label-value>

kubectl label nodes node-01 size=Large # пример

После этого в описании Pod можно указать Node Selector:

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
spec:
  containers:
  - name: data-processor
    image:: data-processor
  nodeSelector:
    size: Large

Node Selectors работают по принципу 1:1 совпадения метки Node и Pod. Для более сложных сценариев применяют Node Affinity.

Node Affinity

Для создание свойств Node Affinity нужно поменять свойства в манифесте Pod:

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
spec:
  containers:
  - name: data-processor
    image:: data-processor
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: size
            operator: In # условие, может быть наоборот NotIn, или Exists - есть ли вообще такой label, необязательно имеющий значение?
            values:
            - Large # условие действует на любое из значений списка
            - Medium

В случае, если нужные labels отсутствуют на Nodes кластера, есть 2 типа поведения, которое задаётся свойством Pod:

  • requiredDuringSchedulingIgnoredDuringExecution - если Nodes с нужными labels нет, вообще не размещать данный Pod на кластере;
  • preferredDuringSchedulingIgnoredDuringExecution - если Nodes с нужными labels нет, всё равно разместить данный Pod где-нибудь на кластере.

Если Pod уже запущен на Node в момент, когда добавили label, то в версии 1.27 ничего не произойдёт в обоих случаях. В плане добавить третий тип поведения:

  • requiredDuringSchedulingRequiredDuringExecution - если во время работы Pod произойдёт изменение affinity - удалить Pod с Node.

Taints & Tolerations

Для распределения Pods по Nodes применяется сочетание покраски (taint) и восприимчивости (toleration) к ней.

Taints

Покраска Node говорит kube-scheduler, что есть 1 из 3 эффектов:

  • NoSchedule - не назначать сюда Pods без toleration;
  • PreferNo Schedule - назначать Pods без toleration с наименьшим приоритетом, если больше некуда;
  • NoExecute - не назначать сюда Pods без toleration, уже имеющиеся тут Pods удалить и перенести куда-то ещё.

Покраска node:

kubectl taint nodes <имя node> key=value:effect

kubectl taint nodes node01 app=myapp:NoSchedule # пример
kubectl taint nodes node01 app=myapp:NoSchedule- # минус в конце снимает покрас

Tolerations

Поменять восприимчивость Pod к покраске:

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
spec:
  containers:
  - name: nginx-controller
    image: nginx
  tolerations:
  - key: "app"
    operator: "Equal"
    value: "blue"
    effect: "NoSchedule"

Replicasets & Deployments

ReplicaSets

ReplicaSet следит за тем, чтобы количество Pod всегда было заданным числом штук (параметр replicas) - не больше и не меньше. ReplicaSet Controller является более современным аналогом ReplicationController:

  • ReplicationController apiVersion = v1
  • ReplicaSetController apiVersion = apps/v1

Отличие ReplicaSet в том, что в нём обязательным параметром есть selectors, который отслеживает контейнеры, в том числе созданные ДО создания самого ReplicaSet. Отслеживание можно производить по их меткам labels. Для совпадающих меток работает алгоритм приведения к нужному количеству.

Создание ReplicaSet:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myreplicaset
  labels:
    name: myapp
spec:
  selector:
    matchLabels:
      env: production ### метка должна совпадать
  replicas: 3
  template:
    metadata:  ### шаблон контейнера берётся из описания Pod
      name: myapp
      labels:
        env: production ### метка должна совпадать
    spec:
      containers:
        - name: nginx-container
          image: nginx
    

Шаблон Pod всегда должен быть описан в части template, чтобы ReplicaSet знал, какие Pod создавать.

Команды для работы с ReplicaSets

kubectl create -f <имя файла с описанием replicaset>
kubectl get rs # вывести все ReplicaSet в кластере
kubectl describe rs <имя replicaset> # подробности о ReplicaSet
kubectl delete rs <имя replicaset> # удаляет все Pods и сам ReplicaSet
kubectl edit rs <имя replicaset> # отредактировать описание Replicaset
kubectl scale rs <имя replicaset> --replicas=<новое количество копий> 
kubectl replace -f <имя файла с описанием replicaset> # заменить ReplicaSet 

Deployments

Deployment - это надстройка над ReplicaSet, добавляет логику обновления Pod с одной версии на другую. При обновлении Deployment имеет 2 стратегии:

  • Rolling Update - (по умолчанию) при обновлении Pods, делает это поштучно: один Pod старой версии кладёт (Terminated), сразу поднимает заместо него новый Pod;
  • Recreate - при обновлении сначала удаляются все Pods, после чего взамен поднимаются новые. Сопровождается падением/отключением приложения для потребителей.
kubectl apply -f <имя Deployment> # запустить процесс rollout после внесения изменений в манифест
kubectl rollout status <имя Deployment> # узнать о статусе выкатывания

kubectl rollout history <имя Deployment> # узнать о всех ревизиях и причинах их перехода
kubectl rollout history <deployment> --revision=1 # узнать статус конкретной версии

kubectl rollout undo <имя Deployment> # откатить назад обновление Deployment
kubectl rollout undo <имя Deployment> --to-revision=1 # откатить до версии 1
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mydeployment
  labels:
    name: myapp
spec:
  selector:
    matchLabels:
      env: production ### метка должна совпадать
  replicas: 3
  strategy: 
    type: RollingUpdate ### стратегия замены Pods
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:  ### шаблон контейнера берётся из описания Pod
      name: myapp
      labels:
        env: production ### метка должна совпадать
    spec:
      containers:
        - name: nginx-container
          image: nginx
    

Команды для работы с Deployments

kubectl create -f <имя файла с описанием Deployment>
kubectl get deploy # вывести все Deployment в кластере
kubectl describe deploy <имя Deployment> # подробности о Deployment
kubectl delete rs <имя Deployment> # удаляет все Pods и сам Deployment
kubectl edit rs <имя Deployment> # отредактировать описание Deployment и произвести его обновление
kubectl edit rs <имя Deployment> --record # отредактировать описание, вызвав обновление и записать команду как причину обновления в список ревизий
kubectl set image deploy <имя Deployment> nginx=nginx:1.18 # пример обновления без редактирования YAML

Императивные команды

В отличие от декларативных, такие команды позволяют быстро решить однократную задачу.

kubectl create deploy nginx --image=nginx --replicas=4
kubectl scale deploy nginx --replicas=4

kubectl create deployment nginx --image=nginx --dry-run=client -o yaml > nginx-deployment.yaml # вывести манифест YAML для последующего редактирования 

Services

Services

Service - логический объект в k8s, который позволяет внешним клиентам подключаться к сетевым портам на контейнерах внутри кластеров Pod.

Service делятся на 3 вида:

  • NodePort - публикация порта с Pod наружу на уровень Worker Node (причём, если Pods размазаны лишь по части от всех нод кластера k8s, то всё равно они доступны при обращении К ЛЮБОЙ ноде кластера);
  • ClusterIP - по сути внутренний балансировщик для обращения, например, части frontend приложения к множеству Pods, реализующих backend;
  • LoadBalancer - NodePort с заданием стратегии балансировки (в NodePort - случайный алгоритм балансировки), поддерживается лишь на ряде облачных площадок (AWS, GCM и т.д.). В остальных площадках ведёт себя как NodePort.

NodePort

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec: 
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30004 # порты ограничены диапазоном 30000-32767
  selector:
    app: myapp

ClusterIP

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec: 
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: myapp

LoadBalancer

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec: 
  type: LoadBalancer # only works with a supported platform
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: myapp

Команды работы с Service

kubectl create -f <имя файла с описанием Service>
kubectl get svc # вывести все Service в кластере
kubectl describe svc <имя service> # подробности о service
kubectl delete svc <имя service> # удаляет все объект service
kubectl edit svc <имя service> # отредактировать описание service и произвести его обновление

Императивные команды

В отличие от декларативных, такие команды позволяют быстро решить однократную задачу.

kubectl create service clusterip redis --tcp=6379:6379 --dry-run=client -o yaml # нельзя подавать selectors в команду, следует вывести YAML и отредактировать, что небыстро

kubectl expose pod redis --port=6379 --name=redis-service --dry-run=client -o yaml # такой вариант CLusterIP использует labels самого pod как selectors, что намного эффективнее

kubectl expose pod nginx --port=80 --name=nginx-service --type=NodePort --dry-run=client -o yaml # такой вариант NodePort использует labels самого pod как selectors

Subsections of Linux CLI

CMD Hotkeys

Базовый терминал

Список всех команд: stty -a

  • Ctrl+W для удаления предыдущего слова.
  • Ctrl+U для удаления всей строки.
  • Ещё несколько возможностей, не связанных с редактированием текста (например, Ctrl+C для прерывания процесса, Ctrl+Z для его приостановки и так далее).

Readline (bash, psql, python3)

Если есть программа вроде nc без поддержки readline, то можно выполнить rlwrap nc, чтобы эту поддержку в неё встроить.

  • Ctrl+E (или End) для перехода в конец строки (из emacs).
  • Ctrl+A (или Home) для перехода в начало строки (из emacs).
  • Ctrl+влево/вправо для перемещения вперёд/назад на 1 слово.
  • Стрелка вверх для возврата к предыдущей команде.
  • Ctrl+R для поиска по истории.

Спец-ПО

Atuin - прекрасный инструмент для поиска по истории оболочки

grep

GREP рекурсивный поиск текста в файлах и папках

grep -Rnw '/path/to/somewhere/' -e 'pattern'
  • -r or -R is recursive ; use -R to search entirely
  • -n is line number, and
  • -w stands for match the whole word.
  • -l (lower-case L) can be added to just give the file name of matching files.
  • -e is the pattern used during the search

Filters

--exclude, --include, --exclude-dir flags used for efficient searching.

  • This will only search through those files which have .c or .h extensions:
    grep --include=\*.{c,h} -rnw '/path/to/somewhere/' -e "pattern"
  • This will exclude searching all the files ending with .o extension:
    grep --exclude=\*.o -rnw '/path/to/somewhere/' -e "pattern"
  • For directories it’s possible to exclude one or more directories using the --exclude-dir parameter. For example, this will exclude the dirs dir1/, dir2/ and all of them matching *.dst/:
    grep --exclude-dir={dir1,dir2,*.dst} -rnw '/path/to/search/' -e "pattern"

Linux Containers

Статьи в разделе

Контейнеры

Изолированные единицы ПО.

  • Не делятся. Запускаются на 1 хосте. 2 хоста не могут запустить 1 контейнер;
  • Имеют корневой процесс, от которого работают дочерние процессы внутри контейнера;
  • Должны быть изолированными;
  • Должны исполнять набор типовых функций (требует уточнения).

Subsections of Linux Containers

Images

В проекте Docker впервые предложили паковать контейнеры в послойные образы в 2013. Это позволило переносить контейнеры между машинами.

skopeo, jq

Проверим работу с образами на утилите skopeo и пробном контейнере:

skopeo copy docker://saschagrunert/mysterious-image oci:mysterious-image
sudo apt install tree
tree mysterious-image

Видим, что мы скачали индекс образа (image index) и blob. Изучим индекс:

jq . mysterious-image/index.json
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:bc2baac64f1088c56c259a21c280cde5a0110749328454a2b931df6929315acf",
      "size": 559
    }
  ]
}

По сути индекс есть манифест более высокого уровня, который содержит указатели на конкретные манифесты для определённых ОС (linux) и архитектур (amd).

jq . mysterious-image/blobs/sha256/bc2baac64f1088c56c259a21c280cde5a0110749328454a2b931df6929315acf
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:0503825856099e6adb39c8297af09547f69684b7016b7f3680ed801aa310baaa",
      "size": 2789742
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:6d8c9f2df98ba6c290b652ac57151eab8bcd6fb7574902fbd16ad9e2912a6753",
      "size": 120
    }
  ]
}

Image manifest указывает на расположение конфига и набора слоёв для образа контейнера на конкретной ОС и архитектуре. Поле size указывает общий размер объекта. Теперь можно исследовать далее:

jq . mysterious-image/blobs/sha256/0503825856099e6adb39c8297af09547f69684b7016b7f3680ed801aa310baaa
{
  "created": "2019-08-09T12:09:13.129872299Z",
  "architecture": "amd64",
  "os": "linux",
  "config": {
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ],
    "Cmd": [
      "/bin/sh"
    ]
  },
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:1bfeebd65323b8ddf5bd6a51cc7097b72788bc982e9ab3280d53d3c613adffa7",
      "sha256:56e2c46a3576a8c1a222f9a263dc57ae3c6b8baf6a68d03065f4b3ea6c0ae8d1"
    ]
  },
  "history": [
    {
      "created": "2019-07-11T22:20:52.139709355Z",
      "created_by": "/bin/sh -c #(nop) ADD file:0eb5ea35741d23fe39cbac245b3a5d84856ed6384f4ff07d496369ee6d960bad in / "
    },
    {
      "created": "2019-07-11T22:20:52.375286404Z",
      "created_by": "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
      "empty_layer": true
    },
    {
      "created": "2019-08-09T14:09:12.848554218+02:00",
      "created_by": "/bin/sh -c echo Hello",
      "empty_layer": true
    },
    {
      "created": "2019-08-09T12:09:13.129872299Z",
      "created_by": "/bin/sh -c touch my-file"
    }
  ]
}

Распакуем базовый первый слой из архива и изучим его:

$ mkdir rootfs
$ tar -C rootfs -xf mysterious-image/blobs/sha256/0503825856099e6adb39c8297af09547f69684b7016b7f3680ed801aa310baaa
$ tree -L 1 rootfs/
rootfs/
├── bin
├── dev
├── etc
├── home
├── lib
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
└── var

Это файловая система ОС! Можно изучить версию дистрибутива

$ cat rootfs/etc/issue 
Welcome to Alpine Linux 3.10
Kernel \r on an \m (\l)

$ cat rootfs/etc/os-release 
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.10.1
PRETTY_NAME="Alpine Linux v3.10"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"

Распакуем следующий слой и изучим его:

$ tar -C layer -xf mysterious-image/blobs/sha256/6d8c9f2df98ba6c290b652ac57151eab8bcd6fb7574902fbd16ad9e2912a6753 
$ tree -L 1 layer/
layer/
└── my-file

1 directory, 1 file

Тут лежим доп файл, созданный командой "/bin/sh -c touch my-file" - это можно увидеть в секции history. По сути исходный Dockerfile выглядел так:

FROM alpine:latest
RUN echo Hello
RUN touch my-file

Buildah

В 2017 году Red Hat разработали инструмент для создания образов контейнеров по стандарту OCI - как аналог docker build.

Создадим Dockerfile vim Dockerfile и впишем в него:

FROM alpine:latest
RUN echo Hello
RUN touch my-file

Запустим сборку контейнера на базе этого файла - buildah bud Buildah поддерживает много команд:

buildah images # список образов
buildah rmi # удалить все образы
buildah ps # показать запущенные контейнеры

Почему вдруг buildah ps показ запущенных контейнеров, когда это инструмент для их СОЗДАНИЯ? А потому что в процессе создания как в buildah, так и в docker идёт запуск промежуточных контейнеров, их модификация в runtime. Каждый шаг модификации создаёт записи в history. Это потенциальная проблема ИБ: можно влезть в контейнер, пока он собирается (и запущен), если там что-то большое, и модифицировать его.

Например, приготовим манифест:

FROM debian:buster
RUN apt-get update -y && \
    apt-get install -y clang

Начнём сборку:

docker build -t clang

Залезем в работающий контейнер:

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
05f4aa4aa95c        54da47293a0b        "/bin/sh -c 'apt-get…"   11 seconds ago      Up 11 seconds                           interesting_heyrovsky

> docker exec -it 05f4aa4aa95c sh
# echo "Hello world" >> my-file

После сборки можно удостовериться, что созданный файл на месте:

> docker run clang cat my-file
Hello world

Создание контейнеров без Dockerfile

У buildah есть императивные команды к любой команде из Dockerfile, типа RUN или COPY. Это даёт огромную гибкость, т.к вместо огромных Dockerfile можно делить процесс создания контейнеров на части, между ними запускать любые вспомогательные инструменты UNIX.

Создадим базовый контейнер с Alpine Linux и посмотрим как он работает:

buildah from alpine:latest
buildah ps

Можно запускать команды в контейнере, а также создать в нёс файл:

> buildah run alpine-working-container cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.20.3
PRETTY_NAME="Alpine Linux v3.20"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"

echo test > file
> buildah copy alpine-working-container test-file file
86f68033be6f25f54127091bb410f2e65437c806e7950e864056d5c272893edb

По-умолчанию, buildah не делает записи history в контейнер, это значит порядок команд и частота их вызова не влияют на итоговые слои. Можно поменять это поведение ключом --add-history или переменной ENV BUILDAH_HISTORY=true.

Сделаем коммит нового контейнера в образ для финализации процесса:

buildah commit alpine-working-container my-image
buildah images # новый образ теперь в локальном реестре

Можно выпустить образ в реестр Docker, либо на локальный диск в формате OCI:

> buildah push my-image oci:my-image
> jq . my-image/index.json
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:bd3f673eb834eb86682a100768a14ae484b554fb42083666ba1b88fe4fe5c1ec",
      "size": 1189
    }
  ]
}

Теперь сделаем в обратную сторону: удалим образ my-image из реестра и вытащим его с диска из формата OCI:

buildah rmi my-image
buildah images
buildah pull oci:my-image

Контейнер alpine-working-container при этом ещё работает. Запустим CLI в контейнере:

buildah run -t alpine-working-container sh
ls
cat file

Проведём mount файл-системы контейнера на локальную (В ДРУГОМ ТЕРМИНАЛЕ):

> buildah unshare --mount MOUNT=alpine-working-container
> echo it-works > "$MOUNT"/test-from-mount
> buildah commit alpine-working-container my-new-image
  • buildah unshare создаёт новое пространство имён, что позволяет подключить файловую систему как текущий не-root пользователь;
  • --mount автоматически подключает, путь кладём в переменную среды MOUNT;
  • Далее мы делаем commit на изменения, и mount на автомате убирается при покидании buildah unshare сессии.

Мы успешно модифицировали файловую систему контейнера через локальный mount. Проверим наличие файла:

> buildah run -t alpine-working-container cat test-from-mount
it-works

Вложенные контейнеры

У buildah нет даемона, значит, не нужно подключать docker.sock в контейнер для работы с Docker CLI. Это даёт гибкость и возможность делать вложенные схемы: установим buildah в контейнер, созданный buildah:

> buildah from opensuse/tumbleweed
tumbleweed-working-container
> buildah run -t tumbleweed-working-container bash
# zypper in -y buildah

Теперь вложенный buildah готов к использованию. Нужно указать драйвер хранения VFS в контейнере, чтобы получить рабочий стек файловой системы:

# buildah --storage-driver=vfs from alpine:latest
# buildah --storage-driver=vfs commit alpine-working-container my-image

Получили вложенный контейнер. Заложим его в хранилище на локальной машине -> для начала заложим образ в /my-image:

buildah --storage-driver=vfs push my-image oci:my-image

Выходим из вложенного контейнера (В ДРУГОМ ТЕРМИНАЛЕ) и копируем образ из рабочего контейнера путём mount его файловой системы:

> buildah unshare
> export MOUNT=$(buildah mount tumbleweed-working-container)
> cp -R $MOUNT/my-image .
> buildah unmount tumbleweed-working-container

Теперь мы вытаскиваем образ контейнера из директории прямо в локальный реестр buildah:

> buildah pull oci:my-image
> buildah images my-image

ВАЖНОЕ ЗАМЕЧАНИЕ: все действия с buildah не потребовали sudo. Buildah создаёт всё необходимое для каждого пользователя в папках:

  • ~/.config/containers, конфигурация
  • ~/.local/share/containers, хранилища контейнеров

Декомпозиция Dockerfile в несколько разных с помощью CPP макросов.

podman

Инструмент для замены Docker. podman использует buildah как API для создания Dockerfile с помощью podman build. Это значит, что они разделяют одно хранилище под капотом. А это значит, что podman может запускать созданные buildah контейнеры:

buildah images
podman run -it my-image sh
/ # ls

Kernel

External link:

chroot

  • Впервые в Minix и UNIX Version 7 (released 1979)
  • В Linux этот syscall - функция ядра kernel API function.
> mkdir -p new-root/{bin,lib64}
> cp /bin/bash new-root/bin
> cp /lib64/{ld-linux-x86-64.so*,libc.so*,libdl.so.2,libreadline.so*,libtinfo.so*} new-root/lib64
> sudo chroot new-root

chroot - утилита, которая предназначена для изоляции файловой среды приложения. Создана в Minix 1.7. Для процессов и ОЗУ не подходит, но вдохновила создание Namespaces в Linux позднее.

Пример работы

Для работы bash в новой среде chroot необходимо внести его копию в папку jail:

mkdir $HOME/jail
mkdip -p $HOME/jail/bin
cp -v /bin/bash $HOME/jail/bin
cp -v /bin/ls $HOME/jail/bin

Далее нужно увидеть зависимости и перенести их:

ldd /bin/bash
ldd /bin/ls

Либо, гораздо проще перенести разом все библиотеки:

cp -a /usr jail/
cp -a /lib jail/
cp -a /lib64 jail/

Далее, заход в окружение:

sudo chroot $HOME/jail /bin/bash
bash-5.0# ls
bin  lib  lib64  usr


#### Побег из chroot
```c
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
    mkdir(".out", 0755); // нужны права root в контейнере 
    chroot(".out");
    chdir("../../../../../"); // относительный путь за пределы корня
    chroot(".");
    return execl("/bin/bash", "-i", NULL);
}
  • Only privileged processes with the capability CAP_SYS_CHROOT are able to call chroot.
  • Modern systems use pivot_mount (calling process must have the CAP_SYS_ADMIN capability). - has the benefit of putting the old mounts into a separate directory on calling.

Linux Namespaces

  • Задача: обернуть системные ресурсы в уровень абстракции;
  • Introduced in Linux 2.4.19 (2002), became “container ready” in 3.8 in 2013 with the introduction of the user namespace;
  • Seven distinct namespaces implemented: mnt, pid, net, ipc, uts, usercgroup; time and syslog introduced in 2016;
  • функция clone . Создаёт дочерние процессы. Unlike fork(2), the clone(2) API allows the child process to share parts of its execution context with the calling process, such as the memory space, the table of file descriptors, and the table of signal handlers. You can pass different namespace flags to clone(2)to create new namespaces for the child process.

unshare(2) - отсоединение частей контекста выполнения процесса.

setns(2) позволяет запрашивающему процессу присоединяться в разные namespaces.

proc - Besides the available syscalls, the proc filesystem populates additional namespace related files. Since Linux 3.8, each file in /proc/$PID/ns is a “magic“ link which can be used as a handle for performing operations (like setns(2)) to the referenced namespace.

> ls -Gg /proc/self/ns/
total 0
lrwxrwxrwx 1 0 Feb  6 18:32 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 0 Feb  6 18:32 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 0 Feb  6 18:32 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 0 Feb  6 18:32 net -> 'net:[4026532008]'
lrwxrwxrwx 1 0 Feb  6 18:32 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 0 Feb  6 18:32 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 0 Feb  6 18:32 user -> 'user:[4026531837]'
lrwxrwxrwx 1 0 Feb  6 18:32 uts -> 'uts:[4026531838]'

mnt namespace

Ввели в 2002 первым, ещё не знали, что понадобится много разных, потому обозвали флаг клонирования CLONE_NEWNS, что не соответствует флагам других namespaces. С помощью mnt в Linux можно изолировать группу точек монтирования для групп процессов.

> sudo unshare -m
# mkdir mount-dir
# mount -n -o size=10m -t tmpfs tmpfs mount-dir
# df mount-dir

We have a successfully mounted tmpfs, which is not available on the host system level:

> ls mount-dir
> grep mount-dir /proc/mounts

The actual memory being used for the mount point is laying in an abstraction layer called Virtual File System (VFS), which is part of the kernel and where every other filesystem is based on.

> grep mount-dir /proc/$(pgrep -u root bash)/mountinfo

Можно создавать на лету гибкие файловые системы на лету. Mounts can have different flavors (shared, slave, private, unbindable), which is best explained within the shared subtree documentation of the Linux kernel.

uts namespace (UNIX Time-sharing System)

Ввели в 2006 в Linux 2.6.19. Можно отсоединить домен и имя хоста от системы.

> sudo unshare -u
# hostname
nb
# hostname new-hostname
# hostname
new-hostname

And if we look at the system level nothing has changed, hooray:

exit
> hostname
nb

ipc namespace

Ввели в 2006 в Linux 2.6.19. Можно изолировать связи между процессами. Например, общую память (shared memory = SHM) между процессами. Два процесса будут использовать 1 идентификатор для общей памяти, но при этом писать в 2 разных региона памяти.

pid namespace (Process ID)

Ввели в 2008 в Linux 2.6.24. Возможность для процессов иметь одинаковые PID в разных namespace. У одного процесса могут быть 2 PID: один внутри namespace, а второй вовне его - на хост системе. Можно делать вложенные namespace, и тогда PID у 1 процесса будет больше. Первый процесс в namespace получается PID=1 и привилегии init-процесса.

> sudo unshare -fp --mount-proc
# ps aux

Флаг --mount-proc нужен чтобы переподключить proc filesystem из нового namespace. Иначе PID в namespace будут не видны.

net namespace (Network)

Ввели в 2009 в Linux 2.6.29 для виртуализации сетей. Каждая сеть имеет свои свойства в разделе /proc/net. При создании нового namespace он содержит только loopback интерфейсы. Создадим:

> sudo unshare -n
# ip l
# ip a
  • Каждый интерфейс (физ или вирт) присутствует единожды в каждом namespace. Интерфейсы можно перемещать между namespace;
  • Каждый namespace имеет свой набор ip, таблицу маршрутизации, список сокетов, таблицу отслеживания соединений, МЭ и т.д. ресурсы;
  • Удаление net namespace разрушает все вирт интерфейсы и перемещает оттуда все физические.

Применение: создание SDN через пары виртуальных интерфейсов. Один конец пары подключается к bridge, а другой конец - к целевому контейнеру. Так работают CNI типа Flannel.
Создадим новый net namepsace:

> sudo ip netns add mynet
> sudo ip netns list
mynet

Когда команда ip создаёт network namespace, она создаёт it will create a bind mount for it under /var/run/netns too. This allows the namespace to persist even when no processes are running within it.

> sudo ip netns exec mynet ip l
> sudo ip netns exec mynet ping 127.0.0.1

The network seems down, let’s bring it up:

> sudo ip netns exec mynet ip link set dev lo up
> sudo ip netns exec mynet ping 127.0.0.1

Let’s create a veth pair which should allow communication later on:

> sudo ip link add veth0 type veth peer name veth1
> sudo ip link show type veth

Both interfaces are automatically connected, which means that packets sent to veth0 will be received by veth1 and vice versa. Now we associate one end of the veth pair to our network namespace:

> sudo ip link set veth1 netns mynet
> ip link show type veth

Добавляем адреса ip:

> sudo ip netns exec mynet ip addr add 172.2.0.1/24 dev veth1
> sudo ip netns exec mynet ip link set dev veth1 up
> sudo ip addr add 172.2.0.2/24 dev veth0
> sudo ip link set dev veth0 up

Теперь можно связываться в обе стороны:

> ping -c1 172.2.0.1
> sudo ip netns exec mynet ping -c1 172.2.0.2

It works, but we wouldn’t have any internet access from the network namespace. We would need a network bridge or something similar for that and a default route from the namespace.

user namespace

Ввели в 2012-2013 в Linux 3.5-3.8 для изоляции пользователей, групп пользователей. Пользователь получает разные ID внутри и вовне namespace, а также разные привилегии.

> id -u
1000
> unshare -U
> whoami
nobody

После создания namespace, файлы /proc/$PID/{u,g}id_map раскрывают соответствия user+groupID и PID. Эти файлы пишутся лишь единожды для определения соответствий.

> cat /proc/$PID/uid_map
0 1000 1

cgroups

Ввели в 2008 в Linux 2.6.24 для квотирования и далее переделали капитально в 2016 в Linux 4.6 - ввели cgroups namespace.

Cgroup version check:

stat -fc %T /sys/fs/cgroup/

For cgroup v2, the output is cgroup2fs. For cgroup v1, the output is tmpfs.

cgroups memory limit

Cgroups memory.limit_in_bytes was deprecated because it is prone to race condition: https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#deprecated-v1-core-features

Использовать memory.max (in bytes)! https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#namespace

Система выдаёт список ограничений. Поменяем ограничения памяти для этой cgroup. Также отключим swap, чтобы реализация сработала:

unshare -c # unshare cgroupns in some cgroup

cat /proc/self/cgroup
sudo mkdir /sys/fs/cgroup/demo
cd /sys/fs/cgroup/demo/
sudo su
echo 100000000 > memory.max
echo 0 > memory.swap.max

cat /proc/self/cgroup
echo 0 > cgroup.procs
cat /proc/self/cgroup

После того как установлено ограничение в 100Mb памяти ОЗУ, напишем приложение, которое забирает память больше чем положенные 100Mb (в случае отсутствия ограничений приложение закрывается при занятии 200Mb):

fn main() {
    let mut vec = vec![];
    let max_switch: usize = 20; // запасное ограничение =200Mb
    let mut memcount: usize;
    loop {
        vec.extend_from_slice(&[1u8; 10_000_000]);
        memcount = vec.len() / 10_000_000;
        println!("{}0 MB", memcount);
        if memcount > max_switch {
            break;
        }
    }
    println!("Program terminated by MAX MEM = {}0 Mb", memcount);
}

Если его запустить, то увидим, что PID будет убит из-за ограничений памяти:

# rustc memory.rs
# ./memory
10 MB
20 MB
30 MB
40 MB
50 MB
60 MB
70 MB
80 MB
90 MB
Killed

Составление пространств имен

Можно составлять пространства имён вместе, чтобы они делили 1 сетевой интерфейс. Так работают k8s Pods. Создадим новое пространство имён с изолированным PID:

> sudo unshare -fp --mount-proc
# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.1  0.6  18688  6904 pts/0    S    23:36   0:00 -bash
root        39  0.0  0.1  35480  1836 pts/0    R+   23:36   0:00 ps aux

Вызов ядра setns с приложением-обёрткой nsenter теперь можно использовать для присоединения к пространству имён. Для этого нужно понять, в какое пространство мы хотим присоединиться:

> export PID=$(pgrep -u root bash)
> sudo ls -l /proc/$PID/ns

Теперь присоединяемся с помощью nsenter:

> sudo nsenter --pid=/proc/$PID/ns/pid unshare --mount-proc
# ps aux
root         1  0.1  0.0  10804  8840 pts/1    S+   14:25   0:00 -bash
root        48  3.9  0.0  10804  8796 pts/3    S    14:26   0:00 -bash
root        88  0.0  0.0   7700  3760 pts/3    R+   14:26   0:00 ps aux

Своё приложение, создающее контейнер

https://brianshih1.github.io/mini-container/preface.html

runc

Система сборки и запуска контейнеров:

> sudo runc run -b bundle container

Можно исследовать, что runc создал mnt, uts, ipc, pid и net:

> sudo lsns | grep bash
4026532499 mnt         1  6409 root   /bin/bash
4026532500 uts         1  6409 root   /bin/bash
4026532504 ipc         1  6409 root   /bin/bash
4026532505 pid         1  6409 root   /bin/bash
4026532511 net         1  6409 root   /bin/bash

Runtime

External Link:

В 2007 году Google сделали проект Let Me Contain That For You (LMCTFY), в 2008 году появился Linux Containers (LXC). Для управления LXC в 2013 году появился инструмент Docker. Далее в 2015, команда Docker разработали проект libcontainer на языке Go. Также, в 2015 вышел Kubernetes 1.0. В 2015 собрали Open Container Initiative (OCI), которые стали разрабатывать стандарты на метаданные (манифесты-спецификации), образы контейнеров, методы управления ими. В том числе, в рамках OCI создали инструмент запуска и работы с контейнерами runc.

runc

sudo apt install runc
runc spec
cat config.json

В спецификации от runc можно увидеть всё необходимое для создания и запуска контейнера: environment variables, user + group IDs, mount points, Linux namespaces. Не хватает только файловой системы (rootfs), базового образа контейнера:

sudo apt install skopeo, umoci # Ubuntu 2404+
skopeo copy docker://opensuse/tumbleweed:latest oci:tumbleweed:latest
sudo umoci unpack --image tumbleweed:latest bundle

В распакованном образе можно найти готовую Runtime Specification:

sudo chown -R $(id -u) bundle
cat bundle/config.json

В ней можно увидеть обычные поля из runc, а доп заполненные annotations:

  "annotations": {
    "org.opencontainers.image.title": "openSUSE Tumbleweed Base Container",
    "org.opencontainers.image.url": "https://www.opensuse.org/",
    "org.opencontainers.image.vendor": "openSUSE Project",
    "org.opencontainers.image.version": "20190517.6.190",

Чтобы создать контейнер с runc, нужно его зацепить на терминал ввода команд TTY:

sudo runc create -b bundle container

ERRO[0000] runc create failed: cannot allocate tty if runc will detach without setting console socket

На существующий TTY зацепить контейнер нельзя (потому что окно удалённого xTerm не поддерживает такое), нужно создать новый виртуальный TTY и указать его сокет. Для этого надо установить Golang, скачать приложение rectty, создать с его помощью виртуальный терминал, после чего В ДРУГОМ ОКНЕ терминала создать контейнер и зацепить его на создвнный TTY:

sudo apt install wget
wget https://go.dev/dl/go1.23.3.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.23.3.linux-amd64.tar.gz

/usr/local/go/bin/go install github.com/opencontainers/runc/contrib/cmd/recvtty@latest

rectty tty.sock

В ДРУГОМ ОКНЕ терминала создать контейнер и зацепить его на создвнный TTY:

sudo runc create -b bundle --console-socket $(pwd)/tty.sock container
sudo runc list # контейнер в статуса created, не запущен
sudo runc ps container # посмотрим что внутри него
UID        PID  PPID  C STIME TTY          TIME CMD
root     29772     1  0 10:35 ?        00:00:00 runc init

runc init создаёт новую среду со всеми namespaces. /bin/bash ещё не запущен в контейнере, но уже можно запускать в нём свои процессы, полезно чтоб настроить сеть:

sudo runc exec -t container echo "Hello, world!"
Hello, world!

Для запуска контейнера выполним:

sudo runc start container
sudo runc list
sudo runc ps container
UID        PID  PPID  C STIME TTY          TIME CMD
root      6521  6511  0 14:25 pts/0    00:00:00 /bin/bash

Исходный runc init пропал, теперь только /bin/bash существует в контейнере. На ПЕРВОМ ОКНЕ терминала появилась консоль контейнера:

$ ps aux
ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   5156  4504 pts/0    Ss   10:28   0:00 /bin/bash
root        29  0.0  0.0   6528  3372 pts/0    R+   10:32   0:00 ps aux

Можно проверить управление: заморозим контейнер. Во ВТОРОМ ОКНЕ терминала выполним:

sudo runc pause container
# в первом окне ввод команд прервётся
sudo runc resume container

# Рассмотрим события контейнера:
sudo runc events container
#{...}

Для остановки контейнера достаточно выйти из rectty-сессии, после чего удалить контейнер. Остановленный контейнер нельзя перезапустить, можно лишь пересоздать в новом состоянии:

> sudo runc list
ID          PID         STATUS      BUNDLE      CREATED                         OWNER
container   0           stopped     /bundle     2019-05-21T10:28:32.765888075Z  root
> sudo runc delete container
> sudo runc list
ID          PID         STATUS      BUNDLE      CREATED     OWNER

Можно модифицировать спецификацию в контейнере (bundle/config.json):

> sudo apt install moreutils, jq # инструмент jq для работы с JSON
> cd bundle
> jq '.process.args = ["echo", "Hello, world!"]' config.json | sponge config.json
> sudo runc run container
> Hello, world!

Можно удалить разделение PID namespace процессов в контейнере с хостом:

> jq '.process.args = ["ps", "a"] | del(.linux.namespaces[0])' config.json | sponge config.json
> sudo runc run container
16583 ?        S+     0:00 sudo runc run container
16584 ?        Sl+    0:00 runc run container
16594 pts/0    Rs+    0:00 ps a
[output truncated]
  • runc очень низкоуровневый и позволяет серьёзно нарушить работу и безопасность контейнеров.
  • Поэтому сделаны надстройки обеспечения ИБ уровня ОС: seccomp, SELinux и AppArmor
  • Однако, их намного удобнее использовать на уровне управления выше
  • Для защиты также можно запускать контейнеры в режиме rootless из runc
  • С помощью runc нужно руками настраивать сетевые интерфейсы, очень трудоемко

CRI-O

Инструмент CRI-O разработан в 2016 при участии OCI в рамках проекта Kubernetes. Философия UNIX, максимально лёгкий аналог Docker/containerd. Он НЕ предназначен как инструмент для приёма команд от разработчиков. Задача - принимать команды от K8s. Внутри себя CRI-O использует runc как backend, и принимает команды по gRPC API как frontend.

Попробуем CRI-O с помощью спец-контейнера с crictl:

sudo apt install podman
sudo vim /etc/containers/registries.conf
# нужно задать репозиторий для скачивания:
# unqualified-search-registries=["docker.io"] 
sudo podman run --privileged -h crio-playground -it saschagrunert/crio-playground

Внутри лежит файл sandbox.yml:

---
metadata:
  name: sandbox
  namespace: default
dns_config:
  servers:
    - 8.8.8.8

Из него можно создать Pod:

$ crictl runp sandbox.yml
5f2b94f74b28c092021ad8eeae4903ada4b1ef306adf5eaa0e985672363d6336

SSG Hugo

Install (on NIX)

Need to install:

Choose a site directory and create site + repo:

hugo new site ./
git init
git add *
git commit -m "Initial commit"
cd themes/
git submodule add https://github.com/halogenica/beautifulhugo.git themes/beautifulhugo
git status
git add *
git commit -m "Theme added"

Some themes (Relearn) have to be downloaded as zip. So we have a git repo inside a git repo now. Copy contents exampleSite of theme:

cp -r themes/hugo-theme-relearn/exampleSite/* ./

Launch local web-server:

hugo server

Visit local site and check it.

Gitlab sync

Create a Gitlab account, make SSH Key exchange. After that, create project by CLI from hugo site folder:

git branch -m master main # change master to main branch for gitlab
git push --set-upstream git@gitlab.com:aagern/hugotest.git main

Where hugotest.git is the project name.

  • Click Setup CI/CD button.

Use Hugo template (should be in Gitlab):

---
# All available Hugo versions are listed here:
# https://gitlab.com/pages/hugo/container_registry
# image: "${CI_TEMPLATE_REGISTRY_HOST}/pages/hugo:latest"
image:
  name: "${CI_TEMPLATE_REGISTRY_HOST}/pages/hugo:latest"
  entrypoint: ["/bin/sh", "-c"]
variables:
  GIT_SUBMODULE_STRATEGY: recursive
test:
  script:
    - hugo
  except:
    variables:
      - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pages:
  script:
    - hugo
  artifacts:
    paths:
      - public
  only:
    variables:
      - $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  environment: production

Change visibility: Settings -> General -> Pages visibility = Everyone.

Digital Art

Articles on using different illustration and video content software.

Subsections of Digital Art

Darktable

Denoise with Wavelets

  • Use the Denoise (Profiled) module (type “denoise” in search bar);
  • Default options - eliminate chroma noise;
  • Increase Strength = 1.1-1.2 if needed (not too much).
  • Create a second instance of Denoise (Profiled) module;
  • Turn mode=non-local means instead of wavelets to remove grain;
  • Select Drawn Mask to expand the preferences;
  • Click Add Brush button and draw a mask on the parts of the main object in focus;
  • Click Toggle polarity of drawn mask (Invert mask) button;
  • The mask can be seen using Display mask button;
  • Increase feathering radius and blurring radius values;
  • Turn down the Strength to the minimal viable =~0.5-0.6;
  • Applying Denoise might lead to an “oil painting” effect. To fix this, add a Contrast Equalizer module;
  • Click on Drawn Mask button, choose drawn mask = denoise (profiled);
  • Click Toggle polarity of drawn mask (Invert mask) button;
  • The mask can be seen using Display mask button;
  • Increase feathering radius and blurring radius values;
  • Create a luma graphic curve in the fine section to smooth the oil effect:

Subsections of DaVinci Resolve

Configuration

Global View Settings

Save Monitor Space

  • Menu Workspace → Turn OFF Show Page Navigation

Dual Monitor Config

Note

Requires DaVinci Studio license.

  • Use second monitor for interface: menu Workspace → Dual Screen = ON;
  • Use second monitor for full-screen view of video: menu Workspace → Video Clean Feed=.

Timeline Config

Turn ON Timeline -> Selection follows playhead

Database and files

Create a new database from scratch. Choose PostgreSQL or disk files.

It is recommended to create a separate database + folders for video, pictures, cache etc. for each global project, like Youtube-channel.

Folder structure on disk:

-PROJECT
–Database
–Cache
–SourceMedia (source video)
–Gallery (color correction templates)
–Output (master videos)
–Backups

Project Settings

  • Timeline resolution - the resolution in which the project will be edited. This is NOT the resolution, in which the project will be rendered. So you can edit the whole project in FullHD or 2048x1080DCI faster mode and then render it in 4K UHD or 4096x2160 DCI - DaVinci Resolve will mathematically scale all masks & effects to match the higher resolution on final render. Blackmagic does warn that such scale may cause some approximation;
  • Timeline resolution - may be changed in process of work. Convenient to choose before work;
  • Timeline frame rate - cannot be changed in process of work. Need to choose wisely! For example, transitions correction layers may be only made for specific frame rate (29.97 FPS);
  • Playback frame rate the same as timeline frame rate, but is the playback rate of the connected reference monitor;
  • Video Monitoring section contains parameters for the connected reference monitor;
  • Optimized media resolution - may be changed later, but better to change right away to save performance and space on disk = Half/Quarter from original;
  • Optimized Media Format / Render Cache Format - fastest codec on Win = DNxHR LB, on macos = ProRes LT/Proxy;
  • Enable all checkboxes in this section to save performance the most;
  • Cache files location - insert folder Cache made earlier;
  • Gallery stills location - insert folder Gallery made earlier;

Save current settings as a preset: go to Presets tab in Project Settings, choose Save As.. After saving the preset, choose it with a right mouse click and choose Save As User Default Config.

Image Scaling

Mismatched resolution files = Scale full frame with crop, this setting greatly helps in conform process, if material was re-scaled before color correction.

Color Management

Broadcast Safe: turn ON to help monitor colors to be safe for TV broadcast.

  • -20 - 120 - most lenient option
  • 0 - 100 - most strict option 

Check current colors in Color room go in menu View → Display Safe Broadcast Exceptions

Get the colors back to safe with either using HUE vs SAT instrument, or Gamut Mapping OpenFX plugin (Gamut mapping method = Saturation Compression, lower Saturation Max level). 

Global Preferences

These preferences work for the whole system and all projects.

System - Memory & GPU

  • Resolve Memory usage - give all available memory to DaVinci, if no games played or apps used same time as DaVinci;
  • Fusion Memory Cache - needed for Fusion compositing rendering and playback in real time;
  • GPU - leave Auto in case no problems. For macOS choose = Metal, for PC Intel/AMD card = OpenCL, for NVIDIA choose CUDA. GPU Selection Mode needed for several GPU cards on one computer: one can be used for interface, the other for rendering etc. Are 2 better than 1, should you connect both? Actually this depends on type of cards - does not give significant speed in notebook setup with external videocard, if both the external card and the inner card of the notebook are connected: this gives a few bonus seconds faster render but makes the notebook unusable during render process. Also inner notebook card gets connected to interface and previews, which makes them work slower.

System - Media Storage

  • Create shortcuts for all folders actively used in the project;

System - Decode Options

Needed for work with RAW. Enable all checkboxes - for the GPU to use hardware Debayer on RAW material.

System - Audio Plugins

The folder containing 3rd party VST plugins for Fairlight, in case DaVinci did not find them itself.

User - Color

  • Always perform copy and paste on selected nodes. Should be turned ON to do copy-pasting in DaVinci.

User - Project Save and Load

  • Live Save checkbox. This makes DaVinci save any change, like FCPX;
  • Configure Project backups: every 6 min, every 1 hour, every 1 day. 
  • Choose Backup location folder, created previously.

Annotations to video

Create anntations to video to future self or other artists:

  • Use M,M to label the created annotation markers;
  • Use ALT+click on the marker to change its’ duration.

Annotations translate to every other room in DaVinci Resolve.

Proxy Media

Use Proxy to optimize performance. Use column header Proxy to check if proxies were created for video.

In Color Room, turn off proxies to check color drift.

Exposure Guide in DVR

Use Ansel Adams Zone Model to divide exposure in 10 parts

Check the video by using Effects -> False Colors. Configure the effect: Plugin Mode = Creative, Creative Style = Exposure Guide

DVR Hotkeys

Global Keys

CTRL+Z - Undo

CTRL+SHIFT+Z - Redo

Rooms, Sections

  • SHIFT+1 - Project Manager
  • SHIFT+2 - Media
  • SHIFT+3 - Cut
  • SHIFT+4 - Edit
  • SHIFT+5 - Fusion
  • SHIFT+6 - Color
  • SHIFT+7 - Fairlight
  • SHIFT+8 - Deliver
  • SHIFT+9 - Project Settings

EDIT TAB - Show/Hide Media Pool

EDIT SHIFT+TAB - Show/Hide Inspector

~ - Enter DaVinci Full Screen

ctrl+~ - See video from Preview in Full Screen (to switch betwwen clips use this from Color tab and ARROWUP/ARROWDOWN)

Playhead keys

  • 1 - backwards
  • 2 - pause/run
  • 3 - forwards
  • 4 - In Point
  • 5 - Out Point
  • 6 - Insert selected to timeline at playhead position

In/Out Points

EDIT X - create In/Out points around selected clip

EDIT ALT+X - delete In/Out points

EDIT SHIFT+A - create In/Out points around several selected clips

EDIT Q - Trim to playhead from left

Markers

EDIT M - Create marker (double-tap M,M - create maker & launch marker config window)

EDIT ALT+M - Delete selected marker

EDIT SHIFT+ARROWUP/ARROWDOWN - move playhead between markers

Editing Keys

EDIT W - Split on playhead

EDIT E - Trim to playhead from right

EDIT S - Ripple delete selected clip

EDIT D - Enable/disable clip

EDIT T - Trim edit mode tool

EDIT CTRL+D - Delete all gaps between selected clips

EDIT - ARROWUP/ARROWDOWN - Select prev/next clip in timeline

EDIT - ARROWLEFT/ARROWRIGHT - Move by 1 frame left/right

EDIT - SHIFT+ARROWLEFT/ARROWRIGHT - Move by 1 second left/right

EDIT - . / , - Move selected clip left/right by 1 frame

EDIT - - / = - Zoom Out/In timeline (Also ALT+SCROLLBUTTON or ALT+SCROLLTRACKPAD gesture), also SHIFT+W for Zoom In

EDIT CTRL+SCROLLTRACKPAD - scroll timeline (or CTRL+SCROLLBUTTON)

EDIT SHIFT+SCROLLTRACKPAD - scale in/out track on timeline (or SHIFT+SCROLLBUTTON)

EDIT - SHIFT+Z - Zoom to fit timeline

EDIT - ALT+ARROWUP/ARROWDOWN - bring selected clip up/down a track on timeline

EDIT - SHIFT+~ - Reveal Transform border over selected clip in viewer window

EDIT - CTRL+SHIFT+V (done after CTRL+C) - Paste selected attributes on selected clip / paste clip with no overwrite - moves adjacent clips to the right

EDIT - CTRL+SHIFT+X - Remove attributes from selected clip

EDIT - P - Open clip attributes windows

EDIT - F - turn snapping ON/OFF (also use N)

EDIT - ALT+F - find selected clip from timeline in Media Pool

EDIT - CTRL+SHIFT+F - find & show selected clip from Media Pool in Finder

Compound Clips

EDIT - CTRL+E - create a Compound clip from selected clips

EDIT - CTRL+SHIFT+E - enter inside Compound clip

EDIT - ALT+SHIFT+E - decompose a Compound clip

Clip Timeline Properties / Retime

EDIT - G - turn linked selection ON/OFF

EDIT - C - unlink/link video+audio of selected clip

EDIT - R - Open clip speed window (+Ripple timeline in config will move adjacent clips without overwirting them)

EDIT - SHIFT+R - clip speed overlay on selected clip

EDIT - CTRL+R** - clip speed menu for clip

EDIT - CTRL+SHIFT+R - show retime curve: ALT+click on curve to add speed points fast

EDIT - CTRL+ALT+R - reset retime controls to 100%

EDIT - SHIFT+C - Curve Editor on selected clip

EDIT - CTRL+SHIFT+C - Keyframe Editor on selected clip

EDIT - L - Loop mode ON/OFF

EDIT - SHIFT+SPACEBAR - play the loop (use In/Out points to define the loop interval)

Y - select clip & press to select all other clips to the right in the track from the current selected one CTRL+Y - clip & press to select all other clips to the left in the track from the current selected one

EDIT H - show current Timeline you are working in at Media Pool

Room - Fusion

Articles in section

Fusion Settings

Menu Fusion → Turn OFF Show Toolbar;

Menu Fusion → Fusion Settings…

  • General tab: ProxyStandard change to 4:1;
  • Flow tab: Source Tile Pictures=ON will show video thumbnails in node graph instead of plain rectangles;
  • Flow tab: Arrange to Connected=ON helps organize connected nodes;
  • Flow tab: Show grid=OFF lessens visual garbage on Node graph;
  • Flow tab: Build direction=Vertical to streamline node creation with software such as The Foundry Nuke or SideFX Houdini;
  • Frame format tab: Lock depths=16bit float per channel(64 bit) will significantly enhance inner image processing quality;
  • User interface tab: Zoom Cmd=OFF, Pan Cmd=ON - this will enable zoom with simple mouse wheel like in many other editors.
Note

Reboot of DaVinci Resolve needed after settings saved.

Hotkeys in Node Graph

SHIFT+SPACE - search&create node by name menu

CTRL+T - switch inputs (in merge node switch background & foreground)

Connecting output of one node to the output of another node will automatically create a Merge node;

Tips

Editing Color Curves color correction and other nodes splines in Spline Editor

  • Put a Color Curves node;
  • Open Spline Editor. In its’ menu , choose Show only Selected Tool, then click Zoom to Fit button (diagonal arrows in a square).

Copy perspective view to camera view

  • Connect camera node to Merge node;
  • While Merge node is selected in graph, in perspective windows rotate the view, the Mouse Right-Click → Camera → Copy PoV To → Choose the connected camera.

Reactor Plugin Manager

Link: https://www.steakunderwater.com/wesuckless/viewtopic.php?f=32&t=3067

  • Drag & Drop the Reactor LUA script into Fusion nodes field, install;
  • Check out Reactor in Workspace → Scripts → Reactor → Open Reactor…

To remove plugins from Reactor, use Remove button. Do not un-tick them.

Handy plugins

  • xGlow - advanced version of Glow node;
  • FastExpoGlow - gives a golden light;
  • HeatDistortion - hot air effect.

Subsections of Room - Fusion

Travel Maps

External Link: https://www.youtube.com/watch?v=Vb42fYP6FBU

  • Choose a large map picture, throw it in DaVinci Resolve Timeline;
  • Make the still image video 10 seconds in length;
  • Open the map image video in Fusion Room;
  • Add a Background node and Merge it with the MediaIn node;
  • Add a Polygon node and connect it to Background node;

  • Choose the Polygon node, then in the Viewer draw line segments from point A to B;

  • In Polygon node → Controls → Border Width = 0.006 add border width to see the line. Choose Polygon → Controls → Border Style to make line ends round or square;

  • Change Background node Background → Color to make the line colored appropriately;

  • Animate the Polygon node → Controls → Length from 0 and frame 0 to 1.0 at last frame;

  • In Spline Editor choose both keyframes, make them smooth (hotkey S). Then use hotkey T to select Ease In = 45 and Ease Out = 35;

  • Create map points: add a new Background node, Merge it with the previous Merge node;

  • Add an Ellipse node, drag the border of the ellipse to make it a small point;

  • Animate Ellipse → Controls → Border Width from small negative number to 0 to make the point pop out when travel line starts from it;

  • Go to Spline Editor, choose the Ellipse keyframes and press S to make them smooth;

  • Ctrl+C copy the Ellipse, then Ctrl+V paste it to make other points for the travel line. Move them to appropriate positions;

  • Use the Keyframes editor to move the keyframes of ellipse copies in the appropriate frames of time.

Make Pseudo-3D Effects

  • Add Drop Shadow node, connect it after the Polygon + Background nodes, before the Merge node;
  • Reduce Drop Shadow → Controls → Blur parameter, reduce the Drop Shadow → Controls → Drop Distance parameter to make the shadow closer;
  • Add the DVE node just before MediaOut node;
  • Adjust DVE → Controls → Z Move = 0.3. Adjust DVE → Controls → Rotation → X and Y to make cool 3D-like effect;
  • Make the Viewer into Dual-View, and put result of DVE in one viewer and Merge node before it in second Viewer windows;
  • Adjust DVE → Controls → Pivot → X slightly to see the large X in the viewers. Grab the X and move it to the first point of the line;
  • Animate the Pivot by dragging it to points following the travel line. The use the Spline editor to choose all keyframes and make the animation smooth (S);
  • Go to Polygon node and edit Polygon → Settings → Motion Blur = ON, Quality = 9.

Room - Deliver

General Web Export

Preset = H.264

Quality restrict to:

  • 10000 - 40000 Kb/s - for 1080p
  • 20000 - 80000 Kb/s - for 4K UHD/DCI
  • 6000 - 9000 Kb/s - for Instagram, 720p. More is not needed since video will be re-encoded by their codec.

For RAW footage:

  • Force sizing to highest quality
  • Force debayer to highest quality

Room - Color

Articles in section

Color Room Hotkeys

General

  • SHIFT+D - Grade Bypass (Disable). Disable all nodes to see the original image;
  • CTRL+D - Grade Bypass (Disable) on current node;
  • SHIFT+H - see node mask, or qualifier mask etc. in monitor windows;
  • CTRL+F - See preview window in Full screen.

Create nodes

  • ALT+S - New Corrector (Serial) AFTER selected node;
  • SHIFT+S - New Corrector (Serial) BEFORE selected node;
  • ALT+L - Add Layer lower than current node;
  • ALT+O - New Outside node (Includes everything NOT selected in previous node)
  • ALT+P - New Parallel node.

Tracker

  • CTRL+T - Track mask forward;
  • ALT+T - Track mask backward.

Versions

  • CTRL+Y - add new color grade version;
  • CTRL+N - switch to next version;
  • CTRL+B - switch to previous version.

PowerGrades

Create a PowerGrade folder to store color grading across all projects.

Captured stills can be put into this folder → go into the DaVinci Database and are shared across all projects. A convenient way to store ready-made Color Space Transforms for different camera colorspace/gamma types. Use Append Node Graph to apply the grade, do NOT use Apply Grade since this changes the original ISO of the footage,  which may differ in the specific scene.

Tracker

Cloud tracker - tracks using a whole mask with all its’ points.

Point tracker - needs control points on contrast areas, which it uses to track.

Stills

Right-click on footage → Grab Still: this saves the footage with currently applied grade. After any changes to the image, you can find the still in Gallery, and:

  • double-click on still to make a A|B compare picture. CTRL+W to see changes on full screen (disable still);
  • right-click → Apply Grade to apply to what was saved. Stills can be applied to any footage to copy correction or try interesting results.

Subsections of Room - Color

Color Balance

Tip to help reach ideal neutral color balance.

  • Create Serial Node for color balance correction  and 2 Layer nodes;
  • Apply OpenFX module Color Generator to second Layer Node;
  • In Color Generator options choose Color = HEX #808080 (neutral gray);
  • Choose Composite Mode = Luminosity;
  • In balance Serial Node, add +Saturation to see color offset;
  • Turn on Scopes = Parade;
  • Use Offset Wheel to neutralize colors (Parade RGB levels equal);
  • Turn OFF the layer node with color generator.

Color Grading

Scenarios

Свет в городском пейзаже

  • Закат - много красного, оранжевого в средних и в светлых тонах, синий цвет в тенях противопоставляется;
  • Рассвет - более холодный чем закат, больше зелёного чем красного/оранжевого;
  • Ночь - в кино обычно синяя ночь, чем чёрная. Небо сохраняет светлость;
  • Унылый серый город - снизить насыщенность (Saturation), чтобы это подчеркнуть.

Цвет в портрете

Хакрактер рисуется контрастом. Обычно грейдинг начинается с опусканием вниз теней. Дальше отделение от фона.

Работа с ЧБ портретом

Человек не воспринимает ЧБ как именно ЧБ, он ищет и выдумывает немного цвета. Поэтому ЧБ не делают полностью лишённым цвета, его чуть-чуть подкрашивают для придания настроения: для тёплого настроения подкрашивают в коричневый, для “глянцево-журнального” настроения - в синий. Количество подкраса совсем маленькое, совсем чуть-чуть, как петрушка в супе.

Teal & Orange

  • Start by grading the image into nice blue using all color wheels (Lift, Gamma, Gain), and distribute the wheels in a clockwise fashion between each other!

  • All colors in the image turn blue, but shadows have to stay neutral: user the Log wheels to fine tune: Shadow, Midtone, Highlight wheels between Low Range (default lower than 33% = shadows) and High Range (default higher than 66% = highlights). Turn the Shadow wheel to blue counterpart to make shadows neutral. Lower the Low Range (around 10%) so that midtones are not affected;

  • THEN mask out skin and other “warm elements” in the picture (in parallel node) and grade them into the opposite color - yellow-orange;

  • Use the Log wheels to balance the colors: make a soft bridge between warm skin and cold background by turning Shadow wheel to green a bit, Highlight to magenta;

  • Balance the footage colors, so the color work together, and not appear as isolated elements: one cheater method to do this is to radically darken the background. Since with teal&orange we get the maximum color contrast, it is better to make the light contrast stronger as well. Another method is to make subtle changes - bring the overcolored ares back by lowering saturation, for example.

Color Wheels → Color Boost (number in the below section) - increases/decreases the saturation of the MOST SATURATED colors in the image. Helps if you have some over-saturated colors which you want to level down, without touching the rest if the image.

Add Saturation to high values, then subtract Color Boost to remove color spills.
Example: vegetables in a white glass bowl will pass their color to the bowl. To keep it white, subtract Color Boost.

​​Цветокоррекция и покрас плёночного кино

В плёночном кино грейдинг являлся этапом PRE-Production. Это связано с необходимостью подбора правильной плёнки, дающей нужную гамму цветов. Для проявки плёнки проявочные машины (пример - российская машина МПМ—16—3М), которые работали по заданному СТАНДАРТУ.

Color Management

Videocard sends a signal to the monitor, divided in 2 components: COLORSPACE (color) and GAMMA (light)

COLORSPACE

Initial colorspace is a triangle-like model of all colors which are discriminated by the human eye (mathematical model): Different color spaces encompass different sets of colors as compared to this model: rec709 - came with digital TV, P3 - used for digital cinema theater, rec2020 - UHDTV / new 4K video standard.

GAMMA (Dynamic Range)

Digital cameras see the world in a linear fashion. If we have a room with 1 lamp and we bring in a second lamp, camera will register 2x increase of light. Human eye will register around +20% increase of light. Transfer function - math function to compress the light data of an image and then use this file to restore the whole dynamic range seen by the human eye.

  • Human eye dynamic range: 10-13 stops active range, 24 stops total range
  • Film dynamic range: 13 stops
  • RED Weapon Camera: 16.5+ (incl HDRX-mode) stops
  • HLG: 12 stops
  • REC709: 6 stops

REC709 GAMMA 2.4 is the native format for DaVinci Resolve, Adobe Premiere etc. If you throw a different colorspace file in DaVinci, you must tell it what it is and convert: Color room → Library → ResolveFX Color → Color Space Transform

Fir SONY SLog3 shoot with +2 or +3 F-stops higher to quell noise, then choose:

  • Input Colorspace = Sony S-Gamut3
  • Input Gamma = Sony S-Log3
  • Output Colorspace = Rec709
  • Output Gamma = Rec709
  • Tonemapping = Luminance Mapping

Color Masks

Using Masks for Correction

Always use the curved mask: it is the only one that has soft transitions, which can be changed individually for different sides:

Neural Masks

Davinci Resolve Studio 18+ brings Object Mask to select parts of a person. Draw lines with dropper+ over face & other parts of a person to highlight them. Use dropper- to leave other objects out of the mask. Use Better quality with Refine Edge BEFORE calculating the mask.

You can see the mask using Corrector node.

Use Key Mixer node to combine several masks (you can input more than 2, links are added in Key Mixer node automatically upon connection), invert them using button in Key Mixer config.

De-Noiser

Noise

Most of the noise is in red channel. Use DaVinci Resolve Studion version with 2 types of de-noise.

Motion Effects → Temporal NR

  • In case of light noise: Frames=3, Mo. Est. Type=Faster, Luma=Chroma=~10+
  • In case of heavy noise: Frames=5, Mo. Est. Type=Better, Luma=Chroma=~20+

Motion Effects → Spatial NR

Unlock Chroma != Luma. Raise Chroma=6-7

Moire

Moire on buildings and other landscape objects is countered by motion blur.

Motion Effects → Motion Blur

Mo. Est. Type: Better, Motion Range=Small, Motion Blur=100 (maximum).

Highlights

Blown Highlights

They can be restored using several methods:

  • Use the wheels: lower Gain, then create node and restore contrast;
  • Use the Curves: lower the curve in the highlight area, make “S” form to restore contrast;
  • In the Curves instrument, use Soft Clip section: Low / High restores shadows / highlighs, while Low Soft / High Soft makes a smooth transition - ideal for blown sky areas.

Controlled Restoring of Blown Highlights

  • Create a Parallel Node after the input together with the denoise node
  • Use Luminance Qualifier to select smoothly the bright part of the image;
  • Turn down Highlights to get the details back.

Parasite color in highlights

Typically such things appear when shooting on 8bit cameras. Use Lum Vs Sat Curve to fix.

  1. Isolate highlights by luminosity;
  2. Bring saturation level in highlights down to remove color.

Node Structures

Color Correction Structure

Color Correction structure contents:

  • global denoise to reach pristine look for selection
  • node for primaries
  • node for curves
  • basic secondaries + additional secondaries nodes
  • vignette node, then Add Node → Outside Node, then finally the global look

Color Grading Structure

Color Grading structure contents:

  • 3 global corrector nodes: color balance, global exposition, misc global node (noise reduction?)
  • 3+ local exposition corrector nodes: use masks for areas and add light, like lighting up the face
  • Parallel mixer with 3 inputs
  • 3+ local color corrector nodes: use masks for areas and change saturation, like de-saturating items with bright colors, which may drag unwanted attention
  • Parallel mixer with 3 inputs
  • 1 corrector node with a combination of LUTs to produce a special look (color grading for drama / atmosphere / mood goes here)

Parallel Nodes

Changes in parallel nodes are made to the image with the same intersection, as if the nodes were in sequence (Serial type). The difference is that all parallel nodes read data from one initial node, while in serial sequence the next node reads data from previous node with all corrections applied on the previous step.

Node Layers

Layers are parallel nodes, but work in opposite direction: lowest layer has the highest precedence. Layers can be composited using connecting node settings.

Node Sequence

Nodes sequence matters

  • Create 2 nodes: in the first darken image using a LIFT wheel. In the second node make it red with the LIFT wheel and raise Saturation to 100%;
  • Make a second version with nodes in reverse sequence;
  • See the difference: if darken is first, then more areas become red using the second node than vice-versa.
Tip

Instruments sequence also matters: in the Color Wheels section, Lift/Gamma/Gain wheels are applied BEFORE the general Offset wheel.

Contrast sequence matters

When you use a node to make stronger contrast on a footage, you basically stretch the image info between Dark & Light boundaries. If you create a second node after the “contrast creation” one, you will be changing the resulting stretched image data, and the changes will be more subtle and soft.

Skin and Sky

Skin Exposure

For dark skin (african-american) use contrast+pivot to the left to lighten up, while maintaining contrast:

For pale skin (caucasian) use contrast+pivot to the right to darken, while maintaining contrast:

Tip

If you lift the lower midtones, you need to make up for Saturation loss - raise it.

Glow

Use Glow OpenFX plugin on skin selection to add a bit of skin light (Shine Threshold = ~0.6)

(Quick) Skin Retouch

Skin tones are “Midtones”. So use Qualifier and masks to select the skin, and subtract the eyes & brows & mouth.

  • Raise Color Wheels → (page 2) Midtone Detail (MD) for boys to underline harshness and manliness;
  • Lower Color Wheels → (page 2) Midtone Detail (MD) for girls to smooth out their faces (around -80);
  • Lower overall effect of filter to make it look more natural Key → Key Output → Gain = 0.8 - 1.0

Sky Selection

Use Luminance qualifier for the cleanest sky selection.

Vignette

  • Хорошо работают при неоднородном фоне. На однородном они слишком очевидны
  • Делают видео более объёмным, отделяют передний и средний планы от заднего

Способ создания 1

  • OpenFX → ResolveFX Stylize → Vignette

Способ создания 2

  • Обычно виньетки применяются на уровне Timeline
  • Добавить новую ноду (ALT+S), добавить к ней круговую маску
  • Добавить outside node (ALT+O), с помощью колец Color Wheels опустить Gain и Saturation, в разделе Key снизить Key Output → Gain до 0.8-0.9, чтобы смешать с оригинальной картинкой Способ 2 лучше чем 1, т.к при способе 1 по углам создаются чёрные области с растушёвкой, в то время как при способе 2 происходит “multiply” видеопотока на себя с коррекцией = такой способ даёт менее “грязный” результат.

Способ создания 3

  • Создать ноду Vignette;
  • Создать круговую маску на субъекте, с высокой мягкостью (Soft = 25);
  • Зайти в Curves, основную кривую опустить примерно по центру вниз;
  • Добавить outside node (ALT+O),
  • В этой ноде зайти в Curves, основную кривую немного поднять (либо создать обратную S-кривую, поднять лишь тени), чтобы высветлить субъекта.

VR360

External links:

Подготовка видео

Для Insta360 X2 - видео 6K@30FPS:

  • Собрать видео и экспорировать из родной программы в 360VR-ProRes 422 формат.
    Поставить галочки FlowState, Lock Direction (для видео с рук или с дрона, для видео со штатива не надо).
  • Очистить видео от артефактов и увеличить до 8K. С помощью Topaz Video AI сделать 2 прохода:
    • Первый проход с алгоритмом Artemis DeHalo, размер 100%, добавить Grain (Size=1);
    • Второй проход с алгоритмом Gaia High Quality, размер 133,33%.

Подготовка проекта

Разрешение:

  • 3840x1920 - для соц сетей, интерактивного просмотра с моб телефонов подойдёт
  • 5760х2880 - для YouTube 6K показа
  • 7680x3840 - для Youtube 8K показа

Проксирование (Optimized Media and Render Cache):

  • Proxy media resolution = Quarter
  • Кодек = на Apple это Prores 422
  • Указать папку для складывания видео Proxy.

Просмотр VR360

Положить видео VR360 на timeline. Далее перейти в комнату Fusion, нажать правой кнопкой на видео в окне и выбрать 360 View -> LatLog. После этого можно панорамировать с помощью ALT+средняя кнопка мышки. Когда закончил панормирование, можно нажать правой кнопкой на видео в окне и выбрать Settings -> Reset Defaults

Для ускорения просмотра выбрать режим отображения Proxy и отключить высокое качество. Также в главном меню выбрать Playback -> Timeline Proxy Resolution -> Half.

Смена направления на север и завал горизонта

Для начальной ориентации камеры нужно добавить ноду PanoMap. Далее перевести отображение обоих окон с видео на эту ноду. Выбрать просмотр в одном из окон 360 View -> LatLog. С помощью настроек PanoMap поменять ориентацию по осям.
При съёмке с дрона может быть завал горизонта: исправить можно путём анимации осей. XYZ в настройках PanoMap.

Редактировать - убрать штатив/дрона

  • Добавить в Fusion ноду Lat Long Patcher. По оси X направить чётко вниз/вверх, чтобы было видо основание штатива или дрон.

  • Добавить ноду Paint. Выбрать в окне видео Stroke, в настройках ноды Paint выбрать режим Clone, размер кисти большой и повысить гладкость. С помощью ALT обозначить точку для клонирования и закрасить крестовину штатива или дрон. Скопировать ноду Lat Long Patcher после Paint, поменять у неё в настройках режим Mode=Apply. После этого добавить ноду Merge и соединить в неё вторую ноды MediaIn, Lat Long Patcher и результат вывести на Media Out. Объект должен пропасть в кадре.

Стабилизация VR360

В команте Fusion добавить ноду Spherical Stabilizer. Stabilize Strength = 1, Smooting = 1. Произвести трекинг всего видео.

Создание объектов в 3D видео

  • Добавить ноды Spherical Camera, Merge 3D, Renderer 3D, Shape 3D;
  • У ноды Shape 3D установить Shape = Sphere и соединить в неё Media In.
  • Подключить вывод видео в окно просмотра в ноде Merge 3D;
  • Можно панорамировать в окне просмотра с помощью ALT+средняя кнопка мышки;
  • У ноды Shape 3D установить Radius = 10, чтобы “войти в сферу”, увеличить Base Subdivisions = 60 и выше, чтобы убрать полигональные артефакты;
  • Выделить ноду Render 3D, включить отображение видео в 1 окно. Обратить внимание, что точка обзора поменялась. Нажать CTRL+G, чтобы отобразить перекрестье и направляющие;
  • У ноды Spherical Camera во вкладке Transform по оси Y подвинуть ось камеры, чтобы установить её в нужном направлении;
  • Проверить, что включено низкое качество показа, Proxy, в главном меню выбрать Playback -> Timeline Proxy Resolution -> Quarter для ускорения Renderer 3D;
  • Добавить ноду Text 3D, написать текст, подключить к ноде Merge 3D;
  • Использовать направляющие перемещения и поворота для правильной ориентации текста по отношению к камере;
  • Перейти в настройках Text 3D во вкладку Transform, добавить ключи анимации по осям XYZ;
  • Включить в интерфейсе Fusion вкладку Spline, включить в ней все оси анимации, нажать CTRL+F для масштабирования графиков анимации в экране;
  • Сделать анимацию по времени, нажать на ключевые кадры и усиками Безье придать плавность анимации;
  • Соединить выход Renderer 3D с Media Out;

Отображение 2D текста и графики в 3D

  • Добавить ноды Text, Background, соединить их через ноду Merge, после чего внести в 3D через ноду Image Plane 3D;
  • В настройках Image Plane 3D -> Transform регулировать масштаб Scale = 3;
  • Можно таким же образом перестаскивать из Media Pool обычные видеоролики и подключать через Image Plane 3D.

Экспорт видео

Для 6К - 8К видео выбирать кодек H.265. Битрейт Quality = 100000 Kb/s для большей плавности картинки.

Warning

Для шлема Oculus Quest 1 нужно указывать не более чем Quality = 60000 Kb/s.

Включить Advance Settings -> Force Sizing to highest quality, Force debayer to highest quality.

DaVinci Resolve не делает инъекции метаданных в видео, поэтому для публикации на Youtube нужно это сделать отдельно (для Facebook этого делать не нужно - можно просто закачать видео, и оно будет VR360).

Getting Things Done

Everything around documenting, note taking, mind maps, schemes & diagrams.

Subsections of Getting Things Done

CVE vs CWE

Определения

  • Задачи информационной безопасности (ИБ) - обеспечить конфиденциальность, целостность, доступность (Confidentiality, Integrity, Availability - CIA triad) в информационной системе (ИС);
  • Угрозы ИБ (threat) - потенциальные опасности ИС, если нарушитель использует её уязвимости для атак. Угрозы бывают трёх видов: нарушение конфиденциальности, целостности, доступности;
  • Уязвимости (vulnerability) - недостатки ПО, оборудования или в мерах по обеспечению безопасности с точки зрения человеческого фактора, которые дают возможность злоумышленнику проникнуть в ИС и совершать там противоправные действия (реализация угрозы);
  • Источник угрозы (threat agent) - хакер, недобросовестный или ошибшийся сотрудник, через которого произошла реализация угрозы, повреждение ИС, утечка данных.

Каждая уязвимость получает ранг на основе факторов:

  1. Какие системы затронуты;
  2. Какие данные в опасности;
  3. Какие бизнес-функции находятся под угрозой;
  4. Насколько легко реализовать атаку и добиться компрометации ИС;
  5. Потенциальный ущерб в результате уязвимости.

Mermaid

Tutorials: https://mermaid.js.org/config/Tutorials.html

Online editor + exporter: https://mermaid.live

Installation

Mermaid has plugins for VS Code, Obsidian, works with GitHub, GitLab etc.

Full list of integrations: https://github.com/mermaid-js/mermaid/blob/develop/docs/misc/integrations.md

Flowcharts

Example:

graph LR  %% TD = Top->Down, LR = Left->Right etc.
S[Start] --> A;
A(Enter your EMail) --> E{Existing user?};
E -->|No| Create(Enter name)
E -->|Yes| Send(Send a letter to address)
Create --> EULA{Accept conditions}
EULA -->|Yes| Send
EULA -->|No|A

Result:

graph LR
S[Start] --> A;
A(Enter your EMail) --> E{Existing user?};
E -->|No| Create(Enter name)
E -->|Yes| Send(Send a letter to address)
Create --> EULA{Accept conditions}
EULA -->|Yes| Send
EULA -->|No|A

Sequence Diagrams

Example:

sequenceDiagram
autonumber               %% action numbers placed on every arrow
actor C as Client
Note left of C: User     %% [ right of | left of | over ] supported
participant I as Identity Provider
participant S as Service Provider
Note right of S: Blitz Identity
C->>S: Resource request
activate C
activate S
S-->>C: Redirect to Identity Provider
deactivate S
loop every hour           %% loop start
C->>I: Request Access Token
activate C
activate I
I-->>C: Access Token
deactivate C
deactivate I
end                       %% loop end
C->>S: Access granted
Note over C,S: Browser access
deactivate C

Result:

sequenceDiagram 
autonumber
actor C as Client
Note left of C: User
participant I as Identity Provider
participant S as Service Provider
Note right of S: Blitz Identity
C->>S: Resource request
activate C
activate S
S-->>C: Redirect to Identity Provider
deactivate S
loop every hour
C->>I: Request Access Token
activate C
activate I
loop
I->>I: Kerberos cert
end
I-->>C: Access Token
deactivate C
deactivate I
end
C->>S: Access granted
Note over C,S: Browser access
deactivate C

Obsidian

Obsidian Articles

Obsidian + Dataview https://habr.com/ru/articles/710508/

Чтобы немного упростить процесс ведения дневника, можно поставить плагин календаря (Calendar), периодических заметок (Periodic notes) и шаблонов (Templater). Логика думаю тут довольно проста – календарь позволяет лучше ориентироваться во времени, периодические заметки - организуют заметки, а шаблоны упрощают создание самих дневниковых заметок.

Сначала мы быстро читаем какой-то отрывок, главу. Не отвлекаемся, не тормозим, не ходим по ссылкам.

Далее читаем заново и оставляем метки, по которым сформируем впоследствии конспект.

Пишем конспект:

    Сначала пишем просто изложение (значит своими словами). Не думаем о ссылках, не думаем о том, как атомизировать. Просто пишем четкое последовательное изложение по прочитанному.

    Расширяем и дополняем конспект своими мыслями и наблюдениями, вставляем ссылки и источники.

    (при необходимости формируем какой-то сопроводительный текст)

Начинаем атомизировать наш конспект на отдельные заметки.

Связываем заметки с другими, если, конечно, в голову приходят эти связи.

Медитируем

  • Okular, потому что эта программа открывает pdf и djvu (и многие другие), а также в ней прям очень легко и быстро можно аннотировать текст.

Zotero

Это ультимативная, бесплатная программа для работы с источниками информации

  • Obsidian to Anki.

Плагины

  • ReadItLater - способ быстро добавлять статьи и видео в заметки;
  • Timestamp Notes - смотреть видео и делать заметки по времени;
  • Templater - заполнить пустую заметку по шаблону;
  • Various complement - подсказки по словам при написании заметки;
  • Kanban - канбан доска;
  • Dataview - можно сводить таблицы из разрозненных заметок;
  • Advanсed tables - удобная работа с markdown таблицами;
  • Tracker - визуализация данных;
  • Quick Add - автоматизация процессов;
  • Homepage - плагин для создания домашних страниц;
  • Spaced Repetition- интервальное запоминание.

Инструменты

  • Peerdraft - совместная работа над заметками;
  • Qartz - расшарить в веб свой обсидиан;
  • Obsidian Publish - тот же Кварц, но его не надо хостить, крутится на серверах Обсидиан (платный).

Средства публикации заметок

Обсидианы других людей

Кварц позволяет открыть свои markdown-страницы для других людей через веб-сайт.

Есть даже целая концепция learn in public, когда люди открывают аудитории свои незавершённые наработки с целью эффективного обучения. Закон Каннингема гласит:

Лучшим способом получить правильный ответ в Интернете будет не задавать вопрос, а разместить ложный ответ

Вот обсидианы некоторых инженеров:

Кстати, статьи, которые я пишу для своего телеграм канала тоже написаны в Обсидиан и опубликованы через Quartz.

Для того, чтобы их можно было удобно читать, не выходя из приложения, я написал шаблон для telegram instant view.

Top Plugins

Calendar

Позволяет создать календарь и писать в него быстрые заметки по дням.

DataView

Позволяет делать выборки из файлов с SQL Syntax и отображать результат в виде таблицы

Outliner

Создание и управление структурированными списками.

Novel Word Count

Отображение количества страниц и текста в папке.

Style Settings

Изменить стиль Obsidian, сделать Softpaper стиль. Установить стиль (Theme) = AnuPpuccin Зайти в настройки плагина Style Settings, выбрать конфиг для AnuPpuccin темы -> Import… и ввести:

{
  "anuppuccin-theme-settings@@anuppuccin-theme-light": "ctp-rosepine-light",
  "anuppuccin-theme-settings@@anuppuccin-theme-dark": "ctp-frappe",
  "anuppuccin-theme-settings@@anuppuccin-light-theme-accents": "ctp-accent-light-teal",
  "anuppuccin-theme-settings@@anuppuccin-theme-accents": "ctp-accent-teal",
  "anuppuccin-theme-settings@@anuppuccin-accent-toggle": true,
  "anuppuccin-theme-settings@@ctp-custom-peach@@light": "#DD7F67",
  "anuppuccin-theme-settings@@ctp-custom-teal@@dark": "#11B7C5",
  "anuppuccin-theme-settings@@ctp-custom-teal@@light": "#1A7DA4",
  "anuppuccin-theme-settings@@ctp-custom-subtext1@@light": "#EE653A",
  "anuppuccin-theme-settings@@ctp-custom-subtext0@@dark": "#FB35D8",
  "anuppuccin-theme-settings@@ctp-custom-subtext0@@light": "#0C9FCE",
  "anuppuccin-theme-settings@@ctp-custom-overlay2@@dark": "#0AD1D0",
  "anuppuccin-theme-settings@@ctp-custom-overlay2@@light": "#525252",
  "anuppuccin-theme-settings@@ctp-custom-overlay1@@dark": "#FFA600",
  "anuppuccin-theme-settings@@ctp-custom-overlay1@@light": "#CCCCCC",
  "anuppuccin-theme-settings@@ctp-custom-overlay0@@dark": "#4CFFD2",
  "anuppuccin-theme-settings@@ctp-custom-overlay0@@light": "#0C9FCE",
  "anuppuccin-theme-settings@@anp-active-line": "anp-no-highlight",
  "anuppuccin-theme-settings@@anp-callout-select": "anp-callout-sleek",
  "anuppuccin-theme-settings@@anp-callout-color-toggle": true,
  "anuppuccin-theme-settings@@anp-custom-checkboxes": true,
  "anuppuccin-theme-settings@@anp-speech-bubble": true,
  "anuppuccin-theme-settings@@tag-radius": 2,
  "anuppuccin-theme-settings@@cards-border-width": "4px",
  "anuppuccin-theme-settings@@anp-color-transition-toggle": true,
  "anuppuccin-theme-settings@@anp-cursor": "pointer",
  "anuppuccin-theme-settings@@anp-toggle-scrollbars": true,
  "anuppuccin-theme-settings@@anp-editor-font-source": "\"\"",
  "anuppuccin-theme-settings@@anp-editor-font-lp": "\"New York\"",
  "anuppuccin-theme-settings@@bold-weight": "700",
  "anuppuccin-theme-settings@@anp-font-live-preview-wt": "400",
  "anuppuccin-theme-settings@@anp-header-color-toggle": true,
  "anuppuccin-theme-settings@@anp-header-divider-color-toggle": true,
  "anuppuccin-theme-settings@@h1-weight": 900,
  "anuppuccin-theme-settings@@h1-line-height": 1.2,
  "anuppuccin-theme-settings@@anp-h1-divider": true,
  "anuppuccin-theme-settings@@h2-size": 1.9,
  "anuppuccin-theme-settings@@h2-weight": 100,
  "anuppuccin-theme-settings@@h3-size": 1.6,
  "anuppuccin-theme-settings@@h3-weight": 700,
  "anuppuccin-theme-settings@@anp-h3-color-custom": "anp-h3-green",
  "anuppuccin-theme-settings@@h4-weight": 700,
  "anuppuccin-theme-settings@@h5-weight": 700,
  "anuppuccin-theme-settings@@h6-size": 1.1,
  "anuppuccin-theme-settings@@h6-weight": 700,
  "anuppuccin-theme-settings@@anp-decoration-toggle": true,
  "anuppuccin-theme-settings@@anp-colorful-frame": true,
  "anuppuccin-theme-settings@@anp-colorful-frame-opacity": 1,
  "anuppuccin-theme-settings@@anp-file-icons": true,
  "anuppuccin-theme-settings@@anp-file-label-align": "0",
  "anuppuccin-theme-settings@@anp-alt-rainbow-style": "anp-full-rainbow-color-toggle",
  "anuppuccin-theme-settings@@anp-rainbow-file-toggle": true,
  "anuppuccin-theme-settings@@anp-rainbow-folder-bg-opacity": 0.9,
  "anuppuccin-theme-settings@@anp-simple-rainbow-title-toggle": true,
  "anuppuccin-theme-settings@@anp-simple-rainbow-indentation-toggle": true,
  "anuppuccin-theme-settings@@anp-stacked-header-width": 30,
  "anuppuccin-theme-settings@@anp-alt-tab-style": "anp-safari-tab-toggle",
  "anuppuccin-theme-settings@@anp-alt-tab-custom-height": 40,
  "anuppuccin-theme-settings@@anp-disable-newtab-align": true,
  "anuppuccin-theme-settings@@anp-depth-tab-opacity": 0.6,
  "anuppuccin-theme-settings@@anp-depth-tab-gap": 10,
  "anuppuccin-theme-settings@@anp-safari-tab-radius": 5,
  "anuppuccin-theme-settings@@anp-safari-tab-gap": 3,
  "anuppuccin-theme-settings@@anp-safari-tab-animated": true,
  "anuppuccin-theme-settings@@anp-layout-select": "anp-card-layout",
  "anuppuccin-theme-settings@@anp-card-radius": 8,
  "anuppuccin-theme-settings@@anp-card-layout-padding": 1,
  "anuppuccin-theme-settings@@anp-card-shadows": true,
  "anuppuccin-theme-settings@@anp-card-layout-actions": true,
  "anuppuccin-theme-settings@@anp-card-layout-filebrowser": true,
  "anuppuccin-theme-settings@@anp-border-radius": 16,
  "anuppuccin-theme-settings@@anp-border-padding": 20
}

Obsidian Notes

Общие советы по вводу текста

  • Обязательно использовать слепой метод печати;
  • Смена языка 1 клавишей - при слепом вводе лучше всего использовать caps lock;
  • Плагины Note refactor и Better Link Inserter позволяют быстро создавать заметки из куска текста другой заметки и сразу создавать на неё ссылку;
  • Плагин templater позволяет создавать быстрые шаблоны для записи содержимого.

Ultralearning

Основные техники обучения

  1. Метаобучение. Дорожная карта предмета изучения. Как использовать свои текущие навыки, чтобы легче освоить новый. Перед изучением темы, разобраться с предметом изучения: * Зачем учить, что учить, как учить? Написать в таблице; * Интервью с экспертом: найти спеца, кто достиг высот в теме, которую планируешь изучать, и пообщаться - насколько ему нравится, помогают знания эти, как он учился; * Выделить, что надо понимать, что просто запомнить (факты), что надо отработать; * Закон убывающей ценности: поиск источников знаний - это процесс, ценность начальных источников будет убывать, надо будет искать новые. * Эталонный сопоставительный анализ: взять программу курса (например, программу ВУЗа), и по ней следовать как по дорожной карте; * Метод подчеркивания/исключения: учить тему, постоянно взвешивая её ценность. Например, изучение программирования для создания проекта - это учить конкретные языки, но не теорию вычислений; * Сколько исследовать тему проекта перед его началом - 10% от времени на проект.
  2. Фокус: выделение времени на интенсивное изучение. Способность концентрироваться, бороться с прокрастинацией: * Определить причину прокрастинации: сопротивление конкретному делу или более привлекательные альтернативы? * Поток от Михай Чиксентмихайи не годится (согласно Андерсону Эриксону) для обучения сложным навыкам. Потому как в потоке нет места для осознанной рефлексии. Также обучение - это уровень сложности более высокий, чем годится для потока. Человек к потоке занимается обычно делом, которое хорошо знает; * Техника “я ещё 5 минут поделаю и пойду отдыхать” для начала борьбы с прокрастинацией, далее техника помидорок для концентрации.
  3. Целенаправленность - идти вперёд, не заменять обучение другими проектами попроще. Фокус на практической значимости изучаемого навыка: * Делать pet-project; * Погружение в среду (языковую); * Работа с симулятором (например, у лётчиков); * Не бояться ставить высокие сложные задачи.
  4. Упражнения. Атаковать свои слабые места безжалостно. Разбить большие задачи на мелкие, освоить их и собрать вместе. В процессе разбора на части и изучения подзадач могут найтись доселе неизвестные вспомогательные навыки. Например, задача изучения языка включает задачи скоростного и эффективного использования словарей, переводчиков. Среди эти под-задач могут быть “бутылочные горлышки”, из-за которых в целом изучение навыка тормозит. На них унужно сфокусироваться до их улучшения;
  5. Самоконтроль. Вспоминать изученное активно через задачи и тесты, а не пассивно, заглянув в источник. Активно вспоминать, в том числа тестировать себя ДО изучения темы;
  6. Обратная связь. Принимать критику на себя, не избегать её (но фильтровать полезную, остальную игнорировать). В том числе тренер/наставник даёт 1-1 обратную свзяь, отчего занятия с ними так эффекктивны;
  7. Запоминание. Учиться помнить инфу навсегда. Метод интервального повторения;
  8. Интуиция. Углубление знаний через игру. Усвоить как работает понимание, не прибегать к дешёвым трюкам запоминания;
  9. Экспериментирование. Исследовать вне своей зоны комфорта.

News

01.02.2024 В процессе решения задачек я потихоньку прихожу к пониманию, что такое “смена мышления” при работе с Rust относительно других языков: это упор на типы данных и конструкции языка заместо привычных общепринятых. Например, использовать перечисления (enum) и match всегда и везде заместо if-else. Попутно я дополнил статью о векторах темой перевода туда-обратно в строки. Для составных типов данных это наиболее актуальный вопрос: как собрать в такой тип данные, а потом отдать данные назад в правильном формате.

28.01.2024 Этот год я решил вернуться к Rust после перерыва и научиться решать задачки с ним в codewars. В процессе я потихоньку осваиваю расширенную библиотеку, например, модуль itertools тут есть, как и в Python, даёт возможность делать полезные преобразования и сортировку строк.

11.11.2023 Неожиданное открытие на днях: давно я искал RAW-редактор фото а-ля Lightroom, но при этом бесплатный-открытый, и чтобы при этом умел работать с масками, а вдобавок делать много проб под кривые коррекции в LAB. И такой оказался у меня прямо под носом - Darktable. Я иногда им пользовался, но совершенно проморгал наличие там мощного механизма работы с масками, а вдобавок мощнейших функций шумодава и микроконтраста. Навёрстываю упущенное, создал раздел в заметках и конспект по шумодаву с масками

09.11.2023 Подборка полезных/интересных фреймворков для Rust:

  • Bevy - игровой 2D и 3D движок на Rust;
  • Сравнение веб-фреймворков;
  • Serde - конвертация между структурами Rust и строками JSON;
  • Tokio - асинхронность, распараллеливание потоков данных;
  • Nannou - креативное программирование, рисование фрактальных форм;
  • Polars - аналог Pandas, обработка массивов данных;

20.10.2023 Формат “стены” - небольших новостей-блогов - мне показался более интересным для главной страницы сайта. В связи с этим я буду тут писать небольшие заметки, которые далее перемещать в новый раздел новостей сайта.

Позавчера мы обсудили с коллегами от Kaspersky Labs, VK Cloud и Positive Technologies перспективные технологии на рынке ИБ. Особый упор на ePBF и WebAssembly. Ссылку на стрим я добавил в список своих видео выступлений

Более ранние обновления Раздел новостей сайта

06.10.2023 27.09.2023 22.09.2023 18.09.2023 14.09.2023

06.09.2023 Статья контексты безопасности в Pod

31.08.2023 Статья k8s Pod Commands, Env Variables

29.08.2023
  • Обновил статьи по k8s Deployments, Services, Pods - добавил ссылки, императивные команды, дополнил описания
28.08.2023

27.08.2023 Статья k8s Replica Sets

25.08.2023 Статья k8s Pods

02.05.2023 Раздел о библиотеках Rust

27.04.2023 Editing VR360 video in Davinci Resolve

25.04.2023 23.04.2023 05.04.2023

04.04.2023 k8s

16.03.2023 Rust Traits

06.03.2023 Rust Generics

28.02.2023 Обновление Rust Strings

17.02.2023 Handling errors in Rust

15.02.2023 Rust Hashmap

13.02.2023 09.02.2023 08.02.2023 07.02.2023

01.02.2023 Rust structs

30.01.2023 29.01.2023

20.01.2023 Rust Variables

13.01.2023 12.01.2023

11.01.2023 Начало.