{"id":833,"date":"2019-05-15T21:38:45","date_gmt":"2019-05-15T19:38:45","guid":{"rendered":"https:\/\/blog.unetresgrossebite.com\/?p=833"},"modified":"2019-06-02T21:52:06","modified_gmt":"2019-06-02T19:52:06","slug":"openshift-cephfs","status":"publish","type":"post","link":"https:\/\/blog.unetresgrossebite.com\/?p=833","title":{"rendered":"OpenShift &#038; CephFS"},"content":{"rendered":"\n<p>If you&#8217;re not yet familiar with it, OpenShift is a container orchestration solution based on Kubernetes. Among others, it integrates with several storage providers such as Ceph.<\/p>\n\n\n\n<p>Although GlusterFS is probably the best choice in terms of OpenShift integration, we could argue Ceph is a better pick overall. And while this post doesn&#8217;t aim at offering an exhaustive comparison between the two, we could mention GlusterFS split-brains requiring manual recoveries, poor block devices performances, poor performances dealing with lots (100s) of volumes, the lack of kernel-land client dealing with file volumes, &#8230;<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>The most common way to integrate Ceph with OpenShift is to register a StorageClass, as we could find in OpenShift documentations, managing Rados Block Devices.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: storage.k8s.io\/v1<br> kind: StorageClass<br>metadata:<br>&nbsp;&nbsp;annotations:<br>&nbsp;&nbsp;&nbsp;&nbsp;storageclass.beta.kubernetes.io\/is-default-class: &#8220;true&#8221;<br>&nbsp;&nbsp;name: ceph-storage<br>parameters:<br>   adminId: kube<br>&nbsp;&nbsp;adminSecretName: ceph-secret-kube<br>&nbsp;&nbsp;adminSecretNamespace: default<br>&nbsp;&nbsp;monitors: 10.42.253.110:6789,10.42.253.111:6789,10.42.253.112:6789<br>&nbsp;&nbsp;pool: kube<br>&nbsp;&nbsp;userId: kube<br>&nbsp;&nbsp;userSecretName: ceph-secret-kube<br>&nbsp;&nbsp;userSecretNamespace: default<br> provisioner: kubernetes.io\/rbd<br>reclaimPolicy: Retain<\/p><\/blockquote>\n\n\n\n<p>We would also need to create a Secret, holding our Ceph client key. First, we would create our client, granting it with proper permissions:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>$&gt; ceph auth get-or-create client.kube mon &#8216;allow r&#8217; osd &#8216;allow class-read object_prefix rbd_children, allow rwx pool=kube&#8217; -o ceph.client.kube.keyring<\/p><\/blockquote>\n\n\n\n<p>Next, we would base64-encode our key:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>$&gt; awk &#8216;\/^[ \\t]*key\/{print $3} ceph.client.kube.keyring | base64<\/p><\/blockquote>\n\n\n\n<p>And register our Secret, including our encoded secret:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>cat &lt;&lt;EOF | oc apply -n default -f-<\/p><p>apiVersion: v1<br>data:<br>&nbsp;&nbsp;key: &lt;base64-encoded-string&gt;<br>kind: Secret<br> metadata:<br>&nbsp;&nbsp;name: ceph-secret-kube<br>type: kubernetes.io\/rbd<\/p><p>EOF<\/p><\/blockquote>\n\n\n\n<p><br>The previous configurations would then allow us to dynamically provision block devices deploying new applications to OpenShift.<\/p>\n\n\n\n<p>And while block devices is a nice thing to have, dealing with stateful workloads such as databases, up until now, GlusterFS main advantage over Ceph was its ability to provide with ReadWriteMany volumes &#8211; that can be mounted from several Pods at once, as opposed to ReadWriteOnce or ReadWriteOnly volumes, that may only be accessed by one deployment, unless mounted as without write capabilities.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>On the other hand, in addition to Rados Block Devices, Ceph offers with an optional CephFS share, that is similar to NFS or GlusterFS, in that several clients can concurrently write the same folder. And while CephFS isn&#8217;t much mentioned reading through OpenShift documentations, Kubernetes officially supports it. Today, we would try and guess how to make that work with OpenShift.<br>CephFS is considered to be stable since Ceph 12 (Luminous), released a couple years ago. Since then, I&#8217;ve been working for a practical use case. Here it is.<\/p>\n\n\n\n<p>We would mostly rely on the configurations offered in <a rel=\"noreferrer noopener\" aria-label=\"kubernetes-incubator external-storage's GitHub repository (opens in a new tab)\" href=\"https:\/\/github.com\/kubernetes-incubator\/external-storage\/tree\/master\/ceph\/cephfs\/deploy\/rbac\" target=\"_blank\">kubernetes-incubator external-storage&#8217;s GitHub repository<\/a>.<\/p>\n\n\n\n<p>First, let&#8217;s create a namespace hosting CephFS provisioner:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>$&gt; oc new-project cephfs<\/p><\/blockquote>\n\n\n\n<p>Then, in that namespace, we would register a Secret. Note that the CephFS provisioner offered by Kubernetes requires with near-admin privileges over your Ceph cluster. For each Persistent Volume registered through OpenShift API, the provisioner would create a dynamic user with limited privileges over the sub-directoriy hosting our data. Here, we would just pass it with our admin key:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: v1<br>kind: Secret<br>data:<br>&nbsp;&nbsp;key: &lt;base64-encoded-admin-key&gt;<br>metadata:<br>&nbsp;&nbsp;name: ceph-secret-admin<\/p><\/blockquote>\n\n\n\n<p>Then, we would create a ClusterRole<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: rbac.authorization.k8s.io\/v1<br>kind: ClusterRole<br>metadata:<br>&nbsp;&nbsp;name: cephfs-provisioner<br>rules:<br>&#8211; apiGroups: [&#8220;&#8221;]<br>&nbsp;&nbsp;resources: [&#8220;persistentvolumes&#8221;]<br>&nbsp;&nbsp;verbs: [&#8220;get&#8221;, &#8220;list&#8221;, &#8220;watch&#8221;, &#8220;create&#8221;, &#8220;delete&#8221;]<br>&#8211; apiGroups: [&#8220;&#8221;]<br>&nbsp;&nbsp;resources: [&#8220;secrets&#8221;]<br>&nbsp;&nbsp;verbs: [&#8220;create&#8221;, &#8220;get&#8221;, &#8220;delete&#8221;]<br>&#8211; apiGroups: [&#8220;&#8221;]<br>&nbsp;&nbsp;resources: [&#8220;persistentvolumeclaims&#8221;]<br>&nbsp;&nbsp;verbs: [&#8220;get&#8221;, &#8220;list&#8221;, &#8220;watch&#8221;, &#8220;update&#8221;]<br>&#8211; apiGroups: [&#8220;storage.k8s.io&#8221;]<br>&nbsp;&nbsp;resources: [&#8220;storageclasses&#8221;]<br>&nbsp;&nbsp;verbs: [&#8220;get&#8221;, &#8220;list&#8221;, &#8220;watch&#8221;]<br>&#8211; apiGroups: [&#8220;&#8221;]<br>&nbsp;&nbsp;resources: [&#8220;events&#8221;]<br>&nbsp;&nbsp;verbs: [&#8220;create&#8221;, &#8220;update&#8221;, &#8220;patch&#8221;]<br>&#8211; apiGroups: [&#8220;&#8221;]<br>&nbsp;&nbsp;resources: [&#8220;services&#8221;]<br>  resourceNames: [&#8220;kube-dns&#8221;,&#8221;coredns&#8221;]<br>&nbsp;&nbsp;verbs: [&#8220;list&#8221;, &#8220;get&#8221;]<\/p><\/blockquote>\n\n\n\n<p>A Role<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: rbac.authorization.k8s.io\/v1<br>kind: Role<br>metadata:<br>&nbsp;&nbsp;name: cephfs-provisioner<br>rules:<br>&#8211; apiGroups: [&#8220;&#8221;]<br>&nbsp;&nbsp;resources: [&#8220;secrets&#8221;]<br>&nbsp;&nbsp;verbs: [&#8220;create&#8221;, &#8220;get&#8221;, &#8220;delete&#8221;]<br>&#8211; apiGroups: [&#8220;&#8221;]<br>&nbsp;&nbsp;resources: [&#8220;endpoints&#8221;]<br>&nbsp;&nbsp;verbs: [&#8220;get&#8221;, &#8220;list&#8221;, &#8220;watch&#8221;, &#8220;create&#8221;, &#8220;update&#8221;, &#8220;patch&#8221;]<\/p><\/blockquote>\n\n\n\n<p>A ServiceAccount<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>$&gt; oc create sa cephfs-provisioner<\/p><\/blockquote>\n\n\n\n<p>That we would associate with previously-defined ClusterRole and Role:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: rbac.authorization.k8s.io\/v1<br>kind: ClusterRoleBinding<br>metadata:<br>&nbsp;&nbsp;name: cephfs-provisioner<br>subjects:<br>&#8211; kind: ServiceAccount<br>&nbsp;&nbsp;name: cephfs-provisioner<br>&nbsp;&nbsp;namespace: cephfs<br>roleRef:<br>&nbsp;&nbsp;kind: ClusterRole<br>&nbsp;&nbsp;name: cephfs-provisioner<br>&nbsp;&nbsp;apiGroup: rbac.authorization.k8s.io<\/p><p><\/p><\/blockquote>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: rbac.authorization.k8s.io\/v1<br>kind: RoleBinding<br>metadata:<br>&nbsp;&nbsp;name: cephfs-provisioner<br>roleRef:<br>&nbsp;&nbsp;apiGroup: rbac.authorization.k8s.io<br>&nbsp;&nbsp;kind: Role<br>&nbsp;&nbsp;name: cephfs-provisioner<br>subjects:<br>&#8211; kind: ServiceAccount<br>&nbsp;&nbsp;name: cephfs-provisioner<\/p><\/blockquote>\n\n\n\n<p>Next, we would allow our ServiceAccount using the anyuid SecurityContextConstraint:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>$&gt; oc adm policy add-scc-to-user anyuid -z cephfs-provisioner<\/p><\/blockquote>\n\n\n\n<p>Then, we would create an ImageStream:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>$&gt; oc create is cephfs-provisioner<\/p><\/blockquote>\n\n\n\n<p>A BuildConfig patching the cephfs-provisioner image, granting write privileges to owning group, such as OpenShift dynamic users may use our shares:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: v1<br>kind: BuildConfig<br>metadata:<br>&nbsp;&nbsp;name: cephfs-provisioner<br>spec:<br>&nbsp;&nbsp;output:<br>&nbsp;&nbsp;&nbsp;&nbsp;to:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;kind: ImageStreamTag<br>      name: cephfs-provisioner:latest<br>&nbsp;&nbsp;source:<br>&nbsp;&nbsp;&nbsp;&nbsp;dockerfile: |<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FROM quay.io\/external_storage\/cephfs-provisioner:latest<br><br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;USER root<br><br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RUN sed -i &#8216;s|0o755|0o775|g&#8217; \/usr\/lib\/python2.7\/site-packages\/ceph_volume_client.py<br>&nbsp;&nbsp;&nbsp;&nbsp;type: Dockerfile<br>&nbsp;&nbsp;strategy:<br>&nbsp;&nbsp;&nbsp;&nbsp;type: Docker<br>&nbsp;&nbsp;triggers:<br>&nbsp;&nbsp;&#8211; type: ConfigChange<\/p><\/blockquote>\n\n\n\n<p>Next, we would create a StorageClass:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: storage.k8s.io\/v1<br>kind: StorageClass<br>metadata:<br>&nbsp;&nbsp;name: cephfs<br>provisioner: ceph.com\/cephfs<br>parameters:<br>&nbsp;&nbsp;adminId: admin<br>&nbsp;&nbsp;adminSecretName: ceph-secret-admin<br>&nbsp;&nbsp;adminSecretNamespace: cephfs<br>&nbsp;&nbsp;claimRoot: \/kube-volumes<br>&nbsp;&nbsp;monitors: 10.42.253.110:6789,10.42.253.111:6789,10.42.253.112:6789<\/p><\/blockquote>\n\n\n\n<p>And a DeploymentConfig, deploying the CephFS provisioner:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: v1<br>kind: DeploymentConfig<br>metadata:<br>&nbsp;&nbsp;name: cephfs-provisioner<br>spec:<br>&nbsp;&nbsp;replicas: 1<br>&nbsp;&nbsp;strategy:<br>&nbsp;&nbsp;&nbsp;&nbsp;type: Recreate<br>&nbsp;&nbsp;template:<br>&nbsp;&nbsp;&nbsp;&nbsp;metadata:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;labels:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;app: cephfs-provisioner<br>&nbsp;&nbsp;&nbsp;&nbsp;spec:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;containers:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; args: [ &#8220;-id=cephfs-provisioner-1&#8221; ]<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;command: [ &#8220;\/usr\/local\/bin\/cephfs-provisioner&#8221; ]<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;env:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; name: PROVISIONER_NAME<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;value: ceph.com\/cephfs<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; name: PROVISIONER_SECRET_NAMESPACE<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;value: cephfs<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;image: &#8216; &#8216;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;name: cephfs-provisioner<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;serviceAccount: cephfs-provisioner<br>&nbsp;&nbsp;triggers:<br>&nbsp;&nbsp;&#8211; imageChangeParams:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;automatic: true<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;containerNames: [ cephfs-provisioner ]<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;from:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;kind: ImageStreamTag<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;name: cephfs-provisioner:latest<br>&nbsp;&nbsp;&nbsp;&nbsp;type: ImageChange<br>&nbsp;&nbsp;&#8211; type: ConfigChange<\/p><\/blockquote>\n\n\n\n<p>And we should finally be able to create PersistentVolumeClaims, requesting CephFS-backed storage.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: v1<br>kind: PersistentVolumeClaim<br>metadata:<br>&nbsp;&nbsp;name: test-cephfs<br>spec:<br>&nbsp;&nbsp;accessModes: [ ReadWriteMany ]<br>&nbsp;&nbsp;resources:<br>&nbsp;&nbsp;&nbsp;&nbsp;requests:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;storage: 1Gi<br>&nbsp;&nbsp;storageClassName: cephfs<br><\/p><\/blockquote>\n\n\n\n<p>Having registered the previous object, confirm our volume was properly provisioned:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>$&gt; oc get pvc<br>NAME     STATUS  VOLUME  CAPA  ACCESS MODES  STORAGECLASS  AGE<br> test-cephfs   Bound   pvc-xxx      1G        RWX                       cephfs                       5h<\/p><\/blockquote>\n\n\n\n<p>Then, we would create a Pod mounting that volume:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>apiVersion: v1<br>kind: Pod<br>metadata:<br>&nbsp;&nbsp;name: pvc-test-cephfs<br>spec:<br>&nbsp;&nbsp;containers:<br>&nbsp;&nbsp;&#8211; image: docker.io\/centos\/mongodb-34-centos7:latest<br>&nbsp;&nbsp;&nbsp;&nbsp;name: cephfs-rwx<br>&nbsp;&nbsp;&nbsp;&nbsp;securityContext:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;capabilities:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;drop:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; KILL<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; MKNOD<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; SETUID<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; SETGID<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;privileged: false<br>&nbsp;&nbsp;&nbsp;&nbsp;volumeMounts:<br>&nbsp;&nbsp;&nbsp;&nbsp;&#8211; mountPath: \/mnt\/cephfs<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;name: cephfs<br>&nbsp;&nbsp;securityContext:<br>&nbsp;&nbsp;&nbsp;&nbsp;seLinuxOptions:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;level: s0:c23,c2<br>&nbsp;&nbsp;volumes:<br>&nbsp;&nbsp;&#8211; name: cephfs<br>&nbsp;&nbsp;&nbsp;&nbsp;persistentVolumeClaim:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;claimName: test-cephfs-claim<\/p><\/blockquote>\n\n\n\n<p>Once that Pod would have started, we should be able to enter and write our volume:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>$ mount | grep cephfs<br> ceph-fuse on \/mnt\/cephfs type fuse.ceph-fuse (rw,nodev,relatime,user_id=0,group_id=0,allow_other)<br> $ date &gt;\/mnt\/cephfs\/toto<br> $ cat \/mnt\/cephfs\/toto<br> Wed May 15 19:06:20 UTC 2019<\/p><\/blockquote>\n\n\n\n<p><\/p>\n\n\n\n<p>At that point, we should not a non-negligible drawback is the fact the CephFS kernel client doesn&#8217;t seem to allow reading from or writing to shares, from OpenShift Pods. Strangely enough, using a shell on the OpenShift node hosting that Pod, I can successfully write files and open them back. A few months ago, this was not the case: today, it would seem OpenShift is the main responsible, and next thing to fix.<\/p>\n\n\n\n<p>Today, as a workaround, you would have to <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/openshift\/origin\/issues\/21778\" target=\"_blank\">install ceph-fuse<\/a> on all OpenShift nodes. At which point, any CephFS share would be mounted using ceph.fuse, instead of ceph kernel client.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>Bearing in mind that CephFS main concurrent, GlusterFS, also uses a fuse-based client &#8211; while not providing with any kernel implementation &#8211; we can start infering Gluster is living its last days, as the most popular solution offering file-based storage in OpenShift.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you&#8217;re not yet familiar with it, OpenShift is a container orchestration solution based on Kubernetes. Among others, it integrates with several storage providers such as Ceph. Although GlusterFS is probably the best choice in terms of OpenShift integration, we could argue Ceph is a better pick overall. And while this post doesn&#8217;t aim at [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[12,5,13,2],"tags":[],"_links":{"self":[{"href":"https:\/\/blog.unetresgrossebite.com\/index.php?rest_route=\/wp\/v2\/posts\/833"}],"collection":[{"href":"https:\/\/blog.unetresgrossebite.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.unetresgrossebite.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.unetresgrossebite.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.unetresgrossebite.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=833"}],"version-history":[{"count":10,"href":"https:\/\/blog.unetresgrossebite.com\/index.php?rest_route=\/wp\/v2\/posts\/833\/revisions"}],"predecessor-version":[{"id":858,"href":"https:\/\/blog.unetresgrossebite.com\/index.php?rest_route=\/wp\/v2\/posts\/833\/revisions\/858"}],"wp:attachment":[{"href":"https:\/\/blog.unetresgrossebite.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=833"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.unetresgrossebite.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=833"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.unetresgrossebite.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=833"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}