1"use strict";(globalThis.webpackChunkdocumentation=globalThis.webpackChunkdocumentation||[]).push([[4358],{5075(e,a,t){t.r(a),t.d(a,{assets:()=>o,contentTitle:()=>i,default:()=>d,frontMatter:()=>s,metadata:()=>c,toc:()=>r});const c=JSON.parse('{"id":"tutorials/healthcheck_policy_and_update_callbacks","title":"Healthcheck Policy","description":"This document explains how to use two features that are separate yet closely related: OPA healthcheck policy and Data update callbacks.","source":"@site/docs/tutorials/healthcheck_policy_and_update_callbacks.mdx","sourceDirName":"tutorials","slug":"/tutorials/healthcheck_policy_and_update_callbacks","permalink":"/tutorials/healthcheck_policy_and_update_callbacks","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":4,"frontMatter":{"sidebar_position":4,"title":"Healthcheck Policy"},"sidebar":"opalSidebar","previous":{"title":"Configure External Data Sources","permalink":"/tutorials/configure_external_data_sources"},"next":{"title":"Run OPAL as Python Packages","permalink":"/tutorials/install_as_python_packages"}}');var l=t(1684),n=t(506);const s={sidebar_position:4,title:"Healthcheck Policy"},i="How to use data update callbacks and OPA healthcheck policy",o={},r=[{value:"Working example configuration:",id:"working-example-configuration",level:2},{value:"<a></a> OPA healthcheck policy",id:"-opa-healthcheck-policy",level:2},{value:"What is the healthcheck policy?",id:"what-is-the-healthcheck-policy",level:4},{value:"What is the healthcheck policy good for?",id:"what-is-the-healthcheck-policy-good-for",level:4},{value:"How can i activate the OPA healthcheck feature?",id:"how-can-i-activate-the-opa-healthcheck-feature",level:4},{value:"How to query the healthcheck policy?",id:"how-to-query-the-healthcheck-policy",level:4},{value:"Advanced: How does the healthcheck feature work?",id:"advanced-how-does-the-healthcheck-feature-work",level:4},{value:"<a></a> Data update callbacks",id:"-data-update-callbacks",level:2},{value:"What is the update callback feature?",id:"what-is-the-update-callback-feature",level:4},{value:"When should I use update callbacks?",id:"when-should-i-use-update-callbacks",level:4},{value:"How can I activate the update callback feature?",id:"how-can-i-activate-the-update-callback-feature",level:4},{value:"What are the values I can set inside <code>callbacks</code>?",id:"what-are-the-values-i-can-set-inside-callbacks",level:4},{value:"If my update was successful, what is the expected log output?",id:"if-my-update-was-successful-what-is-the-expected-log-output",level:4},{value:"Setting up a one-time callback in the update message",id:"setting-up-a-one-time-callback-in-the-update-message",level:4}];function h(e){const a={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h4:"h4",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,n.R)(),...e.components};return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(a.header,{children:(0,l.jsx)(a.h1,{id:"how-to-use-data-update-callbacks-and-opa-healthcheck-policy",children:"How to use data update callbacks and OPA healthcheck policy"})}),"\n",(0,l.jsxs)(a.p,{children:["This document explains how to use two features that are separate yet closely related: ",(0,l.jsx)(a.a,{href:"#healthcheck",children:"OPA healthcheck policy"})," and ",(0,l.jsx)(a.a,{href:"#callbacks",children:"Data update callbacks"}),"."]}),"\n",(0,l.jsx)(a.h2,{id:"working-example-configuration",children:"Working example configuration:"}),"\n",(0,l.jsxs)(a.p,{children:["You can run the example docker compose configuration ",(0,l.jsx)(a.a,{href:"https://github.com/permitio/opal/blob/master/docker/docker-compose-with-callbacks.yml",children:"found here"})," to run OPAL with callbacks and healthcheck policy already configured correctly."]}),"\n",(0,l.jsx)(a.p,{children:"Run this one command on your machine:"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:"curl -L https://raw.githubusercontent.com/permitio/opal/master/docker/docker-compose-with-callbacks.yml \\\n> docker-compose.yml && docker-compose up\n"})}),"\n",(0,l.jsxs)(a.h2,{id:"-opa-healthcheck-policy",children:[(0,l.jsx)("a",{name:"healthcheck"})," OPA healthcheck policy"]}),"\n",(0,l.jsxs)(a.admonition,{type:"tip",children:[(0,l.jsxs)(a.p,{children:["The OPAL Client also exposes its own HTTP ",(0,l.jsx)(a.code,{children:"GET /healthy"})," endpoint, which is the\nrecommended target for kubernetes liveness probes and supervisors. By default,\nthat endpoint combines:"]}),(0,l.jsxs)(a.ol,{children:["\n",(0,l.jsxs)(a.li,{children:["the success of the last ",(0,l.jsx)(a.code,{children:"OPAL server -> policy-store"})," transaction, ",(0,l.jsx)(a.strong,{children:"and"})]}),"\n",(0,l.jsxs)(a.li,{children:["live reachability of the policy store, sampled in the background by\n",(0,l.jsx)(a.code,{children:"OPAL_POLICY_STORE_
1LIVENESS_PROBE_ENABLED"})," (default ",(0,l.jsx)(a.code,{children:"True"}),",\ninterval ",(0,l.jsx)(a.code,{children:"OPAL_POLICY_STORE_LIVENESS_PROBE_INTERVAL_SECONDS=10"}),",\ntimeout ",(0,l.jsx)(a.code,{children:"OPAL_POLICY_STORE_LIVENESS_PROBE_TIMEOUT_SECONDS=2"}),")."]}),"\n"]}),(0,l.jsxs)(a.p,{children:["This means ",(0,l.jsx)(a.code,{children:"/healthy"})," will report unhealthy within one probe interval if OPA\nhangs, drops its listener, or otherwise becomes unreachable, and recovers\nautomatically once OPA returns \u2014 without any restart of the OPAL Client.\nSee the ",(0,l.jsx)(a.a,{href:"/getting-started/configuration#opal_policy_store_liveness_probe_enabled",children:"configuration reference"}),"."]})]}),"\n",(0,l.jsx)(a.h4,{id:"what-is-the-healthcheck-policy",children:"What is the healthcheck policy?"}),"\n",(0,l.jsxs)(a.p,{children:["A special OPA policy that (if activated) is loaded into OPA as the ",(0,l.jsx)(a.code,{children:"system.opal"})," rego package."]}),"\n",(0,l.jsx)(a.p,{children:"This special policy can be used to make sure that OPA is ready to accept authorization queries, and than its state is not out of sync due to failed data updates."}),"\n",(0,l.jsx)(a.h4,{id:"what-is-the-healthcheck-policy-good-for",children:"What is the healthcheck policy good for?"}),"\n",(0,l.jsxs)(a.p,{children:["You can use this policy as a ",(0,l.jsx)(a.strong,{children:"healthcheck for kubernetes"})," or any similar deployment, before shifting traffic into the new version of the opal-client container (i.e: this container contains the OPA agent) from an older deployment."]}),"\n",(0,l.jsx)(a.h4,{id:"how-can-i-activate-the-opa-healthcheck-feature",children:"How can i activate the OPA healthcheck feature?"}),"\n",(0,l.jsx)(a.p,{children:"Set the following env var:"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:"OPAL_OPA_HEALTH_CHECK_POLICY_ENABLED=True\n"})}),"\n",(0,l.jsxs)(a.p,{children:["You can check out a complete docker-compose configuration that uses this feature ",(0,l.jsx)(a.a,{href:"https://github.com/permitio/opal/blob/master/docker/docker-compose-with-callbacks.yml",children:"here"}),"."]}),"\n",(0,l.jsx)(a.h4,{id:"how-to-query-the-healthcheck-policy",children:"How to query the healthcheck policy?"}),"\n",(0,l.jsx)(a.p,{children:"The healthcheck policy defines two main OPA rules:"}),"\n",(0,l.jsxs)(a.ul,{children:["\n",(0,l.jsxs)(a.li,{children:[(0,l.jsx)(a.code,{children:"ready"}),", checks that:","\n",(0,l.jsxs)(a.ul,{children:["\n",(0,l.jsx)(a.li,{children:"policy was synced correctly at least once"}),"\n",(0,l.jsxs)(a.li,{children:[(0,l.jsx)(a.strong,{children:"and"})," all the initial data sources (defined by ",(0,l.jsx)(a.code,{children:"OPAL_DATA_CONFIG_SOURCES"}),") were synced correctly as well.","\n",(0,l.jsxs)(a.ul,{children:["\n",(0,l.jsxs)(a.li,{children:[(0,l.jsx)(a.strong,{children:"or"})," a following data-update was processed successfully (this part of the behavior is subject to change in upcoming versions)"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,l.jsxs)(a.li,{children:[(0,l.jsx)(a.code,{children:"healthy"}),", checks that:","\n",(0,l.jsxs)(a.ul,{children:["\n",(0,l.jsxs)(a.li,{children:["OPA is ",(0,l.jsx)(a.code,{children:"ready"})," (as defined above)"]}),"\n",(0,l.jsxs)(a.li,{children:[(0,l.jsx)(a.strong,{children:"and"})," latest policy update bundle synced correctly"]}),"\n",(0,l.jsxs)(a.li,{children:[(0,l.jsx)(a.strong,{children:"and"})," last published data update was fetched and synced correctly"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,l.jsxs)(a.p,{children:["You can query the ",(0,l.jsx)(a.code,{children:"ready"})," rule like so:"]}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:"curl --request POST 'http://localhost:8181/v1/data/system/opal/ready'\n"})}),"\n",(0,l.jsx)(a.p,{children:"expected output format:"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{className:"language-json",children:'{\n "result": true\n}\n'})}),"\n",(0,l.jsxs)(a.p,{children:["You can query the ",(0,l.jsx)(a.code,{children:"healthy"})," rule like so (same output format):"]}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:"curl --request POST 'http://localhost:8181/v1/data/system/opal/healthy'\n"})}),"\n",(0,l.jsx)(a.p,{children:"You can also query the entire document (contains latest policy git hash and last successful data update id):"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:"curl --request GET http://localhost:8181/v1/data/system/opal\n"})}),"\n",(0,l.jsx)(a.p,{children:"You'll get something like this as output:"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{className:"language-json",children:'{\n "result": {\n "healthy": true,\n "last_data_transaction": {\n "actions": ["set_policy_data"],\n "error": "",\n "id": "476f290d23964099b5ddd8f10e46873d",\n "success": true\n },\n "last_policy_transaction": {\n "actions": ["set_policies"],\n "error": "",\n "id": "0e45f42f3d7da9b343f5c199934b4bf89a9cacbd",\n "success": true\n },\n "ready": true\n }\n}\n'})}),"\n",(0,l.jsx)(a.h4,{id:"advanced-how-does-the-healthcheck-feature-work",children:"Advanced: How does the healthcheck feature work?"}),"\n",(0,l.jsx)(a.p,{children:"Please note: you don't need to understand this section to use the healthcheck policy. It goes into internal implementation of the feature, to the benefit of the interested reader."}),"\n",(0,l.jsxs)(a.p,{children:["OPAL has an internal OpaClient class (code ",(0,l.jsx)(a.a,{href:"https://github.com/permitio/opal/blob/master/packages/opal-client/opal_client/policy_store/opa_client.py#L140",children:"here"}),") that is used to communicate with the OPA agent via its ",(0,l.jsx)(a.a,{href:"https://www.openpolicyagent.org/docs/latest/rest-api/",children:"REST API"}),". The ",(0,l.jsx)(a.code,{children:"OpaClient"})," class holds a ",(0,l.jsx)(a.code,{children:"OpaTransactionLogState"})," object (code ",(0,l.jsx)(a.a,{href:"https://github.com/permitio/opal/blob/master/packages/opal-client/opal_client/policy_store/opa_client.py#L58",children:"here"}),") that represents (a ",(0,l.jsx)(a.strong,{children:"very"})," simplified version of) the state of synchronization between OPAL client and OPA."]}),"\n",(0,l.jsx)(a.p,{children:"A transaction is initialized in the code using context managers:"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{className:"language-python",children:"async with policy_store.transaction_context(update.id) as store_transaction:\n # do whatever with policy_store, example below:\n await store_transaction.set_policy_data(policy_data, path=policy_store_path)\n"})}),"\n",(0,l.jsxs)(a.p,{children:["Every time a transaction ",(0,l.jsx)(a.a,{href:"https://github.com/permitio/opal/blob/master/packages/opal-client/opal_client/policy_store/base_policy_store_client.py#L116",children:"is ended"})," it is saved into OPA, by rendering the state of ",(0,l.jsx)(a.code,{children:"OpaTransactionLogState"})," using the ",(0,l.jsx)(a.a,{href:"https://github.com/permitio/opal/blob/master/packages/opal-client/opal_client/engine/healthcheck/opal.rego",children:"healthcheck policy template"}),"."]}),"\n",(0,l.jsxs)(a.h2,{id:"-data-update-callbacks",children:[(0,l.jsx)("a",{name:"callbacks"})," Data update callbacks"]}),"\n",(0,l.jsx)(a.h4,{id:"what-is-the-update-callback-feature",children:"What is the update callback feature?"}),"\n",(0,l.jsxs)(a.p,{children:["This feature, if activated, will trigger a callback (HTTP call to a configurable url) after every successful ",(0,l.jsx)(a.a,{href:"trigger_data_updates",children:"data update"}),". It allows you to track which data updates completed successfully and were correctly saved to OPA cache."]}),"\n",(0,l.jsx)(a.h4,{id:"when-should-i-use-update-callbacks",children:"When should I use update callbacks?"}),"\n",(0,l.jsxs)(a.p,{children:["If you are using OPAL to sync your policy agents, you typically have a service (let's call it the ",(0,l.jsx)(a.strong,{children:"update source"})," service) that pushes updates via OPAL server, and OPAL server propagates this update to your fleet of agents."]}),"\n",(0,l.jsxs)(a.p,{children:["If you want your ",(0,l.jsx)(a.strong,{children:"update source"})," service to know that an update was successful (i.e: to resend if failed, to know when you submit bad configuration, etc), you should configure update callbacks."]}),"\n",(0,l.jsx)(a.h4,{id:"how-can-i-activate-the-update-callback-feature",children:"How can I activate the update callback feature?"}),"\n",(0,l.jsx)(a.p,{children:"Set the following env var to turn on the feature:"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:"OPAL_SHOULD_REPORT_ON_DATA_UPDATES=True\n"})}),"\n",(0,l.jsx)(a.p,{children:"Set a default callback (will be called after each successful data update):"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:'OPAL_DEFAULT_UPDATE_CALLBACKS={"callbacks":["http://opal_server:7002/data/callback_report"]}\n'})}),"\n",(0,l.jsxs)(a.p,{children:["You can check out a complete docker-compose configuration that uses this feature ",(0,l.jsx)(a.a,{href:"https://github.com/permitio/opal/blob/master/docker/docker-compose-with-callbacks.yml",children:"here"}),"."]}),"\n",(0,l.jsxs)(a.h4,{id:"what-are-the-values-i-can-set-inside-callbacks",children:["What are the values I can set inside ",(0,l.jsx)(a.code,{children:"callbacks"}),"?"]}),"\n",(0,l.jsxs)(a.p,{children:["As you see, the ",(0,l.jsx)(a.code,{children:"callbacks"})," key is a list; you may define more than one callback url."]}),"\n",(0,l.jsx)(a.p,{children:"Each item in the list can either be a url, or a tuple."}),"\n",(0,l.jsxs)(a.p,{children:["If the item is a url, the configuration defined in ",(0,l.jsx)(a.code,{children:"OPAL_DEFAULT_UPDATE_CALLBACK_CONFIG"})," will be used. By default:"]}),"\n",(0,l.jsxs)(a.ul,{children:["\n",(0,l.jsxs)(a.li,{children:["The HTTP ",(0,l.jsx)(a.code,{children:"POST"})," method will be used."]}),"\n",(0,l.jsxs)(a.li,{children:["The following headers will be used: ",(0,l.jsx)(a.code,{children:'{"content-type": "application/json"}'}),"."]}),"\n"]}),"\n",(0,l.jsx)(a.p,{children:"You may pass a (url, HttpFetcherConfig) tuple instead of a url (i.e: if your callback needs special headers, bearer token, etc.)"}),"\n",(0,l.jsx)(a.p,{children:"For example, you may set a default callback (will be called after each successful data update) that has special headers like this:"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:'OPAL_DEFAULT_UPDATE_CALLBACKS={"callbacks":[("http://opal_server:7002/data/callback_report",{"headers":{"X-My-Token":"token"}})]}\n'})}),"\n",(0,l.jsx)(a.h4,{id:"if-my-update-was-successful-what-is-the-expected-log-output",children:"If my update was successful, what is the expected log output?"}),"\n",(0,l.jsx)(a.p,{children:"After triggering an update via the API, your OPAL server log will look something like this:"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:"opal_server.data.data_update_publisher | INFO | [10] Publishing data update to topics: ['policy_data'], reason: , entries: [('https://api.country.is/23.54.6.78', 'PUT', '/users/bob/location')]\nuvicorn.protocols.http.httptools_impl | INFO | 172.27.0.1:63456 - \"POST /data/config HTTP/1.1\" 200\nfastapi_websocket_pubsub.event_notifier | INFO | calling subscription callbacks for sub_id=0d949d8473824c8280a3ff6ab9146cd0 with topic=policy_data\nfastapi_websocket_pubsub.event_broadc...| INFO | Broadcasting incoming event\nfastapi_websocket_pubsub.event_notifier | INFO | calling subscription callbacks for sub_id=ee10bc3da76444e899c58b861b0079c2 with topic=policy_data\n"})}),"\n",(0,l.jsx)(a.p,{children:"OPAL client will receive the update and will call the callback url (last log line):"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:"opal_client.data.rpc | INFO | Received notification of event: policy_data\nopal_client.data.updater | INFO | Updating policy data, reason:\nopal_client.data.updater | INFO | Triggering data update with id: c83b6862aa354d338d1a9e23794e3efc\nopal_client.data.updater | INFO | Fetching policy data\nopal_client.data.fetcher | INFO | Fetching data from url: https://api.country.is/23.54.6.78\nopal_client.data.updater | INFO | Saving fetched data to policy-store: source url='https://api.country.is/23.54.6.78', destination path='/users/bob/location'\nopal_client.policy_store.opa_client | INFO | processing store transaction: {'id': 'c83b6862aa354d338d1a9e23794e3efc', 'actions': ['set_policy_data'], 'success': True, 'error': ''}\nopal_client.policy_store.opa_client | INFO | persisting health check policy: ready=true, healthy=true\nopal_client.data.updater | INFO | Reporting the update to requested callbacks\nopal_client.data.fetcher | INFO | Fetching data from url: http://opal_server:7002/data/callback_report\n"})}),"\n",(0,l.jsxs)(a.p,{children:["The called-back server will then receive the update:\n(in the example you see here, the OPAL server is the one receiving the callback payload, as was configured in the ",(0,l.jsx)(a.a,{href:"https://github.com/permitio/opal/blob/master/docker/docker-compose-with-callbacks.yml",children:"example config"}),".)"]}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:"opal_server.data.api | INFO | Received update report: {'update_id': 'c83b6862aa354d338d1a9e23794e3efc', 'reports': [{'entry': {'url': 'https://api.country.is/23.54.6.78', 'config': {}, 'topics': ['policy_data'], 'dst_path': '/users/bob/location', 'save_method': 'PUT'}, 'fetched': True, 'saved': True, 'hash': '3eb2f338beb6691bbeeeca60dc3f4afad74ec8c5881f8abe3aa23d57ffa48424'}]}\nuvicorn.protocols.http.httptools_impl | INFO | 172.27.0.4:49720 - \"POST /data/callback_report HTTP/1.1\" 200\n"})}),"\n",(0,l.jsx)(a.h4,{id:"setting-up-a-one-time-callback-in-the-update-message",children:"Setting up a one-time callback in the update message"}),"\n",(0,l.jsxs)(a.p,{children:["When ",(0,l.jsx)(a.a,{href:"trigger_data_updates",children:"triggering an update"})," using the OPAL server REST API, you can pass a callback definition inside the update message, like this:"]}),"\n",(0,l.jsxs)(a.p,{children:["Assuming your opal server is deployed at ",(0,l.jsx)(a.code,{children:"http://my-opal-server.com:7002"}),", you will send a POST request to the ",(0,l.jsx)(a.code,{children:"/data/config"})," route:"]}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{children:"POST http://my-opal-server.com:7002/data/config\n"})}),"\n",(0,l.jsx)(a.p,{children:"You will need to pass the following data in the HTTP PO
1ST request body:"}),"\n",(0,l.jsx)(a.pre,{children:(0,l.jsx)(a.code,{className:"language-json",children:'{\n "entries": [\n ...\n ],\n "callback": {\n "callbacks": [\n [\n "http://opal_server:7002/data/callback_report",\n ]\n ]\n }\n}\n'})})]})}function d(e={}){const{wrapper:a}={...(0,n.R)(),...e.components};return a?(0,l.jsx)(a,{...e,children:(0,l.jsx)(h,{...e})}):h(e)}},506(e,a,t){t.d(a,{R:()=>s,x:()=>i});var c=t(2888);const l={},n=c.createContext(l);function s(e){const a=c.useContext(n);return c.useMemo(function(){return"function"==typeof e?e(a):{...a,...e}},[a,e])}function i(e){let a;return a=e.disableParentContext?"function"==typeof e.components?e.components(l):e.components||l:s(e.components),c.createElement(n.Provider,{value:a},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.