🤔

Rustでささやかに実用的なツールを: Dockerコンテナを確認する

2023/08/06に公開

なんでこんなん作るの

仕事(授業)で、学生にDockerコンテナを使ったLinuxの練習環境を入れているのですが、学生のレベルはめちゃくちゃで、環境ができているのかとかがろくに確認できないというのがいたりします。

ということで、立ち上げたら

  • Docker Engineに接続できるか
  • 必要なコンテナが動いているか

が確認できるようなツールがほしいという野望があったりします。
なおDocker Desktopを使って特定名称のコンテナがあればOKなんですが、そのあたりもよくわかってないのがいたりするので、必要なことだけが出るウィンドウアプリを作ってあげたいという事になるのです。

GUIまわりはTauriで組めれば良いよね感があります。

ということで基礎データ

なんと言ってもコンテナの一覧から特定のコンテナが抜き出せるかが必要となりますので、Dockerとの通信を行うクレートが無いかなと検索したところ、2つありました。

名前からすると、後から作り直してよりよくしようとしてるのがnextのほうと思いますので、それを使うこと前提で進めてみます。

% cargo new control-docker
% cd control-docker
% cargo add bollard-next
% cargo add tokio --features=full # tokioを使うそうなので

まずはDockerとの疎通ですよね

疎通を行うのであれば、bollard_next::Docker以下を使えばどうにかなります。

use bollard_next::Docker;


fn main() {
    let docker = Docker::connect_with_local_defaults().expect("Dockerに接続できません");

}

おや? Docker Engine(OrbStack)止めてもエラーにならないぞ。
ということはあくまで接続設定を作るだけなのか。
だとすると、Engine側のバージョンでも取得するコードにすれば良いのかな。

なるほど、バージョン取得はasyncによるものなので、値が欲しくなったらawaitしろということですね。追加でmainをtokioでラップしてあげないといけないということか。

use bollard_next::Docker;

#[tokio::main]
async fn main() {
    let docker = Docker::connect_with_local_defaults().expect("Dockerに接続できません");
    docker.version().await.expect("Dockerのバージョン情報が取得できません");
}

たしかにエラーはバージョン情報取得側にて出てくれます。

% cargo run
...
thread 'main' panicked at 'Dockerのバージョン情報が取得できません: HyperResponseError { err: hyper::Error(Connect, Os { code: 2, kind: NotFound, message: "No such file or directory" }) }', src/main.rs:6:28

Docker Engine(OrbStack)を起動したら通過しています。ついでにバージョン情報を出してみましょうか。

use bollard_next::Docker;

#[tokio::main]
async fn main() {
    let docker = Docker::connect_with_local_defaults().expect("Dockerに接続できません");
    let version_info = docker.version().await.expect("Dockerのバージョン情報が取得できません");
    println!("{:?}", version_info);
}
% cargo run
...
Version { platform: Some(SystemVersionPlatform { name: "" }), components: Some([VersionComponents { name: "Engine", version: "24.0.4", details: Some({"Experimental": String("false"), "KernelVersion": String("6.3.13-orbstack-00216-g2a13dff74586"), "Os": String("linux"), "ApiVersion": String("1.43"), "BuildTime": String("2023-07-14T13:18:38.000000000+00:00"), "MinAPIVersion": String("1.12"), "GitCommit": String("4ffc61430bbe6d3d405bdf357b766bf303ff3cc5"), "GoVersion": String("go1.20.6"), "Arch": String("amd64")}) }, VersionComponents { name: "containerd", version: "v1.7.2", details: Some({"GitCommit": String("0cae528dd6cb557f7201036e9f43420650207b58")}) }, VersionComponents { name: "runc", version: "1.1.8", details: Some({"GitCommit": String("82f18fe0e44a59034f3e1f45e475fa5636e539aa")}) }, VersionComponents { name: "docker-init", version: "0.19.0", details: Some({"GitCommit": String("")}) }]), version: Some("24.0.4"), api_version: Some("1.43"), min_api_version: Some("1.12"), git_commit: Some("4ffc61430bbe6d3d405bdf357b766bf303ff3cc5"), go_version: Some("go1.20.6"), os: Some("linux"), arch: Some("amd64"), kernel_version: Some("6.3.13-orbstack-00216-g2a13dff74586"), experimental: None, build_time: Some("2023-07-14T13:18:38.000000000+00:00") }

イメージを取得してみようか

疎通確認できたらイメージの取得ですね。
イメージ情報を取得するには、取得するイメージに関する構造体が必要になりますね。

