うなすけとあれこれ

2019年08月06日

Railsの config/routes.rb の内容からOpenAPIのpathsの定義を生成する

GitHub

OpenAPIによる定義から実装を生成したいニーズはあり、その方法は存在します。

「スキーマファースト開発」という言葉もあるように、一般的にはREST API schemaを定義してから実装にとりかかります。

しかし、様々な事情で「既存のREST API実装に対してOpenAPI schemaを記述したい」というニーズがあります。

例えばRailsの config/routes.rb の内容から OpenAPI の paths に相当するYAMLやJSONを出力するようなgemがあると助かるのですが、rubygems.org を “openapi” で検索してもそれらしいgemは見当りませんでした。

なので、そういうgemをつくりました。

実装にあたって

※ 以下、特記していない場合には Rails v5.2.3 時点のコードになります。

実装にあたって、まず参考にしたのがお馴染み bin/rails routes の処理になります。このとき何が行われているのでしょうか。

bin/rails routes で実行されるコードは以下です。

# frozen_string_literal: true

require "optparse"

desc "Print out all defined routes in match order, with names. Target specific controller with -c option, or grep routes using -g option"
task routes: :environment do
  all_routes = Rails.application.routes.routes
  require "action_dispatch/routing/inspector"
  inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)

  routes_filter = nil

  OptionParser.new do |opts|
    opts.banner = "Usage: rails routes [options]"

    Rake.application.standard_rake_options.each { |args| opts.on(*args) }

    opts.on("-c CONTROLLER") do |controller|
      routes_filter = { controller: controller }
    end

    opts.on("-g PATTERN") do |pattern|
      routes_filter = pattern
    end

  end.parse!(ARGV.reject { |x| x == "routes" })

  puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, routes_filter)

  exit 0 # ensure extra arguments aren't interpreted as Rake tasks
end

https://github.com/rails/rails/blob/v5.2.3/railties/lib/rails/tasks/routes.rake

ここでの本質は

inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)

puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, routes_filter)

の2行でしょう。

では、 ActionDispatch::Routing::RoutesInspector は何でしょう。

##
# This class is just used for displaying route information when someone
# executes `rails routes` or looks at the RoutingError page.
# People should not use this class.
class RoutesInspector # :nodoc:
  def initialize(routes)
    @engines = {}
    @routes = routes
  end
  ...sinp

https://github.com/rails/rails/blob/v5.2.3/actionpack/lib/action_dispatch/routing/inspector.rb#L54-L127

はい、private API ですね。

この RoutesInspector に適切なFormatterを渡して、routesの結果を整形すればよさそうです。

では ActionDispatch::Routing::ConsoleFormatter を見てみます。

class ConsoleFormatter
  def initialize
    @buffer = []
  end

  def result
    @buffer.join("\n")
  end

  def section_title(title)
    @buffer << "\n#{title}:"
  end

  def section(routes)
    @buffer << draw_section(routes)
  end

  def header(routes)
    @buffer << draw_header(routes)
  end

  def no_routes(routes)
    @buffer <<
    if routes.none?
      <<-MESSAGE.strip_heredoc
      You don't have any routes defined!

      Please add some routes in config/routes.rb.
      MESSAGE
    else
      "No routes were found for this controller"
    end
    @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
  end

  private
  # ...snip

https://github.com/rails/rails/blob/5.2.3/actionpack/lib/action_dispatch/routing/inspector.rb#L129-L185

RoutesInspectorと同様に(明記されていませんが)これもprivate APIでしょう。

少し下に /rails/info/routes で使用される HtmlTableFormatter も定義されており、それと見比べると、 resultsection_titlesectionheaderno_routes を定義した独自のFormatterを作成すればよさそうに見えます。

OpenAPI v3 の記法

さて、 OpenAPI v3 では、以下のような記述をするよう、仕様で定義されています。

openapi: 3.0.2
info:
  title: example
  description: OpenAPI example
  version: 0.1.0
servers:
  - url: http://api.example.com/v1
    description: example server
paths:
  /users:
    get:
      summary: get users
      description: Return all user list
      responses:
        '200':
          description: users json
          content:
            application/json:
              schema: 
                type: array
                items: 
                  type: string

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md

これらの定義のうち、 paths 以下のいくつかについては、 config/routes.rb から自動生成できそうです。

なので、以下のようなFormatterを作成すると、それらしい定義を生成できます。

