Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Monday, March 27, 2023

PDF split and merge in Python



 

In this post we will review how to split and merge PDFs files.

Why is this required? Sometimes, to fill a form, you edit a PDF file in an online PDF editor site, but don't want to enter your credit card ID or bank account number in this online anonymous site. 

How to do it anyway?

Fill all the form except the details you want to keep for yourself.

Split the PDF file to multiple pages using the following python code.


import os
from PyPDF2 import PdfReader, PdfWriter

def split_pdfs(input_file_path):
inputpdf = PdfReader(open(input_file_path, "rb"))

out_paths = []
if not os.path.exists("outputs"):
os.makedirs("outputs")

for i, page in enumerate(inputpdf.pages):
output = PdfWriter()
output.add_page(page)

out_file_path = f"outputs/{i}.pdf"
with open(out_file_path, "wb") as output_stream:
output.write(output_stream)

out_paths.append(out_file_path)
return out_paths

split_pdfs("document.pdf")


Next open the PDF file with the related page you want to fill offline, and update it using a image editor: make a screenshot, paste in any local image editor application, and add the text you need. Then, paste the image to your google drive as a new document, and download it as PDF.

Replace the page file that you've updated, and use the following python code to merge the pages back.

from PyPDF2 import PdfMerger

merger = PdfMerger()

for i in range(3):
merger.append('outputs/{}.pdf'.format(i))

merger.write("result.pdf")
merger.close()


Done! Your secrets are save...





Monday, March 20, 2023

Deploy MongoDB Community on Kubernetes

 

In this post we will review the step to install MongoDB community version on a kubernetes cluster.

First, we install the MongoDB operator:


helm repo add mongodb https://mongodb.github.io/helm-charts
helm install community-operator mongodb/community-operator


Next, we create a replica.yaml file for the MongoDB custom resources:


---
apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
name: example-mongodb
spec:
members: 1
type: ReplicaSet
version: "5.0.16"
security:
authentication:
modes: ["SCRAM"]
users:
- name: my-user
db: admin
passwordSecretRef: # a reference to the secret that will be used to generate the user's password
name: my-user-password
roles:
- name: clusterAdmin
db: admin
- name: userAdminAnyDatabase
db: admin
scramCredentialsSecretName: my-scram
additionalMongodConfig:
storage.wiredTiger.engineConfig.journalCompressor: zlib

# the user credentials will be generated from this secret
# once the credentials are generated, this secret is no longer required
---
apiVersion: v1
kind: Secret
metadata:
name: my-user-password
type: Opaque
stringData:
password: my-pass


And apply the resources:


kubectl apply -f replica.yaml


Now we can connect to the MongoDB using kubectl:


kubectl exec -it example-mongodb-0 -- mongosh "mongodb+srv://my-user:mongo@example-mongodb-svc.default.svc.cluster.local/admin?ssl=false"



Monday, March 13, 2023

Callback in Java

 



In this post we will review an example of callback in Java. Callbacks enables us to handle task asynchronously. For example, we can run a long execution time query and instead of waiting for the query result to return, we can do something else meanwhile. Asynchronous and callbacks are extensively used in GUI applications, where in most cases a single thread is updating the GUI, and we don't want any long running task to make the GUI freeze. Hence any background long processing is done using asynchronous callbacks.


Lets examine an example: we have query API that runs a query for 1 second, and supplies a callback API.


class QueryApi implements Runnable {
private final String queryText;
private final ApiCallback callback;

QueryApi(String queryText, ApiCallback callback) {
this.queryText = queryText;
this.callback = callback;
}

public void runQuery() {
new Thread(this).start();
}

public void run() {
System.out.println("processing the query");
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {

}

if (new Random().nextBoolean()) {
this.callback.onSuccess("query " + this.queryText + " result: very good");
} else {
this.callback.onFailure("bad luck");
}
}
}



The callback is an interface with success and failure handling:


interface ApiCallback {
void onSuccess(String queryResult);

void onFailure(String errorMessage);
}


To use the API, we implement the callbacks, and call the query API:


public class Main implements ApiCallback {
public static void main(String[] args) throws Exception {
new Main().doWork();
}

private void doWork() {
QueryApi api = new QueryApi("get all data", this);
api.runQuery();
System.out.println("i am not waiting for the query, so I can do other tasks");
}

@Override
public void onSuccess(String queryResult) {
System.out.println("YES!\n" + queryResult);
}

@Override
public void onFailure(String errorMessage) {
System.out.println("AHH!\n" + errorMessage);
}
}


The output from this code is the following:


i am not waiting for the query, so I can do other tasks

processing the query

YES!

query get all data result: very good



Monday, March 6, 2023

Go Logger and Logging Methodology


 

In this post we will review a simple logging wrapper in Go, and discuss a logging methodology for a production grade product.


A Simple Log Wrapper


The following is a simple wrapping for logging.


package log

import (
"fmt"
"go.uber.org/zap"
"os"
"time"
)

const levelFatal = "FATAL"
const levelError = "ERROR"
const levelWarn = "WARN"
const levelInfo = "INFO"
const levelVerb = "VERB"

var zapLogger *zap.Logger

func initLogger() {
configuration := zap.NewProductionConfig()
configuration.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
configuration.EncoderConfig.CallerKey = ""
var err error
zapLogger, err = configuration.Build()
if err != nil {
panic(err)
}
}

func writeRecord(level string, format string, v ...interface{}) {
if Config.ZapLogger {
if zapLogger == nil {
initLogger()
}
message := fmt.Sprintf(format, v...)
switch level {
case "VERB":
zapLogger.Debug(message)
break
case "INFO":
zapLogger.Info(message)
break
case "WARN":
zapLogger.Warn(message)
break
case "ERROR":
zapLogger.Error(message)
break
case "FATAL":
zapLogger.Fatal(message)
break
default:
zapLogger.Info(message)
}
} else {
formattedTimestamp := time.Now().UTC().Format("2006-01-02 15:04:05.000")
updatedFormat := fmt.Sprintf("%v %v: %v\n", formattedTimestamp, level, format)
fmt.Printf(updatedFormat, v...)
}
}

func Error(format string, v ...interface{}) {
writeRecord(levelError, format, v...)
os.Exit(1)
}

func Fatal(format string, v ...interface{}) {
writeRecord(levelFatal, format, v...)
os.Exit(1)
}

func Warn(format string, v ...interface{}) {
writeRecord(levelWarn, format, v...)
}

func Info(format string, v ...interface{}) {
writeRecord(levelInfo, format, v...)
}

func V1(format string, v ...interface{}) {
if Config.Verbose < 1 {
return
}
writeRecord(levelVerb, format, v...)
}

func V2(format string, v ...interface{}) {
if Config.Verbose < 2 {
return
}
writeRecord("VERB", format, v...)
}

func V5(format string, v ...interface{}) {
if Config.Verbose < 5 {
return
}
writeRecord("VERB", format, v...)
}

Out of the score of this post is loading of a configuration based on environment variables that includes the verbosity level, and an indication whether to use zap logger.

The gain here, is that this class encapsulate the actual log mechanism, and hence users of the log do not need to import the log library.

We also easily switch between using zap logger, which is a one liner JSON printer of the log message, suitable for production environment, where logs are collected by an automated system such as GrayLog, and between a simple STDOUT printer, suitable for a development environment where a developer manually examines the logs.


Logging Methodology

So we use this log wrapper, and everything works fine, but then, unexpectedly (or not..) we have an issue in production. Our solution is running on kubernetes which supplies the verbosity in environment variable, so we can edit the deployment, and change the verbosity to further investigate the problem.

Wait.. 
Can we? 
Will it assist the problem investigation?

The answer is "probably not" to both questions.

In a production system, another team (devops), not the developers team, is handling the deployment on kubernetes. The devops teams are hard to reach, and will probably not like the idea of changing production configuration, so the developer will need to convince the devops that this is the only way to investigate the issue. Not a fun task.

Suppose we do change the environment variable, and start getting verbose logs. Can you analyze millions of logs manually spit from all over your code? Will the production log collection mechanism be able the handle this huge stress?

It seems we need to rethink our methodology.

