2025年8月25日月曜日

Vim のマクロで気持ち良くなった話 - Java フィールドから SQL の SELECT 定義を作成編

Java Bean でエンティティ的なものを作って、そのフィールド値を(ほぼ)全部 SELECT で撮ってきたいと思う場面は多いと思います。

ざっくりやってみたら意外とうまくいったので記録。

User.java

public class User {
  /**
   * 氏名
   */
  private String name;
  /**
   * グループ ID
   */
  private Integer groupId;
  /**
   * Role ID
   */
  private Long roleId;
  /**
   * 住所
   */
  private String address;
  /**
   * 電話番号
   */
  private String phoneNumber;
}

最初のコメントの始まりにカーソルを合わせ、以下キーストロークをうつと、フィールド名だけを抽出できる。

これを q に保存して @q で全部フィールド名だけを抽出する。

VjjdvWWhx$r,j

すると、こうなる。

public class User {
  name,
  groupId,
  roleId,
  address,
  phoneNumber,
}

後は lower snake case にすればいいので、対象のフィールドの行で以下キーストロークを実行。

^ve:s/\C\([A-Z]\)/_\1/g<Enter>veuj

これも q に記録して、対象行で @q していく。

すると以下のようになる。

public class User {
  name,
  group_id,
  role_Id,
  address,
  phone_number,
}

後は先頭と末尾を SELECT 文ににしてケツカンマを削除。

select
  name,
  group_id,
  role_Id,
  address,
  phone_number
from
  "user";

OK, 以上。

以下、似たようなことをやっている動画です。

2025年8月22日金曜日

Vim のマクロで気持ち良くなった話 - Java のフィールド定義編

エクセルのインタフェース仕様書とかに、こんな形で入力や出力のパラメーター情報が書かれていたりする。

これをいちいち手作業で Java のフィールドとして記載していくのは骨が折れるため、 Vim のマクロで何とかした。

論理名 物理名 データ型 …(略)
氏名 name String
住所 address String
電話番号 phoneNumber String

論理名からデータ型までの列をコピーして Vim に張り付けると、以下のようなテキストになる。

氏名  name    String
住所  address String
電話番号    phoneNumber String

氏名の行の先頭にカーソルを置き、以下のように入力していく。

i/**<Enter><BS> * <ESC>Ea<Enter>*/<Enter>private <ESC>Elvx<ESC>vexbPa <ESC>A;<ESC>j0

すると、氏名の行が以下のようになる。

/**
 * 氏名
 */
private String name;

このストロークを qq で記録しておき、 @q を 2 回実行すれば他の行も以下のようになる。

/**
 * 住所
 */
private String address;
/**
 * 電話番号
 */
private String phoneNumber;

うーん、便利。

以下、似たようなことをやっている動画です。

2025年8月15日金曜日

Ubuntu20.04 に Minikube をベアメタルインストールする

前提

  • Hyper-V を使用
  • OS: Ubuntu Server 20.04 LST
  • 標準インストール直後からの開始

ファイアウォールを OFF に

本当はまじめに穴あけしなきゃいけないのだけど、とりあえず動作確認という事で…

sudo ufw disable

Minikube の none ドライバ実行に必要なパッケージのインストール

conntrack

sudo apt install conntrack

containernetworking-plugins

CNI_PLUGIN_VERSION="v1.7.1"
CNI_PLUGIN_TAR="cni-plugins-linux-amd64-$CNI_PLUGIN_VERSION.tgz" # change arch if not on amd64
CNI_PLUGIN_INSTALL_DIR="/opt/cni/bin"

curl -LO "https://github.com/containernetworking/plugins/releases/download/$CNI_PLUGIN_VERSION/$CNI_PLUGIN_TAR"
sudo mkdir -p "$CNI_PLUGIN_INSTALL_DIR"
sudo tar -xf "$CNI_PLUGIN_TAR" -C "$CNI_PLUGIN_INSTALL_DIR"
rm "$CNI_PLUGIN_TAR"