module ActionDispatch
  module Routing
    class OpenAPI3Formatter
      def initialize()
        @view  = nil
        @buffer = []
        @openapi_structute = {
          'openapi' => '3.0.0',
          'info' => {
            'title' => '',
            'description' => '',
            'version' => '0.1.0'
          },
          'paths' => {}
        }
      end

      def section_title(title)
      end

      def section(routes)
        routes.filter do |r|
          !r[:verb].empty?
        end.each do |r|
          @openapi_structute['paths'][r[:path]] ||= {}
          @openapi_structute['paths'][r[:path]][r[:verb].downcase] = {}
          @openapi_structute['paths'][r[:path]][r[:verb].downcase] = {
            'summary' => r[:name],
            'description' => r[:reqs],
            'responses' => nil
          }
        end
      end

      def header(routes)
      end

      def no_routes(*)
      end

      def result
        YAML.dump @openapi_structute
      end
    end
  end
end

https://github.com/unasuke/openapi3_definition_generator-rails/blob/3973f11c50a1ccdc69c1d97fce502222ecd92870/lib/openapi3_definition_generator/rails/openapi3_formatter.rb

gemify

そして、それをgemにしたのがこれです。

https://github.com/unasuke/openapi3_definition_generator-rails

使いかたはREADMEにあるとおり、Gemfileに追記して bundle installした上で、 $ bin/rails openapi3_definition:generate_yaml

雑に表参道.rbで話したのが、これです。

omotesandorb #49

上で述べたように、内部でPrivate APIにしっかり依存しているので、いつ動かなくなるかは保障できず、そのため Rails v5.2.3 以上 v6 未満でしかインストールできないようになっています。Pull Requestは大歓迎です。

今後

今後、実装するとしたら

くらいと、あとは表参道.rbでもアドバイスを頂いたように、Rails本体への機能追加も考えています。

Tweet
2019年08月06日
2019年07月08日

異常独身男性エントリ

abnormal bachelor

オタク!退職エントリより
異常独身男性エントリ書いて!

— 衰咲ふち💬アーバンキャット (@otoroesaki) July 2, 2019

経緯

「異常独身男性注意マスキングテープ (マスキングテープ - テープ幅 15mm)」を 限界集落 で購入しました! https://t.co/7Z8WodNZtK #booth_pm

— HolyGrail / 蜘蛛糸まな🕸️@新人VTuber (@HolyGrail) June 17, 2019

異常独身男性にマスキングテープ貼り付けるいたずらをやっていこうと思います

— HolyGrail / 蜘蛛糸まな🕸️@新人VTuber (@HolyGrail) June 17, 2019

左隣に座ってる同僚には絶対貼りたい

— HolyGrail / 蜘蛛糸まな🕸️@新人VTuber (@HolyGrail) June 17, 2019

ふちちゃん、一枚セールスしたわよ! pic.twitter.com/sgvFb7L114

— HolyGrail / 蜘蛛糸まな🕸️@新人VTuber (@HolyGrail) June 17, 2019

アアアアアーーーアアアアアーアーアーアーアーアーアアアーーーーーアアーア

異常独身男性:背面有〼 Tシャツ ブラック Mを買ったよ。 https://t.co/kWBTbhCXKp #suzurijp @suzurijpさんから

— うなすけ (@yu_suke1994) June 17, 2019

そして

pic.twitter.com/HcTk2jQFtU

— うなすけ (@yu_suke1994) July 5, 2019

異常独身男性 pic.twitter.com/WLL3xaBUUg

— HolyGrail / 蜘蛛糸まな🕸️@新人VTuber (@HolyGrail) July 8, 2019

異常独身男性 pic.twitter.com/GYMkDIDBO4

— HolyGrail / 蜘蛛糸まな🕸️@新人VTuber (@HolyGrail) July 8, 2019

絶対に嫌シリーズ

うなすけくんには異常独身男性界を背負って立ってほしい

— 衰咲ふち💬アーバンキャット (@otoroesaki) June 23, 2019

今生まれ出づる異常独身男性ニューエイジ
うなすけ

— 衰咲ふち💬アーバンキャット (@otoroesaki) June 28, 2019

独身男性の皆さん買いましょう

衰咲ふちの限界集落で異常独身男性シリーズが発売されたわよ! #衰咲

異常独身男性:背面有〼Tシャツ https://t.co/jk7jWiaztm

異常独身男性ステッカー(COLOR) | 限界集落 https://t.co/MIDepETLUe

異常独身男性マスキングテープ | 限界集落 https://t.co/wEfJrhzywj pic.twitter.com/7csRAsAb5E

— 衰咲ふち💬アーバンキャット (@otoroesaki) June 15, 2019

カラー色々!選べる異常独身男性!
ブルーカラーもホワイトカラーも一緒に仲良く異常独身男性着ようね!(皮肉) pic.twitter.com/zkiVFOaoZb

— 衰咲ふち💬アーバンキャット (@otoroesaki) June 15, 2019
Tweet
2019年07月08日
2019年06月25日

Fortitude60を組みました

Fortitude60

経緯

2018年10月頃のgroup buyから積みっぱなしになっていたFortitude60を、重い腰を上げてようやく組み立てました。

