Overview
This is a quickstart for remote executing a Magnus workflow using Google Golang (also Python code available as an example).
Note: Remote Workflow Execution can be done with any client/language that is able to do base64Url encoding, SHA256withRSA signing, and HTTP request.
Golang is just used here as an example
Before you begin
- Have a Google service account ready that ends with .gserviceaccount.com. You will need its .p12 file and the private key's password, or its json file.
- Register the Google service account with Magnus. Steps to register:
- Provide the email address, .p12 file and secret, or .json file of the Google service account to your Potens.io administrator.
Get confirmation from your Potens.io administrator that it is successfully registered with Magnus.
- Provide the email address, .p12 file and secret, or .json file of the Google service account to your Potens.io administrator.
- Have a Magnus workflow ready that you want to use for this tutorial. The service account must be either the owner or delegate of the workflow. You will need the encoded workflow ID. You can obtain the encoded workflow ID from the permalink of the workflow:
- Install Golang on your computer. Steps to install: https://golang.org/doc/install
- Install the pkcs12 Golang package on your computer. This package is used to decode the private key of the service account. Steps to install the package:
- From a command prompt, execute:
export GOPATH=<Your GOPATH>
go get golang.org/x/crypto/pkcs12
Sample - Golang
Code overview
Before diving into the complete code, here is an overview of all its components.
- import: This imports all the needed packages to run the program.
- func base64URLEncode: It base64Url encodes in the input.
- func rsa256: It signs the input data with a private key.
- func createJWT: It creates the JWT required to remotely execute a Magnus workflow.
- func main: This is the entry point to the program. You will set your parameters in main.
Steps
1. Create a file named quickstart.go in your working directory and copy in the following code:
// Sample quickstart to remotely execute a Magnus workflow.
package main
import (
"fmt"
"time"
"io/ioutil"
"golang.org/x/crypto/pkcs12"
"strings"
"net/url"
"encoding/base64"
"crypto/rsa"
"crypto/rand"
"crypto/sha256"
"crypto"
"net/http"
"log"
"bytes"
"encoding/json"
"encoding/pem"
"crypto/x509"
)
// base64URLEncode baseUrl encode the input.
func base64URLEncode(s []byte)(string) {
//base64 encode first, using RawStdEncoding here so that there will be no trailing padding char(=) used
b := base64.RawStdEncoding.EncodeToString(s)
//In order to make it more url friendly, need to do two replacements:
//replace + with -
//replace / with _
b = strings.Replace(b, "+", "-", -1)
b = strings.Replace(b, "/", "_", -1)
//url encode it
v := url.Values{}
v.Set("v", b)
urlencoded := v.Encode()
//strip out the prefix v=
return urlencoded[2:]
}
// rsa256 signs the input using SHA256withRSA with the private key
// It returns base64Url encoded representation of the signature
func rsa256(data string, privateKey *rsa.PrivateKey)(string) {
rng := rand.Reader
message := []byte(data)
hashed := sha256.Sum256(message)
signature, err := rsa.SignPKCS1v15(rng, privateKey, crypto.SHA256, hashed[:])
if err != nil {
log.Panic(err.Error())
}
return base64URLEncode(signature)
}
// createJWT returns the JWT needed to run a Magnus workflow remotely
func createJWT(serviceAccount string, privateKeyFile string, password string)(string) {
jwt_header := "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"
iat := time.Now().Unix()
//expires 5 minutes from now
exp := iat + (5 * 60)
jwt_claimset := "{\n" +
"\"iss\":\"" + serviceAccount + "\",\n" +
"\"exp\":" + fmt.Sprintf("%v",exp) + ",\n" +
"\"iat\":" + fmt.Sprintf("%v",iat) + "\n" +
"}";
//read the private key file
buf, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
log.Panic(err)
}
var pk *rsa.PrivateKey
if(strings.HasSuffix(privateKeyFile, ".p12")) {
//for p12 file
//decode the private key using secret
privateKey, _, err := pkcs12.Decode(buf, password)
if err != nil {
log.Panic(err)
}
//type assertion to make sure that it is indeed a rsa privateKey
if k, ok := privateKey.(*rsa.PrivateKey); !ok {
log.Panic("not rsa privatekey")
} else {
pk = k
}
} else if(strings.HasSuffix(privateKeyFile, ".json")) {
//for json file
//first convert json to map
var obj map[string]interface{}
if err := json.Unmarshal(buf, &obj); err != nil {
panic(err)
}
//get the private_key value
v := fmt.Sprintf("%v", obj["private_key"])
//decode the private key
block, _ := pem.Decode([]byte(v))
if parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
panic(err)
} else {
if k, ok := parsed.(*rsa.PrivateKey); !ok {
panic("not rsa privatekey")
} else {
pk = k
}
}
} else {
//not supported file format
log.Panic("private key file format not supported")
}
inputToSignature := base64URLEncode([]byte(jwt_header)) + "." + base64URLEncode([]byte(jwt_claimset));
jwt := inputToSignature + "." + rsa256(inputToSignature,pk);
return jwt
}
func main() {
//////////////////////////////////////////////////////////////////////
//set all the configurations here
url = "https://magnus.potens.io/remote"
encodedWorkflowId := "<encoded workflow ID"
serviceAccount := "<Google service account email address>"
privateKeyFile := "<path to .p12 or .json file>"
password := "<password for .p12 file>"
//set parameter overrides if any
var paramOverrides = make(map[string]interface{})
paramOverrides["<parameter name>"] = <parameter value>
//////////////////////////////////////////////////////////////////////
jwt := createJWT(serviceAccount, privateKeyFile, password)
//construct the request body
var requestBody bytes.Buffer
fmt.Fprintf(&requestBody, `{"jwt":"%v", "c":"713aa58d", "configuration":{"runWorkflow":{"l":"%v"`, jwt, encodedWorkflowId)
//construct parameter overrides if any
if(len(paramOverrides) > 0) {
fmt.Fprintf(&requestBody, `,"parameters":[`)
isFirst := true
for pname, pvalue := range paramOverrides {
if(!isFirst) {
fmt.Fprintf(&requestBody, ",")
}
isFirst = false
fmt.Fprintf(&requestBody, `{"name":"%v","value":`, pname)
if err := json.NewEncoder(&requestBody).Encode(pvalue); err != nil {
log.Panic(err.Error())
}
fmt.Fprintf(&requestBody, "}")
}
fmt.Fprintf(&requestBody, "]")
}
fmt.Fprintf(&requestBody, "}}}")
method := "POST"
//send the HTTP request
client := &http.Client{}
req, err := http.NewRequest(method, url, &requestBody)
if err != nil {
log.Panic(err.Error())
}
//get the HTTP response
resp, _ := client.Do(req)
fmt.Printf("Response HTTP status code=%v\n", resp.Status)
respText, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("Response body=%s\n", respText)
}
2. Set your configurations in the main function:
encodedWorkflowId := "<encoded workflow ID>"
serviceAccount := "<Google service account email address>"
privateKeyFile := "<path to .p12 or .json file>"
password := "<password for .p12 file>"
3. If there is any parameter override, set it in the main function:
paramOverrides["<parameter name>"] = <parameter_value>
4. Run the sample. From a command prompt, run:
export GOPATH=<Your GOPATH>
go run <path to quickstart.go>
5. If the workflow has been started successfully, you will see this in the console output:
Response HTTP status code=200 OK
Response body={"status":"ok","result":{"workflowId":"<workflowId>","historyId":"<historyId>"}}
6. Accordingly, you will see the corresponding run history in Magnus' History Browser:
The icon indicates that this workflow was executed remotely.
Sample - Python
import json
import requests
import time
from base64 import b64encode
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from OpenSSL import crypto
def createJWT(service_account, p12_file, password):
iat = int(time.time())
#expires one hour from now
exp = iat + (1 * 60 * 60)
jwt_header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"
jwt_claimset = (
"{\n"
"\"iss\":\"" + service_account + "\",\n"
"\"exp\":" + str(exp) + ",\n"
"\"iat\":" + str(iat) + "\n"
"}"
)
#read the p12 from file
with open(p12_file, 'r') as p12_file:
p12 = crypto.load_pkcs12(p12_file.read(), password)
private_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey())
input_to_signature = base64URLEncode(jwt_header) + "." + base64URLEncode(jwt_claimset)
jwt = input_to_signature + "." + rsa256(input_to_signature, private_key)
return jwt
def base64URLEncode(data):
#remove trailing equal sign
encode_data = b64encode(data).rstrip("=")
#replace + with -
encode_data = encode_data.replace('+', '-')
#replace / with _
encode_data = encode_data.replace('/', '_')
return encode_data
def rsa256(data, private_key):
#generate rsa key
rsakey = RSA.importKey(private_key)
signer = PKCS1_v1_5.new(rsakey)
digest = SHA256.new(data)
sign = signer.sign(digest)
return base64URLEncode(sign)
def main():
url = "https://magnus.potens.io/remote"
encoded_workflow_id = "<encoded workflow ID>"
service_account = "<Google service account email address>"
p12_file = '<path to .p12 file>'
password = '<password for the private key>'
jwt = createJWT(service_account, p12_file, password)
#set parameter overrides if any
param_list = [
{"name": "<parameter name1>", "value": "<parameter value1>"},
{"name": "<parameter name2>", "value": "<parameter value2>"}
#.......another parameters......
]
request_body = {
"jwt": jwt,
"c": "713aa58d",
"configuration": {
"runWorkflow": {
"l": encoded_workflow_id,
"parameters": param_list
}
}
}
try:
r = requests.post(url, data=json.dumps(request_body))
r.raise_for_status()
print "The request has been sent to magnus successfully!"
except requests.exceptions.RequestException as e:
# log error
print str(e)
if __name__ == '__main__':
main()