Data Governance part of Global Digital Compact

メモ

Global Digital Compact (United Nation) に掲載されている国連のGlobal Digital Compact資料のうち、データガバナンスに関する箇所を軽く解説。

参考

共有

Trouble using Docker Desktop and docker

メモ

Ubuunu22で、Docker Desktopとディストリ向けパッケージのDockerを混在させたときにトラブったので備忘録。 ひとまず、Contextの指定を忘れないように。(自分向けメモ)

1
2
3
4
# コンテキストの確認
docker context ls
# コンテキスト切り替え
docker context use <任意のコンテキスト>

なお、DOCKER_HOSTの環境変数にも注意。 うっかり、なにかの名残で指定していることがあるので。

参考

共有

Trouble about Qemu when I use minikube

メモ

Problems detected in kubelet #17638にも記載されているが、Ubuntu22上でminikubeを実行していた際、minikube serviceを利用するときに以下のエラーが生じた。

1
MK_UNIMPLEMENTED が原因で終了します: minikube サービスは現在、QEMU 上のビルトインネットワークでは実装されていません
1
Exiting due to MK_UNIMPLEMENTED: minikube service is not currently implemented with the builtin network on QEMU

ひとまず、Dockerをドライバとして使用することにした。

1
2
3
4
# 既存のクラスタを破棄
minikube delete
# dockerを利用するよう指定して起動
minikube start --driver=docker

参考

共有

memo of Ouranos Ecosystem IDI and data-transaction-system

メモ

ouranos-ecosystem-idi / data-transaction-systemOuranos Ecosystem の オープンソースソフトウェアが公開されている。

このOSSの主旨は、READMEにある通り、

本リポジトリ(Minimum Ouranos data platform)は企業・業界・国境を跨いだデータ連携・利活用を目指すイニシアティブの 「ウラノス・エコシステム(Ouranos Ecosystem)」におけるデータ流通システムの最小実装を体験するため、 実装の一部をオープンソースとして公開する。

である。

基本的にはREADMEに書かれている通りなので、特別に補足することはないが、なんとなくメモしておく。

README 実行環境 に実行環境の情報が記載されている。記載の通り、基本的にはGo言語を用いて実装されている。

なお、以降以下のディレクトリ内に、各種レポジトリをクローンして用いる。

1
2
3
mkdir -p ~/Sources/ouranos-ecosystem-idi
cd ~/Sources/ouranos-ecosystem-idi
export OE_WORKDIR=~/Sources/ouranos-ecosystem-idi

なお、暫定的にワーキングディレクトリを示す便宜的な環境変数を作成しておいた。

認証データの構成

ユーザ認証システム にレポジトリがあるのでクローンして、認証システムを立ち上げる。

1
2
3
git clone git@github.com:ouranos-ecosystem-idi/user-authentication-system.git
cd ${OE_WORKDIR}/user-authentication-system/
docker compose up -d

docker-compose.ymlの通り、実態はDBMS(postgres)と簡易認証用のfirebaseである。 firebaseではエミュレータを用いる。

エミュレータコンフィグ配下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"emulators": {
"auth": {
"host": "0.0.0.0",
"port": 9099
},
"ui": {
"enabled": true,
"host": "0.0.0.0",
"port": 4000
},
"singleProjectMode": true
}
}

データベースのスキーマ定義。

1
2
3
cd ${OE_WORKDIR}/user-authentication-system/
export POSTGRESQL_URL='postgres://dhuser:passw0rd@localhost:5432/dhlocal?sslmode=disable'
migrate -path setup/migrations -database ${POSTGRESQL_URL} up

ダミーデータをDBに入力。

1
2
docker cp setup postgres:/setup
docker exec -it postgres bash /setup/setup_seeds.sh

シェルスクリプトは、setup/seeders に含まれているSQLを実行するというもの。

1
2
3
4
5
setup/seeders
setup/seeders/000001_seed_api_keys.sql
setup/seeders/000002_seed_operators.sql
setup/seeders/000003_seed_apikey_operators.sql
setup/seeders/000004_cidrs.sql

上記の通り、APIキー、オペレータのIDなどを入力している。2件ずつ。

確認。

1
docker exec -i postgres bash -c "PGPASSWORD=passw0rd psql -h 127.0.0.1 -p 5432 -U dhuser dhlocal -c 'select * from operators'"
1
2
3
4
5
             operator_id              | operator_name | deleted_at |     created_at      | created_user_id |     updated_at      | updated_user_id | operator_address | open_operator_id |  global_operator_id  
--------------------------------------+---------------+------------+---------------------+-----------------+---------------------+-----------------+------------------+------------------+----------------------
b39e6248-c888-56ca-d9d0-89de1b1adc8e | A社 | | 2024-03-26 12:00:00 | seed | 2024-03-26 12:00:00 | seed | 東京都渋谷区xx | 1234567890123 | 1234ABCD5678EFGH0123
15572d1c-ec13-0d78-7f92-dd4278871373 | B社 | | 2024-03-26 12:00:00 | seed | 2024-03-26 12:00:00 | seed | 東京都渋谷区xx | 1234567890124 | 1234ABCD5678EFGH0124
(2 rows)

エミュレータに事業者情報を追加。

1
make idp-add-local
1
2
3
go run cmd/add_local_user/main.go
Successfully created user with email: oem_a@example.com and set custom claims. Password: oemA&user_01
Successfully created user with email: supplier_b@example.com and set custom claims. Password: supplierB&user_01

Makefileの中身は以下の通り。

Makefile:78

1
2
idp-add-local:
go run cmd/add_local_user/main.go

このプログラムは、シード用CSVから事業者情報を登録する。

cmd/add_local_user/main.go:46

1
2
3
4
5
6
7
8
9
10
11
12
func addOperatorFromCSV(ctx context.Context, app *firebase.App, csvPath string) {

(snip)

// create user by each record
for _, record := range records {
email := record[0]
password := record[1]
operatorID := record[2]
operator := Operator{operatorID, email, password}
addOperator(ctx, authClient, operator)
}

cmd/add_local_user/main.go:76

1
2
3
4
5
func addOperator(ctx context.Context, authClient *auth.Client, operator Operator) {

(snip)

err = authClient.SetCustomUserClaims(ctx, userRecord.UID, customClaims)

上記の通り、firebaseのauth.Clientを用いて登録している。

シードは、 cmd/add_local_user/data/seed.csv である。

1
2
oem_a@example.com,oemA&user_01,b39e6248-c888-56ca-d9d0-89de1b1adc8e
supplier_b@example.com,supplierB&user_01,15572d1c-ec13-0d78-7f92-dd4278871373

OperatorのIDは、先程DBとに登録したものと同じ。

データ流通システムの起動

ビルド

1
2
3
cd ${OE_WORKDIR}/data-transaction-system
go build main.go
docker build -t data-spaces-backend .

起動

1
docker run -v $(pwd)/config/:/app/config/ -td -i --network docker.internal --env-file config/local.env -p 8080:8080 --name data-spaces-backend data-spaces-backend

(このあたりはcomposeになっていない…と)

ユーザ認証システムの起動

ビルド

1
2
3
cd ${OE_WORKDIR}/user-authentication-system/
go build main.go
docker build -t authenticator-backend .

起動

1
docker run -v $(pwd)/config/:/app/config/ -td -i --network docker.internal --env-file config/local.env -p 8081:8081 --name authenticator-backend authenticator-backend

これで一通り起動したはず。

1
docker ps
1
2
3
4
5
CONTAINER ID   IMAGE                                  COMMAND                   CREATED          STATUS                  PORTS                                                                                  NAMES
aa12c592f114 authenticator-backend "/app/server" 33 seconds ago Up 33 seconds 0.0.0.0:8081->8081/tcp, :::8081->8081/tcp authenticator-backend
46c1d0f82cf7 data-spaces-backend "/app/server" 3 minutes ago Up 3 minutes 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp data-spaces-backend
7ccba96acee9 postgres:14 "docker-entrypoint.s…" 2 hours ago Up 2 hours 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp postgres
9e5bc7cf81ed user-authentication-system-firebase "docker-entrypoint.s…" 2 hours ago Up 2 hours 0.0.0.0:4000->4000/tcp, :::4000->4000/tcp, 0.0.0.0:9099->9099/tcp, :::9099->9099/tcp user-authentication-system-firebase-1

A社の事業者認証

事業者認証の通り、

A社がユーザ認証システムで事業者認証をした後に、自身の事業者情報を取得する例を示します。

認証情報取得

前提:

  • PF認定を受けた事業者に発行されるAPIキーを指定しAPIを利用する
  • 事業者はAcountIdとPasswordを事前に払い出されている

認証情報を取得

1
2
3
4
curl -s --location --request POST 'http://localhost:8081/auth/login' --header 'Content-Type: application/json' --header 'apiKey: Sample-APIKey1' --data-raw '{
"operatorAccountId": "oem_a@example.com",
"accountPassword": "oemA&user_01"
}' | jq
1
2
3
4
{
"accessToken": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzMzQ1OSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzM0NTksImV4cCI6MTczMDAzNzA1OSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.",
"refreshToken": "eyJfQXV0aEVtdWxhdG9yUmVmcmVzaFRva2VuIjoiRE8gTk9UIE1PRElGWSIsImxvY2FsSWQiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDciLCJwcm92aWRlciI6InBhc3N3b3JkIiwiZXh0cmFDbGFpbXMiOnt9LCJwcm9qZWN0SWQiOiJsb2NhbCJ9"
}

事業者情報取得

先程取得したアクセストークンを使い、事業者情報を取得してみる。 先の例と同様、APIキーも用いる。

1
2
3
4
curl -s --location --request GET 'http://localhost:8081/api/v1/authInfo?dataTarget=operator' \
--header 'apiKey: Sample-APIKey1' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzMzQ1OSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzM0NTksImV4cCI6MTczMDAzNzA1OSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.' \
| jq
1
2
3
4
5
6
7
8
9
{
"operatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"operatorName": "A社",
"operatorAddress": "東京都渋谷区xx",
"openOperatorId": "1234567890123",
"operatorAttribute": {
"globalOperatorId": "1234ABCD5678EFGH0123"
}
}

A社の部品登録およびB社への回答依頼

部品登録 にある通り。 事業所登録、親部品情報の作成、部品構成情報の登録、CFP結果提出の依頼(A社=下流・OEMからB社=上流・サプライヤへの依頼)の順番で進める。

事業所登録

この時点では、事「業」所が登録されていないので登録する。 operatorId は先程取得した事業者情報の値を用いる。openPlantIdplantAddressplantNaeは任意の値。 plantIdはNullで渡す。

1
2
3
4
5
6
7
8
9
10
11
12
13
curl -s --location --request PUT 'http://localhost:8081/api/v1/authInfo?dataTarget=plant' \
--header 'apiKey: Sample-APIKey1' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzMzQ1OSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzM0NTksImV4cCI6MTczMDAzNzA1OSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.' \
--data '{
"openPlantId": "1234567890123012345",
"operatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"plantAddress": "xx県xx市xxxx町1-1-1234",
"plantId": null,
"plantName": "A工場",
"plantAttribute": {}
}' \
| jq
1
2
3
4
5
6
7
8
9
10
{
"plantId": "4c110e31-46a3-48d9-89d3-afdefcfcb7dc",
"operatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"plantName": "A工場",
"plantAddress": "xx県xx市xxxx町1-1-1234",
"openPlantId": "1234567890123012345",
"plantAttribute": {
"globalPlantId": null
}
}

結果中の、plantIdが埋まっていることがわかる。

また、APIが/v1/authInfoであることも注意。

親部品情報の作成

事業所を登録できたので、その情報を使って親部品情報を登録する。 plantIdは先程の戻り値に含まれていたものを利用。 traceIdはNullで渡す。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
curl -s --location --request PUT 'http://localhost:8080/api/v1/datatransport?dataTarget=parts' \
--header 'apiKey: Sample-APIKey1' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzMzQ1OSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzM0NTksImV4cCI6MTczMDAzNzA1OSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.' \
--data '{
"amountRequired": null,
"amountRequiredUnit": "kilogram",
"operatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"partsName": "部品A",
"plantId": "4c110e31-46a3-48d9-89d3-afdefcfcb7dc",
"supportPartsName": "modelA",
"terminatedFlag": false,
"traceId": null
}' \
| jq
1
2
3
4
5
6
7
8
9
10
{
"traceId": "675b19a3-380e-45d6-921f-38aad289e0d5",
"operatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"plantId": "4c110e31-46a3-48d9-89d3-afdefcfcb7dc",
"partsName": "部品A",
"supportPartsName": "modelA",
"terminatedFlag": false,
"amountRequired": null,
"amountRequiredUnit": "kilogram"
}

traceIdが含まれた値が返ってきた。

部品構成情報の登録

親部品の登録が終わり、親部品のtraceIdも払い出されたので、その情報を使って、子部品を登録する。 この時点で子部品のtraceIdはNullだが、戻り値に含まれていることがわかる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
curl -s --location --request PUT 'http://localhost:8080/api/v1/datatransport?dataTarget=partsStructure' \
--header 'apiKey: Sample-APIKey1' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzMzQ1OSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzM0NTksImV4cCI6MTczMDAzNzA1OSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.' \
--data '{
"parentPartsModel": {
"amountRequired": null,
"amountRequiredUnit": "kilogram",
"operatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"partsName": "部品A",
"plantId": "4c110e31-46a3-48d9-89d3-afdefcfcb7dc",
"supportPartsName": "modelA",
"terminatedFlag": false,
"traceId": "8fc6aa29-5f4f-476e-85e3-2d1b54715891"
},
"childrenPartsModel": [
{
"amountRequired": 5,
"amountRequiredUnit": "kilogram",
"operatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"partsName": "部品A1",
"plantId": "4c110e31-46a3-48d9-89d3-afdefcfcb7dc",
"supportPartsName": "modelA-1",
"terminatedFlag": false,
"traceId": null
}
]
}' \
| jq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"parentPartsModel": {
"traceId": "8fc6aa29-5f4f-476e-85e3-2d1b54715891",
"operatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"plantId": "4c110e31-46a3-48d9-89d3-afdefcfcb7dc",
"partsName": "部品A",
"supportPartsName": "modelA",
"terminatedFlag": false,
"amountRequired": null,
"amountRequiredUnit": "kilogram"
},
"childrenPartsModel": [
{
"traceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"operatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"plantId": "4c110e31-46a3-48d9-89d3-afdefcfcb7dc",
"partsName": "部品A1",
"supportPartsName": "modelA-1",
"terminatedFlag": false,
"amountRequired": 5,
"amountRequiredUnit": "kilogram"
}
]
}