Fortitude60 group buy round1

発端は、友人の @nikuzuki_29 が職場で布教された結果「自作キーボードを組んでみたい」という気持ちになり、では僕も、とスイッチ、キーキャップを揃えて組み立てることにした、という流れになります。

構成

組み立て

肉好きが初心者であることから、一日中遊舎工房のフリースペースを使うつもりで早起きして秋葉原に行き、組み立てを開始しました。(事実、閉店まで居ました)

僕も久々の半田付けであることから、まずはmeishiを各々組み立てて慣れておく作戦をとったのが良かったです。

遊舎工房で1日かけて組み立てたのですが、その場でArch LinuxからFirmwareを焼こうとするとエラーになってしまいました。レンタルスペースで焦っていたのもあったのでその場での解決は諦めましたが、帰宅後プルリクを出しmergeされました。

✌️

Install avrdude in Arch or Manjaro Linux by unasuke · Pull Request #6132 · qmk/qmkfirmware https://t.co/qhmxedwFbM

— うなすけ (@yusuke1994) June 16, 2019

CとHを取り違えたので、若干ヒビが入ってしまいました。

fortitude60(ヒビ)

トラブル

右手側が全く反応しません。テスターを買ったので、追い追い時間を見つけて調査と修正、あるいは再度購入をしていこうと思っています。

これから

適当なTai-HaoのキーキャップとBlank keycapsを購入したのですが、キーごとに高さが違う構成なので、PimpMyKeyboardで改めて好みのを買おうと思っています。またTRRS、USBケーブルも味気ないので買おうかなと考えており、沼感が出てきました。

Tweet
2019年06月25日
2019年05月11日

Itamae v1.10.4 をリリースしました

7f9b8f00 6834 11e9 9544 1fd0e8427412

v1.10.3からのdiffはこちらです。

https://github.com/itamae-kitchen/itamae/compare/v1.10.3…v1.10.4

Changelogs

Suppress Ruby warnings

RUBYOPT=-w を指定したときに出る警告を修正するもの。

Run test cases correctly by pocke · Pull Request #289 · itamae-kitchen/itamae

これは v1.10.3 にmergeされている Add integration test with itamae local command で、test filesを列挙する正規表現が誤っていた問題を修正するもの。 これで 0 examples139 examples になりました。よかったですね。

このあたりはpull req authorのpockeさんがブログに書いてくれていますね。メンテナとして気付けなかった僕達のミスでもあります。ありがとうございます。

Itamaeのテストを壊してしまっていた話 - pockestrap

Refine itamae docker‘s created message by pocke · Pull Request #288 · itamae-kitchen/itamae

itamae docker コマンドに --tag オプションを指定した場合、成功したときのmessageにtagも表示するようにしたもの。

Add description to –tag option of docker subcommand by pocke · Pull Request #286 · itamae-kitchen/itamae

itamae docker コマンドのhelp messageに --tag オプションの説明を足したもの。

Tweet
2019年05月11日
2019年04月30日

高専DJ部 #22 でした

CDJ

セトリ

  1. Flying Out (Extended Mix) - M.I.K.E. Push
  2. Black Mirror (Extended Mix) - James Dymond
  3. Kick Back (Original Mix) - Ashley Smith
  4. Fight My Way feat. Moses Keenan (Extended Mix) - Morgan Page, Moses Keenan
  5. With You feat. Emelie Cyreus (Progressive Extended Mix) - Magnificence, Venomenal, Emelie Cyreus
  6. Never Say Never (Extended Mix) - SICK INDIVIDUALS
  7. Turn Back Time (Original Mix) (Original Mix) - 3D Nation
  8. Burn (Original Mix) - DallasK, KSHMR
  9. We Are the Sound (Original Mix) - Betsie Larkin

徐々にテンションが上っていく感じの曲順になるようにやってみましたが、総評としてはパリピでしたね。精進します。

遅刻

イベント自体は3月末にあったので公開がとても遅れてしまいました。

今回、高専DJ部の開催スケジュール的に平成最後の開催となるので、IMAP++とのコラボを持ちかけたらなんと実現してしまいました。 Webサイトもmazcoさんに特別にデザインしてもらったりと、本当に記念となる回にできたかなと思います。

また、 #unasukefmのために購入したZOOM H4n Proで録音したものを内々に共有したりもしました。冒頭のmixcloudはそこから切り出したものになります。

Tweet
2019年04月30日
2019年04月22日

Conference Week in Fukuoka 2019

RubyKaigi 2019

4/15からの1週間、福岡のConference Weekで何をしてきたのかという日記です。

#unasukefm 収録

カンファレンス開始前の15日には、GMOペパボで @udzura さんを相手に #unasukefm の収録を行いました。

