diff --git a/404.html b/404.html index ddaa3e852..23b079fd9 100644 --- a/404.html +++ b/404.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/assets/js/2c09ee5d.41b142bb.js b/assets/js/2c09ee5d.41b142bb.js deleted file mode 100644 index 26b56d57f..000000000 --- a/assets/js/2c09ee5d.41b142bb.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[4194,8401],{9365:(e,t,a)=>{a.d(t,{A:()=>s});var n=a(6540),r=a(53);const o={tabItem:"tabItem_Ymn6"};function s(e){let{children:t,hidden:a,className:s}=e;return n.createElement("div",{role:"tabpanel",className:(0,r.A)(o.tabItem,s),hidden:a},t)}},1470:(e,t,a)=>{a.d(t,{A:()=>k});var n=a(8168),r=a(6540),o=a(53),s=a(3104),l=a(6347),i=a(7485),c=a(1682),u=a(9466);function p(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:a,attributes:n,default:r}}=e;return{value:t,label:a,attributes:n,default:r}}))}function d(e){const{values:t,children:a}=e;return(0,r.useMemo)((()=>{const e=t??p(a);return function(e){const t=(0,c.X)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,a])}function m(e){let{value:t,tabValues:a}=e;return a.some((e=>e.value===t))}function g(e){let{queryString:t=!1,groupId:a}=e;const n=(0,l.W6)(),o=function(e){let{queryString:t=!1,groupId:a}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!a)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return a??null}({queryString:t,groupId:a});return[(0,i.aZ)(o),(0,r.useCallback)((e=>{if(!o)return;const t=new URLSearchParams(n.location.search);t.set(o,e),n.replace({...n.location,search:t.toString()})}),[o,n])]}function h(e){const{defaultValue:t,queryString:a=!1,groupId:n}=e,o=d(e),[s,l]=(0,r.useState)((()=>function(e){let{defaultValue:t,tabValues:a}=e;if(0===a.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!m({value:t,tabValues:a}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${a.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const n=a.find((e=>e.default))??a[0];if(!n)throw new Error("Unexpected error: 0 tabValues");return n.value}({defaultValue:t,tabValues:o}))),[i,c]=g({queryString:a,groupId:n}),[p,h]=function(e){let{groupId:t}=e;const a=function(e){return e?`docusaurus.tab.${e}`:null}(t),[n,o]=(0,u.Dv)(a);return[n,(0,r.useCallback)((e=>{a&&o.set(e)}),[a,o])]}({groupId:n}),y=(()=>{const e=i??p;return m({value:e,tabValues:o})?e:null})();(0,r.useLayoutEffect)((()=>{y&&l(y)}),[y]);return{selectedValue:s,selectValue:(0,r.useCallback)((e=>{if(!m({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);l(e),c(e),h(e)}),[c,h,o]),tabValues:o}}var y=a(2303);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function f(e){let{className:t,block:a,selectedValue:l,selectValue:i,tabValues:c}=e;const u=[],{blockElementScrollPositionUntilNextRender:p}=(0,s.a_)(),d=e=>{const t=e.currentTarget,a=u.indexOf(t),n=c[a].value;n!==l&&(p(t),i(n))},m=e=>{let t=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const a=u.indexOf(e.currentTarget)+1;t=u[a]??u[0];break}case"ArrowLeft":{const a=u.indexOf(e.currentTarget)-1;t=u[a]??u[u.length-1];break}}t?.focus()};return r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.A)("tabs",{"tabs--block":a},t)},c.map((e=>{let{value:t,label:a,attributes:s}=e;return r.createElement("li",(0,n.A)({role:"tab",tabIndex:l===t?0:-1,"aria-selected":l===t,key:t,ref:e=>u.push(e),onKeyDown:m,onClick:d},s,{className:(0,o.A)("tabs__item",b.tabItem,s?.className,{"tabs__item--active":l===t})}),a??t)})))}function v(e){let{lazy:t,children:a,selectedValue:n}=e;const o=(Array.isArray(a)?a:[a]).filter(Boolean);if(t){const e=o.find((e=>e.props.value===n));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return r.createElement("div",{className:"margin-top--md"},o.map(((e,t)=>(0,r.cloneElement)(e,{key:t,hidden:e.props.value!==n}))))}function w(e){const t=h(e);return r.createElement("div",{className:(0,o.A)("tabs-container",b.tabList)},r.createElement(f,(0,n.A)({},e,t)),r.createElement(v,(0,n.A)({},e,t)))}function k(e){const t=(0,y.A)();return r.createElement(w,(0,n.A)({key:String(t)},e))}},1202:(e,t,a)=>{a.d(t,{A:()=>I});var n=a(8168),r=a(6540),o=a(2303),s=a(53),l=a(6058),i=a(7559),c=a(4291);const u={codeBlockContainer:"codeBlockContainer_APcc"};function p(e){let{as:t,...a}=e;const o=(0,l.A)(),p=(0,c.M$)(o);return r.createElement(t,(0,n.A)({},a,{style:p,className:(0,s.A)(a.className,u.codeBlockContainer,i.G.common.codeBlock)}))}const d={codeBlockContent:"codeBlockContent_m3Ux",codeBlockTitle:"codeBlockTitle_P25_",codeBlock:"codeBlock_qGQc",codeBlockStandalone:"codeBlockStandalone_zC50",codeBlockLines:"codeBlockLines_p187",codeBlockLinesWithNumbering:"codeBlockLinesWithNumbering_OFgW",buttonGroup:"buttonGroup_6DOT"};function m(e){let{children:t,className:a}=e;return r.createElement(p,{as:"pre",tabIndex:0,className:(0,s.A)(d.codeBlockStandalone,"thin-scrollbar",a)},r.createElement("code",{className:d.codeBlockLines},t))}var g=a(6342),h=a(6591),y=a(8382);const b={codeLine:"codeLine_iPqp",codeLineNumber:"codeLineNumber_F4P7",codeLineContent:"codeLineContent_pOih"};var f=a(6025);function v(e){let{line:t,classNames:a,showLineNumbers:o,getLineProps:l,getTokenProps:i}=e;1===t.length&&"\n"===t[0].content&&(t[0].content="");const c=l({line:t,className:(0,s.A)(a,o&&b.codeLine)}),u=t.map(((e,t)=>r.createElement("span",(0,n.A)({key:t},i({token:e,key:t})))));return r.createElement("span",c,o?r.createElement(r.Fragment,null,r.createElement("span",{className:b.codeLineNumber}),r.createElement("span",{className:b.codeLineContent},u)):u,r.createElement("br",null))}var w=a(6861),k=a(1312),N=a(1473),C=a(4115);const E={copyButtonCopied:"copyButtonCopied__QnY",copyButtonIcons:"copyButtonIcons_FhaS",copyButtonIcon:"copyButtonIcon_phi_",copyButtonSuccessIcon:"copyButtonSuccessIcon_FfTR"};function S(e){let{code:t,className:a}=e;const[n,o]=(0,r.useState)(!1),l=(0,r.useRef)(void 0),i=(0,r.useCallback)((()=>{(0,w.A)(t),o(!0),l.current=window.setTimeout((()=>{o(!1)}),1e3)}),[t]);return(0,r.useEffect)((()=>()=>window.clearTimeout(l.current)),[]),r.createElement("button",{type:"button","aria-label":n?(0,k.T)({id:"theme.CodeBlock.copied",message:"Copied",description:"The copied button label on code blocks"}):(0,k.T)({id:"theme.CodeBlock.copyButtonAriaLabel",message:"Copy code to clipboard",description:"The ARIA label for copy code blocks button"}),title:(0,k.T)({id:"theme.CodeBlock.copy",message:"Copy",description:"The copy button label on code blocks"}),className:(0,s.A)("clean-btn",a,E.copyButton,n&&E.copyButtonCopied),onClick:i},r.createElement("span",{className:E.copyButtonIcons,"aria-hidden":"true"},r.createElement(N.A,{className:E.copyButtonIcon}),r.createElement(C.A,{className:E.copyButtonSuccessIcon})))}var T=a(5048);const L={wordWrapButtonIcon:"wordWrapButtonIcon_iowe",wordWrapButtonEnabled:"wordWrapButtonEnabled_gY8A"};function B(e){let{className:t,onClick:a,isEnabled:n}=e;const o=(0,k.T)({id:"theme.CodeBlock.wordWrapToggle",message:"Toggle word wrap",description:"The title attribute for toggle word wrapping button of code block lines"});return r.createElement("button",{type:"button",onClick:a,className:(0,s.A)("clean-btn",t,n&&L.wordWrapButtonEnabled),"aria-label":o,title:o},r.createElement(T.A,{className:L.wordWrapButtonIcon,"aria-hidden":"true"}))}function A(e){let{children:t,className:a="",metastring:o,title:i,showLineNumbers:u,language:m}=e;const{prism:{defaultLanguage:b,magicComments:w}}=(0,g.p)(),k=m??(0,c.Op)(a)??b,N=(0,l.A)(),C=(0,h.f)(),E=(0,c.wt)(o)||i,{lineClassNames:T,code:L}=(0,c.Li)(t,{metastring:o,language:k,magicComments:w}),A=(0,f.A)("/",{absolute:!0}).slice(0,-1),I=L.replaceAll("${ABSOLUTE_URL}",A),z=u??(0,c._u)(o);return r.createElement(p,{as:"div",className:(0,s.A)(a,k&&!a.includes(`language-${k}`)&&`language-${k}`)},E&&r.createElement("div",{className:d.codeBlockTitle},E),r.createElement("div",{className:d.codeBlockContent},r.createElement(y.Ay,(0,n.A)({},y.Gs,{theme:N,code:I,language:k??"text"}),(e=>{let{className:t,tokens:a,getLineProps:n,getTokenProps:o}=e;return r.createElement("pre",{tabIndex:0,ref:C.codeBlockRef,className:(0,s.A)(t,d.codeBlock,"thin-scrollbar")},r.createElement("code",{className:(0,s.A)(d.codeBlockLines,z&&d.codeBlockLinesWithNumbering)},a.map(((e,t)=>r.createElement(v,{key:t,line:e,getLineProps:n,getTokenProps:o,classNames:T[t],showLineNumbers:z})))))})),r.createElement("div",{className:d.buttonGroup},(C.isEnabled||C.isCodeScrollable)&&r.createElement(B,{className:d.codeButton,onClick:()=>C.toggle(),isEnabled:C.isEnabled}),r.createElement(S,{className:d.codeButton,code:I}))))}function I(e){let{children:t,...a}=e;const s=(0,o.A)(),l=function(e){return r.Children.toArray(e).some((e=>(0,r.isValidElement)(e)))?e:Array.isArray(e)?e.join(""):e}(t),i="string"==typeof l?A:m;return r.createElement(i,(0,n.A)({key:String(s)},a),l)}},9411:(e,t,a)=>{a.r(t),a.d(t,{Terminal:()=>u,assets:()=>i,contentTitle:()=>s,default:()=>m,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var n=a(8168),r=(a(6540),a(5680));a(1202),a(1470),a(9365);const o={sidebar_position:2,title:"Just-in-time PostgreSQL access",image:"/img/quick-tutorials/postgres/social.png"},s=void 0,l={unversionedId:"features/postgresql/tutorials/postgres",id:"features/postgresql/tutorials/postgres",title:"Just-in-time PostgreSQL access",description:"This tutorial will deploy an example cluster to highlight Otterize's PostgreSQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:",source:"@site/docs/features/postgresql/tutorials/postgres.mdx",sourceDirName:"features/postgresql/tutorials",slug:"/features/postgresql/tutorials/postgres",permalink:"/features/postgresql/tutorials/postgres",draft:!1,editUrl:"https://github.com/otterize/docs/edit/main/docs/features/postgresql/tutorials/postgres.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{sidebar_position:2,title:"Just-in-time PostgreSQL access",image:"/img/quick-tutorials/postgres/social.png"},sidebar:"docSidebar",previous:{title:"PostgreSQL | Overview",permalink:"/features/postgresql/"},next:{title:"PostgreSQL table-level access mapping",permalink:"/features/postgresql/tutorials/postgres-mapping"}},i={},c=[{value:"1. Minikube Cluster",id:"1-minikube-cluster",level:4},{value:"2. Deploy Otterize",id:"2-deploy-otterize",level:4},{value:"Deploy tutorial services and request database credentials",id:"deploy-tutorial-services-and-request-database-credentials",level:3},{value:"Deploy a PostgreSQLServerConfig to allow Otterize DB access",id:"deploy-a-postgresqlserverconfig-to-allow-otterize-db-access",level:3},{value:"View logs for the server",id:"view-logs-for-the-server",level:3},{value:"Define your ClientIntents",id:"define-your-clientintents",level:3},{value:"View logs for the server",id:"view-logs-for-the-server-1",level:3}],u=e=>{let{children:t}=e;return(0,r.yg)("div",{style:{backgroundColor:"#eee",borderRadius:"5px",fontSize:"12px",fontWeight:"600",color:"darkgreen",padding:"1rem",fontFamily:"monospace, monospace"}},t)},p={toc:c,Terminal:u},d="wrapper";function m(e){let{components:t,...a}=e;return(0,r.yg)(d,(0,n.A)({},p,a,{components:t,mdxType:"MDXLayout"}),(0,r.yg)("h1",{id:"overview"},"Overview"),(0,r.yg)("p",null,"This tutorial will deploy an example cluster to highlight Otterize's PostgreSQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:"),(0,r.yg)("ol",null,(0,r.yg)("li",{parentName:"ol"},"An ",(0,r.yg)("inlineCode",{parentName:"li"},"INSERT")," operation to append a table within the database"),(0,r.yg)("li",{parentName:"ol"},"A ",(0,r.yg)("inlineCode",{parentName:"li"},"SELECT")," operation to validate the updates.")),(0,r.yg)("p",null,"The server needs appropriate permissions to access the database. You could use one admin user for all services, which is insecure and is the cause for many security breaches. With Otterize, you can specify required access, and have Otterize create users and perform correctly scoped SQL GRANTs just in time, as the service spins up and down."),(0,r.yg)("p",null,"In this tutorial, we will:"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},"Deploy an example cluster"),(0,r.yg)("li",{parentName:"ul"},"Deploy Otterize in our cluster and give it access to our database instance"),(0,r.yg)("li",{parentName:"ul"},"Declare a ClientIntents resource for the server, specifying required access"),(0,r.yg)("li",{parentName:"ul"},"See that the required access has been granted")),(0,r.yg)("h1",{id:"prerequisites"},"Prerequisites"),(0,r.yg)("h4",{id:"1-minikube-cluster"},"1. Minikube Cluster"),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Prepare a Kubernetes cluster with Minikube"),(0,r.yg)("p",null,"For this tutorial you'll need a local Kubernetes cluster. Having a cluster with a ",(0,r.yg)("a",{parentName:"p",href:"https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/"},"CNI")," that supports ",(0,r.yg)("a",{parentName:"p",href:"https://kubernetes.io/docs/concepts/services-networking/network-policies/"},"NetworkPolicies")," isn't required for this tutorial, but is recommended so that your cluster works with other tutorials."),(0,r.yg)("p",null,"If you don't have the Minikube CLI, first ",(0,r.yg)("a",{parentName:"p",href:"https://minikube.sigs.k8s.io/docs/start/"},"install it"),"."),(0,r.yg)("p",null,"Then start your Minikube cluster with Calico, in order to enforce network policies."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"minikube start --cpus=4 --memory 4096 --disk-size 32g --cni=calico\n"))),(0,r.yg)("h4",{id:"2-deploy-otterize"},"2. Deploy Otterize"),(0,r.yg)("p",null,"To deploy Otterize, head over to ",(0,r.yg)("a",{parentName:"p",href:"https://app.otterize.com"},"Otterize Cloud")," and associate a Kubernetes cluster on the ",(0,r.yg)("a",{parentName:"p",href:"https://app.otterize.com/integrations"},"Integrations page"),", and follow the instructions. If you already have a Kubernetes cluster connected, skip this step."),(0,r.yg)("h1",{id:"tutorial"},"Tutorial"),(0,r.yg)("h3",{id:"deploy-tutorial-services-and-request-database-credentials"},"Deploy tutorial services and request database credentials"),(0,r.yg)("p",null,"This will set up the namespace we will use for our tutorial and deploy the client, server, and database."),(0,r.yg)("p",null,"Our server's Deployment spec will specify an annotation on the Pod, which requests that the Otterize operator will provision a username and password for the server."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"}," template:\n metadata:\n annotations:\n credentials-operator.otterize.com/user-password-secret-name: server-creds\n")),(0,r.yg)("p",null,"This specifies that the secret ",(0,r.yg)("inlineCode",{parentName:"p"},"server-creds")," will have keys with the username and password to connect to the database.\nThe secret will only be created by the Otterize operator after it is integrated with your database by applying a MySQLServerConfig resources."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl create namespace otterize-tutorial-postgres\nkubectl apply -n otterize-tutorial-postgres -f ${ABSOLUTE_URL}/code-examples/postgres/client-server-database.yaml\n")),(0,r.yg)("h3",{id:"deploy-a-postgresqlserverconfig-to-allow-otterize-db-access"},"Deploy a PostgreSQLServerConfig to allow Otterize DB access"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: k8s.otterize.com/v1alpha3\nkind: PostgreSQLServerConfig\nmetadata:\n name: postgres-tutorial-db\nspec:\n address: database.otterize-tutorial-postgres.svc.cluster.local:5432\n credentials:\n secretRef:\n name: postgres-tutorial-db-credentials\n---\napiVersion: v1\ntype: Opaque\nkind: Secret\nmetadata:\n name: postgres-tutorial-db-credentials\ndata:\n username: '' # Your PostgreSQL server user\n password: '' # Your PostgreSQL server password\n")),(0,r.yg)("p",null,"The above CRD tells Otterize how to access a database instance named ",(0,r.yg)("inlineCode",{parentName:"p"},"postgres-tutorial-db"),", meaning that when intents\nare applied requesting access permissions to ",(0,r.yg)("inlineCode",{parentName:"p"},"postgres-tutorial-db"),", the Otterize operator will be able to configure\nthem."),(0,r.yg)("p",null,"In this tutorial, the ",(0,r.yg)("inlineCode",{parentName:"p"},"database")," workload already comes with the predefined username & password, but for future uses a\nrole will have to be created in the database to grant Otterize access as well as the ability to configure other users."),(0,r.yg)("p",null,"Let's apply the above ",(0,r.yg)("inlineCode",{parentName:"p"},"PostgreSQLServerConfig")," so Otterize will know how to access our database instance."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},'kubectl apply -n otterize-tutorial-postgres -f ${ABSOLUTE_URL}/code-examples/postgres/postgresqlserverconfig.yaml\nPSQLUSER_B64=$(echo -n otterize-tutorial | base64)\nPSQLPASSWORD_B64=$(echo -n jeffdog523 | base64)\nkubectl patch secret -n otterize-tutorial-postgres postgres-tutorial-db-credentials --type=\'json\' -p="[{\\"op\\": \\"replace\\", \\"path\\": \\"/data/username\\", \\"value\\": \\"$PSQLUSER_B64\\"}, {\\"op\\": \\"replace\\", \\"path\\": \\"/data/password\\", \\"value\\": \\"$PSQLPASSWORD_B64\\"}]"\n')),(0,r.yg)("h3",{id:"view-logs-for-the-server"},"View logs for the server"),(0,r.yg)("p",null,"After the client, server, and database are up and running, we can see that the server does not have the appropriate access to the database by inspecting the logs with the following command."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl logs -f -n otterize-tutorial-postgres deploy/server\n")),(0,r.yg)("p",null,"Example log:"),(0,r.yg)(u,{mdxType:"Terminal"},"Unable to perform INSERT operation",(0,r.yg)("br",null),"Unable to perform SELECT operation"),(0,r.yg)("h3",{id:"define-your-clientintents"},"Define your ClientIntents"),(0,r.yg)("p",null,"ClientIntents are Otterize\u2019s way of defining access through unique relationships, which lead to perfectly scoped access. In this example, we provide our ",(0,r.yg)("inlineCode",{parentName:"p"},"server")," workload the ability to insert and select records to allow it to access the database."),(0,r.yg)("p",null,"Below is our ",(0,r.yg)("inlineCode",{parentName:"p"},"intents.yaml")," file. As you can see, it is scoped to our database named ",(0,r.yg)("inlineCode",{parentName:"p"},"otterize-tutorial")," and our ",(0,r.yg)("inlineCode",{parentName:"p"},"public.example")," table. We also have limited the access to just ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT")," operations. We could add more databases, tables, or operations if our service required more access."),(0,r.yg)("p",null,"Specifying the table and operations is optional. If you don't specify the table, access will be granted to all tables in the specified database. If you don't specify the operations, all operations will be allowed."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: k8s.otterize.com/v1alpha3\nkind: ClientIntents\nmetadata:\n name: client-intents-for-server\n namespace: otterize-tutorial-postgres\nspec:\n service:\n name: server\n calls:\n - name: postgres-tutorial-db # Same name as our PostgreSQLServerConfig metadata.name\n type: database\n databaseResources:\n - databaseName: otterize-tutorial\n table: public.example\n operations:\n - SELECT\n - INSERT\n")),(0,r.yg)("p",null,"We can now apply our intents. Behind the scenes, the Otterize operator created the user for our ",(0,r.yg)("inlineCode",{parentName:"p"},"server")," workload and executed ",(0,r.yg)("inlineCode",{parentName:"p"},"GRANT")," queries on the database, making our ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT")," errors disappear."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl apply -n otterize-tutorial-postgres -f ${ABSOLUTE_URL}/code-examples/postgres/clientintents.yaml\n")),(0,r.yg)("h3",{id:"view-logs-for-the-server-1"},"View logs for the server"),(0,r.yg)("p",null,"We can now view the server logs once again. This time, we should see that the server has the appropriate access to the database:"),(0,r.yg)(u,{mdxType:"Terminal"},"Successfully INSERTED into our table",(0,r.yg)("p",null,"Successfully SELECTED, most recent value: 2024-04-30T13:20:46Z")),(0,r.yg)("p",null,"That\u2019s it! If your service\u2019s functionality changes, adding or removing access is as simple as updating your ClientIntents definitions. For fun, try altering the ",(0,r.yg)("inlineCode",{parentName:"p"},"operations")," to just ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," or ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT"),"."),(0,r.yg)("h1",{id:"teardown"},"Teardown"),(0,r.yg)("p",null,"To remove the deployed examples, run:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl delete clientintents.k8s.otterize.com -n otterize-tutorial-postgres client-intents-for-server && \\\nkubectl delete namespace otterize-tutorial-postgres\n")))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/2c09ee5d.cccc630c.js b/assets/js/2c09ee5d.cccc630c.js new file mode 100644 index 000000000..e8e931edd --- /dev/null +++ b/assets/js/2c09ee5d.cccc630c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[4194,8401],{9365:(e,t,a)=>{a.d(t,{A:()=>o});var n=a(6540),r=a(53);const s={tabItem:"tabItem_Ymn6"};function o(e){let{children:t,hidden:a,className:o}=e;return n.createElement("div",{role:"tabpanel",className:(0,r.A)(s.tabItem,o),hidden:a},t)}},1470:(e,t,a)=>{a.d(t,{A:()=>N});var n=a(8168),r=a(6540),s=a(53),o=a(3104),l=a(6347),i=a(7485),c=a(1682),u=a(9466);function p(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:a,attributes:n,default:r}}=e;return{value:t,label:a,attributes:n,default:r}}))}function d(e){const{values:t,children:a}=e;return(0,r.useMemo)((()=>{const e=t??p(a);return function(e){const t=(0,c.X)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,a])}function m(e){let{value:t,tabValues:a}=e;return a.some((e=>e.value===t))}function g(e){let{queryString:t=!1,groupId:a}=e;const n=(0,l.W6)(),s=function(e){let{queryString:t=!1,groupId:a}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!a)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return a??null}({queryString:t,groupId:a});return[(0,i.aZ)(s),(0,r.useCallback)((e=>{if(!s)return;const t=new URLSearchParams(n.location.search);t.set(s,e),n.replace({...n.location,search:t.toString()})}),[s,n])]}function y(e){const{defaultValue:t,queryString:a=!1,groupId:n}=e,s=d(e),[o,l]=(0,r.useState)((()=>function(e){let{defaultValue:t,tabValues:a}=e;if(0===a.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!m({value:t,tabValues:a}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${a.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const n=a.find((e=>e.default))??a[0];if(!n)throw new Error("Unexpected error: 0 tabValues");return n.value}({defaultValue:t,tabValues:s}))),[i,c]=g({queryString:a,groupId:n}),[p,y]=function(e){let{groupId:t}=e;const a=function(e){return e?`docusaurus.tab.${e}`:null}(t),[n,s]=(0,u.Dv)(a);return[n,(0,r.useCallback)((e=>{a&&s.set(e)}),[a,s])]}({groupId:n}),h=(()=>{const e=i??p;return m({value:e,tabValues:s})?e:null})();(0,r.useLayoutEffect)((()=>{h&&l(h)}),[h]);return{selectedValue:o,selectValue:(0,r.useCallback)((e=>{if(!m({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);l(e),c(e),y(e)}),[c,y,s]),tabValues:s}}var h=a(2303);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function f(e){let{className:t,block:a,selectedValue:l,selectValue:i,tabValues:c}=e;const u=[],{blockElementScrollPositionUntilNextRender:p}=(0,o.a_)(),d=e=>{const t=e.currentTarget,a=u.indexOf(t),n=c[a].value;n!==l&&(p(t),i(n))},m=e=>{let t=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const a=u.indexOf(e.currentTarget)+1;t=u[a]??u[0];break}case"ArrowLeft":{const a=u.indexOf(e.currentTarget)-1;t=u[a]??u[u.length-1];break}}t?.focus()};return r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.A)("tabs",{"tabs--block":a},t)},c.map((e=>{let{value:t,label:a,attributes:o}=e;return r.createElement("li",(0,n.A)({role:"tab",tabIndex:l===t?0:-1,"aria-selected":l===t,key:t,ref:e=>u.push(e),onKeyDown:m,onClick:d},o,{className:(0,s.A)("tabs__item",b.tabItem,o?.className,{"tabs__item--active":l===t})}),a??t)})))}function v(e){let{lazy:t,children:a,selectedValue:n}=e;const s=(Array.isArray(a)?a:[a]).filter(Boolean);if(t){const e=s.find((e=>e.props.value===n));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return r.createElement("div",{className:"margin-top--md"},s.map(((e,t)=>(0,r.cloneElement)(e,{key:t,hidden:e.props.value!==n}))))}function w(e){const t=y(e);return r.createElement("div",{className:(0,s.A)("tabs-container",b.tabList)},r.createElement(f,(0,n.A)({},e,t)),r.createElement(v,(0,n.A)({},e,t)))}function N(e){const t=(0,h.A)();return r.createElement(w,(0,n.A)({key:String(t)},e))}},1202:(e,t,a)=>{a.d(t,{A:()=>I});var n=a(8168),r=a(6540),s=a(2303),o=a(53),l=a(6058),i=a(7559),c=a(4291);const u={codeBlockContainer:"codeBlockContainer_APcc"};function p(e){let{as:t,...a}=e;const s=(0,l.A)(),p=(0,c.M$)(s);return r.createElement(t,(0,n.A)({},a,{style:p,className:(0,o.A)(a.className,u.codeBlockContainer,i.G.common.codeBlock)}))}const d={codeBlockContent:"codeBlockContent_m3Ux",codeBlockTitle:"codeBlockTitle_P25_",codeBlock:"codeBlock_qGQc",codeBlockStandalone:"codeBlockStandalone_zC50",codeBlockLines:"codeBlockLines_p187",codeBlockLinesWithNumbering:"codeBlockLinesWithNumbering_OFgW",buttonGroup:"buttonGroup_6DOT"};function m(e){let{children:t,className:a}=e;return r.createElement(p,{as:"pre",tabIndex:0,className:(0,o.A)(d.codeBlockStandalone,"thin-scrollbar",a)},r.createElement("code",{className:d.codeBlockLines},t))}var g=a(6342),y=a(6591),h=a(8382);const b={codeLine:"codeLine_iPqp",codeLineNumber:"codeLineNumber_F4P7",codeLineContent:"codeLineContent_pOih"};var f=a(6025);function v(e){let{line:t,classNames:a,showLineNumbers:s,getLineProps:l,getTokenProps:i}=e;1===t.length&&"\n"===t[0].content&&(t[0].content="");const c=l({line:t,className:(0,o.A)(a,s&&b.codeLine)}),u=t.map(((e,t)=>r.createElement("span",(0,n.A)({key:t},i({token:e,key:t})))));return r.createElement("span",c,s?r.createElement(r.Fragment,null,r.createElement("span",{className:b.codeLineNumber}),r.createElement("span",{className:b.codeLineContent},u)):u,r.createElement("br",null))}var w=a(6861),N=a(1312),k=a(1473),C=a(4115);const E={copyButtonCopied:"copyButtonCopied__QnY",copyButtonIcons:"copyButtonIcons_FhaS",copyButtonIcon:"copyButtonIcon_phi_",copyButtonSuccessIcon:"copyButtonSuccessIcon_FfTR"};function S(e){let{code:t,className:a}=e;const[n,s]=(0,r.useState)(!1),l=(0,r.useRef)(void 0),i=(0,r.useCallback)((()=>{(0,w.A)(t),s(!0),l.current=window.setTimeout((()=>{s(!1)}),1e3)}),[t]);return(0,r.useEffect)((()=>()=>window.clearTimeout(l.current)),[]),r.createElement("button",{type:"button","aria-label":n?(0,N.T)({id:"theme.CodeBlock.copied",message:"Copied",description:"The copied button label on code blocks"}):(0,N.T)({id:"theme.CodeBlock.copyButtonAriaLabel",message:"Copy code to clipboard",description:"The ARIA label for copy code blocks button"}),title:(0,N.T)({id:"theme.CodeBlock.copy",message:"Copy",description:"The copy button label on code blocks"}),className:(0,o.A)("clean-btn",a,E.copyButton,n&&E.copyButtonCopied),onClick:i},r.createElement("span",{className:E.copyButtonIcons,"aria-hidden":"true"},r.createElement(k.A,{className:E.copyButtonIcon}),r.createElement(C.A,{className:E.copyButtonSuccessIcon})))}var T=a(5048);const L={wordWrapButtonIcon:"wordWrapButtonIcon_iowe",wordWrapButtonEnabled:"wordWrapButtonEnabled_gY8A"};function A(e){let{className:t,onClick:a,isEnabled:n}=e;const s=(0,N.T)({id:"theme.CodeBlock.wordWrapToggle",message:"Toggle word wrap",description:"The title attribute for toggle word wrapping button of code block lines"});return r.createElement("button",{type:"button",onClick:a,className:(0,o.A)("clean-btn",t,n&&L.wordWrapButtonEnabled),"aria-label":s,title:s},r.createElement(T.A,{className:L.wordWrapButtonIcon,"aria-hidden":"true"}))}function B(e){let{children:t,className:a="",metastring:s,title:i,showLineNumbers:u,language:m}=e;const{prism:{defaultLanguage:b,magicComments:w}}=(0,g.p)(),N=m??(0,c.Op)(a)??b,k=(0,l.A)(),C=(0,y.f)(),E=(0,c.wt)(s)||i,{lineClassNames:T,code:L}=(0,c.Li)(t,{metastring:s,language:N,magicComments:w}),B=(0,f.A)("/",{absolute:!0}).slice(0,-1),I=L.replaceAll("${ABSOLUTE_URL}",B),z=u??(0,c._u)(s);return r.createElement(p,{as:"div",className:(0,o.A)(a,N&&!a.includes(`language-${N}`)&&`language-${N}`)},E&&r.createElement("div",{className:d.codeBlockTitle},E),r.createElement("div",{className:d.codeBlockContent},r.createElement(h.Ay,(0,n.A)({},h.Gs,{theme:k,code:I,language:N??"text"}),(e=>{let{className:t,tokens:a,getLineProps:n,getTokenProps:s}=e;return r.createElement("pre",{tabIndex:0,ref:C.codeBlockRef,className:(0,o.A)(t,d.codeBlock,"thin-scrollbar")},r.createElement("code",{className:(0,o.A)(d.codeBlockLines,z&&d.codeBlockLinesWithNumbering)},a.map(((e,t)=>r.createElement(v,{key:t,line:e,getLineProps:n,getTokenProps:s,classNames:T[t],showLineNumbers:z})))))})),r.createElement("div",{className:d.buttonGroup},(C.isEnabled||C.isCodeScrollable)&&r.createElement(A,{className:d.codeButton,onClick:()=>C.toggle(),isEnabled:C.isEnabled}),r.createElement(S,{className:d.codeButton,code:I}))))}function I(e){let{children:t,...a}=e;const o=(0,s.A)(),l=function(e){return r.Children.toArray(e).some((e=>(0,r.isValidElement)(e)))?e:Array.isArray(e)?e.join(""):e}(t),i="string"==typeof l?B:m;return r.createElement(i,(0,n.A)({key:String(o)},a),l)}},9411:(e,t,a)=>{a.r(t),a.d(t,{Terminal:()=>u,assets:()=>i,contentTitle:()=>o,default:()=>m,frontMatter:()=>s,metadata:()=>l,toc:()=>c});var n=a(8168),r=(a(6540),a(5680));a(1202),a(1470),a(9365);const s={sidebar_position:2,title:"Just-in-time PostgreSQL access",image:"/img/quick-tutorials/postgres/social.png"},o=void 0,l={unversionedId:"features/postgresql/tutorials/postgres",id:"features/postgresql/tutorials/postgres",title:"Just-in-time PostgreSQL access",description:"This tutorial will deploy an example cluster to highlight Otterize's PostgreSQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:",source:"@site/docs/features/postgresql/tutorials/postgres.mdx",sourceDirName:"features/postgresql/tutorials",slug:"/features/postgresql/tutorials/postgres",permalink:"/features/postgresql/tutorials/postgres",draft:!1,editUrl:"https://github.com/otterize/docs/edit/main/docs/features/postgresql/tutorials/postgres.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{sidebar_position:2,title:"Just-in-time PostgreSQL access",image:"/img/quick-tutorials/postgres/social.png"},sidebar:"docSidebar",previous:{title:"PostgreSQL | Overview",permalink:"/features/postgresql/"},next:{title:"PostgreSQL table-level access mapping",permalink:"/features/postgresql/tutorials/postgres-mapping"}},i={},c=[{value:"1. Minikube Cluster",id:"1-minikube-cluster",level:4},{value:"2. Deploy Otterize",id:"2-deploy-otterize",level:4},{value:"Deploy tutorial services and request database credentials",id:"deploy-tutorial-services-and-request-database-credentials",level:3},{value:"View logs for the server",id:"view-logs-for-the-server",level:3},{value:"Deploy a PostgreSQLServerConfig to allow Otterize DB access",id:"deploy-a-postgresqlserverconfig-to-allow-otterize-db-access",level:3},{value:"Define your ClientIntents",id:"define-your-clientintents",level:3},{value:"View logs for the server",id:"view-logs-for-the-server-1",level:3}],u=e=>{let{children:t}=e;return(0,r.yg)("div",{style:{backgroundColor:"#eee",borderRadius:"5px",fontSize:"12px",fontWeight:"600",color:"darkgreen",padding:"1rem",fontFamily:"monospace, monospace"}},t)},p={toc:c,Terminal:u},d="wrapper";function m(e){let{components:t,...a}=e;return(0,r.yg)(d,(0,n.A)({},p,a,{components:t,mdxType:"MDXLayout"}),(0,r.yg)("h1",{id:"overview"},"Overview"),(0,r.yg)("p",null,"This tutorial will deploy an example cluster to highlight Otterize's PostgreSQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:"),(0,r.yg)("ol",null,(0,r.yg)("li",{parentName:"ol"},"An ",(0,r.yg)("inlineCode",{parentName:"li"},"INSERT")," operation to append a table within the database"),(0,r.yg)("li",{parentName:"ol"},"A ",(0,r.yg)("inlineCode",{parentName:"li"},"SELECT")," operation to validate the updates.")),(0,r.yg)("p",null,"The server needs appropriate permissions to access the database. You could use one admin user for all services, which is insecure and is the cause for many security breaches. With Otterize, you can specify required access, and have Otterize create users and perform correctly scoped SQL GRANTs just in time, as the service spins up and down."),(0,r.yg)("p",null,"In this tutorial, we will:"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},"Deploy an example cluster"),(0,r.yg)("li",{parentName:"ul"},"Deploy Otterize in our cluster and give it access to our database instance"),(0,r.yg)("li",{parentName:"ul"},"Declare a ClientIntents resource for the server, specifying required access"),(0,r.yg)("li",{parentName:"ul"},"See that the required access has been granted")),(0,r.yg)("h1",{id:"prerequisites"},"Prerequisites"),(0,r.yg)("h4",{id:"1-minikube-cluster"},"1. Minikube Cluster"),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Prepare a Kubernetes cluster with Minikube"),(0,r.yg)("p",null,"For this tutorial you'll need a local Kubernetes cluster. Having a cluster with a ",(0,r.yg)("a",{parentName:"p",href:"https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/"},"CNI")," that supports ",(0,r.yg)("a",{parentName:"p",href:"https://kubernetes.io/docs/concepts/services-networking/network-policies/"},"NetworkPolicies")," isn't required for this tutorial, but is recommended so that your cluster works with other tutorials."),(0,r.yg)("p",null,"If you don't have the Minikube CLI, first ",(0,r.yg)("a",{parentName:"p",href:"https://minikube.sigs.k8s.io/docs/start/"},"install it"),"."),(0,r.yg)("p",null,"Then start your Minikube cluster with Calico, in order to enforce network policies."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"minikube start --cpus=4 --memory 4096 --disk-size 32g --cni=calico\n"))),(0,r.yg)("h4",{id:"2-deploy-otterize"},"2. Deploy Otterize"),(0,r.yg)("p",null,"To deploy Otterize, head over to ",(0,r.yg)("a",{parentName:"p",href:"https://app.otterize.com"},"Otterize Cloud")," and associate a Kubernetes cluster on the ",(0,r.yg)("a",{parentName:"p",href:"https://app.otterize.com/integrations"},"Integrations page"),", and follow the instructions. If you already have a Kubernetes cluster connected, skip this step."),(0,r.yg)("h1",{id:"tutorial"},"Tutorial"),(0,r.yg)("h3",{id:"deploy-tutorial-services-and-request-database-credentials"},"Deploy tutorial services and request database credentials"),(0,r.yg)("p",null,"Next, set up the namespace used for our tutorial and deploy the client, server & database services in it:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl create namespace otterize-tutorial-postgres\nkubectl apply -n otterize-tutorial-postgres -f ${ABSOLUTE_URL}/code-examples/postgres/client-server-database.yaml\n")),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Expand to see the deployment YAML"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: server\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: server\n template:\n metadata:\n annotations:\n credentials-operator.otterize.com/user-password-secret-name: server-creds\n labels:\n app: server\n spec:\n serviceAccountName: server\n containers:\n - name: server\n imagePullPolicy: Always\n image: 'otterize/postgres-tutorial-server'\n ports:\n - containerPort: 80\n env:\n - name: DB_SERVER_USER\n valueFrom:\n secretKeyRef:\n name: server-creds\n key: username\n - name: DB_SERVER_PASSWORD\n valueFrom:\n secretKeyRef:\n name: server-creds\n key: password\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: server\nspec:\n type: ClusterIP\n selector:\n app: server\n ports:\n - name: http\n port: 80\n targetPort: 80\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: server\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: client\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: client\n template:\n metadata:\n labels:\n app: client\n spec:\n containers:\n - name: client\n imagePullPolicy: Always\n image: 'otterize/postgres-tutorial-client'\n ports:\n - containerPort: 80\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: database\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: database\n template:\n metadata:\n labels:\n app: database\n spec:\n containers:\n - name: database\n imagePullPolicy: Always\n image: 'otterize/postgres-tutorial-database'\n ports:\n - containerPort: 5432\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: database\nspec:\n selector:\n app: database\n ports:\n - protocol: TCP\n port: 5432\n targetPort: 5432\n"))),(0,r.yg)("p",null,"Our server's Deployment spec specifies an annotation on its Pod, which requests that the Otterize operator provision a username and password for it:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"}," template:\n metadata:\n annotations:\n credentials-operator.otterize.com/user-password-secret-name: server-creds\n")),(0,r.yg)("p",null,"This specifies that the secret ",(0,r.yg)("inlineCode",{parentName:"p"},"server-creds")," will be populated with keys containing the username and password used by this pod to connect to the database.\nThe secret will only be created by the Otterize operator after it is integrated with your database by applying a ",(0,r.yg)("inlineCode",{parentName:"p"},"PostgreSQLServerConfig")," resources."),(0,r.yg)("h3",{id:"view-logs-for-the-server"},"View logs for the server"),(0,r.yg)("p",null,"After the client, server, and database are up and running, we can see that the server does not have the appropriate access to the database by inspecting the logs with the following command."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl logs -f -n otterize-tutorial-postgres deploy/server\n")),(0,r.yg)("p",null,"Example log:"),(0,r.yg)(u,{mdxType:"Terminal"},"Unable to perform INSERT operation",(0,r.yg)("br",null),"Unable to perform SELECT operation"),(0,r.yg)("h3",{id:"deploy-a-postgresqlserverconfig-to-allow-otterize-db-access"},"Deploy a PostgreSQLServerConfig to allow Otterize DB access"),(0,r.yg)("p",null,"Let's apply a ",(0,r.yg)("inlineCode",{parentName:"p"},"PostgreSQLServerConfig")," so Otterize will know how to access our database instance."),(0,r.yg)("p",null,"First, create a Kuberentes secret containing the database credentials:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl create secret generic postgres-tutorial-db-credentials -n otterize-tutorial-postgres --from-literal=username='otterize-tutorial' --from-literal=password='jeffdog523'\n")),(0,r.yg)("p",null,"In this tutorial, the PostgreSQL database comes with the predefined username & password, but for future uses a\nrole will have to be created in the database to grant Otterize access as well as the ability to configure other users."),(0,r.yg)("p",null,"Next, apply the ",(0,r.yg)("inlineCode",{parentName:"p"},"PostgreSQLServerConfig")," to the cluster:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre"},"kubectl apply -n otterize-tutorial-postgres -f ${ABSOLUTE_URL}/code-examples/postgres/postgresqlserverconfig.yaml\n")),(0,r.yg)("p",null,"This ",(0,r.yg)("inlineCode",{parentName:"p"},"PostgreSQLServerConfig")," tells Otterize how to access a database instance named ",(0,r.yg)("inlineCode",{parentName:"p"},"postgres-tutorial-db"),", meaning that when intents\nare applied requesting access permissions to ",(0,r.yg)("inlineCode",{parentName:"p"},"postgres-tutorial-db"),", the Otterize operator will be able to configure\nthem:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: k8s.otterize.com/v1alpha3\nkind: PostgreSQLServerConfig\nmetadata:\n name: postgres-tutorial-db\nspec:\n address: database.otterize-tutorial-postgres.svc.cluster.local:5432\n credentials:\n secretRef:\n name: postgres-tutorial-db-credentials\n")),(0,r.yg)("h3",{id:"define-your-clientintents"},"Define your ClientIntents"),(0,r.yg)("p",null,"ClientIntents are Otterize\u2019s way of defining access through unique relationships, which lead to perfectly scoped access. In this example, we provide our ",(0,r.yg)("inlineCode",{parentName:"p"},"server")," workload the ability to insert and select records to allow it to access the database."),(0,r.yg)("p",null,"Below is our ",(0,r.yg)("inlineCode",{parentName:"p"},"intents.yaml")," file. As you can see, it is scoped to our database named ",(0,r.yg)("inlineCode",{parentName:"p"},"otterize-tutorial")," and our ",(0,r.yg)("inlineCode",{parentName:"p"},"public.example")," table. We also have limited the access to just ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT")," operations. We could add more databases, tables, or operations if our service required more access."),(0,r.yg)("p",null,"Specifying the table and operations is optional. If you don't specify the table, access will be granted to all tables in the specified database. If you don't specify the operations, all operations will be allowed."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: k8s.otterize.com/v1alpha3\nkind: ClientIntents\nmetadata:\n name: client-intents-for-server\n namespace: otterize-tutorial-postgres\nspec:\n service:\n name: server\n calls:\n - name: postgres-tutorial-db # Same name as our PostgreSQLServerConfig metadata.name\n type: database\n databaseResources:\n - databaseName: otterize-tutorial\n table: public.example\n operations:\n - SELECT\n - INSERT\n")),(0,r.yg)("p",null,"We can now apply our intents. Behind the scenes, the Otterize operator created the user for our ",(0,r.yg)("inlineCode",{parentName:"p"},"server")," workload and executed ",(0,r.yg)("inlineCode",{parentName:"p"},"GRANT")," queries on the database, making our ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT")," errors disappear."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl apply -n otterize-tutorial-postgres -f ${ABSOLUTE_URL}/code-examples/postgres/clientintents.yaml\n")),(0,r.yg)("h3",{id:"view-logs-for-the-server-1"},"View logs for the server"),(0,r.yg)("p",null,"We can now view the server logs once again. This time, we should see that the server has the appropriate access to the database:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl logs -f -n otterize-tutorial-postgres deploy/server\n")),(0,r.yg)("p",null,"Example log:"),(0,r.yg)(u,{mdxType:"Terminal"},"Successfully INSERTED into our table",(0,r.yg)("p",null,"Successfully SELECTED, most recent value: 2024-04-30T13:20:46Z")),(0,r.yg)("p",null,"That\u2019s it! If your service\u2019s functionality changes, adding or removing access is as simple as updating your ClientIntents definitions. For fun, try altering the ",(0,r.yg)("inlineCode",{parentName:"p"},"operations")," to just ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," or ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT"),"."),(0,r.yg)("h1",{id:"teardown"},"Teardown"),(0,r.yg)("p",null,"To remove the deployed examples, run:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl delete clientintents.k8s.otterize.com -n otterize-tutorial-postgres client-intents-for-server && \\\nkubectl delete namespace otterize-tutorial-postgres\n")))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/4a90ba61.508c2b88.js b/assets/js/4a90ba61.ef77e495.js similarity index 58% rename from assets/js/4a90ba61.508c2b88.js rename to assets/js/4a90ba61.ef77e495.js index b61472fd7..542924992 100644 --- a/assets/js/4a90ba61.508c2b88.js +++ b/assets/js/4a90ba61.ef77e495.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2653,8401],{9365:(e,t,a)=>{a.d(t,{A:()=>l});var n=a(6540),r=a(53);const s={tabItem:"tabItem_Ymn6"};function l(e){let{children:t,hidden:a,className:l}=e;return n.createElement("div",{role:"tabpanel",className:(0,r.A)(s.tabItem,l),hidden:a},t)}},1470:(e,t,a)=>{a.d(t,{A:()=>w});var n=a(8168),r=a(6540),s=a(53),l=a(3104),o=a(6347),i=a(7485),c=a(1682),u=a(9466);function p(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:a,attributes:n,default:r}}=e;return{value:t,label:a,attributes:n,default:r}}))}function d(e){const{values:t,children:a}=e;return(0,r.useMemo)((()=>{const e=t??p(a);return function(e){const t=(0,c.X)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,a])}function m(e){let{value:t,tabValues:a}=e;return a.some((e=>e.value===t))}function y(e){let{queryString:t=!1,groupId:a}=e;const n=(0,o.W6)(),s=function(e){let{queryString:t=!1,groupId:a}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!a)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return a??null}({queryString:t,groupId:a});return[(0,i.aZ)(s),(0,r.useCallback)((e=>{if(!s)return;const t=new URLSearchParams(n.location.search);t.set(s,e),n.replace({...n.location,search:t.toString()})}),[s,n])]}function g(e){const{defaultValue:t,queryString:a=!1,groupId:n}=e,s=d(e),[l,o]=(0,r.useState)((()=>function(e){let{defaultValue:t,tabValues:a}=e;if(0===a.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!m({value:t,tabValues:a}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${a.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const n=a.find((e=>e.default))??a[0];if(!n)throw new Error("Unexpected error: 0 tabValues");return n.value}({defaultValue:t,tabValues:s}))),[i,c]=y({queryString:a,groupId:n}),[p,g]=function(e){let{groupId:t}=e;const a=function(e){return e?`docusaurus.tab.${e}`:null}(t),[n,s]=(0,u.Dv)(a);return[n,(0,r.useCallback)((e=>{a&&s.set(e)}),[a,s])]}({groupId:n}),h=(()=>{const e=i??p;return m({value:e,tabValues:s})?e:null})();(0,r.useLayoutEffect)((()=>{h&&o(h)}),[h]);return{selectedValue:l,selectValue:(0,r.useCallback)((e=>{if(!m({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);o(e),c(e),g(e)}),[c,g,s]),tabValues:s}}var h=a(2303);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function f(e){let{className:t,block:a,selectedValue:o,selectValue:i,tabValues:c}=e;const u=[],{blockElementScrollPositionUntilNextRender:p}=(0,l.a_)(),d=e=>{const t=e.currentTarget,a=u.indexOf(t),n=c[a].value;n!==o&&(p(t),i(n))},m=e=>{let t=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const a=u.indexOf(e.currentTarget)+1;t=u[a]??u[0];break}case"ArrowLeft":{const a=u.indexOf(e.currentTarget)-1;t=u[a]??u[u.length-1];break}}t?.focus()};return r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.A)("tabs",{"tabs--block":a},t)},c.map((e=>{let{value:t,label:a,attributes:l}=e;return r.createElement("li",(0,n.A)({role:"tab",tabIndex:o===t?0:-1,"aria-selected":o===t,key:t,ref:e=>u.push(e),onKeyDown:m,onClick:d},l,{className:(0,s.A)("tabs__item",b.tabItem,l?.className,{"tabs__item--active":o===t})}),a??t)})))}function v(e){let{lazy:t,children:a,selectedValue:n}=e;const s=(Array.isArray(a)?a:[a]).filter(Boolean);if(t){const e=s.find((e=>e.props.value===n));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return r.createElement("div",{className:"margin-top--md"},s.map(((e,t)=>(0,r.cloneElement)(e,{key:t,hidden:e.props.value!==n}))))}function S(e){const t=g(e);return r.createElement("div",{className:(0,s.A)("tabs-container",b.tabList)},r.createElement(f,(0,n.A)({},e,t)),r.createElement(v,(0,n.A)({},e,t)))}function w(e){const t=(0,h.A)();return r.createElement(S,(0,n.A)({key:String(t)},e))}},1202:(e,t,a)=>{a.d(t,{A:()=>B});var n=a(8168),r=a(6540),s=a(2303),l=a(53),o=a(6058),i=a(7559),c=a(4291);const u={codeBlockContainer:"codeBlockContainer_APcc"};function p(e){let{as:t,...a}=e;const s=(0,o.A)(),p=(0,c.M$)(s);return r.createElement(t,(0,n.A)({},a,{style:p,className:(0,l.A)(a.className,u.codeBlockContainer,i.G.common.codeBlock)}))}const d={codeBlockContent:"codeBlockContent_m3Ux",codeBlockTitle:"codeBlockTitle_P25_",codeBlock:"codeBlock_qGQc",codeBlockStandalone:"codeBlockStandalone_zC50",codeBlockLines:"codeBlockLines_p187",codeBlockLinesWithNumbering:"codeBlockLinesWithNumbering_OFgW",buttonGroup:"buttonGroup_6DOT"};function m(e){let{children:t,className:a}=e;return r.createElement(p,{as:"pre",tabIndex:0,className:(0,l.A)(d.codeBlockStandalone,"thin-scrollbar",a)},r.createElement("code",{className:d.codeBlockLines},t))}var y=a(6342),g=a(6591),h=a(8382);const b={codeLine:"codeLine_iPqp",codeLineNumber:"codeLineNumber_F4P7",codeLineContent:"codeLineContent_pOih"};var f=a(6025);function v(e){let{line:t,classNames:a,showLineNumbers:s,getLineProps:o,getTokenProps:i}=e;1===t.length&&"\n"===t[0].content&&(t[0].content="");const c=o({line:t,className:(0,l.A)(a,s&&b.codeLine)}),u=t.map(((e,t)=>r.createElement("span",(0,n.A)({key:t},i({token:e,key:t})))));return r.createElement("span",c,s?r.createElement(r.Fragment,null,r.createElement("span",{className:b.codeLineNumber}),r.createElement("span",{className:b.codeLineContent},u)):u,r.createElement("br",null))}var S=a(6861),w=a(1312),N=a(1473),k=a(4115);const L={copyButtonCopied:"copyButtonCopied__QnY",copyButtonIcons:"copyButtonIcons_FhaS",copyButtonIcon:"copyButtonIcon_phi_",copyButtonSuccessIcon:"copyButtonSuccessIcon_FfTR"};function E(e){let{code:t,className:a}=e;const[n,s]=(0,r.useState)(!1),o=(0,r.useRef)(void 0),i=(0,r.useCallback)((()=>{(0,S.A)(t),s(!0),o.current=window.setTimeout((()=>{s(!1)}),1e3)}),[t]);return(0,r.useEffect)((()=>()=>window.clearTimeout(o.current)),[]),r.createElement("button",{type:"button","aria-label":n?(0,w.T)({id:"theme.CodeBlock.copied",message:"Copied",description:"The copied button label on code blocks"}):(0,w.T)({id:"theme.CodeBlock.copyButtonAriaLabel",message:"Copy code to clipboard",description:"The ARIA label for copy code blocks button"}),title:(0,w.T)({id:"theme.CodeBlock.copy",message:"Copy",description:"The copy button label on code blocks"}),className:(0,l.A)("clean-btn",a,L.copyButton,n&&L.copyButtonCopied),onClick:i},r.createElement("span",{className:L.copyButtonIcons,"aria-hidden":"true"},r.createElement(N.A,{className:L.copyButtonIcon}),r.createElement(k.A,{className:L.copyButtonSuccessIcon})))}var C=a(5048);const T={wordWrapButtonIcon:"wordWrapButtonIcon_iowe",wordWrapButtonEnabled:"wordWrapButtonEnabled_gY8A"};function q(e){let{className:t,onClick:a,isEnabled:n}=e;const s=(0,w.T)({id:"theme.CodeBlock.wordWrapToggle",message:"Toggle word wrap",description:"The title attribute for toggle word wrapping button of code block lines"});return r.createElement("button",{type:"button",onClick:a,className:(0,l.A)("clean-btn",t,n&&T.wordWrapButtonEnabled),"aria-label":s,title:s},r.createElement(C.A,{className:T.wordWrapButtonIcon,"aria-hidden":"true"}))}function A(e){let{children:t,className:a="",metastring:s,title:i,showLineNumbers:u,language:m}=e;const{prism:{defaultLanguage:b,magicComments:S}}=(0,y.p)(),w=m??(0,c.Op)(a)??b,N=(0,o.A)(),k=(0,g.f)(),L=(0,c.wt)(s)||i,{lineClassNames:C,code:T}=(0,c.Li)(t,{metastring:s,language:w,magicComments:S}),A=(0,f.A)("/",{absolute:!0}).slice(0,-1),B=T.replaceAll("${ABSOLUTE_URL}",A),M=u??(0,c._u)(s);return r.createElement(p,{as:"div",className:(0,l.A)(a,w&&!a.includes(`language-${w}`)&&`language-${w}`)},L&&r.createElement("div",{className:d.codeBlockTitle},L),r.createElement("div",{className:d.codeBlockContent},r.createElement(h.Ay,(0,n.A)({},h.Gs,{theme:N,code:B,language:w??"text"}),(e=>{let{className:t,tokens:a,getLineProps:n,getTokenProps:s}=e;return r.createElement("pre",{tabIndex:0,ref:k.codeBlockRef,className:(0,l.A)(t,d.codeBlock,"thin-scrollbar")},r.createElement("code",{className:(0,l.A)(d.codeBlockLines,M&&d.codeBlockLinesWithNumbering)},a.map(((e,t)=>r.createElement(v,{key:t,line:e,getLineProps:n,getTokenProps:s,classNames:C[t],showLineNumbers:M})))))})),r.createElement("div",{className:d.buttonGroup},(k.isEnabled||k.isCodeScrollable)&&r.createElement(q,{className:d.codeButton,onClick:()=>k.toggle(),isEnabled:k.isEnabled}),r.createElement(E,{className:d.codeButton,code:B}))))}function B(e){let{children:t,...a}=e;const l=(0,s.A)(),o=function(e){return r.Children.toArray(e).some((e=>(0,r.isValidElement)(e)))?e:Array.isArray(e)?e.join(""):e}(t),i="string"==typeof o?A:m;return r.createElement(i,(0,n.A)({key:String(l)},a),o)}},3826:(e,t,a)=>{a.r(t),a.d(t,{Terminal:()=>u,assets:()=>i,contentTitle:()=>l,default:()=>m,frontMatter:()=>s,metadata:()=>o,toc:()=>c});var n=a(8168),r=(a(6540),a(5680));a(1202),a(1470),a(9365);const s={sidebar_position:2,title:"Just-in-time MySQL access",image:"/img/quick-tutorials/mysql/social.png"},l=void 0,o={unversionedId:"features/mysql/tutorials/mysql",id:"features/mysql/tutorials/mysql",title:"Just-in-time MySQL access",description:"This tutorial will deploy an example cluster to highlight Otterize's MySQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:",source:"@site/docs/features/mysql/tutorials/mysql.mdx",sourceDirName:"features/mysql/tutorials",slug:"/features/mysql/tutorials/mysql",permalink:"/features/mysql/tutorials/mysql",draft:!1,editUrl:"https://github.com/otterize/docs/edit/main/docs/features/mysql/tutorials/mysql.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{sidebar_position:2,title:"Just-in-time MySQL access",image:"/img/quick-tutorials/mysql/social.png"},sidebar:"docSidebar",previous:{title:"MySQL | Overview",permalink:"/features/mysql/"},next:{title:"Reference",permalink:"/features/mysql/reference"}},i={},c=[{value:"1. Minikube Cluster",id:"1-minikube-cluster",level:4},{value:"2. Deploy Otterize",id:"2-deploy-otterize",level:4},{value:"3. Deploy a MySQL database instance",id:"3-deploy-a-mysql-database-instance",level:4},{value:"Setup MySQL database and table for the tutorial",id:"setup-mysql-database-and-table-for-the-tutorial",level:3},{value:"Deploy tutorial services and request database credentials",id:"deploy-tutorial-services-and-request-database-credentials",level:3},{value:"View logs for the server",id:"view-logs-for-the-server",level:3},{value:"Deploy a MySQLServerConfig to allow Otterize DB access",id:"deploy-a-mysqlserverconfig-to-allow-otterize-db-access",level:3},{value:"Define your ClientIntents",id:"define-your-clientintents",level:3},{value:"View logs for the server",id:"view-logs-for-the-server-1",level:3}],u=e=>{let{children:t}=e;return(0,r.yg)("div",{style:{backgroundColor:"#eee",borderRadius:"5px",fontSize:"12px",fontWeight:"600",color:"darkgreen",padding:"1rem",fontFamily:"monospace, monospace"}},t)},p={toc:c,Terminal:u},d="wrapper";function m(e){let{components:t,...a}=e;return(0,r.yg)(d,(0,n.A)({},p,a,{components:t,mdxType:"MDXLayout"}),(0,r.yg)("h1",{id:"overview"},"Overview"),(0,r.yg)("p",null,"This tutorial will deploy an example cluster to highlight Otterize's MySQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:"),(0,r.yg)("ol",null,(0,r.yg)("li",{parentName:"ol"},"An ",(0,r.yg)("inlineCode",{parentName:"li"},"INSERT")," operation to append a table within the database"),(0,r.yg)("li",{parentName:"ol"},"A ",(0,r.yg)("inlineCode",{parentName:"li"},"SELECT")," operation to validate the updates.")),(0,r.yg)("p",null,"The server needs appropriate permissions to access the database. You could use one admin user for all services, which is insecure and is the cause for many security breaches. With Otterize, you can specify required access, and have Otterize create users and perform correctly scoped SQL GRANTs just in time, as the service spins up and down."),(0,r.yg)("p",null,"In this tutorial, we will:"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},"Optionally, spin up a MySQL database instance on AWS, based on Amazon RDS for MySQL, or in your Kubernetes cluster, based on the official MySQL Docker image. Alternatively, you could use any MySQL server of your choice."),(0,r.yg)("li",{parentName:"ul"},"Deploy an example cluster"),(0,r.yg)("li",{parentName:"ul"},"Deploy Otterize in our cluster and give it access to our database instance"),(0,r.yg)("li",{parentName:"ul"},"Declare a ClientIntents resource for the server, specifying required access"),(0,r.yg)("li",{parentName:"ul"},"See that the required access has been granted")),(0,r.yg)("h1",{id:"prerequisites"},"Prerequisites"),(0,r.yg)("h4",{id:"1-minikube-cluster"},"1. Minikube Cluster"),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Prepare a Kubernetes cluster with Minikube"),(0,r.yg)("p",null,"For this tutorial you'll need a local Kubernetes cluster. Having a cluster with a ",(0,r.yg)("a",{parentName:"p",href:"https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/"},"CNI")," that supports ",(0,r.yg)("a",{parentName:"p",href:"https://kubernetes.io/docs/concepts/services-networking/network-policies/"},"NetworkPolicies")," isn't required for this tutorial, but is recommended so that your cluster works with other tutorials."),(0,r.yg)("p",null,"If you don't have the Minikube CLI, first ",(0,r.yg)("a",{parentName:"p",href:"https://minikube.sigs.k8s.io/docs/start/"},"install it"),"."),(0,r.yg)("p",null,"Then start your Minikube cluster with Calico, in order to enforce network policies."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"minikube start --cpus=4 --memory 4096 --disk-size 32g --cni=calico\n"))),(0,r.yg)("h4",{id:"2-deploy-otterize"},"2. Deploy Otterize"),(0,r.yg)("p",null,"To deploy Otterize, head over to ",(0,r.yg)("a",{parentName:"p",href:"https://app.otterize.com"},"Otterize Cloud")," and associate a Kubernetes cluster on the ",(0,r.yg)("a",{parentName:"p",href:"https://app.otterize.com/integrations"},"Integrations page"),", and follow the instructions. If you already have a Kubernetes cluster connected, skip this step."),(0,r.yg)("h4",{id:"3-deploy-a-mysql-database-instance"},"3. Deploy a MySQL database instance"),(0,r.yg)("p",null,"Already have a MySQL database instance? ",(0,r.yg)("a",{parentName:"p",href:"#tutorial"},"Skip to the tutorial.")),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Deploy a MySQL database instance, based on Amazon RDS for MySQL"),(0,r.yg)("p",null,"Follow the ",(0,r.yg)("a",{parentName:"p",href:"https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_GettingStarted.CreatingConnecting.MySQL.html#CHAP_GettingStarted.Creating.MySQL"},"installation instructions on the AWS RDS documentation"),"."),(0,r.yg)("li",null,"You may use the Free tier template for this tutorial."),(0,r.yg)("li",null,'Under "Settings", choose "Auto generate password". Make sure you save the generated password after the instance is created.'),(0,r.yg)("li",null,'Under "Connectivity", enable public access to allow access from your Kubernetes cluster. Otterize will require that access to manage credentials for you. Additionally, make sure you choose a security group that allows inbound access from the internet.')),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Deploy a MySQL database instance, based on the official MySQL Docker image"),(0,r.yg)("p",null,"To deploy a local MySQL database instance, you can use the official MySQL Docker image. Run the following command to deploy a MySQL instance with the root password set to ",(0,r.yg)("inlineCode",{parentName:"p"},"password"),":"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl create namespace otterize-tutorial-mysql\nkubectl apply -n otterize-tutorial-mysql -f ${ABSOLUTE_URL}/code-examples/mysql/database.yaml\n")),(0,r.yg)("p",null,"Use the following values as your MySQL host and password:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"export MYSQLHOST=mysql.otterize-tutorial-mysql.svc.cluster.local\nexport MYSQLUSER=root\nexport MYSQLPASSWORD=password\n"))),(0,r.yg)("h1",{id:"tutorial"},"Tutorial"),(0,r.yg)("h3",{id:"setup-mysql-database-and-table-for-the-tutorial"},"Setup MySQL database and table for the tutorial"),(0,r.yg)("p",null,"Throughout this tutorial, we will refer to your MySQL host & credentials via environment variables, so make sure to set them up:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"export MYSQLHOST= # For RDS, this is the endpoint; for the official MySQL docker image, this is `mysql.otterize-tutorial-mysql.svc.cluster.local`\nexport MYSQLUSER= # For RDS, this is the username set during the RDS instance deployment, typically 'admin'; for the official MySQL docker image, this is `root`\nexport MYSQLPASSWORD= # For RDS, this is the password set during the RDS instance deployment; for the official MySQL docker image, this is `password`\n")),(0,r.yg)("p",null,"Next, start a MySQL client to connect to your MySQL instance, and create a database named ",(0,r.yg)("inlineCode",{parentName:"p"},"otterize_tutorial")," and a table named ",(0,r.yg)("inlineCode",{parentName:"p"},"example")," in your MySQL instance.\nOur tutorial server will use this database and table to perform ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," operations."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl create namespace otterize-tutorial-mysql\nkubectl run -n otterize-tutorial-mysql -it --rm --image=mysql:latest --restart=Never mysql-client -- mysql -h $MYSQLHOST -u $MYSQLUSER -p$MYSQLPASSWORD \\\n -e 'CREATE DATABASE IF NOT EXISTS otterize_example;\n\nUSE otterize_example;\n\nCREATE TABLE IF NOT EXISTS example\n(\n file_name VARCHAR(255),\n upload_time BIGINT\n);\n\nexit;\n'\n")),(0,r.yg)("h3",{id:"deploy-tutorial-services-and-request-database-credentials"},"Deploy tutorial services and request database credentials"),(0,r.yg)("p",null,"Next, set up the namespace used for our tutorial and deploy the client & server services in it:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},'kubectl create namespace otterize-tutorial-mysql\nkubectl apply -n otterize-tutorial-mysql -f ${ABSOLUTE_URL}/code-examples/mysql/client-server.yaml\nkubectl patch deployment -n otterize-tutorial-mysql server --type=\'json\' -p="[{\\"op\\": \\"replace\\", \\"path\\": \\"/spec/template/spec/containers/0/env/0/value\\", \\"value\\": \\"$MYSQLHOST\\"}]"\n')),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Expand to see the deployment YAML"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: server\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: server\n template:\n metadata:\n annotations:\n credentials-operator.otterize.com/user-password-secret-name: server-creds\n labels:\n app: server\n spec:\n serviceAccountName: server\n containers:\n - name: server\n imagePullPolicy: Always\n image: 'otterize/mysql-tutorial-server'\n ports:\n - containerPort: 80\n env:\n - name: DB_HOST\n value: database\n - name: DB_NAME\n value: otterize_example\n - name: DB_PORT\n value: \"3306\"\n - name: DB_SERVER_USER\n valueFrom:\n secretKeyRef:\n name: server-creds\n key: username\n - name: DB_SERVER_PASSWORD\n valueFrom:\n secretKeyRef:\n name: server-creds\n key: password\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: server\nspec:\n type: ClusterIP\n selector:\n app: server\n ports:\n - name: http\n port: 80\n targetPort: 80\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: server\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: client\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: client\n template:\n metadata:\n labels:\n app: client\n spec:\n containers:\n - name: client\n imagePullPolicy: Always\n image: 'otterize/mysql-tutorial-client'\n ports:\n - containerPort: 80\n\n"))),(0,r.yg)("p",null,"Our server's Deployment spec specify an annotation on its Pod, which requests that the Otterize operator provision a username and password for it:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"}," template:\n metadata:\n annotations:\n credentials-operator.otterize.com/user-password-secret-name: server-creds\n")),(0,r.yg)("p",null,"This specifies that the secret ",(0,r.yg)("inlineCode",{parentName:"p"},"server-creds")," will be populated with keys containing the username and password used by this pod to connect to the database.\nThe secret will only be created by the Otterize operator after it is integrated with your database by applying a MySQLServerConfig resources."),(0,r.yg)("h3",{id:"view-logs-for-the-server"},"View logs for the server"),(0,r.yg)("p",null,"After the client, server, and database are up and running, we can see that the server does not have the appropriate access to the database by inspecting the logs with the following command."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl logs -f -n otterize-tutorial-mysql deploy/server\n")),(0,r.yg)("p",null,"Example log:"),(0,r.yg)(u,{mdxType:"Terminal"},"Unable to perform INSERT operation",(0,r.yg)("br",null),"Unable to perform SELECT operation"),(0,r.yg)("h3",{id:"deploy-a-mysqlserverconfig-to-allow-otterize-db-access"},"Deploy a MySQLServerConfig to allow Otterize DB access"),(0,r.yg)("p",null,"Let's apply a ",(0,r.yg)("inlineCode",{parentName:"p"},"MySQLServerConfig")," so Otterize will know how to access our database instance:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},'kubectl apply -n otterize-tutorial-mysql -f ${ABSOLUTE_URL}/code-examples/mysql/mysqlserverconfig.yaml\nkubectl patch mysqlserverconfig -n otterize-tutorial-mysql mysql-tutorial-db --type=\'json\' -p="[{\\"op\\": \\"replace\\", \\"path\\": \\"/spec/address\\", \\"value\\": \\"$MYSQLHOST\\"}]"\nMYSQLUSER_B64=$(echo -n $MYSQLUSER | base64)\nMYSQLPASSWORD_B64=$(echo -n $MYSQLPASSWORD | base64)\nkubectl patch secret -n otterize-tutorial-mysql mysql-tutorial-db-credentials --type=\'json\' -p="[{\\"op\\": \\"replace\\", \\"path\\": \\"/data/username\\", \\"value\\": \\"$MYSQLUSER_B64\\"}, {\\"op\\": \\"replace\\", \\"path\\": \\"/data/password\\", \\"value\\": \\"$MYSQLPASSWORD_B64\\"}]"\n')),(0,r.yg)("p",null,"This applies the following ",(0,r.yg)("inlineCode",{parentName:"p"},"MySQLServerConfig")," to your cluster, and patches it with your DB instance address & credentials:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: k8s.otterize.com/v1alpha3\nkind: MySQLServerConfig\nmetadata:\n name: mysql-tutorial-db\nspec:\n address: mysql.otterize-tutorial-mysql.svc.cluster.local:3306 # Your MySQL server address\n credentials:\n secretRef:\n name: mysql-tutorial-db-credentials\n---\napiVersion: v1\ntype: Opaque\nkind: Secret\nmetadata:\n name: mysql-tutorial-db-credentials\ndata:\n username: '' # Your MySQL server user\n password: '' # Your MySQL server password\n")),(0,r.yg)("p",null,"The above CRD tells Otterize how to access a database instance named ",(0,r.yg)("inlineCode",{parentName:"p"},"mysql-tutorial-db"),", meaning that when intents\nare applied requesting access permissions to ",(0,r.yg)("inlineCode",{parentName:"p"},"mysql-tutorial-db"),", the Otterize operator will be able to configure\nthem."),(0,r.yg)("p",null,"In this tutorial, we use the admin user to grant Otterize permissions to create users and grant them access to the database.\nIn a production environment, it is recommended to create a dedicated user for Otterize, and grant it the necessary permissions to create and manage other users."),(0,r.yg)("h3",{id:"define-your-clientintents"},"Define your ClientIntents"),(0,r.yg)("p",null,"ClientIntents are Otterize\u2019s way of defining access through unique relationships, which lead to perfectly scoped access. In this example, we provide our ",(0,r.yg)("inlineCode",{parentName:"p"},"server")," workload the ability to insert and select records to allow it to access the database."),(0,r.yg)("p",null,"Below is our ",(0,r.yg)("inlineCode",{parentName:"p"},"intents.yaml")," file. As you can see, it is scoped to our database named ",(0,r.yg)("inlineCode",{parentName:"p"},"otterize_tutorial")," and our ",(0,r.yg)("inlineCode",{parentName:"p"},"example")," table. We also have limited the access to just ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT")," operations. We could add more databases, tables, or operations if our service required more access."),(0,r.yg)("p",null,"Specifying the table and operations is optional. If you don't specify the table, access will be granted to all tables in the specified database. If you don't specify the operations, all operations will be allowed."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: k8s.otterize.com/v1alpha3\nkind: ClientIntents\nmetadata:\n name: client-intents-for-server\nspec:\n service:\n name: server\n calls:\n - name: mysql-tutorial-db\n type: database\n databaseResources:\n - databaseName: otterize_example\n table: example\n operations:\n - SELECT\n - INSERT\n")),(0,r.yg)("p",null,"We can now apply our intents. Behind the scenes, the Otterize operator created the user for our ",(0,r.yg)("inlineCode",{parentName:"p"},"server")," workload and executed ",(0,r.yg)("inlineCode",{parentName:"p"},"GRANT")," queries on the database, making our ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT")," errors disappear."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl apply -n otterize-tutorial-mysql -f ${ABSOLUTE_URL}/code-examples/mysql/clientintents.yaml\n")),(0,r.yg)("h3",{id:"view-logs-for-the-server-1"},"View logs for the server"),(0,r.yg)("p",null,"We can now view the server logs once again. This time, we should see that the server has the appropriate access to the database:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl logs -f -n otterize-tutorial-mysql deploy/server\n")),(0,r.yg)("p",null,"Example log:"),(0,r.yg)(u,{mdxType:"Terminal"},"Successfully INSERTED into our table",(0,r.yg)("p",null,"Successfully SELECTED, most recent value: 2024-04-30T13:20:46Z")),(0,r.yg)("p",null,"That\u2019s it! If your service\u2019s functionality changes, adding or removing access is as simple as updating your ClientIntents definitions. For fun, try altering the ",(0,r.yg)("inlineCode",{parentName:"p"},"operations")," to just ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," or ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT"),"."),(0,r.yg)("h1",{id:"teardown"},"Teardown"),(0,r.yg)("p",null,"To remove the deployed examples, run:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl delete clientintents.k8s.otterize.com -n otterize-tutorial-mysql client-intents-for-server\nkubectl delete namespace otterize-tutorial-mysql\n")))}m.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2653,8401],{9365:(e,t,a)=>{a.d(t,{A:()=>o});var n=a(6540),r=a(53);const l={tabItem:"tabItem_Ymn6"};function o(e){let{children:t,hidden:a,className:o}=e;return n.createElement("div",{role:"tabpanel",className:(0,r.A)(l.tabItem,o),hidden:a},t)}},1470:(e,t,a)=>{a.d(t,{A:()=>N});var n=a(8168),r=a(6540),l=a(53),o=a(3104),s=a(6347),i=a(7485),c=a(1682),u=a(9466);function p(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:a,attributes:n,default:r}}=e;return{value:t,label:a,attributes:n,default:r}}))}function d(e){const{values:t,children:a}=e;return(0,r.useMemo)((()=>{const e=t??p(a);return function(e){const t=(0,c.X)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,a])}function m(e){let{value:t,tabValues:a}=e;return a.some((e=>e.value===t))}function y(e){let{queryString:t=!1,groupId:a}=e;const n=(0,s.W6)(),l=function(e){let{queryString:t=!1,groupId:a}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!a)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return a??null}({queryString:t,groupId:a});return[(0,i.aZ)(l),(0,r.useCallback)((e=>{if(!l)return;const t=new URLSearchParams(n.location.search);t.set(l,e),n.replace({...n.location,search:t.toString()})}),[l,n])]}function g(e){const{defaultValue:t,queryString:a=!1,groupId:n}=e,l=d(e),[o,s]=(0,r.useState)((()=>function(e){let{defaultValue:t,tabValues:a}=e;if(0===a.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!m({value:t,tabValues:a}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${a.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const n=a.find((e=>e.default))??a[0];if(!n)throw new Error("Unexpected error: 0 tabValues");return n.value}({defaultValue:t,tabValues:l}))),[i,c]=y({queryString:a,groupId:n}),[p,g]=function(e){let{groupId:t}=e;const a=function(e){return e?`docusaurus.tab.${e}`:null}(t),[n,l]=(0,u.Dv)(a);return[n,(0,r.useCallback)((e=>{a&&l.set(e)}),[a,l])]}({groupId:n}),h=(()=>{const e=i??p;return m({value:e,tabValues:l})?e:null})();(0,r.useLayoutEffect)((()=>{h&&s(h)}),[h]);return{selectedValue:o,selectValue:(0,r.useCallback)((e=>{if(!m({value:e,tabValues:l}))throw new Error(`Can't select invalid tab value=${e}`);s(e),c(e),g(e)}),[c,g,l]),tabValues:l}}var h=a(2303);const b={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function f(e){let{className:t,block:a,selectedValue:s,selectValue:i,tabValues:c}=e;const u=[],{blockElementScrollPositionUntilNextRender:p}=(0,o.a_)(),d=e=>{const t=e.currentTarget,a=u.indexOf(t),n=c[a].value;n!==s&&(p(t),i(n))},m=e=>{let t=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const a=u.indexOf(e.currentTarget)+1;t=u[a]??u[0];break}case"ArrowLeft":{const a=u.indexOf(e.currentTarget)-1;t=u[a]??u[u.length-1];break}}t?.focus()};return r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,l.A)("tabs",{"tabs--block":a},t)},c.map((e=>{let{value:t,label:a,attributes:o}=e;return r.createElement("li",(0,n.A)({role:"tab",tabIndex:s===t?0:-1,"aria-selected":s===t,key:t,ref:e=>u.push(e),onKeyDown:m,onClick:d},o,{className:(0,l.A)("tabs__item",b.tabItem,o?.className,{"tabs__item--active":s===t})}),a??t)})))}function v(e){let{lazy:t,children:a,selectedValue:n}=e;const l=(Array.isArray(a)?a:[a]).filter(Boolean);if(t){const e=l.find((e=>e.props.value===n));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return r.createElement("div",{className:"margin-top--md"},l.map(((e,t)=>(0,r.cloneElement)(e,{key:t,hidden:e.props.value!==n}))))}function S(e){const t=g(e);return r.createElement("div",{className:(0,l.A)("tabs-container",b.tabList)},r.createElement(f,(0,n.A)({},e,t)),r.createElement(v,(0,n.A)({},e,t)))}function N(e){const t=(0,h.A)();return r.createElement(S,(0,n.A)({key:String(t)},e))}},1202:(e,t,a)=>{a.d(t,{A:()=>B});var n=a(8168),r=a(6540),l=a(2303),o=a(53),s=a(6058),i=a(7559),c=a(4291);const u={codeBlockContainer:"codeBlockContainer_APcc"};function p(e){let{as:t,...a}=e;const l=(0,s.A)(),p=(0,c.M$)(l);return r.createElement(t,(0,n.A)({},a,{style:p,className:(0,o.A)(a.className,u.codeBlockContainer,i.G.common.codeBlock)}))}const d={codeBlockContent:"codeBlockContent_m3Ux",codeBlockTitle:"codeBlockTitle_P25_",codeBlock:"codeBlock_qGQc",codeBlockStandalone:"codeBlockStandalone_zC50",codeBlockLines:"codeBlockLines_p187",codeBlockLinesWithNumbering:"codeBlockLinesWithNumbering_OFgW",buttonGroup:"buttonGroup_6DOT"};function m(e){let{children:t,className:a}=e;return r.createElement(p,{as:"pre",tabIndex:0,className:(0,o.A)(d.codeBlockStandalone,"thin-scrollbar",a)},r.createElement("code",{className:d.codeBlockLines},t))}var y=a(6342),g=a(6591),h=a(8382);const b={codeLine:"codeLine_iPqp",codeLineNumber:"codeLineNumber_F4P7",codeLineContent:"codeLineContent_pOih"};var f=a(6025);function v(e){let{line:t,classNames:a,showLineNumbers:l,getLineProps:s,getTokenProps:i}=e;1===t.length&&"\n"===t[0].content&&(t[0].content="");const c=s({line:t,className:(0,o.A)(a,l&&b.codeLine)}),u=t.map(((e,t)=>r.createElement("span",(0,n.A)({key:t},i({token:e,key:t})))));return r.createElement("span",c,l?r.createElement(r.Fragment,null,r.createElement("span",{className:b.codeLineNumber}),r.createElement("span",{className:b.codeLineContent},u)):u,r.createElement("br",null))}var S=a(6861),N=a(1312),w=a(1473),k=a(4115);const L={copyButtonCopied:"copyButtonCopied__QnY",copyButtonIcons:"copyButtonIcons_FhaS",copyButtonIcon:"copyButtonIcon_phi_",copyButtonSuccessIcon:"copyButtonSuccessIcon_FfTR"};function E(e){let{code:t,className:a}=e;const[n,l]=(0,r.useState)(!1),s=(0,r.useRef)(void 0),i=(0,r.useCallback)((()=>{(0,S.A)(t),l(!0),s.current=window.setTimeout((()=>{l(!1)}),1e3)}),[t]);return(0,r.useEffect)((()=>()=>window.clearTimeout(s.current)),[]),r.createElement("button",{type:"button","aria-label":n?(0,N.T)({id:"theme.CodeBlock.copied",message:"Copied",description:"The copied button label on code blocks"}):(0,N.T)({id:"theme.CodeBlock.copyButtonAriaLabel",message:"Copy code to clipboard",description:"The ARIA label for copy code blocks button"}),title:(0,N.T)({id:"theme.CodeBlock.copy",message:"Copy",description:"The copy button label on code blocks"}),className:(0,o.A)("clean-btn",a,L.copyButton,n&&L.copyButtonCopied),onClick:i},r.createElement("span",{className:L.copyButtonIcons,"aria-hidden":"true"},r.createElement(w.A,{className:L.copyButtonIcon}),r.createElement(k.A,{className:L.copyButtonSuccessIcon})))}var C=a(5048);const T={wordWrapButtonIcon:"wordWrapButtonIcon_iowe",wordWrapButtonEnabled:"wordWrapButtonEnabled_gY8A"};function q(e){let{className:t,onClick:a,isEnabled:n}=e;const l=(0,N.T)({id:"theme.CodeBlock.wordWrapToggle",message:"Toggle word wrap",description:"The title attribute for toggle word wrapping button of code block lines"});return r.createElement("button",{type:"button",onClick:a,className:(0,o.A)("clean-btn",t,n&&T.wordWrapButtonEnabled),"aria-label":l,title:l},r.createElement(C.A,{className:T.wordWrapButtonIcon,"aria-hidden":"true"}))}function A(e){let{children:t,className:a="",metastring:l,title:i,showLineNumbers:u,language:m}=e;const{prism:{defaultLanguage:b,magicComments:S}}=(0,y.p)(),N=m??(0,c.Op)(a)??b,w=(0,s.A)(),k=(0,g.f)(),L=(0,c.wt)(l)||i,{lineClassNames:C,code:T}=(0,c.Li)(t,{metastring:l,language:N,magicComments:S}),A=(0,f.A)("/",{absolute:!0}).slice(0,-1),B=T.replaceAll("${ABSOLUTE_URL}",A),z=u??(0,c._u)(l);return r.createElement(p,{as:"div",className:(0,o.A)(a,N&&!a.includes(`language-${N}`)&&`language-${N}`)},L&&r.createElement("div",{className:d.codeBlockTitle},L),r.createElement("div",{className:d.codeBlockContent},r.createElement(h.Ay,(0,n.A)({},h.Gs,{theme:w,code:B,language:N??"text"}),(e=>{let{className:t,tokens:a,getLineProps:n,getTokenProps:l}=e;return r.createElement("pre",{tabIndex:0,ref:k.codeBlockRef,className:(0,o.A)(t,d.codeBlock,"thin-scrollbar")},r.createElement("code",{className:(0,o.A)(d.codeBlockLines,z&&d.codeBlockLinesWithNumbering)},a.map(((e,t)=>r.createElement(v,{key:t,line:e,getLineProps:n,getTokenProps:l,classNames:C[t],showLineNumbers:z})))))})),r.createElement("div",{className:d.buttonGroup},(k.isEnabled||k.isCodeScrollable)&&r.createElement(q,{className:d.codeButton,onClick:()=>k.toggle(),isEnabled:k.isEnabled}),r.createElement(E,{className:d.codeButton,code:B}))))}function B(e){let{children:t,...a}=e;const o=(0,l.A)(),s=function(e){return r.Children.toArray(e).some((e=>(0,r.isValidElement)(e)))?e:Array.isArray(e)?e.join(""):e}(t),i="string"==typeof s?A:m;return r.createElement(i,(0,n.A)({key:String(o)},a),s)}},3826:(e,t,a)=>{a.r(t),a.d(t,{Terminal:()=>u,assets:()=>i,contentTitle:()=>o,default:()=>m,frontMatter:()=>l,metadata:()=>s,toc:()=>c});var n=a(8168),r=(a(6540),a(5680));a(1202),a(1470),a(9365);const l={sidebar_position:2,title:"Just-in-time MySQL access",image:"/img/quick-tutorials/mysql/social.png"},o=void 0,s={unversionedId:"features/mysql/tutorials/mysql",id:"features/mysql/tutorials/mysql",title:"Just-in-time MySQL access",description:"This tutorial will deploy an example cluster to highlight Otterize's MySQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:",source:"@site/docs/features/mysql/tutorials/mysql.mdx",sourceDirName:"features/mysql/tutorials",slug:"/features/mysql/tutorials/mysql",permalink:"/features/mysql/tutorials/mysql",draft:!1,editUrl:"https://github.com/otterize/docs/edit/main/docs/features/mysql/tutorials/mysql.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{sidebar_position:2,title:"Just-in-time MySQL access",image:"/img/quick-tutorials/mysql/social.png"},sidebar:"docSidebar",previous:{title:"MySQL | Overview",permalink:"/features/mysql/"},next:{title:"Reference",permalink:"/features/mysql/reference"}},i={},c=[{value:"1. Minikube Cluster",id:"1-minikube-cluster",level:4},{value:"2. Deploy Otterize",id:"2-deploy-otterize",level:4},{value:"3. Deploy a MySQL database instance",id:"3-deploy-a-mysql-database-instance",level:4},{value:"Setup MySQL database and table for the tutorial",id:"setup-mysql-database-and-table-for-the-tutorial",level:3},{value:"Deploy tutorial services and request database credentials",id:"deploy-tutorial-services-and-request-database-credentials",level:3},{value:"View logs for the server",id:"view-logs-for-the-server",level:3},{value:"Deploy a MySQLServerConfig to allow Otterize DB access",id:"deploy-a-mysqlserverconfig-to-allow-otterize-db-access",level:3},{value:"Define your ClientIntents",id:"define-your-clientintents",level:3},{value:"View logs for the server",id:"view-logs-for-the-server-1",level:3}],u=e=>{let{children:t}=e;return(0,r.yg)("div",{style:{backgroundColor:"#eee",borderRadius:"5px",fontSize:"12px",fontWeight:"600",color:"darkgreen",padding:"1rem",fontFamily:"monospace, monospace"}},t)},p={toc:c,Terminal:u},d="wrapper";function m(e){let{components:t,...a}=e;return(0,r.yg)(d,(0,n.A)({},p,a,{components:t,mdxType:"MDXLayout"}),(0,r.yg)("h1",{id:"overview"},"Overview"),(0,r.yg)("p",null,"This tutorial will deploy an example cluster to highlight Otterize's MySQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:"),(0,r.yg)("ol",null,(0,r.yg)("li",{parentName:"ol"},"An ",(0,r.yg)("inlineCode",{parentName:"li"},"INSERT")," operation to append a table within the database"),(0,r.yg)("li",{parentName:"ol"},"A ",(0,r.yg)("inlineCode",{parentName:"li"},"SELECT")," operation to validate the updates.")),(0,r.yg)("p",null,"The server needs appropriate permissions to access the database. You could use one admin user for all services, which is insecure and is the cause for many security breaches. With Otterize, you can specify required access, and have Otterize create users and perform correctly scoped SQL GRANTs just in time, as the service spins up and down."),(0,r.yg)("p",null,"In this tutorial, we will:"),(0,r.yg)("ul",null,(0,r.yg)("li",{parentName:"ul"},"Optionally, spin up a MySQL database instance on AWS, based on Amazon RDS for MySQL, or in your Kubernetes cluster, based on the official MySQL Docker image. Alternatively, you could use any MySQL server of your choice."),(0,r.yg)("li",{parentName:"ul"},"Deploy an example cluster"),(0,r.yg)("li",{parentName:"ul"},"Deploy Otterize in our cluster and give it access to our database instance"),(0,r.yg)("li",{parentName:"ul"},"Declare a ClientIntents resource for the server, specifying required access"),(0,r.yg)("li",{parentName:"ul"},"See that the required access has been granted")),(0,r.yg)("h1",{id:"prerequisites"},"Prerequisites"),(0,r.yg)("h4",{id:"1-minikube-cluster"},"1. Minikube Cluster"),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Prepare a Kubernetes cluster with Minikube"),(0,r.yg)("p",null,"For this tutorial you'll need a local Kubernetes cluster. Having a cluster with a ",(0,r.yg)("a",{parentName:"p",href:"https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/"},"CNI")," that supports ",(0,r.yg)("a",{parentName:"p",href:"https://kubernetes.io/docs/concepts/services-networking/network-policies/"},"NetworkPolicies")," isn't required for this tutorial, but is recommended so that your cluster works with other tutorials."),(0,r.yg)("p",null,"If you don't have the Minikube CLI, first ",(0,r.yg)("a",{parentName:"p",href:"https://minikube.sigs.k8s.io/docs/start/"},"install it"),"."),(0,r.yg)("p",null,"Then start your Minikube cluster with Calico, in order to enforce network policies."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"minikube start --cpus=4 --memory 4096 --disk-size 32g --cni=calico\n"))),(0,r.yg)("h4",{id:"2-deploy-otterize"},"2. Deploy Otterize"),(0,r.yg)("p",null,"To deploy Otterize, head over to ",(0,r.yg)("a",{parentName:"p",href:"https://app.otterize.com"},"Otterize Cloud")," and associate a Kubernetes cluster on the ",(0,r.yg)("a",{parentName:"p",href:"https://app.otterize.com/integrations"},"Integrations page"),", and follow the instructions. If you already have a Kubernetes cluster connected, skip this step."),(0,r.yg)("h4",{id:"3-deploy-a-mysql-database-instance"},"3. Deploy a MySQL database instance"),(0,r.yg)("p",null,"Already have a MySQL database instance? ",(0,r.yg)("a",{parentName:"p",href:"#tutorial"},"Skip to the tutorial.")),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Deploy a MySQL database instance, based on Amazon RDS for MySQL"),(0,r.yg)("p",null,"Follow the ",(0,r.yg)("a",{parentName:"p",href:"https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_GettingStarted.CreatingConnecting.MySQL.html#CHAP_GettingStarted.Creating.MySQL"},"installation instructions on the AWS RDS documentation"),"."),(0,r.yg)("li",null,"You may use the Free tier template for this tutorial."),(0,r.yg)("li",null,'Under "Settings", choose "Auto generate password". Make sure you save the generated password after the instance is created.'),(0,r.yg)("li",null,'Under "Connectivity", enable public access to allow access from your Kubernetes cluster. Otterize will require that access to manage credentials for you. Additionally, make sure you choose a security group that allows inbound access from the internet.')),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Deploy a MySQL database instance, based on the official MySQL Docker image"),(0,r.yg)("p",null,"To deploy a local MySQL database instance, you can use the official MySQL Docker image. Run the following command to deploy a MySQL instance with the root password set to ",(0,r.yg)("inlineCode",{parentName:"p"},"password"),":"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl create namespace otterize-tutorial-mysql\nkubectl apply -n otterize-tutorial-mysql -f ${ABSOLUTE_URL}/code-examples/mysql/database.yaml\n")),(0,r.yg)("p",null,"Use the following values as your MySQL host and password:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"export MYSQLHOST=mysql.otterize-tutorial-mysql.svc.cluster.local\nexport MYSQLUSER=root\nexport MYSQLPASSWORD=password\n"))),(0,r.yg)("h1",{id:"tutorial"},"Tutorial"),(0,r.yg)("h3",{id:"setup-mysql-database-and-table-for-the-tutorial"},"Setup MySQL database and table for the tutorial"),(0,r.yg)("p",null,"Throughout this tutorial, we will refer to your MySQL host & credentials via environment variables, so make sure to set them up:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"export MYSQLHOST= # For RDS, this is the endpoint; for the official MySQL docker image, this is `mysql.otterize-tutorial-mysql.svc.cluster.local`\nexport MYSQLUSER= # For RDS, this is the username set during the RDS instance deployment, typically 'admin'; for the official MySQL docker image, this is `root`\nexport MYSQLPASSWORD= # For RDS, this is the password set during the RDS instance deployment; for the official MySQL docker image, this is `password`\n")),(0,r.yg)("p",null,"Next, start a MySQL client to connect to your MySQL instance, and create a database named ",(0,r.yg)("inlineCode",{parentName:"p"},"otterize_tutorial")," and a table named ",(0,r.yg)("inlineCode",{parentName:"p"},"example")," in your MySQL instance.\nOur tutorial server will use this database and table to perform ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," operations."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl create namespace otterize-tutorial-mysql\nkubectl run -n otterize-tutorial-mysql -it --rm --image=mysql:latest --restart=Never mysql-client -- mysql -h $MYSQLHOST -u $MYSQLUSER -p$MYSQLPASSWORD \\\n -e 'CREATE DATABASE IF NOT EXISTS otterize_example;\n\nUSE otterize_example;\n\nCREATE TABLE IF NOT EXISTS example\n(\n file_name VARCHAR(255),\n upload_time BIGINT\n);\n\nexit;\n'\n")),(0,r.yg)("h3",{id:"deploy-tutorial-services-and-request-database-credentials"},"Deploy tutorial services and request database credentials"),(0,r.yg)("p",null,"Next, set up the namespace used for our tutorial and deploy the client & server services in it:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},'kubectl create namespace otterize-tutorial-mysql\nkubectl apply -n otterize-tutorial-mysql -f ${ABSOLUTE_URL}/code-examples/mysql/client-server.yaml\nkubectl patch deployment -n otterize-tutorial-mysql server --type=\'json\' -p="[{\\"op\\": \\"replace\\", \\"path\\": \\"/spec/template/spec/containers/0/env/0/value\\", \\"value\\": \\"$MYSQLHOST\\"}]"\n')),(0,r.yg)("details",null,(0,r.yg)("summary",null,"Expand to see the deployment YAML"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: server\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: server\n template:\n metadata:\n annotations:\n credentials-operator.otterize.com/user-password-secret-name: server-creds\n labels:\n app: server\n spec:\n serviceAccountName: server\n containers:\n - name: server\n imagePullPolicy: Always\n image: 'otterize/mysql-tutorial-server'\n ports:\n - containerPort: 80\n env:\n - name: DB_HOST\n value: database\n - name: DB_NAME\n value: otterize_example\n - name: DB_PORT\n value: \"3306\"\n - name: DB_SERVER_USER\n valueFrom:\n secretKeyRef:\n name: server-creds\n key: username\n - name: DB_SERVER_PASSWORD\n valueFrom:\n secretKeyRef:\n name: server-creds\n key: password\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: server\nspec:\n type: ClusterIP\n selector:\n app: server\n ports:\n - name: http\n port: 80\n targetPort: 80\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: server\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: client\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: client\n template:\n metadata:\n labels:\n app: client\n spec:\n containers:\n - name: client\n imagePullPolicy: Always\n image: 'otterize/mysql-tutorial-client'\n ports:\n - containerPort: 80\n\n"))),(0,r.yg)("p",null,"Our server's Deployment spec specifies an annotation on its Pod, which requests that the Otterize operator provision a username and password for it:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"}," template:\n metadata:\n annotations:\n credentials-operator.otterize.com/user-password-secret-name: server-creds\n")),(0,r.yg)("p",null,"This specifies that the secret ",(0,r.yg)("inlineCode",{parentName:"p"},"server-creds")," will be populated with keys containing the username and password used by this pod to connect to the database.\nThe secret will only be created by the Otterize operator after it is integrated with your database by applying a ",(0,r.yg)("inlineCode",{parentName:"p"},"MySQLServerConfig")," resources."),(0,r.yg)("h3",{id:"view-logs-for-the-server"},"View logs for the server"),(0,r.yg)("p",null,"After the client, server, and database are up and running, we can see that the server does not have the appropriate access to the database by inspecting the logs with the following command."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl logs -f -n otterize-tutorial-mysql deploy/server\n")),(0,r.yg)("p",null,"Example log:"),(0,r.yg)(u,{mdxType:"Terminal"},"Unable to perform INSERT operation",(0,r.yg)("br",null),"Unable to perform SELECT operation"),(0,r.yg)("h3",{id:"deploy-a-mysqlserverconfig-to-allow-otterize-db-access"},"Deploy a MySQLServerConfig to allow Otterize DB access"),(0,r.yg)("p",null,"Let's apply a ",(0,r.yg)("inlineCode",{parentName:"p"},"MySQLServerConfig")," so Otterize will know how to access our database instance."),(0,r.yg)("p",null,"First, create a Kuberentes secret containing the database credentials:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl create secret generic mysql-tutorial-db-credentials -n otterize-tutorial-mysql --from-literal=username=$MYSQLUSER --from-literal=password=$MYSQLPASSWORD\n")),(0,r.yg)("p",null,"Next, apply the ",(0,r.yg)("inlineCode",{parentName:"p"},"MySQLServerConfig")," to the cluster, and patch it with your DB instance address:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre"},'kubectl apply -n otterize-tutorial-mysql -f ${ABSOLUTE_URL}/code-examples/mysql/mysqlserverconfig.yaml\nkubectl patch mysqlserverconfig -n otterize-tutorial-mysql mysql-tutorial-db --type=\'json\' -p="[{\\"op\\": \\"replace\\", \\"path\\": \\"/spec/address\\", \\"value\\": \\"$MYSQLHOST\\"}]"\n')),(0,r.yg)("p",null,"This ",(0,r.yg)("inlineCode",{parentName:"p"},"MySQLServerConfig")," tells Otterize how to access a database instance named ",(0,r.yg)("inlineCode",{parentName:"p"},"mysql-tutorial-db"),", meaning that when intents\nare applied requesting access permissions to ",(0,r.yg)("inlineCode",{parentName:"p"},"mysql-tutorial-db"),", the Otterize operator will be able to configure\nthem:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: k8s.otterize.com/v1alpha3\nkind: MySQLServerConfig\nmetadata:\n name: mysql-tutorial-db\nspec:\n address: mysql.otterize-tutorial-mysql.svc.cluster.local:3306 # Your MySQL server address\n credentials:\n secretRef:\n name: mysql-tutorial-db-credentials\n")),(0,r.yg)("p",null,"In this tutorial, we use the admin user to grant Otterize permissions to create users and grant them access to the database.\nIn a production environment, it is recommended to create a dedicated user for Otterize, and grant it the necessary permissions to create and manage other users."),(0,r.yg)("h3",{id:"define-your-clientintents"},"Define your ClientIntents"),(0,r.yg)("p",null,"ClientIntents are Otterize\u2019s way of defining access through unique relationships, which lead to perfectly scoped access. In this example, we provide our ",(0,r.yg)("inlineCode",{parentName:"p"},"server")," workload the ability to insert and select records to allow it to access the database."),(0,r.yg)("p",null,"Below is our ",(0,r.yg)("inlineCode",{parentName:"p"},"intents.yaml")," file. As you can see, it is scoped to our database named ",(0,r.yg)("inlineCode",{parentName:"p"},"otterize_tutorial")," and our ",(0,r.yg)("inlineCode",{parentName:"p"},"example")," table. We also have limited the access to just ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT")," operations. We could add more databases, tables, or operations if our service required more access."),(0,r.yg)("p",null,"Specifying the table and operations is optional. If you don't specify the table, access will be granted to all tables in the specified database. If you don't specify the operations, all operations will be allowed."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-yaml"},"apiVersion: k8s.otterize.com/v1alpha3\nkind: ClientIntents\nmetadata:\n name: client-intents-for-server\nspec:\n service:\n name: server\n calls:\n - name: mysql-tutorial-db\n type: database\n databaseResources:\n - databaseName: otterize_example\n table: example\n operations:\n - SELECT\n - INSERT\n")),(0,r.yg)("p",null,"We can now apply our intents. Behind the scenes, the Otterize operator created the user for our ",(0,r.yg)("inlineCode",{parentName:"p"},"server")," workload and executed ",(0,r.yg)("inlineCode",{parentName:"p"},"GRANT")," queries on the database, making our ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," and ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT")," errors disappear."),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl apply -n otterize-tutorial-mysql -f ${ABSOLUTE_URL}/code-examples/mysql/clientintents.yaml\n")),(0,r.yg)("h3",{id:"view-logs-for-the-server-1"},"View logs for the server"),(0,r.yg)("p",null,"We can now view the server logs once again. This time, we should see that the server has the appropriate access to the database:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl logs -f -n otterize-tutorial-mysql deploy/server\n")),(0,r.yg)("p",null,"Example log:"),(0,r.yg)(u,{mdxType:"Terminal"},"Successfully INSERTED into our table",(0,r.yg)("p",null,"Successfully SELECTED, most recent value: 2024-04-30T13:20:46Z")),(0,r.yg)("p",null,"That\u2019s it! If your service\u2019s functionality changes, adding or removing access is as simple as updating your ClientIntents definitions. For fun, try altering the ",(0,r.yg)("inlineCode",{parentName:"p"},"operations")," to just ",(0,r.yg)("inlineCode",{parentName:"p"},"SELECT")," or ",(0,r.yg)("inlineCode",{parentName:"p"},"INSERT"),"."),(0,r.yg)("h1",{id:"teardown"},"Teardown"),(0,r.yg)("p",null,"To remove the deployed examples, run:"),(0,r.yg)("pre",null,(0,r.yg)("code",{parentName:"pre",className:"language-shell"},"kubectl delete clientintents.k8s.otterize.com -n otterize-tutorial-mysql client-intents-for-server\nkubectl delete namespace otterize-tutorial-mysql\n")))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.cbba4083.js b/assets/js/runtime~main.a1c4a5b4.js similarity index 98% rename from assets/js/runtime~main.cbba4083.js rename to assets/js/runtime~main.a1c4a5b4.js index cf7deef71..c371e2cb1 100644 --- a/assets/js/runtime~main.cbba4083.js +++ b/assets/js/runtime~main.a1c4a5b4.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,f,b,c,d={},t={};function r(e){var a=t[e];if(void 0!==a)return a.exports;var f=t[e]={id:e,loaded:!1,exports:{}};return d[e].call(f.exports,f,f.exports,r),f.loaded=!0,f.exports}r.m=d,r.c=t,e=[],r.O=(a,f,b,c)=>{if(!f){var d=1/0;for(i=0;i=c)&&Object.keys(r.O).every((e=>r.O[e](f[o])))?f.splice(o--,1):(t=!1,c0&&e[i-1][2]>c;i--)e[i]=e[i-1];e[i]=[f,b,c]},r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},f=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,b){if(1&b&&(e=this(e)),8&b)return e;if("object"==typeof e&&e){if(4&b&&e.__esModule)return e;if(16&b&&"function"==typeof e.then)return e}var c=Object.create(null);r.r(c);var d={};a=a||[null,f({}),f([]),f(f)];for(var t=2&b&&e;"object"==typeof t&&!~a.indexOf(t);t=f(t))Object.getOwnPropertyNames(t).forEach((a=>d[a]=()=>e[a]));return d.default=()=>e,r.d(c,d),c},r.d=(e,a)=>{for(var f in a)r.o(a,f)&&!r.o(e,f)&&Object.defineProperty(e,f,{enumerable:!0,get:a[f]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((a,f)=>(r.f[f](e,a),a)),[])),r.u=e=>"assets/js/"+({760:"13f991ea",765:"6f40fd2f",801:"bb52994a",805:"b6b04eda",918:"01027e3c",1040:"f1151ea7",1205:"698135b2",1295:"97c2207d",1386:"a43681c9",1538:"40ccd1ee",1601:"013a04c2",1625:"f7d1f224",1709:"e93b0177",1733:"fab3c742",1836:"87583c6c",1886:"9a9c42f7",1907:"a630a6fc",1952:"5e4199df",2138:"1a4e3797",2148:"13e8e5b0",2194:"3fbbf99b",2229:"8e233326",2242:"96faae56",2324:"93fb3e94",2343:"be46e07c",2368:"2894b329",2653:"4a90ba61",2703:"6bd33d99",2706:"7b307392",2934:"751ef2b5",3024:"fef0a614",3381:"f8b70272",4194:"2c09ee5d",4312:"89d24bb8",4496:"dcc76ebc",4713:"44e49d37",4911:"a28f5f9a",4940:"f26efbc5",5066:"dff029d6",5106:"fba06901",5290:"ed63a978",5305:"797a5a6f",5548:"247783bb",5582:"da523844",5864:"671fd195",5878:"6303d649",5894:"f1a90138",6326:"42fb6e35",6587:"2e135cc6",6589:"efaf9258",6633:"c6ec7a52",6964:"6ee27f91",7199:"51639cf9",7202:"fec8a912",7255:"64420156",7317:"4d029c8d",8102:"96b2b7a2",8263:"fb8f3a82",8397:"62ac5b94",8401:"17896441",8502:"d05b304a",8581:"935f2afb",8595:"a3ff3870",8626:"944eab8f",8639:"9a6944ab",8714:"1be78505",8750:"ceba1265",8968:"59b068d1",9060:"4388075d",9184:"3ba2fa8f",9441:"c38cf504",9514:"4291f23d",9581:"f89eda61",9715:"b1654ad1",9990:"2bd353df"}[e]||e)+"."+{416:"5a82d981",760:"ff662baa",765:"21d36fb9",801:"4d9cd4d8",805:"68a50ecd",918:"1299f9a1",1040:"b6365de7",1205:"080f42cf",1295:"329b7ba8",1386:"8469fc27",1427:"db807010",1538:"58779a6e",1601:"fc45ce5e",1625:"354ae921",1709:"7754b93d",1733:"edf0efef",1774:"9ea8a3a2",1836:"d4b5f4ad",1886:"75113969",1907:"8904fcd8",1952:"89e69ac5",2138:"b3d3408a",2148:"5fc4ed15",2194:"0285d213",2229:"d3d2c4f4",2242:"feb3beba",2324:"bc45af9d",2343:"274b90ff",2368:"dc2b31bc",2653:"508c2b88",2703:"40cc61c9",2706:"36b34522",2934:"ed338caa",3024:"d0c7bb15",3381:"b387f121",4194:"41b142bb",4312:"25e48f8c",4496:"ec21e679",4713:"1717d835",4911:"09d958b2",4940:"57e7591b",5066:"5b15b888",5106:"baef74eb",5290:"9b969f70",5305:"604c0287",5548:"54c9fe51",5582:"051940bd",5864:"31425a69",5878:"c96fb342",5894:"8b6c041c",6140:"25b4571d",6326:"cdd85e86",6587:"4dc5ca9a",6589:"60ccc42b",6633:"360a7e79",6964:"31f4b69d",7199:"b2d2a9e0",7202:"9c52c13d",7255:"45a8e519",7317:"078ac474",8102:"bce72d60",8263:"8f9da060",8397:"f6ec7d8a",8401:"57dfb6c6",8502:"cf0eda4b",8581:"09c75987",8595:"2d74a1ff",8626:"b2a2d847",8639:"9f62fd9a",8714:"de56d63e",8750:"aca67551",8913:"64e5ee35",8968:"11ea3e06",9060:"e23af36f",9184:"d2ac052b",9441:"0b454f35",9462:"a16127cd",9514:"5c71d58e",9581:"bc4eba67",9715:"116e133d",9990:"cdf9b5f7"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),b={},c="docs:",r.l=(e,a,f,d)=>{if(b[e])b[e].push(a);else{var t,o;if(void 0!==f)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var c=b[e];if(delete b[e],t.parentNode&&t.parentNode.removeChild(t),c&&c.forEach((e=>e(f))),a)return a(f)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=l.bind(null,t.onerror),t.onload=l.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"8401",64420156:"7255","13f991ea":"760","6f40fd2f":"765",bb52994a:"801",b6b04eda:"805","01027e3c":"918",f1151ea7:"1040","698135b2":"1205","97c2207d":"1295",a43681c9:"1386","40ccd1ee":"1538","013a04c2":"1601",f7d1f224:"1625",e93b0177:"1709",fab3c742:"1733","87583c6c":"1836","9a9c42f7":"1886",a630a6fc:"1907","5e4199df":"1952","1a4e3797":"2138","13e8e5b0":"2148","3fbbf99b":"2194","8e233326":"2229","96faae56":"2242","93fb3e94":"2324",be46e07c:"2343","2894b329":"2368","4a90ba61":"2653","6bd33d99":"2703","7b307392":"2706","751ef2b5":"2934",fef0a614:"3024",f8b70272:"3381","2c09ee5d":"4194","89d24bb8":"4312",dcc76ebc:"4496","44e49d37":"4713",a28f5f9a:"4911",f26efbc5:"4940",dff029d6:"5066",fba06901:"5106",ed63a978:"5290","797a5a6f":"5305","247783bb":"5548",da523844:"5582","671fd195":"5864","6303d649":"5878",f1a90138:"5894","42fb6e35":"6326","2e135cc6":"6587",efaf9258:"6589",c6ec7a52:"6633","6ee27f91":"6964","51639cf9":"7199",fec8a912:"7202","4d029c8d":"7317","96b2b7a2":"8102",fb8f3a82:"8263","62ac5b94":"8397",d05b304a:"8502","935f2afb":"8581",a3ff3870:"8595","944eab8f":"8626","9a6944ab":"8639","1be78505":"8714",ceba1265:"8750","59b068d1":"8968","4388075d":"9060","3ba2fa8f":"9184",c38cf504:"9441","4291f23d":"9514",f89eda61:"9581",b1654ad1:"9715","2bd353df":"9990"}[e]||e,r.p+r.u(e)},(()=>{var e={5354:0,1869:0};r.f.j=(a,f)=>{var b=r.o(e,a)?e[a]:void 0;if(0!==b)if(b)f.push(b[2]);else if(/^(1869|5354)$/.test(a))e[a]=0;else{var c=new Promise(((f,c)=>b=e[a]=[f,c]));f.push(b[2]=c);var d=r.p+r.u(a),t=new Error;r.l(d,(f=>{if(r.o(e,a)&&(0!==(b=e[a])&&(e[a]=void 0),b)){var c=f&&("load"===f.type?"missing":f.type),d=f&&f.target&&f.target.src;t.message="Loading chunk "+a+" failed.\n("+c+": "+d+")",t.name="ChunkLoadError",t.type=c,t.request=d,b[1](t)}}),"chunk-"+a,a)}},r.O.j=a=>0===e[a];var a=(a,f)=>{var b,c,d=f[0],t=f[1],o=f[2],n=0;if(d.some((a=>0!==e[a]))){for(b in t)r.o(t,b)&&(r.m[b]=t[b]);if(o)var i=o(r)}for(a&&a(f);n{"use strict";var e,a,f,b,c,d={},t={};function r(e){var a=t[e];if(void 0!==a)return a.exports;var f=t[e]={id:e,loaded:!1,exports:{}};return d[e].call(f.exports,f,f.exports,r),f.loaded=!0,f.exports}r.m=d,r.c=t,e=[],r.O=(a,f,b,c)=>{if(!f){var d=1/0;for(i=0;i=c)&&Object.keys(r.O).every((e=>r.O[e](f[o])))?f.splice(o--,1):(t=!1,c0&&e[i-1][2]>c;i--)e[i]=e[i-1];e[i]=[f,b,c]},r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},f=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,b){if(1&b&&(e=this(e)),8&b)return e;if("object"==typeof e&&e){if(4&b&&e.__esModule)return e;if(16&b&&"function"==typeof e.then)return e}var c=Object.create(null);r.r(c);var d={};a=a||[null,f({}),f([]),f(f)];for(var t=2&b&&e;"object"==typeof t&&!~a.indexOf(t);t=f(t))Object.getOwnPropertyNames(t).forEach((a=>d[a]=()=>e[a]));return d.default=()=>e,r.d(c,d),c},r.d=(e,a)=>{for(var f in a)r.o(a,f)&&!r.o(e,f)&&Object.defineProperty(e,f,{enumerable:!0,get:a[f]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((a,f)=>(r.f[f](e,a),a)),[])),r.u=e=>"assets/js/"+({760:"13f991ea",765:"6f40fd2f",801:"bb52994a",805:"b6b04eda",918:"01027e3c",1040:"f1151ea7",1205:"698135b2",1295:"97c2207d",1386:"a43681c9",1538:"40ccd1ee",1601:"013a04c2",1625:"f7d1f224",1709:"e93b0177",1733:"fab3c742",1836:"87583c6c",1886:"9a9c42f7",1907:"a630a6fc",1952:"5e4199df",2138:"1a4e3797",2148:"13e8e5b0",2194:"3fbbf99b",2229:"8e233326",2242:"96faae56",2324:"93fb3e94",2343:"be46e07c",2368:"2894b329",2653:"4a90ba61",2703:"6bd33d99",2706:"7b307392",2934:"751ef2b5",3024:"fef0a614",3381:"f8b70272",4194:"2c09ee5d",4312:"89d24bb8",4496:"dcc76ebc",4713:"44e49d37",4911:"a28f5f9a",4940:"f26efbc5",5066:"dff029d6",5106:"fba06901",5290:"ed63a978",5305:"797a5a6f",5548:"247783bb",5582:"da523844",5864:"671fd195",5878:"6303d649",5894:"f1a90138",6326:"42fb6e35",6587:"2e135cc6",6589:"efaf9258",6633:"c6ec7a52",6964:"6ee27f91",7199:"51639cf9",7202:"fec8a912",7255:"64420156",7317:"4d029c8d",8102:"96b2b7a2",8263:"fb8f3a82",8397:"62ac5b94",8401:"17896441",8502:"d05b304a",8581:"935f2afb",8595:"a3ff3870",8626:"944eab8f",8639:"9a6944ab",8714:"1be78505",8750:"ceba1265",8968:"59b068d1",9060:"4388075d",9184:"3ba2fa8f",9441:"c38cf504",9514:"4291f23d",9581:"f89eda61",9715:"b1654ad1",9990:"2bd353df"}[e]||e)+"."+{416:"5a82d981",760:"ff662baa",765:"21d36fb9",801:"4d9cd4d8",805:"68a50ecd",918:"1299f9a1",1040:"b6365de7",1205:"080f42cf",1295:"329b7ba8",1386:"8469fc27",1427:"db807010",1538:"58779a6e",1601:"fc45ce5e",1625:"354ae921",1709:"7754b93d",1733:"edf0efef",1774:"9ea8a3a2",1836:"d4b5f4ad",1886:"75113969",1907:"8904fcd8",1952:"89e69ac5",2138:"b3d3408a",2148:"5fc4ed15",2194:"0285d213",2229:"d3d2c4f4",2242:"feb3beba",2324:"bc45af9d",2343:"274b90ff",2368:"dc2b31bc",2653:"ef77e495",2703:"40cc61c9",2706:"36b34522",2934:"ed338caa",3024:"d0c7bb15",3381:"b387f121",4194:"cccc630c",4312:"25e48f8c",4496:"ec21e679",4713:"1717d835",4911:"09d958b2",4940:"57e7591b",5066:"5b15b888",5106:"baef74eb",5290:"9b969f70",5305:"604c0287",5548:"54c9fe51",5582:"051940bd",5864:"31425a69",5878:"c96fb342",5894:"8b6c041c",6140:"25b4571d",6326:"cdd85e86",6587:"4dc5ca9a",6589:"60ccc42b",6633:"360a7e79",6964:"31f4b69d",7199:"b2d2a9e0",7202:"9c52c13d",7255:"45a8e519",7317:"078ac474",8102:"bce72d60",8263:"8f9da060",8397:"f6ec7d8a",8401:"57dfb6c6",8502:"cf0eda4b",8581:"09c75987",8595:"2d74a1ff",8626:"b2a2d847",8639:"9f62fd9a",8714:"de56d63e",8750:"aca67551",8913:"64e5ee35",8968:"11ea3e06",9060:"e23af36f",9184:"d2ac052b",9441:"0b454f35",9462:"a16127cd",9514:"5c71d58e",9581:"bc4eba67",9715:"116e133d",9990:"cdf9b5f7"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),b={},c="docs:",r.l=(e,a,f,d)=>{if(b[e])b[e].push(a);else{var t,o;if(void 0!==f)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var c=b[e];if(delete b[e],t.parentNode&&t.parentNode.removeChild(t),c&&c.forEach((e=>e(f))),a)return a(f)},s=setTimeout(l.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=l.bind(null,t.onerror),t.onload=l.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"8401",64420156:"7255","13f991ea":"760","6f40fd2f":"765",bb52994a:"801",b6b04eda:"805","01027e3c":"918",f1151ea7:"1040","698135b2":"1205","97c2207d":"1295",a43681c9:"1386","40ccd1ee":"1538","013a04c2":"1601",f7d1f224:"1625",e93b0177:"1709",fab3c742:"1733","87583c6c":"1836","9a9c42f7":"1886",a630a6fc:"1907","5e4199df":"1952","1a4e3797":"2138","13e8e5b0":"2148","3fbbf99b":"2194","8e233326":"2229","96faae56":"2242","93fb3e94":"2324",be46e07c:"2343","2894b329":"2368","4a90ba61":"2653","6bd33d99":"2703","7b307392":"2706","751ef2b5":"2934",fef0a614:"3024",f8b70272:"3381","2c09ee5d":"4194","89d24bb8":"4312",dcc76ebc:"4496","44e49d37":"4713",a28f5f9a:"4911",f26efbc5:"4940",dff029d6:"5066",fba06901:"5106",ed63a978:"5290","797a5a6f":"5305","247783bb":"5548",da523844:"5582","671fd195":"5864","6303d649":"5878",f1a90138:"5894","42fb6e35":"6326","2e135cc6":"6587",efaf9258:"6589",c6ec7a52:"6633","6ee27f91":"6964","51639cf9":"7199",fec8a912:"7202","4d029c8d":"7317","96b2b7a2":"8102",fb8f3a82:"8263","62ac5b94":"8397",d05b304a:"8502","935f2afb":"8581",a3ff3870:"8595","944eab8f":"8626","9a6944ab":"8639","1be78505":"8714",ceba1265:"8750","59b068d1":"8968","4388075d":"9060","3ba2fa8f":"9184",c38cf504:"9441","4291f23d":"9514",f89eda61:"9581",b1654ad1:"9715","2bd353df":"9990"}[e]||e,r.p+r.u(e)},(()=>{var e={5354:0,1869:0};r.f.j=(a,f)=>{var b=r.o(e,a)?e[a]:void 0;if(0!==b)if(b)f.push(b[2]);else if(/^(1869|5354)$/.test(a))e[a]=0;else{var c=new Promise(((f,c)=>b=e[a]=[f,c]));f.push(b[2]=c);var d=r.p+r.u(a),t=new Error;r.l(d,(f=>{if(r.o(e,a)&&(0!==(b=e[a])&&(e[a]=void 0),b)){var c=f&&("load"===f.type?"missing":f.type),d=f&&f.target&&f.target.src;t.message="Loading chunk "+a+" failed.\n("+c+": "+d+")",t.name="ChunkLoadError",t.type=c,t.request=d,b[1](t)}}),"chunk-"+a,a)}},r.O.j=a=>0===e[a];var a=(a,f)=>{var b,c,d=f[0],t=f[1],o=f[2],n=0;if(d.some((a=>0!==e[a]))){for(b in t)r.o(t,b)&&(r.m[b]=t[b]);if(o)var i=o(r)}for(a&&a(f);n - + @@ -197,7 +197,7 @@ - + \ No newline at end of file diff --git a/features.html b/features.html index a60cfb4f6..9a211e656 100644 --- a/features.html +++ b/features.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/aws-iam.html b/features/aws-iam.html index 79f381f88..47e747a75 100644 --- a/features/aws-iam.html +++ b/features/aws-iam.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/aws-iam/reference.html b/features/aws-iam/reference.html index 038730c99..0e35fca7d 100644 --- a/features/aws-iam/reference.html +++ b/features/aws-iam/reference.html @@ -14,7 +14,7 @@ - + @@ -155,7 +155,7 @@ - + \ No newline at end of file diff --git a/features/aws-iam/tutorials/aws-iam-eks.html b/features/aws-iam/tutorials/aws-iam-eks.html index 378be5d00..7061e589d 100644 --- a/features/aws-iam/tutorials/aws-iam-eks.html +++ b/features/aws-iam/tutorials/aws-iam-eks.html @@ -14,7 +14,7 @@ - + @@ -159,7 +159,7 @@ - + \ No newline at end of file diff --git a/features/aws-iam/tutorials/aws-visibility.html b/features/aws-iam/tutorials/aws-visibility.html index b04e549f6..edffddd22 100644 --- a/features/aws-iam/tutorials/aws-visibility.html +++ b/features/aws-iam/tutorials/aws-visibility.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/azure-iam.html b/features/azure-iam.html index 1a1a2a4ef..d53b2f0eb 100644 --- a/features/azure-iam.html +++ b/features/azure-iam.html @@ -14,7 +14,7 @@ - + @@ -157,7 +157,7 @@ - + \ No newline at end of file diff --git a/features/azure-iam/reference.html b/features/azure-iam/reference.html index 134766bc4..f147ef7f0 100644 --- a/features/azure-iam/reference.html +++ b/features/azure-iam/reference.html @@ -14,7 +14,7 @@ - + @@ -155,7 +155,7 @@ - + \ No newline at end of file diff --git a/features/azure-iam/tutorials/azure-iam-aks.html b/features/azure-iam/tutorials/azure-iam-aks.html index 7fdce802b..60db3396e 100644 --- a/features/azure-iam/tutorials/azure-iam-aks.html +++ b/features/azure-iam/tutorials/azure-iam-aks.html @@ -14,7 +14,7 @@ - + @@ -163,7 +163,7 @@ - + \ No newline at end of file diff --git a/features/gcp-iam.html b/features/gcp-iam.html index 6d35d4d53..9d0f3c9fa 100644 --- a/features/gcp-iam.html +++ b/features/gcp-iam.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/gcp-iam/reference.html b/features/gcp-iam/reference.html index 628fbd4a5..ff4ea22c0 100644 --- a/features/gcp-iam/reference.html +++ b/features/gcp-iam/reference.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/gcp-iam/tutorials/gcp-iam-gke.html b/features/gcp-iam/tutorials/gcp-iam-gke.html index 08051e4b0..1f98b70b4 100644 --- a/features/gcp-iam/tutorials/gcp-iam-gke.html +++ b/features/gcp-iam/tutorials/gcp-iam-gke.html @@ -14,7 +14,7 @@ - + @@ -163,7 +163,7 @@ - + \ No newline at end of file diff --git a/features/github.html b/features/github.html index f6f8c6e31..82894fa1a 100644 --- a/features/github.html +++ b/features/github.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/github/reference.html b/features/github/reference.html index b7b8e03c1..786c5e0d5 100644 --- a/features/github/reference.html +++ b/features/github/reference.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/github/tutorials/automated-pull-requests.html b/features/github/tutorials/automated-pull-requests.html index 001097d7d..0ef610b9a 100644 --- a/features/github/tutorials/automated-pull-requests.html +++ b/features/github/tutorials/automated-pull-requests.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/gitlab.html b/features/gitlab.html index f53c0c6df..1d1528e07 100644 --- a/features/gitlab.html +++ b/features/gitlab.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/gitlab/reference.html b/features/gitlab/reference.html index 37c168309..63d0fd45e 100644 --- a/features/gitlab/reference.html +++ b/features/gitlab/reference.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/gitlab/tutorials/automated-merge-requests.html b/features/gitlab/tutorials/automated-merge-requests.html index 24ad1e72c..17dad5a89 100644 --- a/features/gitlab/tutorials/automated-merge-requests.html +++ b/features/gitlab/tutorials/automated-merge-requests.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/istio.html b/features/istio.html index 67fcb63eb..0c7f3ec68 100644 --- a/features/istio.html +++ b/features/istio.html @@ -14,7 +14,7 @@ - + @@ -156,7 +156,7 @@ - + \ No newline at end of file diff --git a/features/istio/reference.html b/features/istio/reference.html index 21533b932..e9637e778 100644 --- a/features/istio/reference.html +++ b/features/istio/reference.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/istio/tutorials/k8s-istio-authorization-policies.html b/features/istio/tutorials/k8s-istio-authorization-policies.html index 3f7cd03ec..9c8761f85 100644 --- a/features/istio/tutorials/k8s-istio-authorization-policies.html +++ b/features/istio/tutorials/k8s-istio-authorization-policies.html @@ -14,7 +14,7 @@ - + @@ -171,7 +171,7 @@ - + \ No newline at end of file diff --git a/features/istio/tutorials/k8s-istio-watcher.html b/features/istio/tutorials/k8s-istio-watcher.html index 37b09ad3e..8ca4bf88c 100644 --- a/features/istio/tutorials/k8s-istio-watcher.html +++ b/features/istio/tutorials/k8s-istio-watcher.html @@ -14,7 +14,7 @@ - + @@ -164,7 +164,7 @@ - + \ No newline at end of file diff --git a/features/kafka.html b/features/kafka.html index f18eafb5a..57ffa0a36 100644 --- a/features/kafka.html +++ b/features/kafka.html @@ -14,7 +14,7 @@ - + @@ -155,7 +155,7 @@ - + \ No newline at end of file diff --git a/features/kafka/reference.html b/features/kafka/reference.html index 154f62399..b2ad8bd69 100644 --- a/features/kafka/reference.html +++ b/features/kafka/reference.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/kafka/tutorials/k8s-kafka-mapping.html b/features/kafka/tutorials/k8s-kafka-mapping.html index 10993cc90..d71b5fab8 100644 --- a/features/kafka/tutorials/k8s-kafka-mapping.html +++ b/features/kafka/tutorials/k8s-kafka-mapping.html @@ -14,7 +14,7 @@ - + @@ -160,7 +160,7 @@ - + \ No newline at end of file diff --git a/features/kafka/tutorials/k8s-kafka-mtls-cert-manager.html b/features/kafka/tutorials/k8s-kafka-mtls-cert-manager.html index 8c842b5cb..3190adf3a 100644 --- a/features/kafka/tutorials/k8s-kafka-mtls-cert-manager.html +++ b/features/kafka/tutorials/k8s-kafka-mtls-cert-manager.html @@ -14,7 +14,7 @@ - + @@ -164,7 +164,7 @@ - + \ No newline at end of file diff --git a/features/kafka/tutorials/k8s-kafka-mtls.html b/features/kafka/tutorials/k8s-kafka-mtls.html index 9a4f3c0cc..8f4a2bcfb 100644 --- a/features/kafka/tutorials/k8s-kafka-mtls.html +++ b/features/kafka/tutorials/k8s-kafka-mtls.html @@ -14,7 +14,7 @@ - + @@ -163,7 +163,7 @@ - + \ No newline at end of file diff --git a/features/mysql.html b/features/mysql.html index 97eae60ba..85295d5af 100644 --- a/features/mysql.html +++ b/features/mysql.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/mysql/reference.html b/features/mysql/reference.html index e5a102d0b..93aca4880 100644 --- a/features/mysql/reference.html +++ b/features/mysql/reference.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/mysql/tutorials/mysql.html b/features/mysql/tutorials/mysql.html index 4a48d984d..2bcd239c5 100644 --- a/features/mysql/tutorials/mysql.html +++ b/features/mysql/tutorials/mysql.html @@ -14,7 +14,7 @@ - + @@ -60,10 +60,10 @@

Just-in-time MySQL access

Overview

This tutorial will deploy an example cluster to highlight Otterize's MySQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:

  1. An INSERT operation to append a table within the database
  2. A SELECT operation to validate the updates.

The server needs appropriate permissions to access the database. You could use one admin user for all services, which is insecure and is the cause for many security breaches. With Otterize, you can specify required access, and have Otterize create users and perform correctly scoped SQL GRANTs just in time, as the service spins up and down.

In this tutorial, we will:

  • Optionally, spin up a MySQL database instance on AWS, based on Amazon RDS for MySQL, or in your Kubernetes cluster, based on the official MySQL Docker image. Alternatively, you could use any MySQL server of your choice.
  • Deploy an example cluster
  • Deploy Otterize in our cluster and give it access to our database instance
  • Declare a ClientIntents resource for the server, specifying required access
  • See that the required access has been granted

Prerequisites

1. Minikube Cluster

Prepare a Kubernetes cluster with Minikube

For this tutorial you'll need a local Kubernetes cluster. Having a cluster with a CNI that supports NetworkPolicies isn't required for this tutorial, but is recommended so that your cluster works with other tutorials.

If you don't have the Minikube CLI, first install it.

Then start your Minikube cluster with Calico, in order to enforce network policies.

minikube start --cpus=4 --memory 4096 --disk-size 32g --cni=calico

2. Deploy Otterize

To deploy Otterize, head over to Otterize Cloud and associate a Kubernetes cluster on the Integrations page, and follow the instructions. If you already have a Kubernetes cluster connected, skip this step.

3. Deploy a MySQL database instance

Already have a MySQL database instance? Skip to the tutorial.

Deploy a MySQL database instance, based on Amazon RDS for MySQL

Follow the installation instructions on the AWS RDS documentation.

  • You may use the Free tier template for this tutorial.
  • Under "Settings", choose "Auto generate password". Make sure you save the generated password after the instance is created.
  • Under "Connectivity", enable public access to allow access from your Kubernetes cluster. Otterize will require that access to manage credentials for you. Additionally, make sure you choose a security group that allows inbound access from the internet.
  • Deploy a MySQL database instance, based on the official MySQL Docker image

    To deploy a local MySQL database instance, you can use the official MySQL Docker image. Run the following command to deploy a MySQL instance with the root password set to password:

    kubectl create namespace otterize-tutorial-mysql
    kubectl apply -n otterize-tutorial-mysql -f https://docs.otterize.com/code-examples/mysql/database.yaml

    Use the following values as your MySQL host and password:

    export MYSQLHOST=mysql.otterize-tutorial-mysql.svc.cluster.local
    export MYSQLUSER=root
    export MYSQLPASSWORD=password

    Tutorial

    Setup MySQL database and table for the tutorial

    Throughout this tutorial, we will refer to your MySQL host & credentials via environment variables, so make sure to set them up:

    export MYSQLHOST=<YOURMYSQLHOST> # For RDS, this is the endpoint; for the official MySQL docker image, this is `mysql.otterize-tutorial-mysql.svc.cluster.local`
    export MYSQLUSER=<YOURUSER> # For RDS, this is the username set during the RDS instance deployment, typically 'admin'; for the official MySQL docker image, this is `root`
    export MYSQLPASSWORD=<YOURPASSWORD> # For RDS, this is the password set during the RDS instance deployment; for the official MySQL docker image, this is `password`

    Next, start a MySQL client to connect to your MySQL instance, and create a database named otterize_tutorial and a table named example in your MySQL instance. -Our tutorial server will use this database and table to perform INSERT and SELECT operations.

    kubectl create namespace otterize-tutorial-mysql
    kubectl run -n otterize-tutorial-mysql -it --rm --image=mysql:latest --restart=Never mysql-client -- mysql -h $MYSQLHOST -u $MYSQLUSER -p$MYSQLPASSWORD \
    -e 'CREATE DATABASE IF NOT EXISTS otterize_example;

    USE otterize_example;

    CREATE TABLE IF NOT EXISTS example
    (
    file_name VARCHAR(255),
    upload_time BIGINT
    );

    exit;
    '

    Deploy tutorial services and request database credentials

    Next, set up the namespace used for our tutorial and deploy the client & server services in it:

    kubectl create namespace otterize-tutorial-mysql
    kubectl apply -n otterize-tutorial-mysql -f https://docs.otterize.com/code-examples/mysql/client-server.yaml
    kubectl patch deployment -n otterize-tutorial-mysql server --type='json' -p="[{\"op\": \"replace\", \"path\": \"/spec/template/spec/containers/0/env/0/value\", \"value\": \"$MYSQLHOST\"}]"
    Expand to see the deployment YAML
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: server
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: server
    template:
    metadata:
    annotations:
    credentials-operator.otterize.com/user-password-secret-name: server-creds
    labels:
    app: server
    spec:
    serviceAccountName: server
    containers:
    - name: server
    imagePullPolicy: Always
    image: 'otterize/mysql-tutorial-server'
    ports:
    - containerPort: 80
    env:
    - name: DB_HOST
    value: database
    - name: DB_NAME
    value: otterize_example
    - name: DB_PORT
    value: "3306"
    - name: DB_SERVER_USER
    valueFrom:
    secretKeyRef:
    name: server-creds
    key: username
    - name: DB_SERVER_PASSWORD
    valueFrom:
    secretKeyRef:
    name: server-creds
    key: password
    ---
    apiVersion: v1
    kind: Service
    metadata:
    name: server
    spec:
    type: ClusterIP
    selector:
    app: server
    ports:
    - name: http
    port: 80
    targetPort: 80
    ---
    apiVersion: v1
    kind: ServiceAccount
    metadata:
    name: server
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: client
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: client
    template:
    metadata:
    labels:
    app: client
    spec:
    containers:
    - name: client
    imagePullPolicy: Always
    image: 'otterize/mysql-tutorial-client'
    ports:
    - containerPort: 80

    Our server's Deployment spec specify an annotation on its Pod, which requests that the Otterize operator provision a username and password for it:

      template:
    metadata:
    annotations:
    credentials-operator.otterize.com/user-password-secret-name: server-creds

    This specifies that the secret server-creds will be populated with keys containing the username and password used by this pod to connect to the database. -The secret will only be created by the Otterize operator after it is integrated with your database by applying a MySQLServerConfig resources.

    View logs for the server

    After the client, server, and database are up and running, we can see that the server does not have the appropriate access to the database by inspecting the logs with the following command.

    kubectl logs -f -n otterize-tutorial-mysql deploy/server

    Example log:

    Unable to perform INSERT operation
    Unable to perform SELECT operation

    Deploy a MySQLServerConfig to allow Otterize DB access

    Let's apply a MySQLServerConfig so Otterize will know how to access our database instance:

    kubectl apply -n otterize-tutorial-mysql -f https://docs.otterize.com/code-examples/mysql/mysqlserverconfig.yaml
    kubectl patch mysqlserverconfig -n otterize-tutorial-mysql mysql-tutorial-db --type='json' -p="[{\"op\": \"replace\", \"path\": \"/spec/address\", \"value\": \"$MYSQLHOST\"}]"
    MYSQLUSER_B64=$(echo -n $MYSQLUSER | base64)
    MYSQLPASSWORD_B64=$(echo -n $MYSQLPASSWORD | base64)
    kubectl patch secret -n otterize-tutorial-mysql mysql-tutorial-db-credentials --type='json' -p="[{\"op\": \"replace\", \"path\": \"/data/username\", \"value\": \"$MYSQLUSER_B64\"}, {\"op\": \"replace\", \"path\": \"/data/password\", \"value\": \"$MYSQLPASSWORD_B64\"}]"

    This applies the following MySQLServerConfig to your cluster, and patches it with your DB instance address & credentials:

    apiVersion: k8s.otterize.com/v1alpha3
    kind: MySQLServerConfig
    metadata:
    name: mysql-tutorial-db
    spec:
    address: mysql.otterize-tutorial-mysql.svc.cluster.local:3306 # Your MySQL server address
    credentials:
    secretRef:
    name: mysql-tutorial-db-credentials
    ---
    apiVersion: v1
    type: Opaque
    kind: Secret
    metadata:
    name: mysql-tutorial-db-credentials
    data:
    username: '' # Your MySQL server user
    password: '' # Your MySQL server password

    The above CRD tells Otterize how to access a database instance named mysql-tutorial-db, meaning that when intents +Our tutorial server will use this database and table to perform INSERT and SELECT operations.

    kubectl create namespace otterize-tutorial-mysql
    kubectl run -n otterize-tutorial-mysql -it --rm --image=mysql:latest --restart=Never mysql-client -- mysql -h $MYSQLHOST -u $MYSQLUSER -p$MYSQLPASSWORD \
    -e 'CREATE DATABASE IF NOT EXISTS otterize_example;

    USE otterize_example;

    CREATE TABLE IF NOT EXISTS example
    (
    file_name VARCHAR(255),
    upload_time BIGINT
    );

    exit;
    '

    Deploy tutorial services and request database credentials

    Next, set up the namespace used for our tutorial and deploy the client & server services in it:

    kubectl create namespace otterize-tutorial-mysql
    kubectl apply -n otterize-tutorial-mysql -f https://docs.otterize.com/code-examples/mysql/client-server.yaml
    kubectl patch deployment -n otterize-tutorial-mysql server --type='json' -p="[{\"op\": \"replace\", \"path\": \"/spec/template/spec/containers/0/env/0/value\", \"value\": \"$MYSQLHOST\"}]"
    Expand to see the deployment YAML
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: server
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: server
    template:
    metadata:
    annotations:
    credentials-operator.otterize.com/user-password-secret-name: server-creds
    labels:
    app: server
    spec:
    serviceAccountName: server
    containers:
    - name: server
    imagePullPolicy: Always
    image: 'otterize/mysql-tutorial-server'
    ports:
    - containerPort: 80
    env:
    - name: DB_HOST
    value: database
    - name: DB_NAME
    value: otterize_example
    - name: DB_PORT
    value: "3306"
    - name: DB_SERVER_USER
    valueFrom:
    secretKeyRef:
    name: server-creds
    key: username
    - name: DB_SERVER_PASSWORD
    valueFrom:
    secretKeyRef:
    name: server-creds
    key: password
    ---
    apiVersion: v1
    kind: Service
    metadata:
    name: server
    spec:
    type: ClusterIP
    selector:
    app: server
    ports:
    - name: http
    port: 80
    targetPort: 80
    ---
    apiVersion: v1
    kind: ServiceAccount
    metadata:
    name: server
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: client
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: client
    template:
    metadata:
    labels:
    app: client
    spec:
    containers:
    - name: client
    imagePullPolicy: Always
    image: 'otterize/mysql-tutorial-client'
    ports:
    - containerPort: 80

    Our server's Deployment spec specifies an annotation on its Pod, which requests that the Otterize operator provision a username and password for it:

      template:
    metadata:
    annotations:
    credentials-operator.otterize.com/user-password-secret-name: server-creds

    This specifies that the secret server-creds will be populated with keys containing the username and password used by this pod to connect to the database. +The secret will only be created by the Otterize operator after it is integrated with your database by applying a MySQLServerConfig resources.

    View logs for the server

    After the client, server, and database are up and running, we can see that the server does not have the appropriate access to the database by inspecting the logs with the following command.

    kubectl logs -f -n otterize-tutorial-mysql deploy/server

    Example log:

    Unable to perform INSERT operation
    Unable to perform SELECT operation

    Deploy a MySQLServerConfig to allow Otterize DB access

    Let's apply a MySQLServerConfig so Otterize will know how to access our database instance.

    First, create a Kuberentes secret containing the database credentials:

    kubectl create secret generic mysql-tutorial-db-credentials -n otterize-tutorial-mysql --from-literal=username=$MYSQLUSER --from-literal=password=$MYSQLPASSWORD

    Next, apply the MySQLServerConfig to the cluster, and patch it with your DB instance address:

    kubectl apply -n otterize-tutorial-mysql -f https://docs.otterize.com/code-examples/mysql/mysqlserverconfig.yaml
    kubectl patch mysqlserverconfig -n otterize-tutorial-mysql mysql-tutorial-db --type='json' -p="[{\"op\": \"replace\", \"path\": \"/spec/address\", \"value\": \"$MYSQLHOST\"}]"

    This MySQLServerConfig tells Otterize how to access a database instance named mysql-tutorial-db, meaning that when intents are applied requesting access permissions to mysql-tutorial-db, the Otterize operator will be able to configure -them.

    In this tutorial, we use the admin user to grant Otterize permissions to create users and grant them access to the database. +them:

    apiVersion: k8s.otterize.com/v1alpha3
    kind: MySQLServerConfig
    metadata:
    name: mysql-tutorial-db
    spec:
    address: mysql.otterize-tutorial-mysql.svc.cluster.local:3306 # Your MySQL server address
    credentials:
    secretRef:
    name: mysql-tutorial-db-credentials

    In this tutorial, we use the admin user to grant Otterize permissions to create users and grant them access to the database. In a production environment, it is recommended to create a dedicated user for Otterize, and grant it the necessary permissions to create and manage other users.

    Define your ClientIntents

    ClientIntents are Otterize’s way of defining access through unique relationships, which lead to perfectly scoped access. In this example, we provide our server workload the ability to insert and select records to allow it to access the database.

    Below is our intents.yaml file. As you can see, it is scoped to our database named otterize_tutorial and our example table. We also have limited the access to just SELECT and INSERT operations. We could add more databases, tables, or operations if our service required more access.

    Specifying the table and operations is optional. If you don't specify the table, access will be granted to all tables in the specified database. If you don't specify the operations, all operations will be allowed.

    apiVersion: k8s.otterize.com/v1alpha3
    kind: ClientIntents
    metadata:
    name: client-intents-for-server
    spec:
    service:
    name: server
    calls:
    - name: mysql-tutorial-db
    type: database
    databaseResources:
    - databaseName: otterize_example
    table: example
    operations:
    - SELECT
    - INSERT

    We can now apply our intents. Behind the scenes, the Otterize operator created the user for our server workload and executed GRANT queries on the database, making our SELECT and INSERT errors disappear.

    kubectl apply -n otterize-tutorial-mysql -f https://docs.otterize.com/code-examples/mysql/clientintents.yaml

    View logs for the server

    We can now view the server logs once again. This time, we should see that the server has the appropriate access to the database:

    kubectl logs -f -n otterize-tutorial-mysql deploy/server

    Example log:

    Successfully INSERTED into our table

    Successfully SELECTED, most recent value: 2024-04-30T13:20:46Z

    That’s it! If your service’s functionality changes, adding or removing access is as simple as updating your ClientIntents definitions. For fun, try altering the operations to just SELECT or INSERT.

    Teardown

    To remove the deployed examples, run:

    kubectl delete clientintents.k8s.otterize.com -n otterize-tutorial-mysql client-intents-for-server
    kubectl delete namespace otterize-tutorial-mysql
    - + \ No newline at end of file diff --git a/features/network-mapping-network-policies.html b/features/network-mapping-network-policies.html index 4a7114109..6f00efd6d 100644 --- a/features/network-mapping-network-policies.html +++ b/features/network-mapping-network-policies.html @@ -14,7 +14,7 @@ - + @@ -157,7 +157,7 @@ - + \ No newline at end of file diff --git a/features/network-mapping-network-policies/reference.html b/features/network-mapping-network-policies/reference.html index 14d094934..cadf23e8b 100644 --- a/features/network-mapping-network-policies/reference.html +++ b/features/network-mapping-network-policies/reference.html @@ -14,7 +14,7 @@ - + @@ -161,7 +161,7 @@ - + \ No newline at end of file diff --git a/features/network-mapping-network-policies/reference/Network-Policies-Deep-Dive.html b/features/network-mapping-network-policies/reference/Network-Policies-Deep-Dive.html index 881509041..b6fae89af 100644 --- a/features/network-mapping-network-policies/reference/Network-Policies-Deep-Dive.html +++ b/features/network-mapping-network-policies/reference/Network-Policies-Deep-Dive.html @@ -14,7 +14,7 @@ - + @@ -166,7 +166,7 @@ - + \ No newline at end of file diff --git a/features/network-mapping-network-policies/tutorials/aws-eks-cni-mini.html b/features/network-mapping-network-policies/tutorials/aws-eks-cni-mini.html index 85042ca0f..1bcd1a213 100644 --- a/features/network-mapping-network-policies/tutorials/aws-eks-cni-mini.html +++ b/features/network-mapping-network-policies/tutorials/aws-eks-cni-mini.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/network-mapping-network-policies/tutorials/k8s-egress-access-control-tutorial.html b/features/network-mapping-network-policies/tutorials/k8s-egress-access-control-tutorial.html index e9c6de110..53b93f8ef 100644 --- a/features/network-mapping-network-policies/tutorials/k8s-egress-access-control-tutorial.html +++ b/features/network-mapping-network-policies/tutorials/k8s-egress-access-control-tutorial.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/network-mapping-network-policies/tutorials/k8s-network-mapper.html b/features/network-mapping-network-policies/tutorials/k8s-network-mapper.html index ff03ade00..54b7cc726 100644 --- a/features/network-mapping-network-policies/tutorials/k8s-network-mapper.html +++ b/features/network-mapping-network-policies/tutorials/k8s-network-mapper.html @@ -14,7 +14,7 @@ - + @@ -163,7 +163,7 @@ - + \ No newline at end of file diff --git a/features/network-mapping-network-policies/tutorials/k8s-network-policies.html b/features/network-mapping-network-policies/tutorials/k8s-network-policies.html index 519851364..6314a952a 100644 --- a/features/network-mapping-network-policies/tutorials/k8s-network-policies.html +++ b/features/network-mapping-network-policies/tutorials/k8s-network-policies.html @@ -14,7 +14,7 @@ - + @@ -173,7 +173,7 @@ - + \ No newline at end of file diff --git a/features/network-mapping-network-policies/tutorials/protect-1-service-network-policies.html b/features/network-mapping-network-policies/tutorials/protect-1-service-network-policies.html index f7d687388..670279a46 100644 --- a/features/network-mapping-network-policies/tutorials/protect-1-service-network-policies.html +++ b/features/network-mapping-network-policies/tutorials/protect-1-service-network-policies.html @@ -14,7 +14,7 @@ - + @@ -157,7 +157,7 @@ - + \ No newline at end of file diff --git a/features/postgresql.html b/features/postgresql.html index 21ac00295..875cc4127 100644 --- a/features/postgresql.html +++ b/features/postgresql.html @@ -14,7 +14,7 @@ - + @@ -155,7 +155,7 @@ - + \ No newline at end of file diff --git a/features/postgresql/reference.html b/features/postgresql/reference.html index 1a9b6390f..2e9e85c7b 100644 --- a/features/postgresql/reference.html +++ b/features/postgresql/reference.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/features/postgresql/tutorials/postgres-mapping.html b/features/postgresql/tutorials/postgres-mapping.html index c5e52ded7..276d404b9 100644 --- a/features/postgresql/tutorials/postgres-mapping.html +++ b/features/postgresql/tutorials/postgres-mapping.html @@ -14,7 +14,7 @@ - + @@ -175,7 +175,7 @@ - + \ No newline at end of file diff --git a/features/postgresql/tutorials/postgres.html b/features/postgresql/tutorials/postgres.html index 95609d850..bf4bf26c6 100644 --- a/features/postgresql/tutorials/postgres.html +++ b/features/postgresql/tutorials/postgres.html @@ -14,7 +14,7 @@ - + @@ -59,11 +59,11 @@

    Dive into our company’s mission, our philosophy, and the team that makes it all possible.

    -

    Just-in-time PostgreSQL access

    Overview

    This tutorial will deploy an example cluster to highlight Otterize's PostgreSQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:

    1. An INSERT operation to append a table within the database
    2. A SELECT operation to validate the updates.

    The server needs appropriate permissions to access the database. You could use one admin user for all services, which is insecure and is the cause for many security breaches. With Otterize, you can specify required access, and have Otterize create users and perform correctly scoped SQL GRANTs just in time, as the service spins up and down.

    In this tutorial, we will:

    • Deploy an example cluster
    • Deploy Otterize in our cluster and give it access to our database instance
    • Declare a ClientIntents resource for the server, specifying required access
    • See that the required access has been granted

    Prerequisites

    1. Minikube Cluster

    Prepare a Kubernetes cluster with Minikube

    For this tutorial you'll need a local Kubernetes cluster. Having a cluster with a CNI that supports NetworkPolicies isn't required for this tutorial, but is recommended so that your cluster works with other tutorials.

    If you don't have the Minikube CLI, first install it.

    Then start your Minikube cluster with Calico, in order to enforce network policies.

    minikube start --cpus=4 --memory 4096 --disk-size 32g --cni=calico

    2. Deploy Otterize

    To deploy Otterize, head over to Otterize Cloud and associate a Kubernetes cluster on the Integrations page, and follow the instructions. If you already have a Kubernetes cluster connected, skip this step.

    Tutorial

    Deploy tutorial services and request database credentials

    This will set up the namespace we will use for our tutorial and deploy the client, server, and database.

    Our server's Deployment spec will specify an annotation on the Pod, which requests that the Otterize operator will provision a username and password for the server.

      template:
    metadata:
    annotations:
    credentials-operator.otterize.com/user-password-secret-name: server-creds

    This specifies that the secret server-creds will have keys with the username and password to connect to the database. -The secret will only be created by the Otterize operator after it is integrated with your database by applying a MySQLServerConfig resources.

    kubectl create namespace otterize-tutorial-postgres
    kubectl apply -n otterize-tutorial-postgres -f https://docs.otterize.com/code-examples/postgres/client-server-database.yaml

    Deploy a PostgreSQLServerConfig to allow Otterize DB access

    apiVersion: k8s.otterize.com/v1alpha3
    kind: PostgreSQLServerConfig
    metadata:
    name: postgres-tutorial-db
    spec:
    address: database.otterize-tutorial-postgres.svc.cluster.local:5432
    credentials:
    secretRef:
    name: postgres-tutorial-db-credentials
    ---
    apiVersion: v1
    type: Opaque
    kind: Secret
    metadata:
    name: postgres-tutorial-db-credentials
    data:
    username: '' # Your PostgreSQL server user
    password: '' # Your PostgreSQL server password

    The above CRD tells Otterize how to access a database instance named postgres-tutorial-db, meaning that when intents +

    Just-in-time PostgreSQL access

    Overview

    This tutorial will deploy an example cluster to highlight Otterize's PostgreSQL capabilities. Within that cluster is a client service that hits an endpoint on a server, which then connects to a database. The server runs two different database operations:

    1. An INSERT operation to append a table within the database
    2. A SELECT operation to validate the updates.

    The server needs appropriate permissions to access the database. You could use one admin user for all services, which is insecure and is the cause for many security breaches. With Otterize, you can specify required access, and have Otterize create users and perform correctly scoped SQL GRANTs just in time, as the service spins up and down.

    In this tutorial, we will:

    • Deploy an example cluster
    • Deploy Otterize in our cluster and give it access to our database instance
    • Declare a ClientIntents resource for the server, specifying required access
    • See that the required access has been granted

    Prerequisites

    1. Minikube Cluster

    Prepare a Kubernetes cluster with Minikube

    For this tutorial you'll need a local Kubernetes cluster. Having a cluster with a CNI that supports NetworkPolicies isn't required for this tutorial, but is recommended so that your cluster works with other tutorials.

    If you don't have the Minikube CLI, first install it.

    Then start your Minikube cluster with Calico, in order to enforce network policies.

    minikube start --cpus=4 --memory 4096 --disk-size 32g --cni=calico

    2. Deploy Otterize

    To deploy Otterize, head over to Otterize Cloud and associate a Kubernetes cluster on the Integrations page, and follow the instructions. If you already have a Kubernetes cluster connected, skip this step.

    Tutorial

    Deploy tutorial services and request database credentials

    Next, set up the namespace used for our tutorial and deploy the client, server & database services in it:

    kubectl create namespace otterize-tutorial-postgres
    kubectl apply -n otterize-tutorial-postgres -f https://docs.otterize.com/code-examples/postgres/client-server-database.yaml
    Expand to see the deployment YAML
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: server
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: server
    template:
    metadata:
    annotations:
    credentials-operator.otterize.com/user-password-secret-name: server-creds
    labels:
    app: server
    spec:
    serviceAccountName: server
    containers:
    - name: server
    imagePullPolicy: Always
    image: 'otterize/postgres-tutorial-server'
    ports:
    - containerPort: 80
    env:
    - name: DB_SERVER_USER
    valueFrom:
    secretKeyRef:
    name: server-creds
    key: username
    - name: DB_SERVER_PASSWORD
    valueFrom:
    secretKeyRef:
    name: server-creds
    key: password
    ---
    apiVersion: v1
    kind: Service
    metadata:
    name: server
    spec:
    type: ClusterIP
    selector:
    app: server
    ports:
    - name: http
    port: 80
    targetPort: 80
    ---
    apiVersion: v1
    kind: ServiceAccount
    metadata:
    name: server
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: client
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: client
    template:
    metadata:
    labels:
    app: client
    spec:
    containers:
    - name: client
    imagePullPolicy: Always
    image: 'otterize/postgres-tutorial-client'
    ports:
    - containerPort: 80
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: database
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: database
    template:
    metadata:
    labels:
    app: database
    spec:
    containers:
    - name: database
    imagePullPolicy: Always
    image: 'otterize/postgres-tutorial-database'
    ports:
    - containerPort: 5432
    ---
    apiVersion: v1
    kind: Service
    metadata:
    name: database
    spec:
    selector:
    app: database
    ports:
    - protocol: TCP
    port: 5432
    targetPort: 5432

    Our server's Deployment spec specifies an annotation on its Pod, which requests that the Otterize operator provision a username and password for it:

      template:
    metadata:
    annotations:
    credentials-operator.otterize.com/user-password-secret-name: server-creds

    This specifies that the secret server-creds will be populated with keys containing the username and password used by this pod to connect to the database. +The secret will only be created by the Otterize operator after it is integrated with your database by applying a PostgreSQLServerConfig resources.

    View logs for the server

    After the client, server, and database are up and running, we can see that the server does not have the appropriate access to the database by inspecting the logs with the following command.

    kubectl logs -f -n otterize-tutorial-postgres deploy/server

    Example log:

    Unable to perform INSERT operation
    Unable to perform SELECT operation

    Deploy a PostgreSQLServerConfig to allow Otterize DB access

    Let's apply a PostgreSQLServerConfig so Otterize will know how to access our database instance.

    First, create a Kuberentes secret containing the database credentials:

    kubectl create secret generic postgres-tutorial-db-credentials -n otterize-tutorial-postgres --from-literal=username='otterize-tutorial' --from-literal=password='jeffdog523'

    In this tutorial, the PostgreSQL database comes with the predefined username & password, but for future uses a +role will have to be created in the database to grant Otterize access as well as the ability to configure other users.

    Next, apply the PostgreSQLServerConfig to the cluster:

    kubectl apply -n otterize-tutorial-postgres -f https://docs.otterize.com/code-examples/postgres/postgresqlserverconfig.yaml

    This PostgreSQLServerConfig tells Otterize how to access a database instance named postgres-tutorial-db, meaning that when intents are applied requesting access permissions to postgres-tutorial-db, the Otterize operator will be able to configure -them.

    In this tutorial, the database workload already comes with the predefined username & password, but for future uses a -role will have to be created in the database to grant Otterize access as well as the ability to configure other users.

    Let's apply the above PostgreSQLServerConfig so Otterize will know how to access our database instance.

    kubectl apply -n otterize-tutorial-postgres -f https://docs.otterize.com/code-examples/postgres/postgresqlserverconfig.yaml
    PSQLUSER_B64=$(echo -n otterize-tutorial | base64)
    PSQLPASSWORD_B64=$(echo -n jeffdog523 | base64)
    kubectl patch secret -n otterize-tutorial-postgres postgres-tutorial-db-credentials --type='json' -p="[{\"op\": \"replace\", \"path\": \"/data/username\", \"value\": \"$PSQLUSER_B64\"}, {\"op\": \"replace\", \"path\": \"/data/password\", \"value\": \"$PSQLPASSWORD_B64\"}]"

    View logs for the server

    After the client, server, and database are up and running, we can see that the server does not have the appropriate access to the database by inspecting the logs with the following command.

    kubectl logs -f -n otterize-tutorial-postgres deploy/server

    Example log:

    Unable to perform INSERT operation
    Unable to perform SELECT operation

    Define your ClientIntents

    ClientIntents are Otterize’s way of defining access through unique relationships, which lead to perfectly scoped access. In this example, we provide our server workload the ability to insert and select records to allow it to access the database.

    Below is our intents.yaml file. As you can see, it is scoped to our database named otterize-tutorial and our public.example table. We also have limited the access to just SELECT and INSERT operations. We could add more databases, tables, or operations if our service required more access.

    Specifying the table and operations is optional. If you don't specify the table, access will be granted to all tables in the specified database. If you don't specify the operations, all operations will be allowed.

    apiVersion: k8s.otterize.com/v1alpha3
    kind: ClientIntents
    metadata:
    name: client-intents-for-server
    namespace: otterize-tutorial-postgres
    spec:
    service:
    name: server
    calls:
    - name: postgres-tutorial-db # Same name as our PostgreSQLServerConfig metadata.name
    type: database
    databaseResources:
    - databaseName: otterize-tutorial
    table: public.example
    operations:
    - SELECT
    - INSERT

    We can now apply our intents. Behind the scenes, the Otterize operator created the user for our server workload and executed GRANT queries on the database, making our SELECT and INSERT errors disappear.

    kubectl apply -n otterize-tutorial-postgres -f https://docs.otterize.com/code-examples/postgres/clientintents.yaml

    View logs for the server

    We can now view the server logs once again. This time, we should see that the server has the appropriate access to the database:

    Successfully INSERTED into our table

    Successfully SELECTED, most recent value: 2024-04-30T13:20:46Z

    That’s it! If your service’s functionality changes, adding or removing access is as simple as updating your ClientIntents definitions. For fun, try altering the operations to just SELECT or INSERT.

    Teardown

    To remove the deployed examples, run:

    kubectl delete clientintents.k8s.otterize.com -n otterize-tutorial-postgres client-intents-for-server && \
    kubectl delete namespace otterize-tutorial-postgres
    - + \ No newline at end of file diff --git a/index.html b/index.html index 840ab3f3a..7f2e0639b 100644 --- a/index.html +++ b/index.html @@ -14,7 +14,7 @@ - + @@ -155,7 +155,7 @@ - + \ No newline at end of file diff --git a/overview.html b/overview.html index 212cafa82..359d0af3f 100644 --- a/overview.html +++ b/overview.html @@ -14,7 +14,7 @@ - + @@ -155,7 +155,7 @@ - + \ No newline at end of file diff --git a/overview/installation.html b/overview/installation.html index c4cbedbd9..147156c5e 100644 --- a/overview/installation.html +++ b/overview/installation.html @@ -14,7 +14,7 @@ - + @@ -161,7 +161,7 @@ - + \ No newline at end of file diff --git a/overview/intent-based-access-control.html b/overview/intent-based-access-control.html index cc060442f..4da3b673b 100644 --- a/overview/intent-based-access-control.html +++ b/overview/intent-based-access-control.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/overview/otterize-cloud.html b/overview/otterize-cloud.html index fcca1ec84..98815336e 100644 --- a/overview/otterize-cloud.html +++ b/overview/otterize-cloud.html @@ -14,7 +14,7 @@ - + @@ -169,7 +169,7 @@ - + \ No newline at end of file diff --git a/overview/otterize-oss.html b/overview/otterize-oss.html index 08575dcc2..661dceab4 100644 --- a/overview/otterize-oss.html +++ b/overview/otterize-oss.html @@ -14,7 +14,7 @@ - + @@ -159,7 +159,7 @@ - + \ No newline at end of file diff --git a/overview/otterize-oss/error-telemetry.html b/overview/otterize-oss/error-telemetry.html index ab57c5898..d8fdb9aaf 100644 --- a/overview/otterize-oss/error-telemetry.html +++ b/overview/otterize-oss/error-telemetry.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/overview/otterize-oss/usage-telemetry.html b/overview/otterize-oss/usage-telemetry.html index b6cdc9dd2..10aa7c9eb 100644 --- a/overview/otterize-oss/usage-telemetry.html +++ b/overview/otterize-oss/usage-telemetry.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/reference/IBAC-Overview.html b/reference/IBAC-Overview.html index 06d32c1c7..35da692db 100644 --- a/reference/IBAC-Overview.html +++ b/reference/IBAC-Overview.html @@ -14,7 +14,7 @@ - + @@ -182,7 +182,7 @@ - + \ No newline at end of file diff --git a/reference/api.html b/reference/api.html index 15bcec34c..9aab93f7d 100644 --- a/reference/api.html +++ b/reference/api.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/reference/cli.html b/reference/cli.html index 75054b4eb..76a0a8452 100644 --- a/reference/cli.html +++ b/reference/cli.html @@ -14,7 +14,7 @@ - + @@ -172,7 +172,7 @@ - + \ No newline at end of file diff --git a/reference/configuration/credentials-operator.html b/reference/configuration/credentials-operator.html index 31084fa58..a91611ad6 100644 --- a/reference/configuration/credentials-operator.html +++ b/reference/configuration/credentials-operator.html @@ -14,7 +14,7 @@ - + @@ -157,7 +157,7 @@ - + \ No newline at end of file diff --git a/reference/configuration/credentials-operator/helm-chart.html b/reference/configuration/credentials-operator/helm-chart.html index 0217df890..b0fd7505f 100644 --- a/reference/configuration/credentials-operator/helm-chart.html +++ b/reference/configuration/credentials-operator/helm-chart.html @@ -14,7 +14,7 @@ - + @@ -155,7 +155,7 @@ - + \ No newline at end of file diff --git a/reference/configuration/intents-operator.html b/reference/configuration/intents-operator.html index 45b3fa047..c4979a418 100644 --- a/reference/configuration/intents-operator.html +++ b/reference/configuration/intents-operator.html @@ -14,7 +14,7 @@ - + @@ -172,7 +172,7 @@ - + \ No newline at end of file diff --git a/reference/configuration/intents-operator/configuration.html b/reference/configuration/intents-operator/configuration.html index 9a2ac586d..4a39aabaa 100644 --- a/reference/configuration/intents-operator/configuration.html +++ b/reference/configuration/intents-operator/configuration.html @@ -14,7 +14,7 @@ - + @@ -157,7 +157,7 @@ - + \ No newline at end of file diff --git a/reference/configuration/intents-operator/helm-chart.html b/reference/configuration/intents-operator/helm-chart.html index d9c1faf7e..272191cdb 100644 --- a/reference/configuration/intents-operator/helm-chart.html +++ b/reference/configuration/intents-operator/helm-chart.html @@ -14,7 +14,7 @@ - + @@ -155,7 +155,7 @@ - + \ No newline at end of file diff --git a/reference/configuration/network-mapper.html b/reference/configuration/network-mapper.html index e52504edf..c589c4828 100644 --- a/reference/configuration/network-mapper.html +++ b/reference/configuration/network-mapper.html @@ -14,7 +14,7 @@ - + @@ -157,7 +157,7 @@ - + \ No newline at end of file diff --git a/reference/configuration/network-mapper/helm-chart.html b/reference/configuration/network-mapper/helm-chart.html index 502211084..28ed3d17b 100644 --- a/reference/configuration/network-mapper/helm-chart.html +++ b/reference/configuration/network-mapper/helm-chart.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/reference/configuration/network-mapper/kafka-watcher.html b/reference/configuration/network-mapper/kafka-watcher.html index 0187dac82..c0e5cb272 100644 --- a/reference/configuration/network-mapper/kafka-watcher.html +++ b/reference/configuration/network-mapper/kafka-watcher.html @@ -14,7 +14,7 @@ - + @@ -161,7 +161,7 @@ - + \ No newline at end of file diff --git a/reference/configuration/otterize-chart.html b/reference/configuration/otterize-chart.html index 61077a337..19efc8b0e 100644 --- a/reference/configuration/otterize-chart.html +++ b/reference/configuration/otterize-chart.html @@ -14,7 +14,7 @@ - + @@ -159,7 +159,7 @@ - + \ No newline at end of file diff --git a/reference/mtls.html b/reference/mtls.html index 761ca1dac..08409701c 100644 --- a/reference/mtls.html +++ b/reference/mtls.html @@ -14,7 +14,7 @@ - + @@ -165,7 +165,7 @@ - + \ No newline at end of file diff --git a/reference/service-identities.html b/reference/service-identities.html index 73b8fdfd3..6a1418686 100644 --- a/reference/service-identities.html +++ b/reference/service-identities.html @@ -14,7 +14,7 @@ - + @@ -160,7 +160,7 @@ - + \ No newline at end of file diff --git a/reference/shadow-vs-active-enforcement.html b/reference/shadow-vs-active-enforcement.html index 6c7eb542d..05bc67cce 100644 --- a/reference/shadow-vs-active-enforcement.html +++ b/reference/shadow-vs-active-enforcement.html @@ -14,7 +14,7 @@ - + @@ -156,7 +156,7 @@ - + \ No newline at end of file diff --git a/reference/terminology.html b/reference/terminology.html index e3d28c088..2b6bb14cc 100644 --- a/reference/terminology.html +++ b/reference/terminology.html @@ -14,7 +14,7 @@ - + @@ -165,7 +165,7 @@ - + \ No newline at end of file diff --git a/reference/troubleshooting.html b/reference/troubleshooting.html index 6f12e6f43..5164dd5f2 100644 --- a/reference/troubleshooting.html +++ b/reference/troubleshooting.html @@ -14,7 +14,7 @@ - + @@ -156,7 +156,7 @@ - + \ No newline at end of file diff --git a/reference/validating-clientintents.html b/reference/validating-clientintents.html index 3cfe473ac..10bb8d652 100644 --- a/reference/validating-clientintents.html +++ b/reference/validating-clientintents.html @@ -14,7 +14,7 @@ - + @@ -156,7 +156,7 @@ - + \ No newline at end of file diff --git a/search.html b/search.html index 33fb5f21e..da635db3c 100644 --- a/search.html +++ b/search.html @@ -14,7 +14,7 @@ - + @@ -154,7 +154,7 @@ - + \ No newline at end of file diff --git a/security.html b/security.html index e8a49c634..7e0e42bf2 100644 --- a/security.html +++ b/security.html @@ -14,7 +14,7 @@ - + @@ -155,7 +155,7 @@ - + \ No newline at end of file