Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Monday, April 24, 2023

Using MongoDB


In this post we review the basic steps of start using an existing MongoDB. See this post for installing a MongoDB on a kubernetes cluster. 


To add a new document in a a new collection in a new database, we need to perform the following actions (see examples for commands below):

  1. Use the new db
  2. Create new user in the new db
  3. Grant permissions to the new user in the new db
  4. Switch to the new user
  5. Add thew new document


Databases Commands

  • use db1
    Set a database for the next operations
    The database is automatically created when we store data in it

  • show dbs
    Show list of databases

Users Commands

  • db.runCommand({connectionStatus : 1})
    Display current user information

  • db.createUser({ user: 'alon', pwd: 'alon', roles: [] })
    Create a new user

  • db.grantRolesToUser('alon', [{ role: 'readWrite', db: 'db1' }])
    Add permissions to user

  • db.auth('alon','alon')
    Switch to a user in the current database

Documents Commands

  • db.collection1.insertOne({name:'John', Age:45})
    Insert new document

  • db.collection1.find()
    Get all documents from the collection

  • db.collection1.find({'Age':45})
    Query for specific documents

  • db.collection1.insertMany([{'Name':"Foo"},{"Name":"Bar"}])
    Insert multiple documents

    Indexes Command

    • db.collection1.createIndex({'name':1})
      Create new index



    Monday, April 3, 2023

    Go Implementation for IP range to CIDRs


     


    In this post we will review creation of CIDRs from IP range. This was required in my case as we've received input as IP ranges, but the processing application had built a trie representation of the IPs using CIDRs.

    We've started with a simple conversion functions:


    func IntToIp(intIp uint32) net.IP {
    ip := make(net.IP, 4)
    binary.BigEndian.PutUint32(ip, intIp)
    return ip
    }

    func ParseIpv4(ipv4 string) net.IP {
    ip := net.ParseIP(ipv4)
    return ip[12:]
    }

    func IpToInt(ip net.IP) uint32 {
    return binary.BigEndian.Uint32(ip)
    }


    And the implemented a naive CIDR creation function:

    func MakeCidr(
    fromIp net.IP,
    toIp net.IP,
    ) net.IPNet {
    fromIpInt := IpToInt(fromIp)
    toIpInt := IpToInt(toIp)

    ones := 32
    for diff := toIpInt - fromIpInt; diff > 0; diff = diff / 2 {
    ones--
    }

    mask := net.CIDRMask(ones, 32)
    maskInt := IpToInt(net.IP(mask))
    networkAddressInt := fromIpInt & maskInt
    networkAddressEndInt := networkAddressInt | (^maskInt)
    networkAddress := IntToIp(networkAddressInt)

    network := net.IPNet{
    IP: networkAddress,
    Mask: mask,
    }
    if networkAddressInt != fromIpInt || networkAddressEndInt != toIpInt {
    errsimple.RaiseIfError(fmt.Errorf("invalid range %v-%v, network address is: %v",
    fromIp.String(), toIp.String(), network.String()))
    }
    return network
    }


    Just to be on the safe side, we've added a protection for non CIDR range. For example the IP range:

    1.1.1.0 - 1.1.1.255

    Is converted to the CIDR 1.1.1.0/24, but the IP range:

    1.1.1.0 - 1.1.1.254

    Cannot be converted to a single CIDR.


    Then, we run the application, and found that the IP ranges that we have are indeed non-convertable to a single CIDR. To address this issue, we've create another implementation that can split IP range to multiple CIDRs.


    func MakeMultipleCidrs(
    fromIp uint32,
    toIp uint32,
    ) []net.IPNet {

    ones := 0

    for checkBitMask := uint32(1) << 31; checkBitMask != 0 && fromIp&checkBitMask == toIp&checkBitMask; checkBitMask = checkBitMask >> 1 {
    ones++
    }

    mask := net.CIDRMask(ones, 32)
    maskInt := IpToInt(net.IP(mask))
    networkAddressStartInt := fromIp & maskInt
    networkAddressEndInt := networkAddressStartInt | (^maskInt)

    if networkAddressStartInt == fromIp && networkAddressEndInt == toIp {
    networkAddress := IntToIp(networkAddressStartInt)
    network := net.IPNet{
    IP: networkAddress,
    Mask: mask,
    }
    return []net.IPNet{network}

    }
    size := networkAddressEndInt - networkAddressStartInt + 1
    halfSize := size / 2
    secondHalfStartInt := networkAddressStartInt + halfSize
    firstHalfEnd := secondHalfStartInt - 1

    cidrsFirst := MakeMultipleCidrs(fromIp, firstHalfEnd)
    cidrsSecond := MakeMultipleCidrs(secondHalfStartInt, toIp)
    cidrs := append(cidrsFirst, cidrsSecond...)
    return cidrs
    }


    This recursive function breaks the range into 2 parts, until it finds a CIDR that match the range.

    An example of output for this function is below:

    range 1.1.1.1 - 1.1.1.1, cidrs: [1.1.1.1/32]

    range 1.1.1.0 - 1.1.1.1, cidrs: [1.1.1.0/31]

    range 1.1.1.0 - 1.1.1.2, cidrs: [1.1.1.0/31 1.1.1.2/32]

    range 1.1.1.1 - 1.1.1.3, cidrs: [1.1.1.1/32 1.1.1.2/31]

    range 1.1.1.0 - 1.1.1.3, cidrs: [1.1.1.0/30]

    range 1.1.1.0 - 1.1.1.255, cidrs: [1.1.1.0/24]

    range 1.1.1.0 - 1.1.2.255, cidrs: [1.1.1.0/24 1.1.2.0/24]

    range 1.1.2.0 - 1.1.3.255, cidrs: [1.1.2.0/23]



    Notice that this implementation works only for IPv4, feel free to use the same idea for IPv6.













    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.