Haconiwaについて聴いてる #unasukefm pic.twitter.com/gp67dELrVQ

— カルパス (@yoshi_hirano) 2019年4月15日

最新エピソードを配信しました。RubyKaigi 2019のLocal Organizerである @udzura さんにRubyKaigiの舞台裏、福岡移住の話、Haconiwaについて聴いています。ぜひ聴いて下さい - https://t.co/9JjFL6dOBv #4 - #kaigieffect (@udzura) by Railsdm Podcast #rubykaigi2019https://t.co/kjOtdiYJUO

— Railsdm (@railsdm) 2019年4月22日

ハッシュタグ #unasukefmの様子を見るに、皆さんに楽しんでいただけたようでなによりです。

実はudzuraさんを相手に収録をしたいというのは、Railsdm PodcastがunasukefmとしてSeason 2を開始したときからほとんど決まっていたようなものなのです。達成でき、HaconiwaやCloudNativeの話ができてよかったです。

CloudNative Days Fukuoka 2019

そして1番目のカンファレンス、CloudNative Days Fukuoka 2019に参加しました。

ここではKubernetesだけに留まらない、CloudNativeな技術に関する様々なトークを聞くことができました。特に僕が今課題を感じている点である、秘匿情報の管理についてVaultを使うケースを知れたのはよかったです。

懇親会でも、福岡でしか会えない方々をはじめ、Rubyコミュニティとはまた違うCloudNativeなコミュニティの方々とお話しすることができました。

RubyKaigi 2019

次に、1日挟んで迎えたRubyKaigi 2019です。挟んだとは言ったものの、僕は今回(も)helperとしてお手伝いする立場なので、17日も1日中設営作業をしていました。

https://photos.app.goo.gl/svaNw2jvBtzusJ259 ← 様子を軽くまとめました。

今回はネットワーク斑としてケーブルの敷設、同時通訳レシーバーの受け渡し、スピーカーへの同時通訳打ち合わせ案内などを主に担当しました。英語が咄嗟に出てこない&聞きとれないのがまだまだ課題として感じられる3日間でした。

なるほどね pic.twitter.com/jqoAAe4vng

— HolyGrail / 蜘蛛糸まな🕸️@新人VTuber (@HolyGrail) 2019年4月18日

2019年 pic.twitter.com/CCBO4FeLDX

— Yuki AKAMATSU (@ukstudio) 2019年4月21日

今になっても、「ああ、良かったなぁ」というふんわりとした感想しか出てきません。

Tweet
2019年04月22日
2019年03月24日

未読のGitHub notificationを定期的に通知させる

botの様子

participating notification

皆さんは https://github.com/notifications/participating を普段どれだけ見ていますか。僕はこの機能をよく使うのですが、集中していたりすると見に行くのを忘れて、コメントされているのに返事をしそこねてしまったまま長い時間経ってしまうということがしばしばありました。

要件

push型の通知であること

こっちからアクセスしに行かなくても、「未読がこれだけあるよ」と教えてほしいわけです。

即時的でないこと

とはいっても、コメントされて数秒で通知が来る、という即時性は求めていません。なぜなら、例えば社のリポジトリに関することであれば、GitHubのコメントやmergeをSlackに流しているからです。コメントの応酬はそっちで見れます。

https://slack.github.com/

ユースケースとしては、「出社してまず見る」とか、「集中してて気づかなかったけどあのpull reqにコメントついてるっぽい」だとか、そういうのを求めていました。

GitHub notification reminderをつくった

https://github.com/unasuke/github-notification-reminder

これをheroku schedulerで定期的に叩くことによって、このように通知させています。

botの様子

Deploy to herokuボタンを作ったので同様の問題にお困りの方はご活用ください。

困っていること

GitHubのREST API v3でNotificationsを取得するendpointとresponseは以下URLの通りです。

https://developer.github.com/v3/activity/notifications/#list-your-notifications

ここで、通知の対象であるissueやpull requestの情報を見ようとすると、subject.url がそれっぽいなということになります。しかしよく見ると、domainが api.github.com になっています。例としてdocumentに載っている https://api.github.com/repos/octokit/octokit.rb/issues/123 ですが、ここにweb browserからaccessすると、JSONが返ってきます。この中に、html_url として、human accessableなURLが入っています。

これ、しんどくないですか。いわゆるGraphQLが解決しようとした、RESTによるN+1の実例じゃないか!となりました。そしてGraphQL API v4にはまだNotification Objectは来ていないのですね。

この件、supportに投げたのですが、僕の英語力が未熟なのか、「 https://developer.github.com/v3/pulls/#get-a-single-pull-request を使うといいよ」と返事が来ました。そういうことなのでしょう。

ただ、get-a-single-pull-requestしようにも、responseの中にissueやpull requestのnumberが単体では含まれないので、二進も三進も。(このscriptではgsubでhtmlを組み立てています)

