2022年3月18日金曜日

docker-compose で起動した Gitlab にリストアしてみた

docker-compose で起動した Gitlab にリストアしてみた

概要

過去に helm chart で作成した gitlab にリストアしてみました
今回は docker-compose で構築した gitlab にリストアしてみます

環境

  • Gitlab 14.6.6-ee

バックアップの作成

どの環境でも OK です
docker-compose の場合は以下のように作成します
バックアップファイルはコンテナ内に作成されるので何かしらの方法でリストア先のgitlabに持っていきます

  • docker-compose exec gitlab gitlab-backup create
  • docker-compose exec gitlab gitlab-ctl backup-etc

リストア先のGitlabの作成

docker-compose で構築しましょう

puma と sidekiq の停止

リストア先のgitlabで行います

  • docker-compose exec gitlab gitlab-ctl stop puma
  • docker-compose exec gitlab gitlab-ctl stop sidekiq

secret を展開する

まずはシークレットから展開します
今回のシークレット関連のバックアップファイルは「gitlab_config_1234567890_2022_03_16.tar」とします
このファイルをリストア先のコンテナに配置しましょう
今回は /secret 配下に配置しました
あとは tar で展開します

  • docker-compose exec gitlab tar xvf /secret/gitlab_config_1234567890_2022_03_16.tar

展開されると /etc/gitlab 配下に gitlab.rb や gitlab-secrets.json などが上書き展開されます

データファイルの配置

次にデータファイルの展開を行います
今回のデータファイルバックアップファイルは「1234567890_2022_03_16_14.6.6-ee_gitlab_backup.tar」とします
このファイルを /var/opt/gitlab/backups に配置します
また配置したら tar ファイルの権限を git ユーザに変更しましょう

  • docker-compose exec gitlab chown git:git /var/opt/gitlab/backups/1234567890_2022_03_16_14.6.6-ee_gitlab_backup.tar
  • docker-compose exec gitlab ls -ltr /var/opt/gitlab/backups
total 1700
-rw------- 1 git git 389120 Mar 16 01:23 1234567890_2022_03_16_14.6.6-ee_gitlab_backup.tar

リストアの実行

あとはリストアを実行します

  • docker-compose exec gitlab gitlab-backup restore BACKUP=1234567890_2022_03_16_14.6.6-ee

BACKUP 変数で指定する値は tar ファイルの _gitlab_backup.tar より前のファイル名になります

いくつか質問が聞かれますが基本的には yes で OK です
「Restore task is done.」が表示されればリストア完了です

Gitlabの再起動

再起動をして再度 docker-compose に記載されたパラメータを反映します

  • docker-compose restart gitlab

動作確認

  • docker-compose exec gitlab gitlab-rake gitlab:check SANITIZE=true

でエラーにならないことを確認しましょう
またブラウザで gitlab にアクセスしてデータがリストアされていることを確認しましょう

最後に

流れとしては簡単です
ポイントとしては

  • リストア先のバージョンとバックアップ元のGitlabのバージョンを合わせる
  • コンテナの適切な箇所にシークレットとバックアップファイルを配置する
  • リストア先のGitlabは新規のGitlabにする

あたりかなと思います
また ce バージョンと ee バージョンでも挙動が異なる可能性があるので ce or ee も合わせたほうが無難かなと思います

参考サイト

2022年3月17日木曜日

(fog) An error occurred while installing ovirt-engine-sdk

(fog) An error occurred while installing ovirt-engine-sdk

概要

Ruby の fog をインストール中にタイトルのエラーが発生しました
パッケージが足りないので追加でインストールしてあげましょう

環境

  • Ubuntu 18.04
  • Ruby 3.0.0p0
  • fog 2.2.0

エラー詳細