CFP結果提出の依頼(AからBに)

/v1/authInfo APIを用いて、まずはB社の事業者識別子(内部)を取得する。 openOperatorId (事業者識別子(公開))は事前に知っているものとする。

1
2
3
4
curl -s --location --request GET 'http://localhost:8081/api/v1/authInfo?dataTarget=operator&openOperatorId=1234567890124' \
--header 'apiKey: Sample-APIKey1' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzMzQ1OSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzM0NTksImV4cCI6MTczMDAzNzA1OSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.' \
| jq
1
2
3
4
5
6
7
8
9
{
"operatorId": "15572d1c-ec13-0d78-7f92-dd4278871373",
"operatorName": "B社",
"operatorAddress": "東京都渋谷区xx",
"openOperatorId": "1234567890124",
"operatorAttribute": {
"globalOperatorId": "1234ABCD5678EFGH0124"
}
}

続いて、A社からB社に対しての取引関係を作成し、紐付け回答依頼を行う。 downstreamTraceIdには先に取得しておいた、下流(つまり、A社 = OEM側)のもつtraceIdを指定する。先ほど作った親子関係のうち、子部品(つまり、上流由来の部品)のIDを指定する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
curl -s --location --request PUT 'http://localhost:8080/api/v1/datatransport?dataTarget=tradeRequest' \
--header 'apiKey: Sample-APIKey1' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzMzQ1OSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzM0NTksImV4cCI6MTczMDAzNzA1OSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.' \
--data '{
"statusModel": {
"message": "来月中にご回答をお願いします。",
"replyMessage": null,
"requestStatus": {},
"requestType": "CFP",
"statusId": null,
"tradeId": null
},
"tradeModel": {
"downstreamOperatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"downstreamTraceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"tradeId": null,
"upstreamOperatorId": "15572d1c-ec13-0d78-7f92-dd4278871373",
"upstreamTraceId": null
}
}' \
| jq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"tradeModel": {
"tradeId": "aeb60293-2477-4f58-94ac-8b24cb32103f",
"downstreamOperatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"upstreamOperatorId": "15572d1c-ec13-0d78-7f92-dd4278871373",
"downstreamTraceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"upstreamTraceId": null
},
"statusModel": {
"statusId": "a6e4a7f2-bb5c-4414-8026-75a46ea4436d",
"tradeId": "aeb60293-2477-4f58-94ac-8b24cb32103f",
"requestStatus": {
"cfpResponseStatus": "NOT_COMPLETED",
"tradeTreeStatus": "UNTERMINATED"
},
"message": "来月中にご回答をお願いします。",
"replyMessage": null,
"requestType": "CFP"
}
}

リクエスト時はNullだった、statusIdtradeIdが返ってきていることがわかる。(traceIdtradeIdが紛らわしいので注意)

B社の部品登録紐付け

部品登録紐付けの通り。 おおむね、B社の事業者認証(アクセストークン取得)、B社の事業所情報の登録、B社親部品登録、紐付け以来の確認、部品登録紐付けの登録、の流れ。

事業社認証

A社のときと同じく、B社においても認証情報を取得する。

1
2
3
4
5
6
7
8
curl -s --location --request POST 'http://localhost:8081/auth/login' \
--header 'Content-Type: application/json' \
--header 'apiKey: Sample-APIKey1' \
--data-raw '{
"operatorAccountId": "supplier_b@example.com",
"accountPassword": "supplierB&user_01"
}' \
| jq
1
2
3
4
{
"accessToken": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6IjE1NTcyZDFjLWVjMTMtMGQ3OC03ZjkyLWRkNDI3ODg3MTM3MyIsImVtYWlsIjoic3VwcGxpZXJfYkBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYXV0aF90aW1lIjoxNzMwMDM1ODMwLCJ1c2VyX2lkIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3IiwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJzdXBwbGllcl9iQGV4YW1wbGUuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifSwiaWF0IjoxNzMwMDM1ODMwLCJleHAiOjE3MzAwMzk0MzAsImF1ZCI6ImxvY2FsIiwiaXNzIjoiaHR0cHM6Ly9zZWN1cmV0b2tlbi5nb29nbGUuY29tL2xvY2FsIiwic3ViIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3In0.",
"refreshToken": "eyJfQXV0aEVtdWxhdG9yUmVmcmVzaFRva2VuIjoiRE8gTk9UIE1PRElGWSIsImxvY2FsSWQiOiJjN2FlOGYzZS1jZDhjLTRkOTUtOGE2NC02ZjRjYjJlZGQxYTciLCJwcm92aWRlciI6InBhc3N3b3JkIiwiZXh0cmFDbGFpbXMiOnt9LCJwcm9qZWN0SWQiOiJsb2NhbCJ9"
}

事業所の登録

A社のときと同じく、B社においても事業「所」情報を登録する。

1
2
3
4
5
6
7
8
9
10
11
12
13
curl -s --location --request PUT 'http://localhost:8081/api/v1/authInfo?dataTarget=plant' \
--header 'apiKey: Sample-APIKey1' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6IjE1NTcyZDFjLWVjMTMtMGQ3OC03ZjkyLWRkNDI3ODg3MTM3MyIsImVtYWlsIjoic3VwcGxpZXJfYkBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYXV0aF90aW1lIjoxNzMwMDM1ODMwLCJ1c2VyX2lkIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3IiwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJzdXBwbGllcl9iQGV4YW1wbGUuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifSwiaWF0IjoxNzMwMDM1ODMwLCJleHAiOjE3MzAwMzk0MzAsImF1ZCI6ImxvY2FsIiwiaXNzIjoiaHR0cHM6Ly9zZWN1cmV0b2tlbi5nb29nbGUuY29tL2xvY2FsIiwic3ViIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3In0.' \
--data '{
"openPlantId": "1234567890124012345",
"operatorId": "15572d1c-ec13-0d78-7f92-dd4278871373",
"plantAddress": "xx県xx市xxxx町2-1-1234",
"plantId": null,
"plantName": "B工場",
"plantAttribute": {}
}' \
| jq
1
2
3
4
5
6
7
8
9
10
{
"plantId": "2dbe5ede-0140-4a47-8bf7-3ab3947866c7",
"operatorId": "15572d1c-ec13-0d78-7f92-dd4278871373",
"plantName": "B工場",
"plantAddress": "xx県xx市xxxx町2-1-1234",
"openPlantId": "1234567890124012345",
"plantAttribute": {
"globalPlantId": null
}
}

リクエストのときはNullだった、plantIdが返っていることがわかる。

親部品情報の作成

A社のときと同じく、親部品情報を作成する。 認証トークンとplantIdは先程取得したものを用いること。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
curl -s --location --request PUT 'http://localhost:8080/api/v1/datatransport?dataTarget=parts' \
--header 'apiKey: Sample-APIKey1' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6IjE1NTcyZDFjLWVjMTMtMGQ3OC03ZjkyLWRkNDI3ODg3MTM3MyIsImVtYWlsIjoic3VwcGxpZXJfYkBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYXV0aF90aW1lIjoxNzMwMDM1ODMwLCJ1c2VyX2lkIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3IiwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJzdXBwbGllcl9iQGV4YW1wbGUuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifSwiaWF0IjoxNzMwMDM1ODMwLCJleHAiOjE3MzAwMzk0MzAsImF1ZCI6ImxvY2FsIiwiaXNzIjoiaHR0cHM6Ly9zZWN1cmV0b2tlbi5nb29nbGUuY29tL2xvY2FsIiwic3ViIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3In0.' \
--data '{
"amountRequired": null,
"amountRequiredUnit": "kilogram",
"operatorId": "15572d1c-ec13-0d78-7f92-dd4278871373",
"partsName": "部品B",
"plantId": "2dbe5ede-0140-4a47-8bf7-3ab3947866c7",
"supportPartsName": "modelB",
"terminatedFlag": true,
"traceId": null
}' \
| jq
1
2
3
4
5
6
7
8
9
10
{
"traceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f",
"operatorId": "15572d1c-ec13-0d78-7f92-dd4278871373",
"plantId": "2dbe5ede-0140-4a47-8bf7-3ab3947866c7",
"partsName": "部品B",
"supportPartsName": "modelB",
"terminatedFlag": true,
"amountRequired": null,
"amountRequiredUnit": "kilogram"
}