こういう書き捨てのscriptのreadmeを頑張ることについて

よしいっちょブログに書くか〜となり、そんならREADME.mdを整備しておかないと「映え」ないなとなって、heroku appならdeploy to herokuボタン欲しいよなといろいろとmeta dataをつくっていて、正直面倒なんです。

じゃあなんで書くかというと、codeにcommentを書くように、あとで見る自分のためなんですね。未来の自分が環境構築するときに困らないように、という目的があるのかなと思いました。

Tweet
2019年03月24日
2019年02月15日

unasuke.fm が始まりました

unasukefm

経緯

一言で表わすと、「ノリと勢い」で始まりました。

音質改善の足しに、とKyashで投げ銭をしたのをきっかけに、あれよあれよと話が進んで、いつのまにかRailsdm podcast Season 2として unasuke.fm をやることになりました。

Railsdm Podcastに投げ銭したい

— うなすけ (@yu_suke1994) 2018年12月24日

2019年からの Railsdm Podcast は、某うなすけさんが聞き手で、毎回ゲストを変えてトークしていくという内容でリニューアルしていく方向です。実質 https://t.co/Yosts9pvuRです。 #railsdm

— カルパス (@yoshi_hirano) 2018年12月24日

Season 2 episode 1

まずepisode 1として、onkさんをゲストに収録を行いました。以下から聞くことができます。

ジングル

KORG Gadget for iOSで作成しました。カルパスさんから頂いたふんわりとしたイメージをもとに作ってみました。

上のはイントロで使ったshort verで、アウトロで使用したlong verは以下です。

機材

そもそも始めたばかりのpodcastでrebuild.fmレベルの環境・質を実現することは現実的ではありません。

Podcasting Guide 2017 – Tatsuhiko Miyagawa’s Blog

それでも、これまでの収録を聴いて「もっとよくできそうなんだけどな」という気持ちになっていたのも確かです。

今回の収録では、僕がそのとき持っていた機材だけで収録を行いました。それらを以下にまとめます。

録音編

録音にあたって、今回は以下の機材を使用しました。

これはさっちんさんに教えていただいたものです。

> https://t.co/pbAOT5yItE

マイクなら絶対これ。tsとかDiscordとかしてても設定ちゃんとすれば周りの音入らない。スピーカーでも全然OK
tsなら音楽流しながらでもボイスチャット出来る。

— 02/09 11:00 🍲 (@sachin21__) 2017年9月21日

購入してから自宅でのボイスチャットに使用していたものをそのまま持ち込んで録音しました。このマイクに関しては、普段の通話で問題になったことがなく(通話相手から何か言われたことがない、という意味)、それなりに安心して収録に臨めました。

収録ではマイクをUni(単一方向のみ集音)にして僕に向けていました。反対側にカルパスさんが居たのですが、カルパスさんの声が入ることがなく綺麗に音声を拾えていました。こちら側の音声についてはそれなりの質で収録できたのではないかなと思っています。

うなすけ君の編集後の音が全体的にかつてないほどクリアなので、クオリティ爆上がりしてる。それとともに今までスマンという気持ちがかなり溢れてる。

— カルパス (@yoshi_hirano) 2019年1月29日

今回onkさんが京都に居るという都合上、Hangout経由で収録しました。そのため、2人が同時に発声しているタイミングだとどちらか(どちらも?)の声が抑えられてしまい、何を話しているのか不明瞭な部分ができてしまいました。これはどうしようもないですね。

また今後ですが、既にある機材の他にもHolyGrailさんの環境を参考に、以下の機材を用意するつもりです。

ここ何ヶ月かかけていろいろ機材を揃えて環境はほぼ整ったので来年のなんらかの活動にご期待ください! pic.twitter.com/HzVtiqKrsk

— HolyGrail / 蜘蛛糸まな🕸️@新人VTuber (@HolyGrail) 2018年12月22日

機材に関してはキリがないので、あまり凝りすぎてもよくないでしょう。上記構成からさらに買い足していったり、上位機種への乗り換えなどはするつもりはありません。たぶんしないと思う。しないんじゃないかな。

自分はVTuberになるために機材に10万弱かけてきたがうなすけくんははたしてどうかな

— HolyGrail / 蜘蛛糸まな🕸️@新人VTuber (@HolyGrail) 2019年1月11日

機材構成に関しては、他のpodcastでの事例が探すと色々出てくるので、それらを参考にしてみるのもいいかもしれません。

podcastの運営に参加することになった結果、沼にはまっていっているように見えるかもしれませんが、オーディオインターフェースは既に所持していましたし、追加で欲しくなったのはPCMレコーダーくらいなものなので沼ではないです。