crictl

DOWNLOAD_DIR="/usr/local/bin"
sudo mkdir -p "$DOWNLOAD_DIR"
CRICTL_VERSION="v1.33.0"
ARCH="amd64"
curl -L "https://github.com/kubernetes-sigs/cri-tools/releases/download/${CRICTL_VERSION}/crictl-${CRICTL_VERSION}-linux-${ARCH}.tar.gz" | sudo tar -C $DOWNLOAD_DIR -xz

kubectl

curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

Docker

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
           "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
             $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
               sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
               sudo apt-get update
 sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

cri-docker

curl -LO https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.18/cri-dockerd_0.3.18.3-0.ubuntu-focal_amd64.deb
sudo apt install -y ./cri-dockerd_0.3.18.3-0.ubuntu-focal_amd64.deb

Minikube

curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb
sudo dpkg -i minikube_latest_amd64.deb

OS 設定

何やっているか知らないけど、 minikube start --driver=none で怒られたので実行した。

sudo sysctl fs.protected_regular=0

Minikube クラスタの作成

sudo minikube start --driver=none

動作確認

mikoto@kubernetes:~$ sudo kubectl get nodes
NAME         STATUS   ROLES           AGE     VERSION
kubernetes   Ready    control-plane   7m51s   v1.33.1

mikoto@kubernetes:~$ sudo kubectl run hello-world --image=hello-world --restart=Never
pod/hello-world created

mikoto@kubernetes:~$ sudo kubectl get pod
NAME          READY   STATUS      RESTARTS   AGE
hello-world   0/1     Completed   0          23s


mikoto@kubernetes:~$ sudo kubectl logs pod/hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

OK.

参考資料

2025年8月14日木曜日

Ubuntu20.04 に kubeadm で Kubernetes を構築する

コントロールプレーンに接続できたりできなかったりと、よくわからない状態になってしまったが、とりあえず作業記録として残す。

前提

  • OS: Ubuntu Server 20.04
  • 標準インストール直後からの開始

必須パッケージのインストール

環境構築中に必要になる雑多パッケージをインストール。

sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg

swap の無効化

sudo swapoff -a
sudo sed -i '/\sswap\s/s/^/#/' /etc/fstab

containerd のインストール

sudo apt-get update
sudo apt-get install -y containerd

kubeadm のインストール

Kubernetes パッケージリポジトリの公開鍵をダウンロード

curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

Kubernetes パッケージのリポジトリを登録

echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

インストール

kubelet, kubeadm, kubectl をインストールし、バージョンを固定する。

sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

シングルノードクラスターの作成

sudo kubeadm init

root ユーザーでない自分んが kubectl を実行できるように設定

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

コントロールプレーンに Pod を配置できるように設定

シングルノード設定なので、コントロールプレーンに相乗りする。

デフォルトではセキュリティ上相乗りできないので、相乗りできるように設定する。

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

動作確認

hello-world のイメージをデプロイしてみる。

mikoto@kubernetes:~$ sudo docker run -it --rm hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

良さそう。以上。

参考資料

2025年8月11日月曜日

Spring Security で API Key を使った認証を行う

Spring Initializr

これ

アプリの作成

Echo エンドポイントの作成

誰でも叩ける /echo と、 API Key が無いとたたけない /api/echo を用意する。

package dev.mikoto2000.springboot.security.apikey.firststep.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * EchoController
 */
@RestController
public class EchoController {

  @GetMapping("/echo")
  public String echo(
      @RequestParam String message) {
    return message;
  }

  @GetMapping("/api/echo")
  public String apiEcho(
      @RequestParam String message) {
    return message;
  }
}

セキュリティ設定

セキュリティフィルターの作成

properties に設定した API Key と等しいときだけ認証が通るフィルターを作成。

他プロジェクトで使いまわせるように clientName と同じロールを設定するようにしている。

package dev.mikoto2000.springboot.security.apikey.firststep.security;

