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