├── Dockerfile ├── LICENSE ├── README.md ├── action.yml ├── requirements.txt └── src └── main.py /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | RUN mkdir -p /opt/azion 4 | 5 | COPY requirements.txt /opt/azion/requirements.txt 6 | COPY src/main.py /opt/azion/main.py 7 | 8 | WORKDIR /opt/azion 9 | 10 | RUN pip install -r requirements.txt 11 | 12 | ENTRYPOINT ["/opt/azion/main.py"] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 azeight 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azion Edge Function docker action 2 | 3 | Create or update an Edge Functions on Azion Edge Nodes. 4 | 5 | The domain name is the key for decision to a create or update an Edge Function, then if the domain name is already on use by an Edge Function, the function will be updated. 6 | 7 | 8 | ## API Inputs 9 | 10 | ## `azion-auth` 11 | 12 | **Required** Authentication method 13 | 14 | Create your free account on Azion site https://azion.com to use this action. 15 | 16 | For authentication you can use __Basic__ or __Token__ methods. 17 | 18 | Details how to create your Token or generate a base64 for Basic method, please visit https://api.azion.com/#58ba6991-2fe6-415c-9f17-a44dc0bc8cd4. Token authorization ensure more secure method than Basic, tokens are valid up to 24 hours, generate other token if it's expired. 19 | 20 | * For Basic method use in this input “Authorization: Basic ” 21 | * For example: “Authorization: Basic dXNlckdBabn1hiW46cGFzc3dvcmQK” 22 | * For Token method use in this input "Authorization: TOKEN[YOUR TOKEN HERE]" 23 | * For example: "Authorization: TOKEN[455SAFafa#$sfdsf789aswas23casf3=] 24 | 25 | ## `config` 26 | 27 | **Required** Configuration file with Azion Edge Functions details 28 | 29 | In the __action.yaml__ file, you have the configuration example please fill with your own data: 30 | * name: name of your Edge Function 31 | * path: path and filename with the source code using __JavaScript__ language. 32 | * domain: your domain, maybe a __CNAME__ record 33 | * args: parameters for use in the edge function at runtime, the argument name and value of each argument used on the JavaScript code. 34 | * arg1 "first argument name" : "value of first argument" 35 | * arg2 "second argument name" : "value of second argument" 36 | * arg..N "N argument" : "value of N argument" 37 | * path_uri: the __URI__ path of your edge function 38 | * active: boolean that control if your Edge Function is active or not, domain values (true|false). Your function is only accessible when it is active true. 39 | 40 | 41 | ## Outputs 42 | 43 | ## `domain` 44 | 45 | __URI__ of the edge function deployed. 46 | 47 | You could use this __URI__, ir necessary you can create a __CNAME__ at your __DNS__. For local use and testing, you can change your __/etc/hosts__ for your domain. 48 | 49 | 50 | ## Example usage on Github Action 51 | 52 | File available 53 | 54 | ``` 55 | uses: actions/azion-edge-function@v1 56 | with: 57 | azion-auth: "Authorization: TOKEN[455SAFafa#$sfdsf789aswas23casf3=] 58 | config: 'function1.yaml' 59 | ``` 60 | 61 | ## Example of configuration file 62 | 63 | Edit the __action.yaml__ file, containing the details of your edge functions at Azion. 64 | 65 | Don't change the __edge_functions__ name, and name parameters but instead just complete with desired values. The exception is for args, when you need to change the arg names and arg values. 66 | ``` 67 | edge_functions: 68 | - 69 | path: "example/src/messages.js" 70 | domain: "www.yourdomain.com" 71 | name: "Function Hello World Azion" 72 | args: 73 | arg1: "value1" 74 | arg2: "value2" 75 | path_uri: "/api/messages/" 76 | active: "true" 77 | ``` 78 | 79 | More details on [Azion site](www.azion.com) 80 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Azion Edge Functions' 2 | description: 'Create or update an Edge Function on Azion Edge Nodes.' 3 | inputs: 4 | config: 5 | description: 'YAML config file' 6 | required: false 7 | default: 'config.yml' 8 | azion-auth: 9 | description: 'Your auth' 10 | required: false 11 | outputs: 12 | domain: 13 | description: 'Azion domain mapper' 14 | runs: 15 | using: 'docker' 16 | image: 'Dockerfile' 17 | args: 18 | - ${{ inputs.azion-auth }} 19 | - ${{ inputs.config }} 20 | branding: 21 | icon: chevron-up 22 | color: orange 23 | 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2021.10.8 2 | charset-normalizer==2.0.12 3 | idna==3.3 4 | PyYAML==6.0 5 | requests==2.27.1 6 | urllib3==1.26.9 7 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | 3 | import base64 4 | import copy 5 | from importlib.metadata import PathDistribution 6 | from io import StringIO 7 | import string 8 | import json 9 | import requests 10 | import json 11 | import yaml 12 | import re 13 | from datetime import datetime 14 | import csv 15 | import sys 16 | from datetime import datetime, timedelta 17 | 18 | 19 | 20 | 21 | ##################################################### 22 | ## Classe de centralizacao de chamadas a API AZION ## 23 | ## Diego Barbosa Victoria - Azion ## 24 | ## Hackaton develop - 30/04/2022 ## 25 | ##################################################### 26 | class AzionAPI: 27 | def __init__(self, authentication): 28 | self._urlBase='https://api.azionapi.net' 29 | self._moduleApplicationAcceleration=False 30 | self._edgeAppID = 0; 31 | 32 | if ('Basic ' in authentication): 33 | self.createToken(authentication) 34 | else: 35 | self._token=authentication 36 | 37 | 38 | def getEdgeAppID(self): 39 | return self._edgeAppID; 40 | 41 | ################################# 42 | ## Busca Token de autenticacao ## 43 | ################################# 44 | def createToken(self,basic64): 45 | url = self._urlBase + "/tokens" 46 | 47 | payload="{\"query\":\"\",\"variables\":{}}" 48 | headers = { 49 | 'Accept': 'application/json; version=3', 50 | 'Authorization': '{basic64}'.format(basic64=basic64), 51 | 'Content-Type': 'application/json' 52 | } 53 | 54 | #response = self._callPostMethod("/tokens", payload) 55 | 56 | response = requests.request("POST", url, headers=headers, data=payload) 57 | if response.ok == False: 58 | raise Exception("\nERROR: "+response.text+"\nCALL: /token \nPAYLOAD: "+payload) 59 | 60 | responseJson = response.json() 61 | self._token = responseJson.get('token') 62 | 63 | def _callGetMethod(self, path): 64 | 65 | url = self._urlBase + path 66 | 67 | payload = json.dumps({}) 68 | headers = { 69 | 'Accept': 'application/json; version=3', 70 | 'Authorization': 'Token '+self._token, 71 | 'Content-Type': 'application/json' 72 | } 73 | 74 | response = requests.request("GET", url, headers=headers, data=payload) 75 | return response 76 | 77 | def _callPostMethod(self, path, payload): 78 | 79 | url = self._urlBase + path 80 | 81 | headers = { 82 | 'Accept': 'application/json; version=3', 83 | 'Authorization': 'Token '+self._token, 84 | 'Content-Type': 'application/json' 85 | } 86 | 87 | response = requests.request("POST", url, headers=headers, data=payload) 88 | if response.ok == False: 89 | raise Exception("\nERROR: "+response.text+"\nCALL: "+path+"\nPAYLOAD: "+payload) 90 | return response 91 | 92 | def _callPatchMethod(self, path, payload): 93 | 94 | url = self._urlBase + path 95 | 96 | headers = { 97 | 'Accept': 'application/json; version=3', 98 | 'Authorization': 'Token '+self._token, 99 | 'Content-Type': 'application/json' 100 | } 101 | 102 | response = requests.request("PATCH", url, headers=headers, data=payload) 103 | if response.ok == False: 104 | raise Exception("\nERROR: "+response.text+"\nCALL: "+path+"\nPAYLOAD: "+payload) 105 | 106 | return response 107 | 108 | def _callPutMethod(self, path, payload): 109 | 110 | url = self._urlBase + path 111 | 112 | headers = { 113 | 'Accept': 'application/json; version=3', 114 | 'Authorization': 'Token '+self._token, 115 | 'Content-Type': 'application/json' 116 | } 117 | 118 | response = requests.request("PUT", url, headers=headers, data=payload) 119 | if response.ok == False: 120 | raise Exception("\nERROR: "+response.text+"\nCALL: "+path+"\nPAYLOAD: "+payload) 121 | 122 | return response 123 | 124 | def _callDeleteMethod(self, path, payload): 125 | 126 | url = self._urlBase + path 127 | 128 | headers = { 129 | 'Accept': 'application/json; version=3', 130 | 'Authorization': 'Token '+self._token, 131 | 'Content-Type': 'application/json' 132 | } 133 | 134 | response = requests.request("DELETE", url, headers=headers, data=payload) 135 | if response.ok == False: 136 | raise Exception("\nERROR: "+response.text+"\nCALL: "+path+"\nPAYLOAD: "+payload) 137 | 138 | return response 139 | 140 | ################################# 141 | #### Busca dados do Dominio #### 142 | ################################ 143 | def getDomain(self,cname): 144 | 145 | response = self._callGetMethod("/domains?page_size=100") 146 | responseJson = response.json() 147 | 148 | results=responseJson.get('results') 149 | 150 | out=0 151 | for reg in results: 152 | for regcname in reg['cnames']: 153 | if(regcname==cname): 154 | out = reg 155 | break 156 | return out 157 | 158 | ############################ 159 | #### Cria novo Dominio #### 160 | ########################### 161 | def createDomain(self,name, cnames, edgeAppId=False, environment='production', cnameAccessOnly=False, isActive=True): 162 | 163 | if(isinstance(cnames,str)): 164 | if "," in cnames: 165 | cnames = cnames.split(',') 166 | else: 167 | cnames = [cnames] 168 | 169 | payload = json.dumps({ 170 | "name": name, 171 | "cname_access_only": False, 172 | "edge_application_id": edgeAppId, 173 | "is_active": isActive, 174 | "environment": environment 175 | }) 176 | 177 | response = self._callPostMethod("/domains", payload) 178 | 179 | responseJson = response.json() 180 | 181 | results=responseJson.get('results') 182 | 183 | if cnames[0] != '': 184 | domainID=str(responseJson.get('results')['id']) 185 | 186 | payload = json.dumps({ 187 | "cnames": cnames, 188 | "cname_access_only": cnameAccessOnly 189 | }) 190 | 191 | response = self._callPatchMethod("/domains/"+domainID, payload) 192 | 193 | responseJson = response.json() 194 | 195 | results=responseJson.get('results') 196 | 197 | out=0 198 | if (len(results)>0): 199 | out = results 200 | 201 | return out 202 | 203 | ################################# 204 | #### Busca dados de Edge App #### 205 | ################################ 206 | def getEdgeApp(self,id): 207 | 208 | response = self._callGetMethod("/edge_applications/" + str(id)) 209 | 210 | responseJson = response.json() 211 | 212 | results=responseJson.get('results') 213 | 214 | out=0 215 | if (len(results)>0): 216 | out = results 217 | 218 | return out 219 | 220 | ##################################### 221 | #### Criacao de Edge Application #### 222 | ##################################### 223 | def createEdgeApp(self,cname,address="to.remove.com", deliveryProto="http,https", httpPort=80, httpsPort=443, minimunTLS="",active=True, applicationAcceleration=False, caching=True, deviceDetection=False, 224 | edgeFirewall=False, edgeFunctions=True, imageOptimization=False, l2Caching=False, loadBalancer=False, rawLogs=False, waf=False, 225 | originType="single_origin", originProtoPolicy="preserve", hostHeader="${host}", 226 | browserCacheSettings="honor", browserCacheSettingsTTL=0,cdnCacheSettings="honor", cdnCacheSettingsTTL=0): 227 | 228 | 229 | payload = json.dumps({ 230 | "name": cname, 231 | "delivery_protocol": deliveryProto, 232 | "origin_type": originType, 233 | "address": address, 234 | "origin_protocol_policy": originProtoPolicy, 235 | "host_header": hostHeader, 236 | "browser_cache_settings": browserCacheSettings, 237 | "browser_cache_settings_maximum_ttl": browserCacheSettingsTTL, 238 | "cdn_cache_settings": cdnCacheSettings, 239 | "cdn_cache_settings_maximum_ttl": cdnCacheSettingsTTL 240 | }) 241 | 242 | response = self._callPostMethod("/edge_applications", payload) 243 | responseJson = response.json() 244 | 245 | edgeAppID=str(responseJson.get('results')['id']) 246 | edgeAppName=responseJson.get('results')['name'] 247 | 248 | self._edgeAppID = edgeAppID 249 | 250 | 251 | ##PATCH (insere diferencas) 252 | 253 | payload = json.dumps({ 254 | "http_port": httpPort, 255 | "https_port": httpsPort, 256 | "minimum_tls_version": minimunTLS, 257 | "active": active, 258 | "application_acceleration": applicationAcceleration, 259 | "caching": caching, 260 | "device_detection": deviceDetection, 261 | "edge_firewall": edgeFirewall, 262 | "edge_functions": edgeFunctions, 263 | "image_optimization": imageOptimization, 264 | "l2_caching": l2Caching, 265 | "load_balancer": loadBalancer, 266 | "raw_logs": rawLogs, 267 | "web_application_firewall": waf 268 | }) 269 | 270 | response = self._callPatchMethod("/edge_applications/"+edgeAppID, payload) 271 | 272 | responseJson = response.json() 273 | 274 | results=responseJson.get('results') 275 | 276 | out=0 277 | if (len(results)>0): 278 | out = results 279 | 280 | return out 281 | 282 | 283 | ################################## 284 | #### Busca dados de Functions #### 285 | ################################## 286 | def getFunctions(self,name=''): 287 | 288 | response = self._callGetMethod("/edge_functions?page_size=100") 289 | 290 | responseJson = response.json() 291 | 292 | results=responseJson.get('results') 293 | 294 | out=0 295 | if (len(results)>0): 296 | if(name!=''): 297 | for reg in results: 298 | if(reg['name']==name): 299 | out = reg 300 | #else: 301 | # out = results 302 | 303 | return out 304 | 305 | ############################# 306 | #### Criacao de Function ### 307 | ############################ 308 | def createFunction(self, name, code, args, active, language='javascript', initiator='edge_application'): 309 | 310 | originData = { 311 | "name": name, 312 | "code": code, 313 | "language": language, 314 | "json_args": args, 315 | "active": active 316 | } 317 | payload = json.dumps( originData ) 318 | 319 | response = self._callPostMethod("/edge_functions", payload) 320 | 321 | responseJson = response.json() 322 | 323 | results=responseJson.get('results') 324 | 325 | out=0 326 | if (len(results)>0): 327 | out = results 328 | 329 | return out 330 | 331 | ################################# 332 | #### Atualizacao de Function ### 333 | ############################### 334 | def updateFunction(self, functionID, name, code, args, active, language='javascript', initiator='edge_application'): 335 | 336 | originData = { 337 | "name": name, 338 | "code": code, 339 | "json_args": args, 340 | "active": active 341 | } 342 | payload = json.dumps( originData ) 343 | 344 | response = self._callPutMethod("/edge_functions/" + str(functionID), payload) 345 | 346 | responseJson = response.json() 347 | 348 | results=responseJson.get('results') 349 | 350 | out=0 351 | if (len(results)>0): 352 | out = results 353 | 354 | return out 355 | 356 | ####################################### 357 | #### Busca dados de App Functions #### 358 | ###################################### 359 | def getAppFunctions(self,edgeAppID,name=''): 360 | 361 | response = self._callGetMethod("/edge_applications/" + str(edgeAppID) + "/functions_instances?page_size=100") 362 | responseJson = response.json() 363 | 364 | results=responseJson.get('results') 365 | 366 | out=0 367 | if (len(results)>0): 368 | if(name!=''): 369 | for reg in results: 370 | if(reg['name']==name): 371 | out = reg 372 | #else: 373 | # out = results 374 | 375 | return out 376 | 377 | ################################ 378 | #### Criacao de App Function ### 379 | ################################ 380 | def createAppFunction(self, name, edgeAppID, functionID, args): 381 | 382 | originData = { 383 | "name": name, 384 | "edge_function_id": functionID, 385 | "args": args 386 | } 387 | payload = json.dumps( originData ) 388 | 389 | response = self._callPostMethod("/edge_applications/"+str(edgeAppID)+"/functions_instances", payload) 390 | 391 | responseJson = response.json() 392 | 393 | results=responseJson.get('results') 394 | 395 | out=0 396 | if (len(results)>0): 397 | out = results 398 | 399 | return out 400 | 401 | ############################### 402 | #### Busca dados de Rules #### 403 | ############################## 404 | def getRules(self,edgeAppID,name='',phase='request',targetDefault=False): 405 | 406 | response = self._callGetMethod("/edge_applications/" + str(edgeAppID) + "/rules_engine/" + phase + "/rules?page_size=100") 407 | 408 | responseJson = response.json() 409 | 410 | results=responseJson.get('results') 411 | 412 | out=0 413 | if (len(results)>0): 414 | if(name!=''): 415 | for reg in results: 416 | if(reg['name']==name and targetDefault==False): 417 | out = reg 418 | elif(reg['name']==name and targetDefault==True and reg['phase']=='default'): 419 | out = reg 420 | else: 421 | if (targetDefault == True): 422 | out = results 423 | else: 424 | out=[] 425 | for rule in results: 426 | if rule['phase'] != 'default': 427 | out.append(rule) 428 | 429 | return out 430 | 431 | ####################### 432 | #### Altera Rules #### 433 | ###################### 434 | def changeRules(self,edgeAppID,phase,ruleID,newRule): 435 | 436 | payload = json.dumps( newRule ) 437 | 438 | #headers = { 439 | # 'Accept': 'application/json; version=3', 440 | # 'Authorization': 'Token '+self._token, 441 | # 'Content-Type': 'application/json' 442 | #} 443 | 444 | #response = requests.request("PATCH", url, headers=headers, data=payload) 445 | response = self._callPatchMethod("/edge_applications/"+str(edgeAppID)+"/rules_engine/"+phase+"/rules/"+str(ruleID), payload) 446 | 447 | responseJson = response.json() 448 | 449 | results=responseJson.get('results') 450 | 451 | out=0 452 | if (len(results)>0): 453 | out = results 454 | 455 | return out 456 | 457 | ############################# 458 | #### Criacao de Regra ### 459 | ############################ 460 | def createRule(self, name, edgeAppID, functionID, pathURI, phase='request'): 461 | 462 | originData = { 463 | "name": name, 464 | "phase": phase, 465 | "behaviors": [ 466 | { 467 | "name": "run_function", 468 | "target": str(functionID) 469 | } 470 | ], 471 | "criteria": [ 472 | [ 473 | { 474 | "variable": "${uri}", 475 | "operator": "is_equal", 476 | "conditional": "if", 477 | "input_value": pathURI 478 | } 479 | ] 480 | ], 481 | "is_active": True 482 | } 483 | 484 | payload = json.dumps( originData ) 485 | 486 | response = self._callPostMethod("/edge_applications/"+str(edgeAppID)+"/rules_engine/"+str(phase)+"/rules", payload) 487 | 488 | responseJson = response.json() 489 | 490 | results=responseJson.get('results') 491 | 492 | out=0 493 | if (len(results)>0): 494 | out = results 495 | 496 | return out 497 | 498 | 499 | 500 | authentication = sys.argv[1] 501 | yamlFile = sys.argv[2] 502 | 503 | #with open(r'/Users/dv/Scripts/hackaton/config.yaml') as file: 504 | with open(yamlFile) as file: 505 | content = yaml.full_load(file) 506 | 507 | urls = [] 508 | 509 | for edgeFunctionRoot, funcList in content.items(): 510 | for func in funcList: 511 | #print(func, ":", "") 512 | 513 | with open(func['path'],"r") as f: 514 | fileContent = f.read() 515 | 516 | cname = func['domain'] 517 | address = func['origin'] 518 | env = func['env'] 519 | funcName = func['name'] 520 | code = fileContent 521 | args = func['args'] 522 | funcActive = func['active'] 523 | pathURI = func['path_uri'] 524 | 525 | 526 | azion = AzionAPI(authentication) 527 | domain = azion.getDomain(cname) 528 | 529 | domainID = None 530 | edgeAppID = None 531 | 532 | if(domain != 0): 533 | domainID = domain['id'] 534 | edgeAppID = domain['edge_application_id'] 535 | else: 536 | edgeApp = azion.createEdgeApp( cname, address ) 537 | if(edgeApp != 0): 538 | domain = azion.createDomain(cname,cname,edgeApp['id'], env) 539 | if(domain != 0): 540 | domainID = domain['id'] 541 | edgeAppID = domain['edge_application_id'] 542 | 543 | 544 | if (domain != 0 and edgeAppID != 0): 545 | 546 | function = azion.getFunctions(funcName) 547 | if(function!=0): 548 | function = azion.updateFunction(function['id'], function['name'], code, args, funcActive) 549 | else: 550 | function = azion.createFunction(funcName, code, args, funcActive) 551 | 552 | 553 | appFunction = azion.getAppFunctions(edgeAppID,funcName) 554 | if (appFunction == 0): 555 | appFunction = azion.createAppFunction(funcName, edgeAppID, function['id'], args) 556 | 557 | rule = azion.getRules(edgeAppID,pathURI) 558 | if (rule == 0): 559 | azion.createRule(pathURI, edgeAppID, appFunction['id'], pathURI) 560 | 561 | urls.append("https://"+domain['domain_name']+pathURI) 562 | 563 | if(len(urls)>0): 564 | dt=datetime.today() + timedelta(minutes=5) 565 | whenExec = dt.strftime("%H:%M:%S") 566 | s = 's' if len(urls)>1 else '' 567 | print("::set-output name=domain::Access the url"+s+" with your function"+s+" after "+whenExec+" : [ "+(' , '.join(urls))+" ]" ) 568 | 569 | 570 | --------------------------------------------------------------------------------