4月にどういう機材を手元に揃えているのかは誰にもわかりません。

ちょうど先日「Jonanは配信機材をRubyKaigiに持ち込んでいて現地から配信していた。ポッドキャストであれば映像も不要だしオーディオインターフェースとマイクだけで収録が可能なので実質ありとあらゆる場所で収録ができるようになるのでは?」という話をしたところなので期待ができそうですね。

— HolyGrail / 蜘蛛糸まな🕸️@新人VTuber (@HolyGrail) 2019年1月11日

編集編

録音した音声の編集は以下のような環境で行っています。

編集環境に関してはこれ以上のupdateはするつもりはありませんし、してもあまり意味がないかなと思っています。

ただ、マスタリングにあたって、低音域と高音域を削るイコライザーはかませましたが、ノイズ軽減などは今回はしていません。それについては改善の余地があると思います。

今後

今後お話ししてみたい方々をカルパスさんと話して十数人ほどリストアップしています。1年くらいは続くと思います。

#unasukefm の次回の配信は、来週 or 再来週の前半を予定しています。

— Railsdm (@railsdm) 2019年2月15日
Tweet
2019年02月15日
2019年01月30日

[WIP]kibela2esa をつくりました

kibela esa migration params

HolyGrail/kibela2esa: Kibela 2 esa.io

りゆう

やっぱりWIPっていいですよね。

どういうことをしているのか

  1. exportされたmarkdownを読みこむ
  2. exportされた画像を読みこむ(pathと名前だけ持っておく)
  3. 画像をesaにアップロードして、S3のURLを持っておく
  4. markdown内の画像URLをesaのS3 URLに置換する
  5. frontmatterなどの処理や整形した結果のmarkdownをesaに投稿する

できること

(まだ)できないこと

たいへんだったこと

まとめ

esaが好きです。

HolyGrail/kibela2esa: Kibela 2 esa.io

Tweet
2019年01月30日
2018年12月24日

OCI Image Format Specification v1.0.1を読んで

skopeo

まえおき

発端

めちゃくちゃ興味あり〼 https://t.co/gHfAV5rbEU

— うなすけ (@yu_suke1994) 2018年11月30日

頭の中には設計があるんですが、…
OCIの仕様を再確認するところから…

— Uchio KONDO 🔫 (@udzura) 2018年11月30日

とりあえずブログかキータ

— Uchio KONDO 🔫 (@udzura) 2018年11月30日

という訳で、書きました。

OCI Image とは

Open Container Initiativeによって定められた、コンテナイメージフォーマットの標準仕様です。

詳しくは Open Container Initiativeによるコンテナランタイムとコンテナイメージの最初の標準化作業が完了、「OCI v1.0」発表 - Publickey にて。

OCI Imageを触ってみる

現在、OCI ImageをdownloadできるDocker HubのようなWebサイトは知る限りありません。なので、自分でOCI Imageを作成する必要があります。

まずはDocker Imageから

まず、Docker Hubにて公開されているDocker imageをdownloadするところからです。以下のコマンドで、Docker Hubからダウンロードしたimageをtarballとして扱うことができるようになります。

$ docker image pull ruby:2.5.3-slim-stretch
$ docker save ruby:2.5.3-slim-stretch --output ruby_253_slim_stretch.tar

このtarを展開してみましょう。

$ tar -xf ruby_253_slim_stretch

すると、manifest.json というファイルができているので、見てみます。

[
  {
    "Config":"b1c1603e80c648f3ab902b0259ab846a7779d0780124bf9e417dd4b8c3cea296.json",
    "RepoTags":[
      "ruby:2.5.3-slim-stretch"
    ],
    "Layers":[
      "aeff88bcdbbd12ea45c023c45f97b870492092899651c811b2ef26ae7fdf3120/layer.tar",
      "c61a4dce9ddcebd63027d09811998052c9b2cdb3a379c297277cf755dfcf1420/layer.tar",
      "de2944e57fc93c2f354420cb36210fd1181687a990ffd7123600fdaecba3ee83/layer.tar",
      "49c3631e8651776127d66adb995e78af1e2cfc52b7a10a20df0d92d837258419/layer.tar",
      "eb50b8a8210f1b43ff1571598e66b694844b2dcf6fbaa0691e8af6b7c80dcaa7/layer.tar"
    ]
  }
]

なるほど、これはOCI image specを読むとわかるのですが、OCIに定められている形式のJSONではありませんね。

skopeo

ここで、containers/skopeo というtoolを使用して、Docker imageをOCI imageに変換してみます。

$ skopeo copy docker://ruby:2.5.3-slim-stretch oci:ruby-oci:latest

すると、 ruby-oci/index.json というファイルができているので、見てみます。