リクエスト時はNullだったtraceIdが返っていることがわかる。

紐付け依頼確認

今回は、予めA社(OEM、下流)から回答依頼を送っているのだが、改めてB社(サプライヤ、上流)側で依頼を確認する。

1
2
3
4
curl -s --location --request GET 'http://localhost:8080/api/v1/datatransport?dataTarget=tradeResponse' \
--header 'apiKey: Sample-APIKey1' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6IjE1NTcyZDFjLWVjMTMtMGQ3OC03ZjkyLWRkNDI3ODg3MTM3MyIsImVtYWlsIjoic3VwcGxpZXJfYkBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYXV0aF90aW1lIjoxNzMwMDM1ODMwLCJ1c2VyX2lkIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3IiwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJzdXBwbGllcl9iQGV4YW1wbGUuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifSwiaWF0IjoxNzMwMDM1ODMwLCJleHAiOjE3MzAwMzk0MzAsImF1ZCI6ImxvY2FsIiwiaXNzIjoiaHR0cHM6Ly9zZWN1cmV0b2tlbi5nb29nbGUuY29tL2xvY2FsIiwic3ViIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3In0.' \
| jq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[
{
"statusModel": {
"statusId": "a6e4a7f2-bb5c-4414-8026-75a46ea4436d",
"tradeId": "aeb60293-2477-4f58-94ac-8b24cb32103f",
"requestStatus": {
"cfpResponseStatus": "NOT_COMPLETED",
"tradeTreeStatus": "UNTERMINATED"
},
"message": "来月中にご回答をお願いします。",
"replyMessage": null,
"requestType": "CFP"
},
"tradeModel": {
"tradeId": "aeb60293-2477-4f58-94ac-8b24cb32103f",
"downstreamOperatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"upstreamOperatorId": "15572d1c-ec13-0d78-7f92-dd4278871373",
"downstreamTraceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"upstreamTraceId": null
},
"partsModel": {
"traceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"operatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"plantId": "4c110e31-46a3-48d9-89d3-afdefcfcb7dc",
"partsName": "部品A",
"supportPartsName": "modelA",
"terminatedFlag": false,
"amountRequired": null,
"amountRequiredUnit": "kilogram"
}
}
]

statusIdtradeIdが先程A社側で見たものと同じであることがわかる。

部品登録紐付けの登録

依頼に回答する。APIのパラメータtradeIdtraceIdには、先程取得した依頼回答にあった値を用いる。つまり、traddeIdは依頼回答一覧似合ったものを用いる。traceIdには、先程B社側で作成した親部品情報を用いる。(A社側の値ではないことに注意)

1
2
3
4
curl -s --location --request PUT 'http://localhost:8080/api/v1/datatransport?dataTarget=tradeResponse&tradeId=aeb60293-2477-4f58-94ac-8b24cb32103f&traceId=28410c0f-1796-4d1b-b30b-db4aa4b8d28f' \
--header 'apiKey: Sample-APIKey1' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6IjE1NTcyZDFjLWVjMTMtMGQ3OC03ZjkyLWRkNDI3ODg3MTM3MyIsImVtYWlsIjoic3VwcGxpZXJfYkBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYXV0aF90aW1lIjoxNzMwMDM1ODMwLCJ1c2VyX2lkIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3IiwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJzdXBwbGllcl9iQGV4YW1wbGUuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifSwiaWF0IjoxNzMwMDM1ODMwLCJleHAiOjE3MzAwMzk0MzAsImF1ZCI6ImxvY2FsIiwiaXNzIjoiaHR0cHM6Ly9zZWN1cmV0b2tlbi5nb29nbGUuY29tL2xvY2FsIiwic3ViIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3In0.' \
| jq
1
2
3
4
5
6
7
{
"tradeId": "aeb60293-2477-4f58-94ac-8b24cb32103f",
"downstreamOperatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"upstreamOperatorId": "15572d1c-ec13-0d78-7f92-dd4278871373",
"downstreamTraceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"upstreamTraceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f"
}

以上で、A社とB社の間で異なる部品情報を持ったまま、それらをつなぐ関係情報が作成された。

B社のCFP情報の伝達

CFP情報伝達の通り。B社が登録する。 手順書に記載があるが、ここで登録するCFP値は、アプリケーション側で自社の利用する算出されたものを登録する、という前提になっている。

traceIdには、先程作成したB社側の親部品情報の値を用いる。

なお、本来の実装では、この作業を実施後に、A社に対してCFP情報、DQR情報を開示する手順があるのだが、本記事で紹介している手順では自動で許可されるようになっている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
curl -s --location --request PUT 'http://localhost:8080/api/v1/datatransport?dataTarget=cfp' \
--header 'apiKey: Sample-APIKey1' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6IjE1NTcyZDFjLWVjMTMtMGQ3OC03ZjkyLWRkNDI3ODg3MTM3MyIsImVtYWlsIjoic3VwcGxpZXJfYkBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYXV0aF90aW1lIjoxNzMwMDM1ODMwLCJ1c2VyX2lkIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3IiwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJzdXBwbGllcl9iQGV4YW1wbGUuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifSwiaWF0IjoxNzMwMDM1ODMwLCJleHAiOjE3MzAwMzk0MzAsImF1ZCI6ImxvY2FsIiwiaXNzIjoiaHR0cHM6Ly9zZWN1cmV0b2tlbi5nb29nbGUuY29tL2xvY2FsIiwic3ViIjoiYzdhZThmM2UtY2Q4Yy00ZDk1LThhNjQtNmY0Y2IyZWRkMWE3In0.' \
--data '[
{
"cfpId": null,
"traceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f",
"ghgEmission": 1.5,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "preProduction",
"dqrType": "preProcessing",
"dqrValue": {
"TeR": 1,
"GeR": 2,
"TiR": 3
}
},
{
"cfpId": null,
"traceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f",
"ghgEmission": 10.0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "mainProduction",
"dqrType": "mainProcessing",
"dqrValue": {
"TeR": 2,
"GeR": 3,
"TiR": 4
}
},
{
"cfpId": null,
"traceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f",
"ghgEmission": 0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "preComponent",
"dqrType": "preProcessing",
"dqrValue": {
"TeR": 1,
"GeR": 2,
"TiR": 3
}
},
{
"cfpId": null,
"traceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f",
"ghgEmission": 0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "mainComponent",
"dqrType": "mainProcessing",
"dqrValue": {
"TeR": 2,
"GeR": 3,
"TiR": 4
}
}
]' \
| jq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
[
{
"cfpId": "2b289894-846f-4563-916f-0c238aa5216f",
"traceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f",
"ghgEmission": 1.5,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "preProduction",
"dqrType": "preProcessing",
"dqrValue": {
"TeR": 1,
"GeR": 2,
"TiR": 3
}
},
{
"cfpId": "2b289894-846f-4563-916f-0c238aa5216f",
"traceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f",
"ghgEmission": 10,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "mainProduction",
"dqrType": "mainProcessing",
"dqrValue": {
"TeR": 2,
"GeR": 3,
"TiR": 4
}
},
{
"cfpId": "2b289894-846f-4563-916f-0c238aa5216f",
"traceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f",
"ghgEmission": 0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "preComponent",
"dqrType": "preProcessing",
"dqrValue": {
"TeR": 1,
"GeR": 2,
"TiR": 3
}
},
{
"cfpId": "2b289894-846f-4563-916f-0c238aa5216f",
"traceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f",
"ghgEmission": 0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "mainComponent",
"dqrType": "mainProcessing",
"dqrValue": {
"TeR": 2,
"GeR": 3,
"TiR": 4
}
}
]

A社における回答取得

回答取得 の通り。 A社側で、B社の回答を取得する。

回答依頼情報の取得

タイムアウトしてトークンが無効になっているかもしれないので、認証情報を取得

1
2
3
4
curl -s --location --request POST 'http://localhost:8081/auth/login' --header 'Content-Type: application/json' --header 'apiKey: Sample-APIKey1' --data-raw '{
"operatorAccountId": "oem_a@example.com",
"accountPassword": "oemA&user_01"
}' | jq
1
2
3
4
{
"accessToken": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzODQ1NSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzg0NTUsImV4cCI6MTczMDA0MjA1NSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.",
"refreshToken": "eyJfQXV0aEVtdWxhdG9yUmVmcmVzaFRva2VuIjoiRE8gTk9UIE1PRElGWSIsImxvY2FsSWQiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDciLCJwcm92aWRlciI6InBhc3N3b3JkIiwiZXh0cmFDbGFpbXMiOnt9LCJwcm9qZWN0SWQiOiJsb2NhbCJ9"
}

取得したアクセストークンを使いつつ、回答情報を取得。

1
2
3
4
curl -s --location --request GET 'http://localhost:8080/api/v1/datatransport?dataTarget=tradeRequest' \
--header 'apiKey: Sample-APIKey1' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzODQ1NSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzg0NTUsImV4cCI6MTczMDA0MjA1NSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.' \
| jq
1
2
3
4
5
6
7
8
9
[
{
"tradeId": "aeb60293-2477-4f58-94ac-8b24cb32103f",
"downstreamOperatorId": "b39e6248-c888-56ca-d9d0-89de1b1adc8e",
"upstreamOperatorId": "15572d1c-ec13-0d78-7f92-dd4278871373",
"downstreamTraceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"upstreamTraceId": "28410c0f-1796-4d1b-b30b-db4aa4b8d28f"
}
]

依頼情報のステータス確認

  • statusTarget=REQUEST ... A社が依頼元のステータスのみ取得
  • traceId ... A社がB社に依頼している自社のtraceId(子部品のID)を指定する。
1
2
3
4
curl -s --location --request GET 'http://localhost:8080/api/v1/datatransport?dataTarget=status&statusTarget=REQUEST&traceId=c6e15178-90ed-4605-ad69-2abebae7d18a' \
--header 'apiKey: Sample-APIKey1' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzODQ1NSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzg0NTUsImV4cCI6MTczMDA0MjA1NSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.' \
| jq
1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
"statusId": "a6e4a7f2-bb5c-4414-8026-75a46ea4436d",
"tradeId": "aeb60293-2477-4f58-94ac-8b24cb32103f",
"requestStatus": {
"cfpResponseStatus": "COMPLETED",
"tradeTreeStatus": "TERMINATED"
},
"message": "来月中にご回答をお願いします。",
"replyMessage": null,
"requestType": "CFP"
}
]

A社において完成品のCFP値を算出する

