diff --git a/README.md b/README.md new file mode 100644 index 0000000..36c335f --- /dev/null +++ b/README.md @@ -0,0 +1,343 @@ +# Port Agent + +## [Documentation](https://docs.getport.io/create-self-service-experiences/setup-backend/port-execution-agent/) + +## Control the payload of your self-service actions + +Some of the 3rd party applications that you may want to integrate with may not accept the raw payload incoming from Port's +self-service actions. The Port agent allows you to control the payload that is sent to every 3rd party application. + +You can alter the requests sent to your third-party application by providing a payload mapping config file when deploying the +Port-agent container. + +### Control the payload mapping + +The payload mapping file is a JSON file that specifies how to transform the request sent to the Port agent to the +request that is sent to the third-party application. + +The payload mapping file is mounted to the Port agent as a volume. The path to the payload mapping file is set in the +`CONTROL_THE_PAYLOAD_CONFIG_PATH` environment variable. By default, the Port agent will look for the payload mapping +file +at `~/control_the_payload_config.json`. + +The payload mapping file is a json file that contains a list of mappings. Each mapping contains the request fields that +will be overridden and sent to the 3rd party application. + +You can see examples showing how to deploy the Port agent with different mapping configurations for various common use cases below. + +Each of the mapping fields can be constructed by JQ expressions. The JQ expression will be evaluated against the +original payload that is sent to the port agent from Port and the result will be sent to the 3rd party application. + +Here is the mapping file schema: + +``` +[ # Can have multiple mappings. Will use the first one it will find with enabled = True (Allows you to apply mapping over multiple actions at once) + { + "enabled": bool || JQ, + "url": JQ, # Optional. default is the incoming url from port + "method": JQ, # Optional. default is POST. Should return one of the following string values POST / PUT / DELETE / GET + "headers": dict[str, JQ], # Optional. default is {} + "body": ".body", # Optional. default is the whole payload incoming from Port. + "query": dict[str, JQ] # Optional. default is {} + } +] +``` + +**The body can be partially constructed by json as follows:** + +```json +{ + "body": { + "key": 2, + "key2": { + "key3": ".im.a.jq.expression", + "key4": "\"im a string\"" + } + } +} +``` + +### The incoming message to base your mapping on + +
+Example for incoming event + +```json +{ + "action": "action_identifier", + "resourceType": "run", + "status": "TRIGGERED", + "trigger": { + "by": { + "orgId": "org_XXX", + "userId": "auth0|XXXXX", + "user": { + "email": "executor@mail.com", + "firstName": "user", + "lastName": "userLastName", + "phoneNumber": "0909090909090909", + "picture": "https://s.gravatar.com/avatar/dd1cf547c8b950ce6966c050234ac997?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fga.png", + "providers": [ + "port" + ], + "status": "ACTIVE", + "id": "auth0|XXXXX", + "createdAt": "2022-12-08T16:34:20.735Z", + "updatedAt": "2023-11-13T15:11:38.243Z" + } + }, + "origin": "UI", + "at": "2023-11-13T15:20:16.641Z" + }, + "context": { + "entity": "e_iQfaF14FJln6GVBn", + "blueprint": "kubecostCloudAllocation", + "runId": "r_HardNzG6kzc9vWOQ" + }, + "payload": { + "entity": { + "identifier": "e_iQfaF14FJln6GVBn", + "title": "myEntity", + "icon": "Port", + "blueprint": "myBlueprint", + "team": [], + "properties": { + }, + "relations": {}, + "createdAt": "2023-11-13T15:24:46.880Z", + "createdBy": "auth0|XXXXX", + "updatedAt": "2023-11-13T15:24:46.880Z", + "updatedBy": "auth0|XXXXX" + }, + "action": { + "invocationMethod": { + "type": "WEBHOOK", + "agent": true, + "synchronized": false, + "method": "POST", + "url": "https://myGitlabHost.com" + }, + "trigger": "DAY-2" + }, + "properties": { + }, + "censoredProperties": [] + } +} +``` + +
+ +### Examples + +#### Terraform Cloud + +Create the following blueprint, action and mapping to trigger a Terraform Cloud run. + +
+Blueprint + +```json +{ + "identifier": "terraform_cloud_workspace", + "title": "Terraform Cloud Workspace", + "icon": "Terraform", + "schema": { + "properties": { + "workspace_id": { + "title": "Workspace Id", + "type": "string" + } + }, + "required": [ + "workspace_id" + ] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "relations": {} +} +``` +
+ +
+Action + +```json +[ + { + "identifier": "trigger_tf_run", + "title": "Trigger TF Cloud run", + "icon": "Terraform", + "userInputs": { + "properties": {}, + "required": [], + "order": [] + }, + "invocationMethod": { + "type": "WEBHOOK", + "agent": true, + "synchronized": false, + "method": "POST", + "url": "https://app.terraform.io/api/v2/runs/" + }, + "trigger": "DAY-2", + "requiredApproval": false + } +] +``` +
+ +
+Mapping - (Should be saved as `invocations.json`) + +```json +{ + "enabled": ".action == \"trigger_tf_run\"", + "headers": { + "Authorization": "\"Bearer \" + env.TF_TOKEN", + "Content-Type": "\"application/vnd.api+json\"" + }, + "body": { + "data": { + "attributes": { + "is-destroy": false, + "message": "\"Triggered via Port\"", + "variables": ".payload.properties | to_entries | map({key: .key, value: .value})" + }, + "type": "\"runs\"", + "relationships": { + "workspace": { + "data": { + "type": "\"workspaces\"", + "id": ".payload.entity.properties.workspace_id" + } + } + } + } + } + } +``` +
+ +**Port agent installation for Terraform cloud example**: + +```sh +helm repo add port-labs https://port-labs.github.io/helm-charts + +helm repo update + +helm install my-port-agent port-labs/port-agent \ + --create-namespace --namespace port-agent \ + --set env.normal.PORT_ORG_ID=YOUR_ORG_ID \ + --set env.normal.KAFKA_CONSUMER_GROUP_ID=YOUR_KAFKA_CONSUMER_GROUP \ + --set env.secret.KAFKA_CONSUMER_USERNAME=YOUR_KAFKA_USERNAME \ + --set env.secret.KAFKA_CONSUMER_PASSWORD=YOUR_KAFKA_PASSWORD \ + --set env.normal.KAFKA_CONSUMER_BROKERS=PORT_KAFKA_BROKERS \ + --set env.normal.STREAMER_NAME=KAFKA \ + --set env.normal.KAFKA_CONSUMER_AUTHENTICATION_MECHANISM=SCRAM-SHA-512 \ + --set env.normal.KAFKA_CONSUMER_AUTO_OFFSET_RESET=earliest \ + --set env.normal.KAFKA_CONSUMER_SECURITY_PROTOCOL=SASL_SSL \ + --set env.secret.TF_TOKEN=YOU_TERRAFORM_CLOUD_TOKEN \ + --set-file controlThePayloadConfig=./invocations.json +``` + + + +#### CircleCI + +Create the following blueprint, action and mapping to trigger a CircleCI pipeline. + +
+Blueprint + +```json +{ + "identifier": "circle_ci_project", + "title": "CircleCI Project", + "icon": "CircleCI", + "schema": { + "properties": { + "project_slug": { + "title": "Slug", + "type": "string" + } + }, + "required": [ + "project_slug" + ] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "relations": {} +} +``` +
+ +
+Action + +```json +[ + { + "identifier": "trigger_circle_ci_pipeline", + "title": "Trigger CircleCI pipeline", + "icon": "CircleCI", + "userInputs": { + "properties": {}, + "required": [], + "order": [] + }, + "invocationMethod": { + "type": "WEBHOOK", + "agent": true, + "synchronized": false, + "method": "POST", + "url": "https://circleci.com" + }, + "trigger": "DAY-2", + "requiredApproval": false + } +] +``` +
+ +
+Mapping - (Should be saved as `invocations.json`) + +```json +{ + "enabled": ".action == \"trigger_circle_ci_pipeline\"", + "url": "(env.CIRCLE_CI_URL // \"https://circleci.com\") as $baseUrl | .payload.entity.properties.project_slug | @uri as $path | $baseUrl + \"/api/v2/project/\" + $path + \"/pipeline\"", + "headers": { + "Circle-Token": "env.CIRCLE_CI_TOKEN" + }, + "body": { + "branch": ".payload.properties.branch // \"main\"", + "parameters": ".payload.action.invocationMethod as $invocationMethod | .payload.properties | to_entries | map({(.key): (.value | tostring)}) | add | if $invocationMethod.omitUserInputs then {} else . end" + } + } +``` +
+ +**Port agent installation for CircleCI example**: + +```sh +helm repo add port-labs https://port-labs.github.io/helm-charts + +helm repo update + +helm install my-port-agent port-labs/port-agent \ + --create-namespace --namespace port-agent \ + --set env.normal.PORT_ORG_ID=YOUR_ORG_ID \ + --set env.normal.KAFKA_CONSUMER_GROUP_ID=YOUR_KAFKA_CONSUMER_GROUP \ + --set env.secret.KAFKA_CONSUMER_USERNAME=YOUR_KAFKA_USERNAME \ + --set env.secret.KAFKA_CONSUMER_PASSWORD=YOUR_KAFKA_PASSWORD + --set env.normal.KAFKA_CONSUMER_BROKERS=PORT_KAFKA_BROKERS \ + --set env.normal.STREAMER_NAME=KAFKA \ + --set env.normal.KAFKA_CONSUMER_AUTHENTICATION_MECHANISM=SCRAM-SHA-512 \ + --set env.normal.KAFKA_CONSUMER_AUTO_OFFSET_RESET=earliest \ + --set env.normal.KAFKA_CONSUMER_SECURITY_PROTOCOL=SASL_SSL \ + --set env.secret.CIRCLE_CI_TOKEN=YOUR_CIRCLE_CI_PERSONAL_TOKEN \ + --set-file controlThePayloadConfig=./invocations.json +``` diff --git a/app/control_the_payload_config.json b/app/control_the_payload_config.json index 9d2a940..af87190 100644 --- a/app/control_the_payload_config.json +++ b/app/control_the_payload_config.json @@ -1,10 +1,10 @@ [ { "enabled": ".payload.action.invocationMethod.type == \"GITLAB\"", - "url": "($ENV.GITLAB_URL // \"https://gitlab.com\") as $baseUrl | (.payload.action.invocationMethod.groupName + \"/\" +.payload.action.invocationMethod.projectName) | @uri as $path | $baseUrl + \"/api/v4/projects/\" + $path + \"/trigger/pipeline\"", + "url": "(env.GITLAB_URL // \"https://gitlab.com/\") as $baseUrl | (.payload.action.invocationMethod.groupName + \"/\" +.payload.action.invocationMethod.projectName) | @uri as $path | $baseUrl + \"api/v4/projects/\" + $path + \"/trigger/pipeline\"", "body": { "ref": ".payload.properties.ref // .payload.action.invocationMethod.defaultRef // \"main\"", - "token": ".payload.action.invocationMethod.groupName as $gitlab_group | .payload.action.invocationMethod.projectName as $gitlab_project | $ENV[($gitlab_group + \"_\" + ($gitlab_project | gsub(\"/\"; \"_\")))]", + "token": ".payload.action.invocationMethod.groupName as $gitlab_group | .payload.action.invocationMethod.projectName as $gitlab_project | env[($gitlab_group | gsub(\"/\"; \"_\")) + \"_\" + $gitlab_project]", "variables": ".payload.action.invocationMethod as $invocationMethod | .payload.properties | to_entries | map({(.key): (.value | tostring)}) | add | if $invocationMethod.omitUserInputs then {} else . end", "port_payload": "if .payload.action.invocationMethod.omitPayload then {} else . end" }