{
  "schemaVersion":2,
  "manifests":[
    {
      "mediaType":"application/vnd.oci.image.manifest.v1+json",
      "digest":"sha256:a3843587af4f3e838f3e1a10649631144d4dcf4391980b64f3b902d81048057c",
      "size":976,
      "annotations":{
        "org.opencontainers.image.ref.name":"latest"
      },
      "platform":{
        "architecture":"amd64",
        "os":"linux"
      }
    }
  ]
}

なるほど、これは OCI Image Specに定められている image manifest fileですね。

以下、OCI Image Format Specification v1.0.1

自分なりに理解しようと翻訳したもののメモになります。 正確性の保証はないです。誤訳とかあります。最後のほう力尽きてます。


Open Container Initiative Image Format Specification v1.0.1

Overviewで語られていること

high level image manifest にはcontentsとdependencies of the image including the content-addressable(連想?) identity of one of more file system layer changeset archives、展開すると最終的に実行可能なファイルシステムになる

image configuration にはapplication arguments, environmentsなどの情報

image indexには high level manifest list of manifests and discriptorsのpointが含まれる

それらのmanifestsは異なるimageの実装 ←プラットフォームや他の属性によって変化することができる

Build diagram

一度作成されたOCI imageは名前によって探索(discovered)、ダウンロード、hashによる検証、署名による信頼、OCI Runtime Bundleへの展開ができる

(No Title)

Understanding the specification

components of the specは以下を含む

optional featureとしてSignaturesやNamingが仕様に含まれるかもしれない。

OCI Image Media Types

HTTP responseのContent-Typeで上の値を返すなどのように、typeを返すなにかしらの方法を実装してもよい(MAY)、また実装はmedia typeとdigestを期待してよい? 実装は返却されたmedia typeを尊重する必要がある(SHOULD)

Compatibility Matrix

前方・後方互換を可能な限り維持する必要がある。

似た、または関連するmedia typeは以下

Relations

Image indexは複数のImage manifestを持つ。Image manifestとImage JSON(config)は1対1。Image manifestはLayerのtar archiveを複数持つ。 Discriptorは全ての参照を持つ。

OCI Content Discriptors

properties

Digests

こういう形式

sha256とsha512がRegistered algorithmsとされている。sha256はMUSTでsha512はMAY。

Example

{
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "size": 7682,
  "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
  "urls": ["https://example.com/example-manifest" ]
}

OCI Image Layout Specification

OCI Image LayoutはOCI content-addressable blobs と location-addressable references のための directory構造を表す(?) Layoutではtarやzipなどのarchive formats、nfsなどの共有ファイルシステム、http、ftp、rsyncなどのネットワークによるファイル取得を使用してもよい。

あるimage layoutと参照は、manifestと指定された順序で適用されるfilesystem layerとOCI runtime specificationのconfig.jsonへ変換できるimage configurationがあればOCI Runtime Specification bundleを何らかのtoolによって作成できる。(?)

Content

Example

$ cd example.com/app/
$ find . -type f
./index.json
./oci-layout
./blobs/sha256/3588d02542238316759cbf24502f4344ffcc8a60c803870022f335d1390c13b4
./blobs/sha256/4b0bc1c4050b03c95ef2a8e36e25feac42fd31283e8c30b3ee5df6b043155d3c
./blobs/sha256/7968321274dc6b6171697c33df7815310468e694ac5be0ec03ff053bb135e768

$ shasum -a 256 ./blobs/sha256/afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51
afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51 ./blobs/sha256/afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51

Blobs

Example

$ cat ./blobs/sha256/9b97579de92b1c195b85bb42a11011378ee549b02d7fe9c17bf2a6b35d5cb079 | jq
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "size": 7143,
      "digest": "sha256:afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51",
      "platform": {
        "architecture": "ppc64le",
        "os": "linux"
      }
    },
...
$ cat ./blobs/sha256/afff3924849e458c5ef237db5f89539274d5e609db5db935ed3959c90f1f2d51 | jq
{
  "schemaVersion": 2,
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "size": 7023,
    "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 32654,
      "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0"
    },