A good solution is to be able to update loggers on the fly, using a central configuration GUI, while the processes/pods are running. Also, we need ability to specifically activate logs for code sections, and for specific type of data handled.


To do this, we first add a PrefixedLogger.


package log

type PrefixLogger struct {
prefix string
forceVerbose *int
}

func ProducePrefixLogger(
prefix string,
) *PrefixLogger {
return &PrefixLogger{
prefix: prefix,
}
}

func (p *PrefixLogger) ForceVerbose(verbose int) {
p.forceVerbose = &verbose
}

func (p *PrefixLogger) GetVerbose() int {
if p.forceVerbose == nil {
return Config.Verbose
}

return *p.forceVerbose
}

func (p *PrefixLogger) V1(format string, v ...interface{}) {
if p.GetVerbose() < 1 {
return
}
writeRecord(levelVerb, p.prefix+format, v...)
}

func (p *PrefixLogger) V2(format string, v ...interface{}) {
if p.GetVerbose() < 2 {
return
}
writeRecord(levelVerb, p.prefix+format, v...)
}

func (p *PrefixLogger) V5(format string, v ...interface{}) {
if p.GetVerbose() < 5 {
return
}
writeRecord(levelVerb, p.prefix+format, v...)
}

func (p *PrefixLogger) Info(format string, v ...interface{}) {
writeRecord(levelInfo, p.prefix+format, v...)
}

func (p *PrefixLogger) Warn(format string, v ...interface{}) {
writeRecord(levelWarn, p.prefix+format, v...)
}

func (p *PrefixLogger) Fatal(format string, v ...interface{}) {
writeRecord(levelFatal, p.prefix+format, v...)
}


This uses the logger, but adds a constant prefix to each log message. Allowing us later to filter messages by this text.

Next we add a logger that handles a specific type of data. In our case each data is related to a customer, and the code for a customer is PO.



type PoLogger struct {
log.PrefixLogger
specificEnabled bool
component string
}

func ProducePoLogger(
po *types.PoWithId,
component string,
prefix string,
) *PoLogger {
loggerPrefix := fmt.Sprintf("PO %v %v ", po.PoId, component)
if len(prefix) > 0 {
loggerPrefix += " " + prefix
}

logger := log.ProducePrefixLogger(loggerPrefix)

specificEnabled := isComponentEnabled(po.Log.Components, component)

poLogger := PoLogger{
PrefixLogger: *logger,
specificEnabled: specificEnabled,
component: component,
}

poLogger.setVerboseByPo(po)

return &poLogger
}

func (l *PoLogger) IsSpecificEnabled() bool {
return l.specificEnabled
}

func (l *PoLogger) setVerboseByPo(po *types.PoWithId) {
verbose := log.Config.Verbose
if po.Log.Verbose > verbose {
specificEnabled := isComponentEnabled(po.Log.Components, l.component)
if len(po.Log.Components) == 0 || specificEnabled {
verbose = po.Log.Verbose
}
}

l.PrefixLogger.ForceVerbose(verbose)
}

func (l *PoLogger) RefreshPo(po *types.PoWithId) {
l.setVerboseByPo(po)
}

func isComponentEnabled(components []string, myComponent string) bool {
if len(components) == 0 {
return false
}

for _, component := range components {
if component == myComponent {
return true
}
}

return false
}

The customer (PO) configuration is saved in the database, and we can update it using a dedicated GUI. This configuration includes the names of the codes sections that we want to activate. So we can both select which code to get logs for, and which customer to get logs for, which reduces to logs records amount to a reasonable amount.

Notice that the RefreshPo method needs to be activated once in a while (by a scheduler or a callback event) to ensure we use the updated logger configuration.


Final Note

Creating a maintainable product is a process that affects a product both bottom-up and top-bottom, it should be a in the back of our mind through the design and the implementation steps. An important piece of this is a good logging capability. We have reviewed the required pieces to make this happen, and as always, a code review for not just the code, but also the logging behavior is a key part to keep it working.



Sunday, February 26, 2023

Go Docker Build with Internal Shared Package

 


In the post Go Shared Library, we have reviewed a method to use a shared Go library in a single git repository. This is a case where we have multiple Go libraries in a single git repository, and some share libraries that are used by the modules. A possible folders structure for this use case is:



In this example, we have 3 modules and 2 common libraries. 

The go.mod file for module-a, includes the replace section for the internal shared libraries.


module my.company.com/example/modulea

go 1.19

replace (
my.company.com/example/commonlib1 => ../common-lib-1
my.company.com/example/commonlib2 => ../common-lib-2
)

require (
my.company.com/example/commonlib1 v0.0.0-00010101000000-000000000000
my.company.com/example/commonlib2 v0.0.0-00010101000000-000000000000
)


To build a docker image for this module, we need to add the sources of all the requirements. We can do it manually for each of module-a, module-b, and module-c. A better approach is to automatically handle this. 

We present now a script to automatically build a docker image for a Go module, including the internal module source includes. This automation is done based on the parsing of the related module go.mod file.



#!/usr/bin/env bash

#-------------------------------------
# This script builds a single GO image
#-------------------------------------

set -e

DockerRegistry="${DockerRegistry:-my-project-snapshot-local}"
ProjectVersion="${ProjectVersion:-/dev:latest}"
ScriptsFolder=$(dirname "$0")
Project=$(basename ${PWD})
EntryPoint=\\/${Project}
ArtifactName="my-project-${FolderName}"
DockerTag="${DockerRegistry}/my-project-${Project}${ProjectVersion}"


AddKubectl=false
PushImage=false

HandleCommonPackage(){
commonPackage=$1
sourceFolder=$(echo "${commonPackage}" | cut -c 9-)

echo "common module ${sourceFolder}"

if [[ ! -d ./temp-commons-go-manifest/${sourceFolder} ]]; then
mkdir -p ./temp-commons-go-manifest/${sourceFolder}
cp ../${sourceFolder}/go.mod ./temp-commons-go-manifest/${sourceFolder}/go.mod
cp ../${sourceFolder}/go.sum ./temp-commons-go-manifest/${sourceFolder}/go.sum
fi

if [[ ! -d ./temp-commons-go-all/${sourceFolder} ]]; then
mkdir -p ./temp-commons-go-all/${sourceFolder}
sourceParent=$(dirname ./temp-commons-go-all/${sourceFolder})
cp -r ../${sourceFolder} ${sourceParent}
fi
}

ReplaceVariables(){
sed -i "s/___PROJECT___/${Project}/g" temp-Dockerfile
sed -i "s/___ENTRYPOINT___/${EntryPoint}/g" temp-Dockerfile

grep "=" ./src/go.mod | cut -d= -f2 | while read line ; do HandleCommonPackage "$line" ; done
}

CleanTemp(){
rm -f temp-Dockerfile
rm -rf ./temp-commons-go-manifest
rm -rf ./temp-commons-go-all
}

Build(){
CleanTemp
mkdir ./temp-commons-go-manifest
mkdir ./temp-commons-go-all

cat ${ScriptsFolder}/Dockerfile_stage1 >> temp-Dockerfile
ReplaceVariables
echo "COPY files /images/${Project}/files" >> temp-Dockerfile
echo "RUN go test -race -timeout 300s ./..." >> temp-Dockerfile

TagCompileStage="${DockerTag}-stage1"
docker build -t "${DockerTag}" -c ${TagCompileStage} --cache-from=${DockerTag} -f temp-Dockerfile

cat ${ScriptsFolder}/Dockerfile_stage2 >> temp-Dockerfile
ReplaceVariables

if [[ -d files ]]; then
echo "COPY files /" >> temp-Dockerfile
fi

docker build -t "${DockerTag}" -c ${TagCompileStage} --cache-from=${DockerTag} -f temp-Dockerfile

CleanTemp
}

Build


The script uses a two-stages build. These stages docker files templates located in Dockerfile_stage1, and Dockerfile_stage2. Notice that the script automatically replaces ___VARIABLE___ strings in the docker file.


Dockerfile_stage1

FROM golang:1.19.1 AS go-compiler

ENV GOPATH=/go GOBIN=/go/bin