import java.io.IOException;
import java.util.List;

import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class ApiKeyAuthFilter extends OncePerRequestFilter {

  private static final String API_KEY_HEADER = "x-api-key";

  private final String clientName;
  private final String apiKey;

  public ApiKeyAuthFilter(String clientName, String apiKey) {
    this.clientName = clientName;
    this.apiKey = apiKey;
  }

  @Override
  protected void doFilterInternal(HttpServletRequest request,
      HttpServletResponse response,
      FilterChain chain) throws IOException, ServletException {

    // CORSプリフライトは素通し(必要に応じて)
    if (HttpMethod.OPTIONS.matches(request.getMethod())) {
      chain.doFilter(request, response);
      return;
    }

    // 既に認証済みならスキップ
    Authentication current = SecurityContextHolder.getContext().getAuthentication();
    if (current != null && current.isAuthenticated()) {
      chain.doFilter(request, response);
      return;
    }

    // API Key による認証
    String requestApiKey = request.getHeader(API_KEY_HEADER);
    if (StringUtils.hasText(apiKey) && StringUtils.hasText(requestApiKey) && apiKey.equals(requestApiKey)) {
      // API クライアント用トークン作成
      var token = new UsernamePasswordAuthenticationToken(
          clientName,
          "N/A",
          List.of(new SimpleGrantedAuthority(String.format("ROLE_%s", clientName))));
      token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

      // SecurityContextHolder に認証済みトークンを設定
      SecurityContextHolder.getContext().setAuthentication(token);
    }

    // 次のフィルタへ
    chain.doFilter(request, response);
  }
}

SecurityConfig の作成

認証エラー時に相手に与える情報は少ない方が良いので空ボディを返すようにしている。

package dev.mikoto2000.springboot.security.apikey.firststep.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import jakarta.servlet.http.HttpServletResponse;

/**
 * SecurityConfig
 */
@Configuration
public class SecurityConfig {

  @Value("${security.api.client-role}")
  private String apiClientName;

  @Value("${security.api.key}")
  private String apiKey;

  @Bean
  public SecurityFilterChain apiChain(HttpSecurity http,
      AuthenticationEntryPoint emptyBody401EntryPoint) throws Exception {

    var apiKeyFilter = new ApiKeyAuthFilter(apiClientName, apiKey);

    http
        .securityMatcher("/api/**")
        .csrf(csrf -> csrf.disable())
        .cors(Customizer.withDefaults())
        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .exceptionHandling(eh -> eh.authenticationEntryPoint(emptyBody401EntryPoint))
        .addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class)
        .authorizeHttpRequests(auth -> auth
            .anyRequest().hasRole("TEST"));

    return http.build();
  }

  /**
   * 401 を本文なしで返す EntryPoint
   */
  @Bean
  public AuthenticationEntryPoint emptyBody401EntryPoint() {
    return (request, response, ex) -> {
      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
      // ここで Content-Type や Body は書かない(Content-Length: 0)
    };
  }
}

設定ファイル作成

spring.application.name=firststep
security.api.client-role=TEST
security.api.key=0123456789

動作確認

サーバー起動

./mvnw spring-boot:run

リクエスト発行

curl -v localhost:8080/echo?message=aaaaaaaaa
=> aaaaaaaaa

curl -v localhost:8080/api/echo?message=aaaaaaaaa
=> 401 error

curl -v localhost:8080/api/echo?message=aaaaaaaaa -H 'X-API-KEY: 0123456789'
=> aaaaaaaaa

2025年7月22日火曜日

Supabase に入門した

Use Supabase with React | Supabase Docs をベースに、 VanillaJS で DB に接続しに行ってみた。

前提

  • Supabase プロジェクト作成済み

プロジェクト情報のの確認

  1. プロジェクトページを開く
  2. 右下の情報をメモ
    • Project URL
    • API Key

テーブルの作成

プロジェクトページ -> SQL editor を選択。 表示された SQL エディターで SQL を実行していく。