An error occurred while installing ovirt-engine-sdk (4.4.1), and Bundler cannot continue.
Make sure that `gem install ovirt-engine-sdk -v '4.4.1' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  fog was resolved to 2.2.0, which depends on
    fog-ovirt was resolved to 2.0.1, which depends on
      ovirt-engine-sdk

対処方法

  • apt -y install build-essential libcurl4-openssl-dev

Ubuntu 環境の場合は上記で OK です
それ以外は各種環境に合わせた上記同様のパッケージをインストールしてください

参考サイト

2022年3月16日水曜日

Python でリストや辞書のタイピング入門

Python でリストや辞書のタイピング入門

概要

前回は基本的なタイピングに入門しました
今回はリストや辞書などの少し複雑なタイピングを試してみました
記事内では List, Dict, TypedDict, Optional, Any, Literal, TypeVar を使ったサンプルコードとコメントをベースに紹介します

環境

  • macOS 11.6.4
  • Python 3.10.2

List や Dict を使ったタイピングのサンプルコード

"""リストや辞書のタイピングをテストするモジュール."""
from typing import List, Dict, Union


class Profile():
    """プロファイルを管理するクラス."""

    def __init__(self):
        """お気に入り(リスト)とステータス(辞書)を初期化.

        値は文字列と数値を許容するためUnionで定義する
        """
        self.favorites: List[Union[str, int]] = []
        self.status: Dict[str: Union[str, int]] = {}

    def add_f(self, favorite: Union[str, int]) -> None:
        """お気に入りを1つ追加."""
        self.favorites.append(favorite)

    def add_s(self, key: str, value: Union[str, int]) -> None:
        """ステータスを1つ追加."""
        self.status[key] = value

    def show(self) -> None:
        """お気に入りとステータスを表示する."""
        print(self.favorites)
        print(self.status)


if __name__ == '__main__':
    p = Profile()
    p.add_f("game")
    p.add_f(1)
    p.add_s("name", "hawksnowlog")
    p.add_s("age", 10)
    p.show()

mypy を使ってエラーが出ないことを確認しましょう
List や Dict の値で文字列以外も扱いたい場合は Union を使うことで複数の型が入ってくることを定義することができます

TypedDict を使ったタイピングのサンプルコード

from typing import TypedDict
from dataclasses import dataclass


@dataclass
class Address():
    """住所情報を管理するクラス."""

    zip: str
    city: str
    

class Status(TypedDict):
    """ステータスを型ありの辞書として定義するクラス.

    今回は辞書内に独自のクラス(Address)が値に入ることを想定して定義
    """

    name: str
    age: int
    address: Address


class Profile():
    """プロファイルを管理するクラス."""

    def __init__(self):
        """ステータス(型あり辞書)を初期化."""
        self.status: Status = {}

    def update(self, name: str, age: int, address: Address) -> None:
        """指定の値でステータスを更新."""
        self.status['name'] = name
        self.status['age'] = age
        self.status['address'] = address

    def show(self) -> None:
        """ステータスを表示する."""
        print(self.status)


if __name__ == '__main__':
    p = Profile()
    p.update("hawksnowlog", 10, Address("123-4567", "Tokyo"))
    p.update("hawksnowlog", "ten", Address("123-4567", "Tokyo"))  # これは mypy でエラーになる
    p.update("hawksnowlog", 10, Address("123-4567", None))  # これは mypy でエラーになる
    p.show()

ポイントは class Status(TypedDict) のクラス定義で辞書内で定義する型情報をクラスとして定義します

先程の Dict と違ってクラスとして定義する必要があります

また None も今は許容していないので mypy でエラーになります

Optional を使ったタイピングのサンプルコード

"""Optionalのタイピングをテストするモジュール."""
from typing import TypedDict, Optional
from dataclasses import dataclass


@dataclass
class Address():
    """住所情報を管理するクラス.

    city は None の指定も可能です
    """

    zip: str
    city: Optional[str]
    

class Status(TypedDict):
    """ステータスを型ありの辞書として定義するクラス.

    今回は辞書内に独自のクラス(Address)が値に入ることを想定して定義
    """

    name: str
    age: int
    address: Address


class Profile():
    """プロファイルを管理するクラス."""

    def __init__(self):
        """ステータス(型あり辞書)を初期化."""
        self.status: Status = {}

    def update(self, name: str, age: int, address: Address) -> None:
        """指定の値でステータスを更新."""
        self.status['name'] = name
        self.status['age'] = age
        self.status['address'] = address

    def show(self) -> None:
        """ステータスを表示する."""
        print(self.status)


if __name__ == '__main__':
    p = Profile()
    p.update("hawksnowlog", 10, Address("123-4567", "Tokyo"))
    p.update("hawksnowlog", "ten", Address("123-4567", "Tokyo"))  # これは mypy でエラーになる
    p.update("hawksnowlog", 10, Address("123-4567", None))  # Optional で None を許容したので今度は mypy でエラーにならない
    p.show()

Optional は None を許容します
Address クラスの city フィールドで None を許容するように変更してみます

今度は mypy で None の部分がエラーにならないのが確認できると思います

Any を使ったタイピングのサンプルコード

"""Anyのタイピングをテストするモジュール."""
from typing import Any
from dataclasses import dataclass


@dataclass
class Address():
    """住所情報を管理するクラス.

    city はどんな型の値でも指定可能です
    """

    zip: str
    city: Any


if __name__ == '__main__':
    # 以下はすべてmypyでエラーにならない
    addr = Address("123-4567", "Tokyo")
    addr = Address("123-4567", 123)
    addr = Address("123-4567", addr)
    addr = Address("123-4567", None)

入力される型が特定不可能なときに使います
例えば複数の外部サービスなどレスポンスが異なる場合に Any を使うことでいろいろなレスポンスが入ってくるということを明示することができます

Literal を使ったタイピングのサンプルコード

"""Literalのタイピングをテストするモジュール."""
from typing import Literal
from dataclasses import dataclass


@dataclass
class Address():
    """住所情報を管理するクラス.

    city には Tokyo, Osaka, Fukuoka のみ指定可能です
    """

    zip: str = "123-4567"
    city: str = ""

    CITY = Literal['Tokyo', 'Osaka', 'Fukuoka']

    def set_city(self, city: CITY) -> None:
        """cityを設定します."""
        self.city = city

    def show(self) -> None:
        """cityを表示する."""
        print(self.city)


if __name__ == '__main__':
    addr = Address()
    addr.set_city("Tokyo")
    addr.show()
    addr.set_city("Osaka")
    addr.show()
    addr.set_city("Fukuoka")
    addr.show()
    addr.set_city("Kyoto")  # mypyでエラーになる
    addr.show()

Literal は配列内に指定された値のみを許容することができるタイピングです
Union に近い感じはしますが Literal では型ではなく値のチェックをします

TypeVar を使ったタイピングのサンプルコード

"""TypeVarのタイピングをテストするモジュール."""
from typing import TypeVar
from dataclasses import dataclass


@dataclass
class Address():
    """住所情報を管理するクラス."""

    zip: str = "123-4567"
    city: str = ""

    T = TypeVar('T', str)

    def get_city(self) -> T:
        """cityを取得します.

        city は T 型の変数が必ず返却されます
        """
        # return 1  # これは mypy エラー
        return self.city


if __name__ == '__main__':
    addr = Address()
    print(addr.get_city())

これも Union に近いイメージです
Union は複合型ですが TypeVar は型変数になります

生成方法を見るとわかりますが TypeVar はメソッド呼び出しで生成されているので生成されるものが変数になります
Union は配列として定義しています

最後に

今回紹介したタイピング以外にも Set や FrozenSet といったジェネリック型も使うことができます
使用する変数やフィールドがどういった用途なのかに応じてこの辺りのタイピングができるようになるといいのかなと思います

ただ実行時にはどんな値の型でも入ってくるのでエディタやCI側でチェックするようにしましょう

参考サイト

2022年3月15日火曜日

(Ruby) fog でプロキシを設定する方法

(Ruby) fog でプロキシを設定する方法

概要

Ruby の fog でプロキシを使う方法を紹介します

環境

  • Ubuntu 18.04
  • Ruby 3.0.0p0
  • fog 2.2.0

準備

  • bundle init
  • vim Gemfile
gem "fog"
  • bundle install

サンプルコード

connection_options で proxy を指定すれば OK です

require 'fog/aws'

s3 = Fog::Storage.new({
  :provider => 'AWS',
  :region => 'your-region',
  :aws_access_key_id => 'xxx',
  :aws_secret_access_key => 'xxx',
  :use_iam_profile => false,
  :host => 'your.s3.host',
  :endpoint => 'https://your.s3.host',
  :aws_signature_version => 4,
  :connection_options => {
    :proxy => 'http://192.168.100.1:3128/',
  }
})

# save file from cloud storage
directory = s3.directories.get('bucket1')
file = directory.files.get('bucket1/hoge.txt')
File.open('/tmp/hoge.txt', 'w') do |f|
  f.write(file.body)
end

# upload to cloud storage from local
directory = s3.directories.get('bucket2')
file = directory.files.create(key: 'bucket2/hoge.txt')
File.open('/tmp/hoge.txt', 'r') do |f|
  file.body = f.read
  file.save
end

注意点

s3.directories を参照しようとすると endpoint で指定したアクセス先を参照せずにデフォルトの s3 を向いてしまうのでバケットの一覧は s3.directories では取得できませんでした

endpoint を指定した場合のバケットの一覧の取得はできないのかもしれません

参考サイト

2022年3月14日月曜日

jQuery でファイルのサイズを取得する方法

jQuery でファイルのサイズを取得する方法

概要

type=file のオブジェクトを取得して size 属性を参照すれば OK です

環境

  • macOS 11.6.4

サンプルコード

<!DOCTYPE HTML>
<html>
  <head>
  <meta charset="utf-8">
  <title>Upload Test</title>
  </head>
<body>
<input id="upload_file" type="file" name="file">
<button id="upload" type="button">Upload</button>
<div id="result"></div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script>
$(function () {
  $('#upload').click(function() {
    // Fetch file size
    let file = $('#upload_file').prop('files')[0];
    // 3MiB
    let max = 3 * 1024 * 1024
    if (file.size > max) {
      $('#result').text("File size must be 3mib or less.")
    } else {
      let fd = new FormData();
      fd.append("file", file);
      $.ajax({
        url:'/upload',
        type:'post',
        data: fd,
        processData: false,
        contentType: false,
        cache: false,
      }).done(function (data) {
        $('#result').text("uploaded.")
      }).fail(function() {
        $('#result').text("upload failed.")
      });
    }
  });
});
</script>
</body> 
</html>

今回は先頭の1ファイルのみをチェックしているので複数のファイルがある場合はすべてチェックしてください

念の為サーバ側でも同じようなファイルチェックするロジックを実装しておきましょう

参考サイト

2022年3月11日金曜日

Gitlabでバックアップファイルをプロキシ配下でs3にアップロードする方法

Gitlabでバックアップファイルをプロキシ配下でs3にアップロードする方法

概要

過去にバックアップファイルをs3にアップロードする方法を紹介しました
今回はプロキシ配下でアップロードする方法を紹介します
内部的には fog を使っているので fog でプロキシを設定する感じになります

環境

  • Gitlab 14.5.4

gitlab.rb

connection_options を指定します

gitlab_rails['backup_upload_connection'] = {
  'provider' => 'AWS',
  'region' => 'us-east-2',
  'aws_access_key_id' => 'AKIxxxxxxxxxxxxxxxxxxx',
  'aws_secret_access_key' => 'xxxxxxxxxxxxxxxxxxxxx',
  'use_iam_profile' => false,
  'endpoint' => 'https://s3.us-east-2.amazonaws.com',
  'aws_signature_version' => 4,
  'connection_options' => {
    'proxy' => 'http://192.168.100.1:3128',
  },
  'enable_signature_v4_streaming' => false,
  'path_style' => true,
}
gitlab_rails['backup_upload_remote_directory'] = 'test-bucket'

EXCON_DEBUG=true にすると接続時の詳細なログが表示されるようになります

注意点

「InvalidBucketName」が出る場合は path_style の値を調整してください

「STREAMING-AWS4-HMAC-SHA256-PAYLOAD is not supported」が出る場合は enable_signature_v4_streaming を false に設定してください

参考サイト

2022年3月10日木曜日

jQuery でイメージファイルを base64 エンコードデータに変換する方法

jQuery でイメージファイルを base64 エンコードデータに変換する方法

概要

バイナリファイルを base64 エンコードファイルに変換する方法を紹介します
サーバサイドでデータベースにテキストデータとして保存する場合に役に立ちます

FileReader API の readAsDataURL というメソッドを使います

環境

  • macOS 11.6.4
  • Ruby 3.0.3p157
  • sinatra 2.2.0

サーバ側実装

  • bundle init
  • vim Gemfile
gem "sinatra"
gem "thin"
  • bundle install
  • mkdir views public upload
  • vim app.rb
# coding: utf-8
require 'sinatra'

class TestUpload < Sinatra::Base
  get '/' do
    erb :index
  end

  post '/upload' do
    base64str = params['base64str']
    content_type :json, :charset => 'utf-8'
    {"result": base64str}.to_json
  end
end

サーバ側はデータをフォームデータとして取得することを想定しています
今回は受け取ったデータの保存などはせずそのままクライアントに返却しています

クライアント側実装

  • vim views/index.erb
<!DOCTYPE HTML>
<html>
  <head>
  <meta charset="utf-8">
  <title>Upload Test</title>
  </head>
<body>
<input id="upload_file" type="file" name="file">
<button id="upload" type="button">Upload</button>
<div id="result"></div>
<img id="uploaded_img" src=""></img>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script>
$(function () {
  $('#upload').click(function() {
    let file = $('#upload_file').prop('files')[0];
    // 3MiB
    let max = 3 * 1024 * 1024
    if (file.size > max) {
      $('#result').text("File size must be 3mib or less.")
    } else {
      let reader = new FileReader();
      reader.addEventListener("load", function (){
	let fd = new FormData();
        fd.append("base64str", reader.result);
        $.ajax({
          url:'/upload',
          type:'post',
          data: fd,
          processData: false,
          contentType: false,
          cache: false,
        }).done(function (data) {
          $('#result').text("uploaded.")
	  $('#uploaded_img').attr('src', data.result)
        }).fail(function() {
          $('#result').text("upload failed.")
        });
      }, false);
      if (file) {
        reader.readAsDataURL(file);
      }
    }
  });
});
</script>
</body> 
</html>

FileReader を生成し readAsDataURL の引数にファイルオブジェクトを渡します
そして FileReader オブジェクトに対して load イベントリスナーを設定します
すると base64 化が完了したときに load イベントがコールされその中で reader.result のようにすることで base64 化された画像の文字列情報が取得できます

送信するのはフォームデータにしています
理由はいくつかありますが JSON にしてしまうと base64 化された文字列をサーバ側でパースする際にパースエラーが発生するためフォームデータにしています

最後に

サーバ側にファイルデータを送信する際にバイナリをそのまま送信してもいいのですがクライアント側で base64 にしてあげるとサーバ側で頑張る必要が少なくなります

ちなみに ruby でも Base64.strict_encode64 とするだけで base64 化できるのでできるのはサーバサイドでもクライアントサイドでもどちらでもできます

参考サイト