# get dependencies
COPY ./src/go.mod /images/___PROJECT___/src/
COPY ./src/go.sum /images/___PROJECT___/src/
ADD ./temp-commons-go-manifest /images
WORKDIR /images/___PROJECT___/src

RUN go mod download

# compile source
ADD ./src /images/___PROJECT___/src
ADD ./temp-commons-go-all /images

RUN go build -o /output/___PROJECT___


Dockerfile_stage2

FROM ubuntu:20.04
RUN apt update
RUN apt install -y net-tools ca-certificates curl iputils-ping dnsutils
RUN update-ca-certificates

COPY --from=go-compiler /output/___PROJECT___ /___PROJECT___
WORKDIR /
ENTRYPOINT ["___ENTRYPOINT___"]



Final Note


Using this simple script, handling of the docker image creation is automatic and simple. It is a great tool to automate Go modules and Go internal libraries usage.




Monday, February 20, 2023

Create Storage Provisioner for AWS EKS


 


In this post we will review the steps to create a storage provisioner for AWS EKS. This is required to allocate EFS storage as a response to a Physical Volume Claim (PVC).

First we create a service account that will be used for the provisioner.


rm -f iam-policy.json

curl -S https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/v1.2.0/docs/iam-policy-example.json -o iam-policy.json

policyExists=$(aws iam list-policies|grep EFSCSIControllerIAMPolicy|wc -l)
if [[ "${policyExists}" = "0" ]]; then
aws iam create-policy \
--policy-name EFSCSIControllerIAMPolicy \
--policy-document file://iam-policy.json
fi

rm -f iam-policy.json


eksctl create iamserviceaccount \
--name=efs-csi-controller-sa \
--namespace=kube-system \
--cluster=${AWS_EKS_CLUSTER_NAME} \
--region ${AWS_REGION} \
--override-existing-serviceaccounts \
--attach-policy-arn=arn:aws:iam::${AWS_ACCOUNT}:policy/EFSCSIControllerIAMPolicy \
--approve


Next we install the provisioner using helm chart.


helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver
helm repo update
helm upgrade -i aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver \
--namespace kube-system \
--set image.repository=602401143452.dkr.ecr.us-west-2.amazonaws.com/eks/aws-efs-csi-driver \
--set controller.serviceAccount.create=false \
--set controller.serviceAccount.name=efs-csi-controller-sa


Now, we login to the AWS console, and manually create an EFS, and update the EFS ID in the following yaml file, and apply it using kubectl.


kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: efs-sc
provisioner: efs.csi.aws.com
parameters:
provisioningMode: efs-ap
fileSystemId: fs-084ad2344494c65a4
directoryPerms: "700"


Now any PVC with storage class efs-sc will be automatically handled by the storage provisioner.







Monday, February 13, 2023

Capture and view network traffic


 

In this post we will show the steps to capture and view network traffic on a local ubuntu machine. This is useful when we need to check a specific application ingress and egress traffic.

For this post we will use the "application" curl.


Capturing

We start by capturing the traffic using the tcpdump. The tcpdump creates a dump of all the network packets on a specific interface or for a specific host. For this example we will capture all traffic on a specific interface. To get the list of interfaces on the machine use the command:

ip a

and then run the tcpdump, for example:

sudo tcpdump -i enp0s31f6 -w dump.pcap

Notice that the tcpdump gets two arguments here: the interface name, and the pcap output file which includes the captured packets. The tcpdump will keep capturing traffic until we stop it using Ctrl+C.

We keep the tcpdump running, and in another terminal we run our application:

curl http://ynet.co.il


Now we can stop the capturing using Ctrl+C, and we have a pcap file with the captured network packets.


Viewing

To view the pcap file, the most common application is wireshark. To install wireshark, use the following command:

sudo apt install wireshark

And then run it:

sudo wireshark

In the wireshark application, select file and open the dump.pcap file.

This now displays all the network packets, but in most cases, we just want to see http packets, so in the wireshark display filter, type http, and click enter.



We can now see the http related packets, and inspect the ingress and egress traffic, divided by the related protocol layers.

Final Note

In this post we have captured and viewed http traffic for an application. Notice that HTTPS traffic will not be clear text as it is encrypted. To view decrypted traffic, use the steps described here