テーブル作成

-- テーブル作成
create table instruments (
  id bigint primary key generated always as identity,
  name text not null
);
-- テストデータ挿入
insert into instruments (name)
values
  ('violin'),
  ('viola'),
  ('cello');

-- row level security 有効化
alter table instruments enable row level security;

RLS ポリシーの作成

テーブル内のデータを、アノニマスユーザーが読み取り可能に設定。

create policy "public can read instruments"
on public.instruments
for select to anon
using (true);
  • anon: アノニマスユーザー

Vite プロジェクトの作成

プロジェクト作成

node ➜ /workspaces/TIL/supabase (supabase-firststep) $ npm create vite@latest firststep
Need to install the following packages:
create-vite@7.0.3
Ok to proceed? (y) y


> npx
> create-vite firststep


  Select a framework:
  Vanilla

  Select a variant:
  TypeScript

  Scaffolding project in /workspaces/TIL/supabase/firststep...

  Done. Now run:

  cd firststep
  npm install
  npm run dev

Supabase パッケージのインストール

cd firststep && npm install @supabase/supabase-js

環境変数の設定

.env.local を作成し、そこにメモしたプロジェクト情報を指定。

VITE_SUPABASE_URL=<Project URL>
VITE_SUPABASE_ACCESS_TOKEN=<API Key>

実装

src/main.ts:

import { createClient } from "@supabase/supabase-js";

const supabase = createClient(import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_ANON_KEY);

const { data } = await supabase.from("instruments").select();

console.log(data);

const app = document.getElementById('app');

if (app) {
  // ルート要素作成
  const root = document.createElement('div');

  // タイトル追加
  const h1 = document.createElement('h1');
  h1.textContent = "Supabase Instruments";
  root.appendChild(h1);

  // リスト作成
  const ul = document.createElement('ul');
  data?.forEach(instrument => {
    const li = document.createElement('li');
    li.textContent = `id: ${instrument.id}, name: ${instrument.name}`;
    ul.appendChild(li);
  });
  root.appendChild(ul);

  // ルート要素をアプリケーションに追加
  app.appendChild(root);

}

動作確認

npm run dev して http://localhost:5173 にアクセスすると、挿入したデータが表示される。 OK.

参考資料

2025年7月12日土曜日

Pull Request 作成ガイドのひな形を作った

毎プロジェクトで作っていて毎回面倒だと思っている気がするのでひな形を作った。

# 開発者向け Pull Request 作成ガイド

以下の手順で開発環境を整え、プルリクエストを送ってください。

## 1. リポジトリをクローンする

まずは GitHub 上のリポジトリをローカルにクローンします。

```sh
git clone https://github.com/YourOrganization/YourRepository.git
cd YourRepository
```

**既に clone 済みの場合**

すでにリポジトリを clone している場合は、main ブランチに移動し、最新のソースコードを取得します。

```sh
git switch main
git pull origin main
```

## 2. 新しいブランチを作成する

開発作業は必ず新しいブランチで行ってください。
ブランチ名は以下のように issue 番号を含め、分かりやすい名前をつけるようにしてください。


例:

```sh
git switch -c 123_add-test
```

## 3. コードを修正する

### 3-1. 修正後、変更内容を確認する

修正を行った後、以下のコマンドで修正ファイルの一覧を確認します。

```sh
git status
```

さらに、修正内容の差分を確認する場合は次のコマンドを使います。

```sh
git diff
```

### 3-2. 修正をステージングする

修正内容をステージング(インデックスに追加)します。

```sh
git add .
```

### 3-3. ステージング後の確認

再度、ステージング済みファイルを確認します。

```sh
git status
```

ステージング済みの差分を確認したい場合は以下を使います。

```sh
git diff --cached
```

### 3-4. コミットする

コミットメッセージは簡潔かつ内容が分かるように記載してください。

例:

```sh
git commit -m "テストを追加"
```


## 4. プルリクエスト作成前に main ブランチへ rebase する