...
$ cat ./blobs/sha256/5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270 | jq
{
  "architecture": "amd64",
  "author": "Alyssa P. Hacker <alyspdev@example.com>",
  "config": {
    "Hostname": "8dfe43d80430",
    "Domainname": "",
    "User": "",
    "AttachStdin": false,
    "AttachStdout": false,
    "AttachStderr": false,
    "Tty": false,
    "OpenStdin": false,
    "StdinOnce": false,
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ],
    "Cmd": null,
    "Image": "sha256:6986ae504bbf843512d680cc959484452034965db15f75ee8bdd1b107f61500b",
...
$ cat ./blobs/sha256/9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0
[gzipped tar stream]

oci-layout file

これ。

{
    "imageLayoutVersion": "1.0.0"
}

index.json

必須。image-layoutの参照、descriptorsのentry pointになる。/index.json に置かれる。 "org.opencontainers.image.ref.name" にイメージのtagが格納される?

{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.index.v1+json",
      "size": 7143,
      "digest": "sha256:0228f90e926ba6b96e4f39cf294b2586d38fbb5a1e385c05cd1ee40ea54fe7fd",
      "annotations": {
        "org.opencontainers.image.ref.name": "stable-release"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "size": 7143,
      "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
      "platform": {
        "architecture": "ppc64le",
        "os": "linux"
      },
      "annotations": {
        "org.opencontainers.image.ref.name": "v1.0"
      }
    },
    {
      "mediaType": "application/xml",
      "size": 7143,
      "digest": "sha256:b3d63d132d21c3ff4c35a061adf23cf43da8ae054247e32faa95494d904a007e",
      "annotations": {
        "org.freedesktop.specifications.metainfo.version": "1.0",
        "org.freedesktop.specifications.metainfo.type": "AppStream"
      }
    }
  ],
  "annotations": {
    "com.example.index.revision": "r124356"
  }
}

OCI Image Manifest Specification

imageと、そのコンポーネントのために生成された一意なIDからハッシュ可能なimageのconfigurationimage modelをサポートした参照可能なimageを作成すること、platform固有のmanifestを含んだ"fat manifest"による複数architecture対応のimageの実現、OCI Runtime Specificationへの変換の3つを目標にしている。

Image Manifest

image indexはarchitectureやOSごとに展開可能なそれぞれのimageの情報を持つが、image manifestは特定のarchitecture、OSに対する単一のcontainer imageにおけるconfigurationとlayerの集合を提供する。

Image Manifest Property Descriptions

Example

{
  "schemaVersion": 2,
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "size": 7023,
    "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 32654,
      "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0"
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 16724,
      "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 73109,
      "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
    }
  ],
    "annotations": {
    "com.example.key1": "value1",
    "com.example.key2": "value2"
  }
}

OCI Image Index Specification

Image Index Property Descriptions

Example

{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "size": 7143,
      "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
      "platform": {
        "architecture": "ppc64le",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "size": 7682,
      "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    }
  ],
  "annotations": {
    "com.example.key1": "value1",
    "com.example.key2": "value2"
  }
}

Image Layer Filesystem Changeset

change types

file types

File attributes

hardlinks

Platform-specific attributes

Createing

rootfs-c9d-v1/
    etc/
        my-app-config
    bin/
        my-app-binary
        my-app-tools

色々とfilesystemについての解説が続く

Non-Distributable Layers

OCI Image Configuration

用語

properties

Example

{
    "created": "2015-10-31T22:22:56.015925234Z",
    "author": "Alyssa P. Hacker <alyspdev@example.com>",
    "architecture": "amd64",
    "os": "linux",
    "config": {
        "User": "alice",
        "ExposedPorts": {
            "8080/tcp": {}
        },
        "Env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "FOO=oci_is_a",
            "BAR=well_written_spec"
        ],
        "Entrypoint": [
            "/bin/my-app-binary"
        ],
        "Cmd": [
            "--foreground",
            "--config",
            "/etc/my-app.d/default.cfg"
        ],
        "Volumes": {
            "/var/job-result-data": {},
            "/var/log/my-app-logs": {}
        },
        "WorkingDir": "/home/alice",
        "Labels": {
            "com.example.project.git.url": "https://example.com/project.git",
            "com.example.project.git.commit": "45a939b2999782a3f005621a8d0f29aa387e1d6b"
        }
    },
    "rootfs": {
      "diff_ids": [
        "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
        "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
      ],
      "type": "layers"
    },
    "history": [
      {
        "created": "2015-10-31T22:22:54.690851953Z",
        "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
      },
      {
        "created": "2015-10-31T22:22:55.613815829Z",
        "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]",
        "empty_layer": true
      }
    ]
}

Annotations

Rules

Pre-Defined Annotation Keys

Back-compatibility with Label Schema

org.opencontainers.image org.label-schema Compatibility notes
created build-date Compatible
url url Compatible
source vcs-url Compatible
version version Compatible
revision vcs-ref Compatible
vendor vendor Compatible
title name Compatible
description description Compatible
documentation usage URLの場合にはCompatible
authors Label Schemaにはない要素
licenses Label Schemaにはない要素
ref.name Label Schemaにはない要素
schema-version OCI Image Specにはない要素
docker.*, rkt.* OCI Image Specにはない要素

Conversion to OCI Runtime Configuration

Verbatim Fields

annotation fields

Parsed Fields

Optional Fields

Annotations

Considerations

Extensibility

Canonicalization

JSON

Extended Backus-Naur Form

Tweet
2018年12月24日
新しい投稿
古い投稿