完成品CFP値の通り。 本来は、アプリケーションにてCFPを計算する。 traceIdには、A社側の子部品のtraceIdを指定する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
curl -s --location --request PUT 'http://localhost:8080/api/v1/datatransport?dataTarget=cfp' \
--header 'apiKey: Sample-APIKey1' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvcGVyYXRvcl9pZCI6ImIzOWU2MjQ4LWM4ODgtNTZjYS1kOWQwLTg5ZGUxYjFhZGM4ZSIsImVtYWlsIjoib2VtX2FAZXhhbXBsZS5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTczMDAzODQ1NSwidXNlcl9pZCI6IjhmMDVmZWY1LWE3ZGEtNGVjMS1iY2FjLTMyNTIwMzM0ODVkNyIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsib2VtX2FAZXhhbXBsZS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MzAwMzg0NTUsImV4cCI6MTczMDA0MjA1NSwiYXVkIjoibG9jYWwiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbG9jYWwiLCJzdWIiOiI4ZjA1ZmVmNS1hN2RhLTRlYzEtYmNhYy0zMjUyMDMzNDg1ZDcifQ.' \
--data '[
{
"cfpId": null,
"traceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"ghgEmission": 3.0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "preProduction",
"dqrType": "preProcessing",
"dqrValue": {
"TeR": 1,
"GeR": 2,
"TiR": 3
}
},
{
"cfpId": null,
"traceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"ghgEmission": 20.0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "mainProduction",
"dqrType": "mainProcessing",
"dqrValue": {
"TeR": 2,
"GeR": 3,
"TiR": 4
}
},
{
"cfpId": null,
"traceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"ghgEmission": 0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "preComponent",
"dqrType": "preProcessing",
"dqrValue": {
"TeR": 1,
"GeR": 2,
"TiR": 3
}
},
{
"cfpId": null,
"traceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"ghgEmission": 0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "mainComponent",
"dqrType": "mainProcessing",
"dqrValue": {
"TeR": 2,
"GeR": 3,
"TiR": 4
}
}
]' \
| jq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
[
{
"cfpId": "b626d92e-e6e3-4769-98d1-1ff8b6714230",
"traceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"ghgEmission": 3,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "preProduction",
"dqrType": "preProcessing",
"dqrValue": {
"TeR": 1,
"GeR": 2,
"TiR": 3
}
},
{
"cfpId": "b626d92e-e6e3-4769-98d1-1ff8b6714230",
"traceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"ghgEmission": 20,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "mainProduction",
"dqrType": "mainProcessing",
"dqrValue": {
"TeR": 2,
"GeR": 3,
"TiR": 4
}
},
{
"cfpId": "b626d92e-e6e3-4769-98d1-1ff8b6714230",
"traceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"ghgEmission": 0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "preComponent",
"dqrType": "preProcessing",
"dqrValue": {
"TeR": 1,
"GeR": 2,
"TiR": 3
}
},
{
"cfpId": "b626d92e-e6e3-4769-98d1-1ff8b6714230",
"traceId": "c6e15178-90ed-4605-ad69-2abebae7d18a",
"ghgEmission": 0,
"ghgDeclaredUnit": "kgCO2e/kilogram",
"cfpType": "mainComponent",
"dqrType": "mainProcessing",
"dqrValue": {
"TeR": 2,
"GeR": 3,
"TiR": 4
}
}
]

(wip)

データ流通システムの様子

データ流通システムの main.go を見ると概要がわかる。

このアプリケーションは、Echoを使って作られている。

Routerの様子

APIは単純で、/api/v1/datatransportのGETとPUTのみ。

presentation/http/echo/router/router.go:39

1
2
e.GET("/api/v1/datatransport", func(c echo.Context) error { return h.GetOuranos(c) })
e.PUT("/api/v1/datatransport", func(c echo.Context) error { return h.PutOuranos(c) })

部品情報登録のハンドラー

hhandler.AppHandlerなのだが、以下の通り。

1
2
3
4
5
6
// appHandler
// Summary: This is structure which defines appHandler.
type appHandler struct {
handler.AuthHandler
handler.OuranosHandler
}

AuthHandlerは名前の通り、認証関連。APIキーやトークンの処理。

presentation/http/echo/handler/auth_handler.go:10

1
2
3
4
AuthHandler interface {
VerifyAPIKey(c echo.Context) error
VerifyToken(c echo.Context) (*string, error)
}

OuranosHandlerがデータのやり取りのためのハンドラーであり、GET/PUTを扱う。 ハンドラーの定義は以下の通り。

presentation/http/echo/handler/ouranos_handler.go:5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type (
OuranosHandler interface {
GetOuranos(c echo.Context) error
PutOuranos(c echo.Context) error
}

ouranosHandler struct {
cfpHandler ICfpHandler
cfpCertificationHandler ICfpCertificationHandler
partsHandler IPartsHandler
partsStructureHandler IPartsStructureHandler
tradeHandler ITradeHandler
statusHandler IStatusHandler
}
)

PutOuranosを見てみる。

presentation/http/echo/handler/ouranos_put.go:15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (h *ouranosHandler) PutOuranos(c echo.Context) error {
method := c.Request().Method
operatorID := c.Get("operatorID").(string)

dataTarget := c.QueryParam("dataTarget")

switch dataTarget {
case "partsStructure":
return h.partsStructureHandler.PutPartsStructureModel(c)
case "parts":
return h.partsHandler.PutPartsModel(c)
case "tradeRequest":
return h.tradeHandler.PutTradeRequest(c)
case "tradeResponse":
return h.tradeHandler.PutTradeResponse(c)
case "cfp":
return h.cfpHandler.PutCfp(c)
case "status":
return h.statusHandler.PutStatus(c)
default:
errDetails := common.UnexpectedQueryParameter("dataTarget")
return echo.NewHTTPError(common.HTTPErrorGenerate(http.StatusBadRequest, common.HTTPErrorSourceDataspace, common.Err400InvalidRequest, operatorID, dataTarget, method, errDetails))
}
}

dataTargetの中身によって処理を変える。 例えば、親部品作成時には、partsが呼ばれていた。 インターフェース定義は以下。

presentation/http/echo/handler/ouranos_traceability_parts.go:18

1
2
3
4
5
6
7
8
type IPartsHandler interface {
// GetPartsModel
// Summary: This is function which defines #8 GetPartsList.
GetPartsModel(c echo.Context) error
// PutPartsModel
// Summary: This is function which defines #5 PutPartsItem.
PutPartsModel(c echo.Context) error
}

PutPartsModelの実装が以下。

presentation/http/echo/handler/ouranos_traceability_parts.go:144