プルリクエストを作成する前に、最新の main ブランチを取り込み、自分のブランチを rebase します。

### 4-1. main ブランチを最新にする

```sh
git switch main
git pull origin main
```

### 4-2 作業ブランチに戻る

```sh
git switch feature/add-login-page
```

### 4-3. main ブランチの最新履歴を取り込む

```sh
git rebase main
```

もしコンフリクトが発生した場合は、表示される指示に従って解決し、以下で rebase を続行します。

```sh
git add .
git rebase --continue
```

## 5. リモートリポジトリへプッシュする

```sh
git push -u origin 123_add-test
```

## 6. プルリクエストを作成する

GitHub 上で以下の手順でプルリクエスト(PR)を作成してください。

1. GitHub のリポジトリページを開く
2. 「Compare & pull request」ボタンをクリック
3. タイトルと説明を入力
    - どのような変更か
    - 関連する Issue があれば番号を記載(例: closes #123)
4. レビューアを指定(必要であれば)
5. 「Create Pull Request」をクリックして作成


## 7. プルリクエストマージ後、main ブランチへ戻る

プルリクエストのマージが完了したら、作業ブランチから main ブランチへ戻します。

```sh
git switch main
git pull origin main
```

## 注意事項

- main ブランチには直接 push しないでください
- 大きな変更の場合は事前に Issue を立てて相談してください
- レビューコメントが付いたら対応をお願いします

変更履歴

日付 内容
2025/7/12 01 新規作成
2025/7/12 02 pull 操作について追加
リベースの説明追加
マージ後 main に戻る手順を追加

2025年5月3日土曜日

Keycloak の Admin UI の開発環境を整える

やりたいこと

Keycloak の Admin Console に上手く動かない箇所があった ので、自分で直すために環境づくりをする。

前提

  • Docker インストール済み
  • docker exec コマンドが使えること

開発環境の起動

今回はすべての作業をコンテナ上で行う。

必要に応じてバインドマウントするなり、ベアメタルで環境構築するなりしてください。

docker run -it --rm -p "8080:8080" -p "5174:5174" ubuntu:24.04

必要なパッケージのインストール

Keycloak のビルド・実行には Node.js と Java が必要なのでインストール。

その他、開発に必要なものとして curl, git, vim をインストールする。

いろいろ

apt update
DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends curl git vim

OpenJDK

apt install -y openjdk-17-jdk-headless

NodeJS & pnpm

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash
\. "$HOME/.nvm/nvm.sh"
nvm install 22
npm install -g pnpm

Keycloak のクローン

cd ~/
git clone --depth 1 https://github.com/keycloak/keycloak.git

開発用サーバーを起動

フロントエンドの開発用サーバーを起動

別ターミナルで開発用サーバーを実行。

docker exec -it 72cc1a87a306 bash
cd ~/keycloak/js/apps/admin-ui/
pnpm install
pnpm dev

これだけでは動作確認ができないので、後述の「バックエンドの開発用サーバー」と連携させる必要がある。

バックエンドの開発用サーバーを起動

別ターミナルで開発用サーバーを実行。

docker exec -it 72cc1a87a306 bash
cd ~/keycloak/js/apps/keycloak-server/
pnpm start --admin-dev

これで、先ほど起動した admin-ui の開発サーバーといい感じに連携して、 admin-ui のソース更新を反映してくれるようになる。

ここまでの動作確認

ブラウザで http://localhost:8080 にアクセスすると、 Keycloak のログイン画面になる admin/admin でログインできる。

フロントエンドの修正

試しに Welcome to keycloak の文字列を変更してみる。

vim ~/keycloak/js/apps/admin-ui/src/dashboard/Dashboard.tsx

ファイルを開いたら、 t("welcomeTo", { realmDisplayInfo }) となっている箇所を "Customized!!!!!" に変更する。

変更が反映され、 Welcome to Keycloak と書かれていた場所が Customized!!!!! になっているのがわかる。

以上。

参考資料