ということで、ListImageOptionsを用意しましょうか。

あ、allだけではダメなのね。filterやdigestも書き込んでおくか… オプションは無いからNoneでいいよねとか書き込むと赤線ですよ…

ん? ..Default::default()ってなんぞや…

このトレイトを導出している場合、設定してない部分はデフォルト値にしてくれますよという便利なものなのね。

置き換えてみたところ進展ありですが、訳わからんのが出た。

ん? 型Tとか言ってるからジェネリックですよね。型Tが決まらないからinto<String>でStringにできない? ということは、型T部分はStringですよとしてあげればエラーが収まる?

use bollard_next::{Docker, image::ListImagesOptions};

#[tokio::main]
async fn main() {
    let docker = Docker::connect_with_local_defaults().unwrap();
    docker.version().await.expect("Dockerに接続できません。");

    let images_options = ListImagesOptions::<String> {
        all: true,
        ..Default::default()
    };
    let images = docker.list_images(Some(images_options));
}

一応エラーは消えた(images不使用は出るけど)
イメージ取得も Future になっているので、await使って値の取得を実際に行い、結果を取得してみましょう。

use bollard_next::{Docker, image::ListImagesOptions};

#[tokio::main]
async fn main() {
    let docker = Docker::connect_with_local_defaults().unwrap();
    docker.version().await.expect("Dockerに接続できません。");

    let images_options = ListImagesOptions::<String> {
        all: true,
        ..Default::default()
    };
    let images = docker.list_images(Some(images_options))
        .await.expect("イメージ一覧の取得でエラー");

}

ということで、ベクタで取得できたようなので、とりあえず出してみましょうか。

use bollard_next::{Docker, image::ListImagesOptions};

#[tokio::main]
async fn main() {
    let docker = Docker::connect_with_local_defaults().unwrap();
    docker.version().await.expect("Dockerに接続できません。");

    let images_options = ListImagesOptions::<String> {
        all: true,
        ..Default::default()
    };
    let images = docker.list_images(Some(images_options))
        .await.expect("イメージ一覧の取得でエラー");
    println!("{:?}", images);

}

おわ、いろいろ出過ぎる。

出過ぎにも程があるので、ひとつだけ出して確認してみますか。

少しスッキリ、フィールドも取得できるので、これでフィルタをかけることができそうです。
ふむ、フィルタは 「文字列→文字列のベクタ」で構成されるハッシュマップなのね。

use std::collections::HashMap;

use bollard_next::{Docker, image::ListImagesOptions};

#[tokio::main]
async fn main() {
    let docker = Docker::connect_with_local_defaults().unwrap();
    docker.version().await.expect("Dockerに接続できません。");

    let mut filter = HashMap::new();
    filter.insert("repo_tags".to_string(), vec!["python".to_string()]);
    let images_options = ListImagesOptions::<String> {
        all: true,
        filters: filter,
        ..Default::default()
    };
    let images = docker.list_images(Some(images_options))
        .await.expect("イメージ一覧の取得でエラー");
    println!("{:?}", images.iter().nth(0));

}

あれ? エラーだ。

thread 'main' panicked at 'イメージ一覧の取得でエラー: DockerResponseServerError { status_code: 400, message: "invalid filter 'repo_tags'" }', src/main.rs:18:16

そういうキーが内容取得したときに得られたのに、違うのかな?
実際どういうことをしてるのか、クレートのマニュアルで見てみるかな。

なるほど、リポジトリ名で使えそうなのは reference か…

use std::collections::HashMap;

use bollard_next::{Docker, image::ListImagesOptions};

#[tokio::main]
async fn main() {
    let docker = Docker::connect_with_local_defaults().unwrap();
    docker.version().await.expect("Dockerに接続できません。");

    let mut filter = HashMap::new();
    filter.insert("reference".to_string(), vec!["python:3".to_string()]);
    let images_options = ListImagesOptions::<String> {
        all: true,
        filters: filter,
        ..Default::default()
    };
    let images = docker.list_images(Some(images_options))
        .await.expect("イメージ一覧の取得でエラー");
    println!("{:?}", images.iter().nth(0));

}

おぉ、取れたぞ。

Some(ImageSummary { id: "sha256:c0e63845ae986c52da5cd6ac4d56eebf293439bb22a3cee198dd818fd12ba555", parent_id: "", repo_tags: ["python:3"], repo_digests: [], created: 1686678316, size: 1008374080, shared_size: -1, virtual_size: Some(1008374080), labels: {}, containers: -1 })

うぉ、ここまでで随分長くなってしまった…

Discussion