1
2
3
4
5
6
7
8
func (h *partsHandler) PutPartsModel(c echo.Context) error {

dataTarget := c.QueryParam("dataTarget")
method := c.Request().Method

operatorID := c.Get("operatorID").(string)

(snip)

最初にoperatorIDを取得している。

続いて入力を取得。

presentation/http/echo/handler/ouranos_traceability_parts.go:151

1
2
3
4
5
6
7
if err := c.Bind(&putPartsInput); err != nil {
logger.Set(c).Warnf(err.Error())
errDetails := common.FormatBindErrMsg(err)

return echo.NewHTTPError(common.HTTPErrorGenerate(http.StatusBadRequest, common.HTTPErrorSourceDataspace, common.Err400Validation, operatorID, dataTarget, method, errDetails))
}
fmt.Printf("PutPartsInput: %+v\n", putPartsInput)

domain/model/traceability/ouranos_parts_model.go:138

1
2
3
4
5
6
7
8
9
10
type PutPartsInput struct {
OperatorID string `json:"operatorId"`
TraceID *string `json:"traceId"`
PlantID string `json:"plantId"`
PartsName string `json:"partsName"`
SupportPartsName *string `json:"supportPartsName"`
TerminatedFlag *bool `json:"terminatedFlag"`
AmountRequired *float64 `json:"amountRequired"`
AmountRequiredUnit *string `json:"amountRequiredUnit"`
}

バリデーション。

presentation/http/echo/handler/ouranos_traceability_parts.go:159

1
2
3
4
5
6
7
8
9
10
11
if err := putPartsInput.Validate(); err != nil {
logger.Set(c).Warnf(err.Error())
errDetails := err.Error()

return echo.NewHTTPError(common.HTTPErrorGenerate(http.StatusBadRequest, common.HTTPErrorSourceDataspace, common.Err400Validation, operatorID, dataTarget, method, errDetails))
}
if operatorID != putPartsInput.OperatorID {
logger.Set(c).Warnf(common.Err403AccessDenied)

return echo.NewHTTPError(common.HTTPErrorGenerate(http.StatusForbidden, common.HTTPErrorSourceDataspace, common.Err403AccessDenied, operatorID, dataTarget, method))
}

パーツのモデルを作成。

1
2
3
4
5
6
7
8
9
10
11
partsModel, err := putPartsInput.ToModel()
if err != nil {
logger.Set(c).Warnf(err.Error())
errDetails := err.Error()

return echo.NewHTTPError(common.HTTPErrorGenerate(http.StatusBadRequest, common.HTTPErrorSourceDataspace, common.Err400Validation, operatorID, dataTarget, method, errDetails))
}
var partsStructureModel = traceability.PartsStructureModel{
ParentPartsModel: &partsModel,
}
fmt.Printf("PutParts: PartsStructureModel: %+v\n", partsStructureModel)

パーツモデルをPut。

presentation/http/echo/handler/ouranos_traceability_parts.go:183

1
res, err := h.partsStructureUsecase.PutPartsStructure(c, partsStructureModel)

PUtPartsStructureの実装のひとつは以下。

usecase/parts_structure_usecase_datastore_impl.go:55

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (u *partsStructureUsecase) PutPartsStructure(c echo.Context, partsStructureModel traceability.PartsStructureModel) (traceability.PartsStructureModel, error) {

partsStructure, err := u.OuranosRepository.PutPartsStructure(partsStructureModel)
if err != nil {
logger.Set(c).Errorf(err.Error())
return traceability.PartsStructureModel{}, err
}
partsStructureModels, err := partsStructure.ToModel()
if err != nil {
logger.Set(c).Errorf(err.Error())

return traceability.PartsStructureModel{}, err
}
return partsStructureModels, nil
}

もうひとつは、

usecase/parts_structure_usecase_traceability_impl.go:61

1
2
3
func (u *partsStructureTraceabilityUsecase) PutPartsStructure(c echo.Context, partsStructureModel traceability.PartsStructureModel) (traceability.PartsStructureModel, error) {

(snip)

参考

共有

memo of CADDE

メモ

CADDE-sip/connector のREADMEを読んでメモする。

前提

CADDE-sip/connector/README 前提条件 に色々書いてある。

1
カタログ検索、データ交換、認証、認可、来歴、契約の機能を具備します。

これらの機能を利用するためのAPIを具備している、くらいの読み方の方が良いかも。

導入ガイドライン

CADDE-sip/connector/README 導入ガイドライン にドキュメントの置き場所が書いてある。 このあたりは、先に読んでおいたほうが良いだろう。

利用者コネクタ

CADDE-sip/connector/README 利用者コネクタの構築手順 の通り、クローンしたレポジトリの src/consumer/ 内に 利用者コネクタのソースコードがある。

アーキテクチャの概要は以下に載っている。

CADDE-sip/connector/README 利用者アーキテクチャ図

利用者目線でのざっくりとした理解

上記図の、コネクタメイン(利用者側)の左側に、カタログ検索I/Fとデータ取得I/Fがあるが、 これが利用者絡みたときの接点となるI/Fである。

コネクタイメインのSwagger定義で言えば以下。src/consumer/connector-main/swagger_server/swagger/swagger.yaml

カタログ検索 paths./cadde/api/v4/catalog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  /cadde/api/v4/catalog:
get:
tags:
- Search
summary: API. カタログ検索
description: |-
カタログ情報を取得する。APIユーザが指定した条件に従いカタログ情報を取得する。

Response:
* 処理が成功した場合は200を返す。
* 処理に失敗した場合は、2xx以外のコードを返す。 Responsesセクション参照。
operationId: search

(snip)

データ取得 paths./cadde/api/v4/file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  /cadde/api/v4/file:
get:
tags:
- Files
summary: API. データ取得(CADDE)
description: |-
リソースURLに指定されたデータを取得する。

Response:
* 処理が成功した場合は200を返す。
* 処理に失敗した場合は、2xx以外のコードを返す。 Responsesセクションを参照。
operationId: files

(snip)

Swaggerの定義に基づき、PythonのFlask使って作られている。

ロケーション情報 

手順では src/consumer/connector-main/swagger_server/configs/location.json を編集することで、 提供者コネクタのアドレス設定をするように書かれている。

このファイルは以下のような内容。

1
2
3
4
5
6
7
8
9
10
{
"connector_location": {
"test-provider:catalog": {
"provider_connector_url": "http://10.0.4.71:28080"
},
"test-provider:data": {
"provider_connector_url": "http://10.0.4.71:38080"
}
}
}

あるデータ提供者コネクタのカタログAPI、データAPI(?)のURLが記載されている。

このファイルがどこで利用されるかというと、サービス定義内の以下。

src/consumer/connector-main/swagger_server/services/service.py:384

1
def __get_location_info(provider, location_service_url, external_interface) -> (str):

ロケーションサービスから取得ができなかった場合、 location.jsonからコンフィグ情報を取得する。

とある通り、まずロケーションサービスから情報を取得するようにしレスポンスがあったらリターン。

src/consumer/connector-main/swagger_server/services/service.py:418

1
2
3
4
5
6
if response:
if response.endswith('/'):
response = response[:-1]
provider_connector_url = response

return provider_connector_url

だめな場合は、コンフィグから読み込む。

src/consumer/connector-main/swagger_server/services/service.py:425

1
2
3
4
5
6
# コンフィグから再取得を試みる
logger.info(f'Not Found {provider} from {location_service_url}')
try:
config = internal_interface.config_read(__CONFIG_LOCATION_FILE_PATH)
except Exception: # pathミス
raise CaddeException(message_id='020000003E')

この __get_location_info 関数は2箇所で利用されている。

1
2
3
__get_location_info(provider, location_service_url, external_interface)
catalog_search(query_string, search, provider, authorization, external_interface=ExternalInterface())
fetch_data(authorization, resource_url, resource_api_type, provider, options, external_interface=ExternalInterface())

ひとつがカタログ検索内。

src/consumer/connector-main/swagger_server/services/service.py:42

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def catalog_search(
query_string: str,
search: str,
provider: str,
authorization: str,
external_interface: ExternalInterface = ExternalInterface()) -> Response:
"""
カタログ検索I/Fに、カタログ検索を行い、検索結果を返す

Args:
query_string str : クエリストリング
search str : 検索種別
provider str: CADDEユーザID(提供者)
authorization str: 利用者トークン
external_interface ExternalInterface : GETリクエストを行うインタフェース

Returns:
Response : 取得した情報

Raises:
Cadde_excption: 検索種別確認時に不正な値が設定されている場合 エラーコード: 020001002E
Cadde_excption: 認証I/F 認証トークン検証処理でエラーが発生した場合 エラーコード: 020001003E
Cadde_excption: 提供者コネクタURLの取得に失敗した場合 エラーコード: 020001004E
Cadde_excption: カタログ検索I/Fのカタログ検索処理でエラーが発生した場合 エラーコード: 020001005E

"""
(snip)

カタログ検索機能を利用する際、もし詳細検索であれば、ロケーション情報を取得する。

src/consumer/connector-main/swagger_server/services/service.py:99

1
2
3
4
5
if search == 'detail':
provider_connector_url = __get_location_info(
provider, location_service_url, external_interface)
if not provider_connector_url:
raise CaddeException('020001004E')

もうひとつがデータの取得時。

src/consumer/connector-main/swagger_server/services/service.py:142

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def fetch_data(authorization: str,
resource_url: str,
resource_api_type: str,
provider: str,
options: dict,
external_interface: ExternalInterface = ExternalInterface()) -> (BytesIO,
dict):
"""
データ交換I/Fからデータを取得する、もしくはデータ管理から直接データを取得する。

Args:
resource_url str : リソースURL
resource_api_type str : リソース提供手段識別子
provider str : CADDEユーザID(提供者)
authorization str : 利用者トークン
options : dict リクエストヘッダ情報 key:ヘッダ名 value:パラメータ
external_interface ExternalInterface : GETリクエストを行うインタフェース

Returns:
BytesIO :取得データ
dict: レスポンスヘッダ情報 key:ヘッダ名 value:パラメータ レスポンスヘッダがない場合は空のdictを返す

(snip)

提供社のCADDEユーザIDが与えられたときにロケーション情報を取得する。

src/consumer/connector-main/swagger_server/services/service.py:230

1
2
3
4
5
6
7
8
9
    # CADDEユーザID(提供者)あり
else:

provider_connector_url = __get_location_info(
provider, location_service_url, external_interface)
if not provider_connector_url:
raise CaddeException('020004003E')

(snip)

コネクタ設定

READMEの通り、コネクタのIDやシークレットキーの設定は src/consumer/connector-main/swagger_server/configs/connector.json にて行う。

以下のような内容。

1
2
3
4
5
6
{
"consumer_connector_id" : "test_consumer_connector_id",
"consumer_connector_secret" : "test_consumer_connector_secret",
"location_service_url" : "https://testexample.com",
"trace_log_enable" : true
}

また、これが用いられるのは、以下。

src/consumer/connector-main/swagger_server/services/service.py:486

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def __get_connector_config() -> (str, str, str, str):
"""
connector.jsonからコンフィグ情報を取得し、
利用者側コネクタID、利用者側コネクタのシークレット、来歴管理者用トークンを返す。

Returns:
str: 利用者側コネクタID
str: 利用者側コネクタのシークレット
str: ロケーションサービスのURL
str: トレースログ設定

Raises:
Cadde_excption: コンフィグファイルの読み込みに失敗した場合 エラーコード: 020000005E
Cadde_excption: 必須パラメータが設定されていなかった場合(利用者コネクタID) エラーコード: 020000006E
Cadde_excption: 必須パラメータが設定されていなかった場合(利用者コネクタのシークレット) エラーコード: 020000007E
Cadde_excption: 必須パラメータが設定されていなかった場合(ロケーションサービスのURL) エラーコード: 020000008E
Cadde_excption: 必須パラメータが設定されていなかった場合(トレースログ設定) エラーコード: 020000009E

"""

(snip)

この関数が利用されるのは以下。

1
2
3
__get_connector_config()
catalog_search(query_string, search, provider, authorization, external_interface=ExternalInterface())
fetch_data(authorization, resource_url, resource_api_type, provider, options, external_interface=ExternalInterface())

両方とも同様の使われ方。 基本的には、呼び出して必要な値を読み出す。以下は、カタログ検索での例。

src/consumer/connector-main/swagger_server/services/service.py:72

1
consumer_connector_id, consumer_connector_secret, location_service_url, trace_log_enable = __get_connector_config()

得られた値は、例えば認証トークン検証などに用いられる。

src/consumer/connector-main/swagger_server/services/service.py:76

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    if authorization:
# 認証トークン検証
token_introspect_headers = {
'Authorization': authorization,
'x-cadde-consumer-connector-id': consumer_connector_id,
'x-cadde-consumer-connector-secret': consumer_connector_secret
}
token_introspect_response = external_interface.http_get(
__ACCESS_POINT_URL_AUTHENTICATION_AUTHORIZATION_INTROSPECT, token_introspect_headers)

if token_introspect_response.status_code < 200 or 300 <= token_introspect_response.status_code:
raise CaddeException(
message_id='020001003E',
status_code=token_introspect_response.status_code,
replace_str_list=[
token_introspect_response.text])

consumer_id = token_introspect_response.headers['x-cadde-consumer-id']

(snip)

その他

NGSIの設定や、横断検索用CKAN URLの設定の説明がある。

横断検索用CKAN URL設定の使われどころ。 src/consumer/catalog-search/swagger_server/services/service.py:13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def search_catalog_meta(
q: str,
internal_interface: InternalInterface,
external_interface: ExternalInterface) -> Response:
"""
横断検索を行い、横断検索サイトからカタログ情報を取得する

Args:
q str : 検索条件のクエリストリング
internal_interface InternalInterface : コンフィグ情報取得処理を行うインタフェース
external_interface ExternalInterface : GETリクエストを行うインタフェース

Returns:
Response : 取得した情報

Raises:
Cadde_excption : コンフィグファイルからCKANURLを取得できない場合、エラーコード : 020101004E
Cadde_excption : ステータスコード2xxでない場合 エラーコード : 020101005E

"""

(snip)

src/consumer/catalog-search/swagger_server/services/service.py:34

1
2
3
4
    try:
config = internal_interface.config_read(__CONFIG_CKAN_URL_FILE_PATH)
ckan_url = config[__CONFIG_CKAN_URL]
(snip)

また、TLS相互認証のためフォワードプロキシを利用する手順も載っているし、利用者コネクタへのアクセス制限のためのリバースプロキシの設定手順も載っている。

検索について

検索は、src/consumer/catalog-search が担う。以下、このコンポーネントについて。

以下の通り、search APIがエントリポイント。

swagger_server/swagger/swagger.yaml:19

1
operationId: search

ということでコントローラを見る。

swagger_server.controllers.search_controller.search

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def search(q=None, x_cadde_search=None, x_cadde_provider_connector_url=None, Authorization=None):  # noqa: E501
"""API. カタログ検索

横断検索、詳細検索を判定し、 横断検索サイトまたは提供者カタログサイトからカタログ情報を取得する Response: * 処理が成功した場合は200を返す * 処理に失敗した場合は、2xx以外を返す。Responsesセクション参照。 # noqa: E501

:param q: CKAN検索条件クエリ CKAN APIに準拠
:type q: str
:param x-cadde-search: 横断検索、詳細検索を指定する(横断検索:meta、詳細検索:detail)
:type x-cadde-search: str
:param x-cadde-provider-connector-url: 提供者コネクタURL
:type x-cadde-provider-connector-url: str
:param Authorization: 認証トークン
:type Authorization: str

:rtype: None
"""

(snip)

検索は、横断検索(meta)か、詳細検索(detail)か。

swagger_server/controllers/search_controller.py:39

1
search = connexion.request.headers['x-cadde-search']

横断検索は以下。

swagger_server/controllers/search_controller.py:41

1
2
3
4
5
if search == 'meta':
logger.debug(get_message('020101001N', [query_string, search]))

data = search_catalog_meta(
query_string, internal_interface, external_interface)

基本的には、コンフィグ(public_ckan.json)にある、公開CKAN(横断検索用カタログ)の 情報を取得し、問い合わせを投げるのみ。 認証はされない。(認証用トークなどを渡す仕様ではない) これは「公開」なので。

詳細検索は以下。

swagger_server/controllers/search_controller.py:47

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
else:
if 'x-cadde-provider-connector-url' in connexion.request.headers:
provider_connector_url = connexion.request.headers['x-cadde-provider-connector-url']
else:
raise CaddeException(message_id='020101002E')

authorization = None
if 'Authorization' in connexion.request.headers:
authorization = connexion.request.headers['Authorization']

logger.debug(get_message('020101003N', [query_string, log_message_none_parameter_replace(
provider_connector_url), log_message_none_parameter_replace(authorization), search]))

data = search_catalog_detail(
query_string,
provider_connector_url,
authorization,
external_interface)

こちらは、認証情報をヘッダーから取得し、関数に渡している。 swagger_server.services.service.search_catalog_detail 関数が本体。 認証情報を渡しているところが違う、基本的には同様。

データ取得について

Swagger YAML定義に基づくと、利用者側コネクタのコネクタメインのエントリポイントは以下。

consumer/connector-main/swagger_server/controllers/files_controller.py:14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def files(authorization=None, resource_url=None, resource_api_type=None, provider=None):  # noqa: E501
"""API. データ取得(NGSI以外)

CADDEインタフェースを用いて、HTTPサーバ、FTPサーバからファイルを取得する

Response:
* 処理が成功した場合は200を返す
* 処理に失敗した場合は、2xx以外のコードを返す。 Responsesセクションを参照。 # noqa: E501

:param Authorization: 利用者トークン
:type Authorization: str
:param resource_url: リソースURL
:type resource_url: str
:param resource_api_type: リソース提供手段識別子
:type resource_api_type: str
:param provider: CADDEユーザID(提供者)
:type provider: str

:rtype: None
"""

(snip)

基本的には、ユーザ(ないし、ユーザのシステム)から与えられた認証情報(トークン)、リソースURLなどを利用し、 リソースURLにアクセスしてファイルを受領する、というもの。

実際に、データを取得するのは以下の部分。

consumer/connector-main/swagger_server/controllers/files_controller.py:54

1
2
3
4
5
6
7
8
9
10
11
12
13
    data, headers = fetch_data(
authorization,
resource_url,
resource_api_type,
provider,
None,
external_interface)

response = make_response(data.read(), 200)
response.headers = headers
response.headers['Content-Disposition'] = 'attachment; filename=' + \
get_url_file_name(resource_url)
(snip)

fetch_data関数は、servicesにある。

consumer/connector-main/swagger_server/services/service.py:142

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def fetch_data(authorization: str,
resource_url: str,
resource_api_type: str,
provider: str,
options: dict,
external_interface: ExternalInterface = ExternalInterface()) -> (BytesIO,
dict):
"""
データ交換I/Fからデータを取得する、もしくはデータ管理から直接データを取得する。

Args:
resource_url str : リソースURL
resource_api_type str : リソース提供手段識別子
provider str : CADDEユーザID(提供者)
authorization str : 利用者トークン
options : dict リクエストヘッダ情報 key:ヘッダ名 value:パラメータ
external_interface ExternalInterface : GETリクエストを行うインタフェース

Returns:
BytesIO :取得データ
dict: レスポンスヘッダ情報 key:ヘッダ名 value:パラメータ レスポンスヘッダがない場合は空のdictを返す

(snip)

リソースのURLとともに、リソースのAPI種類を受け取る。 下記の通り、いまのところ、NGSI、file/ftp、file/httpに対応。

consumer/connector-main/swagger_server/services/service.py:177

1
2
3
    if resource_api_type != 'api/ngsi' and resource_api_type != 'file/ftp' and resource_api_type != 'file/http':
raise CaddeException('020004001E')
(snip)

認証は以下。

consumer/connector-main/swagger_server/services/service.py:184

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    if authorization:
# 認証トークン検証
token_introspect_headers = {
'Authorization': authorization,
'x-cadde-consumer-connector-id': consumer_connector_id,
'x-cadde-consumer-connector-secret': consumer_connector_secret
}
token_introspect_response = external_interface.http_get(
__ACCESS_POINT_URL_AUTHENTICATION_AUTHORIZATION_INTROSPECT, token_introspect_headers)

if token_introspect_response.status_code < 200 or 300 <= token_introspect_response.status_code:
raise CaddeException(
message_id='020004002E',
status_code=token_introspect_response.status_code,
replace_str_list=[
token_introspect_response.text])

consumer_id = token_introspect_response.headers['x-cadde-consumer-id']

(snip)

利用者コネクタのIDとシークレットキーをヘッダーに入れ、Introspect宛先に問う。 上記の通り、基本的にはCADDE固有の認証サーバ?

これを一般的な外部サービス利用可能にするには、実装の変更の必要がありそう。

データ取得部分。 CADDEユーザIDのありなしで挙動が異なる。

まずCADDEユーザIDなしの場合。

consumer/connector-main/swagger_server/services/service.py:212

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    # CADDEユーザID(提供者)なし
if not provider:
if resource_api_type == 'api/ngsi':
response_bytes, ngsi_response_headers = provide_data_ngsi(
resource_url, options)
header_arry = ngsi_response_headers.keys()
for key_data in header_arry:
response_headers[key_data] = ngsi_response_headers[key_data]
elif resource_api_type == 'file/ftp':
response_bytes = provide_data_ftp(
resource_url, external_interface, internal_interface)

elif resource_api_type == 'file/http':
response_bytes = provide_data_http(
resource_url, options, external_interface, internal_interface)

return response_bytes, response_headers
(snip)

この中で、provide_data_http関数がどこに定義されているかというと、common.swagger_server.services.provide_data_http.provide_data_httpなど。これは common の中にあるが、実はセットアップスクリプトでシンボリックリンクが貼られている。

consumer/setup.sh:18

1
ln -f ../common/swagger_server/services/provide_data_http.py  connector-main/swagger_server/services/provide_data_http.py

関数を見てみる。

common/swagger_server/services/provide_data_http.py:24

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def provide_data_http(
resource_url: str,
headers_dict: dict = None,
file_get_interface: ExternalInterface = ExternalInterface(),
config_get_interface: InternalInterface = InternalInterface()) -> BytesIO:
"""
HTTPサーバからファイルを取得して返却する。
※2020年9月版ではダイジェスト認証、TLS証明書認証、OAuth等の認証処理は実施せず、
※ベーシック認証のみ実施する。

Args:
resource_url str : ファイル取得を行うリソースURL
headers_dict : 設定するheader {ヘッダー名:パラメータ}
file_get_interface object : ファイル取得処理を行うインタフェース
config_get_interface object : コンフィグファイルからの情報取得を行うインタフェース

Returns:
BytesIO :取得データ

(snip)

関数の説明にあるとおりだが、データソースに対する認証については、いまの実装ではBASIC認証のみに対応。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    if 0 < len(http_config_domain):
if __CONFIG_KEY_BASIC_ID not in http_config_domain[0]:
raise CaddeException(
'000201004E',
status_code=None,
replace_str_list=[__CONFIG_KEY_BASIC_ID])

if __CONFIG_KEY_BASIC_PASS not in http_config_domain[0]:
raise CaddeException(
'000201005E',
status_code=None,
replace_str_list=[__CONFIG_KEY_BASIC_PASS])

auth = (
http_config_domain[0][__CONFIG_KEY_BASIC_ID],
http_config_domain[0][__CONFIG_KEY_BASIC_PASS])

(snip)

レスポンスのコンテンツをBytesIOにして戻り値として返す。

common/swagger_server/services/provide_data_http.py:94

1
2
3
4
5
6
7
8
if response.status_code == requests.codes.ok:
return BytesIO(response.content)

if response.status_code == requests.codes.not_found:
raise CaddeException('000201006E')

if response.status_code == requests.codes.unauthorized:
raise CaddeException('000201007E')

なお、FTPの場合はSFTPではなく、FTPのみ。 NGSIの場合は、アクセストークンを取得して利用するようになっている。

提供者コネクタ

提供者コネクタの主なエントリポイントは paths./cadde/api/v4/catalog である。 つまりカタログ詳細検索APIである。

provider/catalog-search/swagger_server/swagger/swagger.yaml:8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  /cadde/api/v4/catalog:
get:
tags:
- Search
summary: API. カタログ検索(詳細検索)
description: |-
詳細検索リクエストを受け付け、メイン制御に処理を依頼する。

Response:
* 処理が成功した場合は200を返す。
* 処理に失敗した場合は、2xx以外を返す。場合によりエラーを示すペイロードがつく場合もある。Responsesセクションを参照すること。
operationId: search

(snip)

上記の通り、呼び出されるのは search 関数。

provider/catalog-search/swagger_server/controllers/search_controller.py:15

1
2
3
4
5
6
7
8
9
10
11
12
13
def search(q=None, Authorization=None):  # noqa: E501
"""API. カタログ検索(詳細検索)

提供者カタログサイトからCKANカタログ情報を取得する. Response: * 処理が成功した場合は200を返す * 処理に失敗した場合は、2xx以外を返す。Responsesセクション参照。 # noqa: E501

:param q: CKAN検索条件クエリ CKAN APIに準拠
:type q: str
:param Authorization: 認証トークン
:type Authorization: str

:rtype: None
"""
(snip)

カタログ詳細検索用のクエリ、認証トークンなどを受領し、それを用いて詳細検索用のCKANに問い合わせる。

provider/catalog-search/swagger_server/controllers/search_controller.py:41

1
data = search_catalog_ckan(query_string, authorization, external_interface)

search_catalog_ckan関数は、services内に定義されている。

provider/catalog-search/swagger_server/services/service.py:8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def search_catalog_ckan(
query_string: str,
auth_token: str,
external_interface: ExternalInterface) -> str:
"""
コネクタメインに詳細検索を送信する

Args:
query_string str : 検索条件のクエリストリング
auth_token str : HTTPリクエストヘッダとしての認証トークン
external_interface ExternalInterface : コネクタ外部とのインタフェース

Returns:
str : 取得したデータ文字列

Raises:
Cadde_excption : ステータスコード200 OKでない場合 エラーコード : 010101002E

"""

response = external_interface.http_get(
__ACCESS_POINT_URL + query_string, {'Authorization': auth_token})

if response.status_code != 200:
raise CaddeException(
message_id='010101002E',
status_code=response.status_code,
replace_str_list=[
response.text])
else:
return response.text

ちなみに、common側にもあるのだが、そちらはデータ提供者コネクタからは使われていない様子。 common/swagger_server/services/ckan_access.py これは、セットアップスクリプトにより、利用者側コネクタ内にシンボリックリンクが作られるようになっているが、呼び出されている形跡がない?

参考

共有

EDCステータス変化の様子の謎メモ

メモ

EDC/Samples のステータス変化の謎メモ。 調べたのは、コミットb7e2220のバージョン。

このバージョンで扱うEDC Connectorのバージョンは以下。

gradle/libs.versions.toml:7

1
edc = "0.8.1"
1
git clone git@github.com:eclipse-edc/Connector.git -b refs/tags/v0.8.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
diff --git a/gradle.properties b/gradle.properties
index 4511d8eb5..1c57d1403 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,7 +1,8 @@
group=org.eclipse.edc
-version=0.8.1-SNAPSHOT
+version=0.8.1
# for now, we're using the same version for the autodoc plugin and the processor, but that could change in the future
-annotationProcessorVersion=0.8.1-SNAPSHOT
-edcGradlePluginsVersion=0.8.1-SNAPSHOT
+annotationProcessorVersion=0.8.1
+edcGradlePluginsVersion=0.8.1
edcScmUrl=https://github.com/eclipse-edc/Connector.git
edcScmConnection=scm:git:git@github.com:eclipse-edc/Connector.git
+org.gradle.jvmargs=-Xmx4096M
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index bdd4f8d83..ac3efafe5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -9,7 +9,7 @@ atomikos = "6.0.0"
awaitility = "4.2.1"
bouncyCastle-jdk18on = "1.78.1"
cloudEvents = "4.0.1"
-edc = "0.8.1-SNAPSHOT"
+edc = "0.8.1"
failsafe = "3.3.2"
h2 = "2.3.230"
httpMockServer = "5.15.0"

デバッガをアタッチして動作を確認

ひとまずContract Negotiation。

どうやらProvider側では、Consumerからリクエストが届いたときに、 org.eclipse.edc.protocol.dsp.negotiation.http.api.controller.DspNegotiationApiController#initialContractRequestが呼ばれる様子。

Provider側でContractNegotiationのログが出るのは2箇所。

org.eclipse.edc.connector.controlplane.services.contractnegotiation.ContractNegotiationProtocolServiceImpl#update ここには、PROVIDER型のContractNegotiationが渡されてくる。

もうひとつは org.eclipse.edc.statemachine.AbstractStateEntityManager#update

1
2
3
4
5
6
protected void update(E entity) {
store.save(entity);
monitor.debug(() -> "[%s] %s %s is now in state %s"
.formatted(this.getClass().getSimpleName(), entity.getClass().getSimpleName(),
entity.getId(), entity.stateAsString()));
}

org.eclipse.edc.connector.controlplane.contract.negotiation.ProviderContractNegotiationManagerImpl が用いいられる。

型階層は以下の通り。

1
2
3
core/common/lib/state-machine-lib/src/main/java/org/eclipse/edc/statemachine/AbstractStateEntityManager.java
core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/negotiation/AbstractContractNegotiationManager.java
core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/negotiation/ProviderContractNegotiationManagerImpl.java

org.eclipse.edc.connector.controlplane.contract.negotiation.ProviderContractNegotiationManagerImpl内のプロセッサの定義で、リクエストなどが届いたときの動作を定義。

org.eclipse.edc.connector.controlplane.contract.negotiation.ProviderContractNegotiationManagerImpl#configureStateMachineManager

1
2
3
4
5
6
7
8
9
10
protected StateMachineManager.Builder configureStateMachineManager(StateMachineManager.Builder builder) {
return builder
.processor(processNegotiationsInState(OFFERING, this::processOffering))
.processor(processNegotiationsInState(REQUESTED, this::processRequested))
.processor(processNegotiationsInState(ACCEPTED, this::processAccepted))
.processor(processNegotiationsInState(AGREEING, this::processAgreeing))
.processor(processNegotiationsInState(VERIFIED, this::processVerified))
.processor(processNegotiationsInState(FINALIZING, this::processFinalizing))
.processor(processNegotiationsInState(TERMINATING, this::processTerminating));
}

ここで管理されるステータスはいかに定義されている。

org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum ContractNegotiationStates {

INITIAL(50),
REQUESTING(100),
REQUESTED(200),
OFFERING(300),
OFFERED(400),
ACCEPTING(700),
ACCEPTED(800),
AGREEING(825),
AGREED(850),
VERIFYING(1050),
VERIFIED(1100),
FINALIZING(1150),
FINALIZED(1200),
TERMINATING(1300),
TERMINATED(1400);

(snip)

例えば、とあるケースではREQUESTED(200)の次はAGREEING(825)に変わった。

org.eclipse.edc.connector.controlplane.contract.negotiation.ProviderContractNegotiationManagerImpl#processAgreeing がProvider側が合意をするためのメソッド。

以下の処理を行うとAgreement情報が生成される。

org/eclipse/edc/connector/controlplane/contract/negotiation/ProviderContractNegotiationManagerImpl.java:121

1
2
3
4
5
6
7
8
9
10
11
12
var agreement = Optional.ofNullable(negotiation.getContractAgreement())
.orElseGet(() -> {
var lastOffer = negotiation.getLastContractOffer();

return ContractAgreement.Builder.newInstance()
.contractSigningDate(clock.instant().getEpochSecond())
.providerId(participantId)
.consumerId(negotiation.getCounterPartyId())
.policy(lastOffer.getPolicy().toBuilder().type(PolicyType.CONTRACT).build())
.assetId(lastOffer.getAssetId())
.build();
});

以下のような感じ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
agreement = {ContractAgreement@7424} 
id = "b092de56-c860-4972-8e09-e00e77ce97c4"
providerId = "provider"
consumerId = "consumer"
contractSigningDate = 1728916940
assetId = "assetId"
policy = {Policy@7429}
permissions = {ArrayList@7431} size = 0
prohibitions = {ArrayList@7432} size = 0
obligations = {ArrayList@7433} size = 0
profiles = {ArrayList@7434} size = 0
extensibleProperties = {HashMap@7435} size = 0
inheritsFrom = null
assigner = null
assignee = null
target = "assetId"
type = {PolicyType@7436} "CONTRACT"

dispatchメソッド。

org/eclipse/edc/connector/controlplane/contract/negotiation/ProviderContractNegotiationManagerImpl.java:136

1
2
3
4
5
6
return dispatch(messageBuilder, negotiation, Object.class)
.onSuccess((n, result) -> transitionToAgreed(n, agreement))
.onFailure((n, throwable) -> transitionToAgreeing(n))
.onFatalError((n, failure) -> transitionToTerminated(n, failure.getFailureDetail()))
.onRetryExhausted((n, throwable) -> transitionToTerminating(n, format("Failed to send agreement to consumer: %s", throwable.getMessage())))
.execute("[Provider] send agreement");

上記の通り、成功か失敗か(どのような失敗か)によってステート変更が変わる。

ここに渡ってくるnegotiationは以下のような感じ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
negotiation = {ContractNegotiation@7419} 
callbackAddresses = {ArrayList@7440} size = 0
correlationId = "028d3abb-5086-4324-be77-087095f382c0"
counterPartyId = "consumer"
counterPartyAddress = "http://localhost:29194/protocol"
protocol = "dataspace-protocol-http"
type = {ContractNegotiation$Type@7444} "PROVIDER"
contractAgreement = null
contractOffers = {ArrayList@7445} size = 1
protocolMessages = {ProtocolMessages@7446}
state = 825
stateCount = 1
stateTimestamp = 1728916898826
traceContext = {HashMap@7447} size = 0
errorDetail = null
pending = false
updatedAt = 1728916898826
id = "af2c8a47-022f-450c-87e2-c425de871ab1"
clock = {Clock$SystemClock@7422} "SystemClock[Z]"
createdAt = 1728916898526

org.eclipse.edc.connector.controlplane.contract.negotiation.AbstractContractNegotiationManager#dispatch

メッセージビルドの様子。

org/eclipse/edc/connector/controlplane/contract/negotiation/AbstractContractNegotiationManager.java:93

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
messageBuilder.counterPartyAddress(negotiation.getCounterPartyAddress())
.counterPartyId(negotiation.getCounterPartyId())
.protocol(negotiation.getProtocol())
.processId(Optional.ofNullable(negotiation.getCorrelationId()).orElse(negotiation.getId()));

if (type() == ContractNegotiation.Type.CONSUMER) {
messageBuilder.consumerPid(negotiation.getId()).providerPid(negotiation.getCorrelationId());
} else {
messageBuilder.providerPid(negotiation.getId()).consumerPid(negotiation.getCorrelationId());
}

if (negotiation.lastSentProtocolMessage() != null) {
messageBuilder.id(negotiation.lastSentProtocolMessage());
}

var message = messageBuilder.build();

これによりビルドされるメッセージは以下のようなもの。

1
2
3
4
5
6
7
8
9
message = {ContractAgreementMessage@7463} 
contractAgreement = {ContractAgreement@7424}
id = "c8a7b6dc-7e44-4bcd-9645-a12d5f8690b6"
processId = "028d3abb-5086-4324-be77-087095f382c0"
consumerPid = "028d3abb-5086-4324-be77-087095f382c0"
providerPid = "af2c8a47-022f-450c-87e2-c425de871ab1"
protocol = "dataspace-protocol-http"
counterPartyAddress = "http://localhost:29194/protocol"
counterPartyId = "consumer"

org/eclipse/edc/connector/controlplane/contract/negotiation/AbstractContractNegotiationManager.java:112

1
return entityRetryProcessFactory.doAsyncStatusResultProcess(negotiation, () -> dispatcherRegistry.dispatch(responseType, message));

上記の通り、org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry を使ってメッセージを送信。 実装は org.eclipse.edc.connector.core.message.RemoteMessageDispatcherRegistryImpl である。

org/eclipse/edc/connector/core/message/RemoteMessageDispatcherRegistryImpl.java:41

1
2
3
4
5
6
7
8
9
public <T> CompletableFuture<StatusResult<T>> dispatch(Class<T> responseType, RemoteMessage message) {
Objects.requireNonNull(message, "Message was null");
var protocol = message.getProtocol();
var dispatcher = dispatchers.get(protocol);
if (dispatcher == null) {
return failedFuture(new EdcException("No provider dispatcher registered for protocol: " + protocol));
}
return dispatcher.dispatch(responseType, message);
}

protocolにはdataspace-protocol-httpが入る。

dispatcherには以下のようなものが入る。

1
2
3
4
5
6
7
8
dispatcher = {DspHttpRemoteMessageDispatcherImpl@7489} 
handlers = {HashMap@7491} size = 13
policyScopes = {HashMap@7492} size = 12
httpClient = {EdcHttpClientImpl@7493}
identityService = {MockIdentityService@7494}
policyEngine = {PolicyEngineImpl@7495}
tokenDecorator = {DspHttpCoreExtension$lambda@7496}
audienceResolver = {IamMockExtension$lambda@7497}

Policyの判定

Policy定義の際に呼び出されるやつ。

org/eclipse/edc/connector/controlplane/api/management/policy/v3/PolicyDefinitionApiV3Controller.java:57

1
2
3
4
5
@POST
@Override
public JsonObject createPolicyDefinitionV3(JsonObject request) {
return createPolicyDefinition(request);
}

コネクタに登録した際に、ログに残るやつ

org/eclipse/edc/connector/controlplane/api/management/policy/BasePolicyDefinitionApiController.java:87

1
2
3
var createdDefinition = service.create(definition)
.onSuccess(d -> monitor.debug(format("Policy Definition created %s", d.getId())))
.orElseThrow(exceptionMapper(PolicyDefinition.class, definition.getId()));

コントラクトのバリデーションサービスは以下。

org.eclipse.edc.connector.controlplane.contract.validation.ContractValidationServiceImpl

この中でorg.eclipse.edc.connector.controlplane.contract.validation.ContractValidationServiceImpl#evaluatePolicyが呼ばれる。

org/eclipse/edc/connector/controlplane/contract/validation/ContractValidationServiceImpl.java:154

1
2
3
4
5
6
7
8
private Result<Policy> evaluatePolicy(Policy policy, String scope, ParticipantAgent agent, ContractOfferId offerId) {
var policyContext = PolicyContextImpl.Builder.newInstance().additional(ParticipantAgent.class, agent).build();
var policyResult = policyEngine.evaluate(scope, policy, policyContext);
if (policyResult.failed()) {
return failure(format("Policy in scope %s not fulfilled for offer %s, policy evaluation %s", scope, offerId.toString(), policyResult.getFailureDetail()));
}
return Result.success(policy);
}

上記のpolicyEngineには以下が入っている。

1
2
3
4
5
6
7
policyEngine = {PolicyEngineImpl@7419} 
constraintFunctions = {TreeMap@7462} size = 1
dynamicConstraintFunctions = {ArrayList@7503} size = 0
ruleFunctions = {TreeMap@7454} size = 0
preValidators = {HashMap@7423} size = 0
postValidators = {HashMap@7517} size = 0
scopeFilter = {ScopeFilter@7509}

ポリシー評価 org.eclipse.edc.policy.evaluator.PolicyEvaluator

デフォルトのポリシーエンジン org.eclipse.edc.policy.engine.PolicyEngineImpl

元になったインターフェース org.eclipse.edc.policy.engine.spi.PolicyEngine

コントラクトネゴシエーションが走ると、Providerで以下が走る。

org.eclipse.edc.policy.engine.PolicyEngineImpl#evaluate org/eclipse/edc/policy/engine/PolicyEngineImpl.java:69

1
2
3
4
5
6
    public Result<Void> evaluate(String scope, Policy policy, PolicyContext context) {
var delimitedScope = scope + ".";

var scopedPreValidators = preValidators.entrySet().stream().filter(entry -> scopeFilter(entry.getKey(), delimitedScope)).flatMap(l -> l.getValue().stream()).toList();

(snip)

評価はPolicyEvaluatorが行う。

org/eclipse/edc/policy/engine/PolicyEngineImpl.java:79

1
var evalBuilder = PolicyEvaluator.Builder.newInstance();

org/eclipse/edc/policy/engine/PolicyEngineImpl.java:115

1
var result = evaluator.evaluate(filteredPolicy);

filteredPolicyには以下のようなものが入る。

1
2
3
4
5
6
7
8
9
10
11
filteredPolicy = {Policy@7361} 
permissions = {ArrayList@7364} size = 0
prohibitions = {ArrayList@7365} size = 0
obligations = {ArrayList@7366} size = 0
profiles = {ArrayList@7367} size = 0
extensibleProperties = {HashMap@7368} size = 0
inheritsFrom = null
assigner = null
assignee = null
target = null
type = {PolicyType@7369} "SET"

org/eclipse/edc/policy/evaluator/PolicyEvaluator.java:66

1
2
3
4

public PolicyEvaluationResult evaluate(Policy policy) {
return policy.accept(this) ? new PolicyEvaluationResult() : new PolicyEvaluationResult(ruleProblems);
}

org.eclipse.edc.policy.evaluator.PolicyEvaluator#visitPolicyが呼ばれる。

org/eclipse/edc/policy/evaluator/PolicyEvaluator.java:71

1
2
3
4
5
6
public Boolean visitPolicy(Policy policy) {
policy.getPermissions().forEach(permission -> permission.accept(this));
policy.getProhibitions().forEach(prohibition -> prohibition.accept(this));
policy.getObligations().forEach(duty -> duty.accept(this));
return ruleProblems.isEmpty();
}

上記の通り、もしpolicyが空だと何も生じず、ruleProblemsが空なのでTrueが返る。

アセットセレクタ

org.eclipse.edc.connector.controlplane.contract.validation.ContractValidationServiceImpl#validateInitialOffer(org.eclipse.edc.connector.controlplane.contract.spi.validation.ValidatableConsumerOffer, org.eclipse.edc.spi.agent.ParticipantAgent) が用いられる。

org/eclipse/edc/connector/controlplane/contract/validation/ContractValidationServiceImpl.java:144

1
2
3
4
5
var testCriteria = new ArrayList<>(consumerOffer.getContractDefinition().getAssetsSelector());
testCriteria.add(new Criterion(Asset.PROPERTY_ID, "=", consumerOffer.getOfferId().assetIdPart()));
if (assetIndex.countAssets(testCriteria) <= 0) {
return failure("Asset ID from the ContractOffer is not included in the ContractDefinition");
}

AssetIndexの型階層は以下の通り。

1
2
3
AssetIndex (org.eclipse.edc.connector.controlplane.asset.spi.index)
InMemoryAssetIndex (org.eclipse.edc.connector.controlplane.defaults.storage.assetindex)
SqlAssetIndex (org.eclipse.edc.connector.controlplane.store.sql.assetindex)

このうち、手元の環境で呼び出されたのはInMemoryAssetIndex。

org/eclipse/edc/connector/controlplane/defaults/storage/assetindex/InMemoryAssetIndex.java:112

1
2
3
public long countAssets(List<Criterion> criteria) {
return filterBy(criteria).count();
}

org.eclipse.edc.connector.controlplane.defaults.storage.assetindex.InMemoryAssetIndex#filterBy

1
2
3
4
5
6
7
8
private Stream<Asset> filterBy(List<Criterion> criteria) {
var predicate = criteria.stream()
.map(criterionOperatorRegistry::toPredicate)
.reduce(x -> true, Predicate::and);

return cache.values().stream()
.filter(predicate);
}

クライテリアの評価用のPredicate生成はorg.eclipse.edc.spi.query.CriterionOperatorRegistry。実装はorg.eclipse.edc.query.CriterionOperatorRegistryImpl

org.eclipse.edc.query.CriterionOperatorRegistryImpl#toPredicate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public <T> Predicate<T> toPredicate(Criterion criterion) {
var predicate = operatorPredicates.get(criterion.getOperator().toLowerCase());
if (predicate == null) {
throw new IllegalArgumentException(format("Operator [%s] is not supported.", criterion.getOperator()));
}

return t -> {

var operandLeft = (String) criterion.getOperandLeft();

var property = propertyLookups.stream()
.map(it -> it.getProperty(operandLeft, t))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);

if (property == null) {
return false;
}

return predicate.test(property, criterion.getOperandRight());
};

}

参考

共有

llama cpp

メモ

build.md に記載の通り、クローンしてmakeする。

1
2
git clone git@github.com:ggerganov/llama.cpp.git
make

参考

共有

Llama3.2

メモ

Llama 3.2 Revolutionizing edge AI and vision with open, customizable models にある通り、 2024/9/25にLlama3.2がリリースされた。

Llama公式Llamaダウンロード からモデルをダウンロードして用いる。

ダウンロードのための情報登録

さらに利用規約に合意すると、具体的なダウンロード手順が表示される。

手順

この手順の中には、リクエストIDやキーなどの情報が含まれているので注意。

もし、Llama CLIをインストールしていなければ以下の通り実行。

1
pip install llama-stack

モデルのリストを確認する。

1
llama model list

結果例

1
2
3
4
5
6
7
8
| Llama3.2-1B                      | meta-llama/Llama-3.2-1B                  | 128K           |
| Llama3.2-3B | meta-llama/Llama-3.2-3B | 128K |
| Llama3.2-11B-Vision | meta-llama/Llama-3.2-11B-Vision | 128K |
| Llama3.2-90B-Vision | meta-llama/Llama-3.2-90B-Vision | 128K |
| Llama3.2-1B-Instruct | meta-llama/Llama-3.2-1B-Instruct | 128K |
| Llama3.2-3B-Instruct | meta-llama/Llama-3.2-3B-Instruct | 128K |
| Llama3.2-11B-Vision-Instruct | meta-llama/Llama-3.2-11B-Vision-Instruct | 128K |
| Llama3.2-90B-Vision-Instruct | meta-llama/Llama-3.2-90B-Vision-Instruct | 128K |

モデルをダウンロードして用いる。ここではテストなので小さなモデルを用いる。

1
2
MODEL_ID=Llama3.2-3B
llama model download --source meta --model-id $MODEL_ID

上記コマンドを実行すると、キー付きのURL入力を求められる。 先程表示した手順の「4」に記載されたURLを入力する。

このメモを書いている時点では混んでいるのか、ダウンロードにそれなりに時間がかかる。

ダウンロードが完了すると、以下のディレクトリにチェックポイントがダウンロードされているはず。

1
~/.llama/checkpoints/Llama3.2-3B

参考

共有

ComfyUI

メモ

導入方法

ComfyUIとは?ノードベースGUIでStable Diffusionを最大活用する機能・特徴・導入方法徹底解説! に導入方法などの基礎が書いてある。 ComfyUIの導入方法が2種類紹介されているが、他のツールなどの管理も兼ねるため、ここではStability Matrixを用いた。

StabilityMatrix のリリースから各環境用のインストーラをダウンロード。 執筆時点では、 v2.12.0が最新だった。

Windwows用は以下。

https://github.com/LykosAI/StabilityMatrix/releases/download/v2.12.0/StabilityMatrix-win-x64.zip

使い方

画像生成AI「Stable Diffusion」を使い倒す! モジュラーシンセみたいな「ComfyUI」をインストール が参考になる。 先に StabilityMatrix のModel Browserで適当なモデルをダウンロードしておくこと。 StabilityMatrix で ComfyUI をインストールした場合、 ComfyUIのコンフィグが自動で編集され、ダウンロードしたモデルのパスが追加されるようになっている。ただし、新しいモデルをダウンロードした場合は ComfyUIの再起動が必要。

参考

共有

Jan

メモ

LLMプラットフォームの「JAN」、様々なモデルでチャットAIを作れる を参考に、 Janを使ってみる。

Janの公式サイト に掲載の通り、パッケージをダウンロードする。 ここではUbuntu環境を前提にAppImageを用いる。 このとき最新版だった、 jan-linux-x86_64-0.5.4.AppImage をダウンロードした。自分の環境の場合は、 ~/Applications/ 以下にダウンロードした。

実行。

1
$ ./Applications/jan-linux-x86_64-0.5.4.AppImage

GUIからひとまず lama3.1-8b-instruct を試すことにした。

続いて、 LLMプラットフォームの「JAN」、様々なモデルでチャットAIを作れる の記事でも用いられている、huggingfaceからモデルを探して試す。 Huggingfaceについては、 Hugging Faceとは?Hugging Face Hubの機能や使い方・ライブラリをわかりやすく解説! を参照。

参考

共有