Category: go

Mar 24

Go Import JSON File Into MongoDB Unstructured

During a recent POC I imported a Terraform state file(JSON) into MongoDB using golang. This may not exactly fit any use case since it is simpler and use actual file(s) and not MongoDB to store a Terraform state file, but this helps record my effort as well as the unstructured nature of the JSON ie I am not defining a structure just using an interface{}.

Insert From File

package main

import (
    "context"
    "fmt"
    "os"
    "io/ioutil"
    "encoding/json"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

func main() {
    clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
    client, e := mongo.Connect(context.TODO(), clientOptions)
    CheckError(e)
    collection := client.Database("tfstatedb").Collection("states")
    jsonFile, err := os.Open("terraform.tfstate")
    if err != nil {
        fmt.Println(err)
    }
    defer jsonFile.Close()

    byteValue, _ := ioutil.ReadAll(jsonFile)
    var result map[string]interface{}
    json.Unmarshal([]byte(byteValue), &result) 
    _, e = collection.InsertOne(context.TODO(), result)
    CheckError(e)
}

func CheckError(e error) {
    if e != nil {
        fmt.Println(e)
    }
}

FindOne From DB

package main

import (
    "flag"
    "log"
    "context"
    "fmt"
    "encoding/json"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

var (
     flagId string
)

func init() {
     flag.StringVar(&flagId, "id", "", "mongodb ObjectId")
}

func PrettyPrint(v interface{}) (err error) {
      b, err := json.MarshalIndent(v, "", "  ")
      if err == nil {
         fmt.Println(string(b))
      }
      return
}

func main() {
    if !flag.Parsed() {
    flag.Parse()
    }
    if flagId != "" {
          fmt.Println("finding id: " + flagId)
    } else {
          log.Fatal("no _id specified")
    }

    clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
    client, e := mongo.Connect(context.TODO(), clientOptions)
    CheckError(e)

    e = client.Ping(context.TODO(), nil)
    CheckError(e)

    collection := client.Database("tfstatedb").Collection("states")

    var res interface{}

    docID, e := primitive.ObjectIDFromHex(flagId)
    CheckError(e)

    filter := bson.M{"_id": docID}

    tempResult := bson.M{}
    e = collection.FindOne(context.TODO(), filter).Decode(&tempResult)
    if e == nil {   
      obj, _ := json.Marshal(tempResult)
      e = json.Unmarshal(obj, &res)
    }
    PrettyPrint(res)
}

func CheckError(e error) {
    if e != nil {
        fmt.Println(e)
    }
}

Comments Off on Go Import JSON File Into MongoDB Unstructured
comments

Mar 22

Terraform Stateserver Using Go

Terraform can utilize a http backend for maintaining state. This is a test of a Terraform http backend using a server implemented with go.

recipe and components

• Using Virtualbox Ubuntu 20.10 and links listed below
• Run as http server
• Change to https
• For a future test terraform http backend locking feature and/or include locking in server

links

• https://www.terraform.io/docs/language/settings/backends/http.html
• https://linuxhint.com/install-golang-ubuntu/
• https://github.com/MerlinDMC/go-terraform-stateserver
• https://github.com/denji/golang-tls

setup go and test

# go version
go version go1.14.7 linux/amd64

$ pwd
/home/rrosso/tf-go-server
$ cat main.go 
package main
import "fmt"
func main() {
fmt.Println("Hello World");
}
$ go run main.go 
Hello World

$ go build main.go 
$ ./main 
Hello World

run server and test response

$ go run tf-stateserver.go -data_path=/home/rrosso/tf-go-server -listen_address=192.168.1.235:8080

$ curl -sL http://192.168.1.235:8080 | xxd
00000000: 3430 3420 7061 6765 206e 6f74 2066 6f75  404 page not fou
00000010: 6e64 0and.

terraform init with http backend

➜  terraform-poc tail -8 main.tf 
#}
## POC to test a go server
terraform {
  backend "http" {
    address = "http://192.168.1.235:8080/states/terraform.tfstate"
  }
}

➜  terraform-poc terraform init

Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "http" backend. No existing state was found in the newly
  configured "http" backend. Do you want to copy this state to the new "http"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes

Successfully configured the backend "http"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.oci: version = "~> 4.17"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

observe server log messages

• file not found if no file exist during init is expected
• if file same and already there GET fine during init
• plan does a GET
• apply does a POST
• did not observe a DELETE
$ go run tf-stateserver.go -data_path=/home/rrosso/tf-go-server -listen_address=192.168.1.235:8080
2021/03/18 09:57:41 received GET for state file: /home/rrosso/tf-go-server/states/terraform.tfstate
...
2021/03/18 09:58:33 received POST for state file: /home/rrosso/tf-go-server/states/terraform.tfstate
...
2021/03/18 10:00:50 received GET for state file: /home/rrosso/tf-go-server/states/terraform.tfstate
2021/03/18 10:00:50 during GET cannot open file: open /home/rrosso/tf-go-server/states/terraform.tfstate: no such file or directory
2021/03/18 10:00:50 received GET for state file: /home/rrosso/tf-go-server/states/terraform.tfstate

change to self signed cert for https test

NOTE:
• use sudo since linux instance is not running low ports like 443 for normal users
• need terraform skip_cert_verification for this self signed certificate

$ openssl genrsa -out server.key 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
..........................+++++
.......................................................+++++
e is 65537 (0x010001)

$ openssl ecparam -genkey -name secp384r1 -out server.key

$ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
...

$ sudo go run tf-stateserver.go -certfile="server.crt" -keyfile="server.key" -data_path=./ -listen_address=192.168.1.235:443

$ curl -sL http://192.168.1.235:443 | xxd
00000000: 436c 6965 6e74 2073 656e 7420 616e 2048  Client sent an H
00000010: 5454 5020 7265 7175 6573 7420 746f 2061  TTP request to a
00000020: 6e20 4854 5450 5320 7365 7276 6572 2e0a  n HTTPS server..

➜  terraform-poc tail -8 main.tf 
#}
## POC to test a go server
terraform {
  backend "http" {
    address = "https://192.168.1.235/states/terraform.tfstate"
    skip_cert_verification = true
  }
}

NOTE: terraform init, plan, apply works

additional links

• https://appdividend.com/2019/11/29/golang-json-example-how-to-use-json-with-go/
• https://www.golangprograms.com/web-application-to-read-and-write-json-data-in-json-file.html
• https://tutorialedge.net/golang/parsing-json-with-golang/
• https://dzone.com/articles/how-to-write-a-http-rest-api-server-in-go-in-minut

source

NOTE: subsequently checked source into github here: https://github.com/rrossouw01/terraform-stateserver-go/blob/main/server.go

Can use the file directly with simple wget to the raw link for example:

$ wget https://raw.githubusercontent.com/rrossouw01/terraform-stateserver-go/main/server.go
$ cat tf-stateserver.go 
package main
//https://github.com/MerlinDMC/go-terraform-stateserver/blob/master/main.go

import (
    "flag"
    "io"
    "log"
    "net/http"
    "os"
    "path/filepath"
)

var (
    flagDataPath string
    flagCertFile string
    flagKeyFile  string

    flagListenAddress string
)

func init() {
    flag.StringVar(&flagCertFile, "certfile", "", "path to the certs file for https")
    flag.StringVar(&flagKeyFile, "keyfile", "", "path to the keyfile for https")
    flag.StringVar(&flagDataPath, "data_path", os.TempDir(), "path to the data storage directory")

    flag.StringVar(&flagListenAddress, "listen_address", "0.0.0.0:8080", "address:port to bind listener on")
    //flag.StringVar(&flagListenAddress, "listen_address", "192.168.1.235:8080", "address:port to bind listener on")
}

func main() {
    if !flag.Parsed() {
        flag.Parse()
    }

    router := http.NewServeMux()
    router.HandleFunc("/", requestHandler)

    if flagCertFile != "" && flagKeyFile != "" {
          log.Fatal(http.ListenAndServeTLS(flagListenAddress, flagCertFile, flagKeyFile, router))
    } else {
          log.Fatal(http.ListenAndServe(flagListenAddress, router))
    }
}

func requestHandler(res http.ResponseWriter, req *http.Request) {
    if req.URL.Path == "/" {
        http.NotFound(res, req)
        return
    }

    stateStorageFile := filepath.Join(flagDataPath, req.URL.Path)
    stateStorageDir := filepath.Dir(stateStorageFile)

    switch req.Method {
    case "GET":
        log.Printf("received GET for state file: %s\n", stateStorageFile)
        fh, err := os.Open(stateStorageFile)
        if err != nil {
            log.Printf("during GET cannot open file: %s\n", err)
            goto not_found
        }
        defer fh.Close()

        res.WriteHeader(200)

        io.Copy(res, fh)

        return
    case "POST":
        log.Printf("received POST for state file: %s\n", stateStorageFile)
        if err := os.MkdirAll(stateStorageDir, 0750); err != nil && !os.IsExist(err) {
            log.Printf("during POST cannot create parent directories: %s\n", err)
            goto not_found
        }

        fh, err := os.OpenFile(stateStorageFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
        if err != nil {
            log.Printf("cannot open file: %s\n", err)
            goto not_found
        }
        defer fh.Close()

        if _, err := io.Copy(fh, req.Body); err != nil {
            log.Printf("failed to stream data into statefile: %s\n", err)
            goto not_found
        }

        res.WriteHeader(200)

        return
    case "DELETE":
        log.Printf("received DELETE for state file: %s\n", stateStorageFile)
        //if os.RemoveAll(stateStorageFile) != nil {
        if err := os.RemoveAll(stateStorageFile); err != nil {
            log.Printf("cannot remove file: %s\n", err)
            goto not_found
        }

        res.WriteHeader(200)

        return
    }

not_found:
    http.NotFound(res, req)
}

Comments Off on Terraform Stateserver Using Go
comments

Aug 12

Go Format Output Column Style

Similar to this article (using python) https://blog.ls-al.com/python-output-align-by-column/ I also did a quick Golang implementation.

It is not quite as done as I would like but it is reasonably close. Similar to the python article the idea is to use something like an array to store column header descriptions and a value on how long the output strings should be and use it for header printing as well as line output statements.

In addition I am doing some file operations here to create the output so the article has additional value.

package main

import (
  "path/filepath"
  "os"
  "flag"
  "fmt"
  "strconv"
)

type rec struct {
  fname, fsize string
}

var i int64
var files map[int64]rec
var FORMAT map[string]int 

func PadLeft(str, pad string, length int) string {
  for {
   str = pad + str
   if len(str) > length {
     return str[0:length]
   }
  }
}

func printHeader(FORMAT map[string]int) {
  for k, v := range FORMAT {
    fmt.Printf("%[1]*[2]s ",v ,k)    
  }
  fmt.Println()
  for k, v := range FORMAT {
    _ = k
    //fmt.Printf("% [1]*[2]s ",v, "#")
    fmt.Printf("%s   ",PadLeft("","#",v))    
  }
  fmt.Println()
}

func visit(path string, f os.FileInfo, err error) error {
  fi, e := os.Stat(path)
  if e != nil {
    return e
  }
  i = i + 1
  files[i] = rec{path, strconv.Itoa(int(fi.Size()))}
  return nil
} 

func main() {
  FORMAT := map[string]int{"File Size": 10, "File Name": 11}
  printHeader(FORMAT)
  files = make(map[int64]rec)
  flag.Parse()
  root := flag.Arg(0)
  err := filepath.Walk(root, visit)
  _ = err
  //fmt.Printf("filepath.Walk() returned %v\n", err)
  for k, v := range files {
    _ = k
    fmt.Printf("%[1]*[2]s   %[3]*[4]s\n", FORMAT["File Size"], v.fsize, FORMAT["File Name"], v.fname)
  }
}

Test Run

$ go run header.go /etc/default/
 File Size   File Name 
##########   ###########   
       346   /etc/default/acpid
       290   /etc/default/anacron
       209   /etc/default/saned
       149   /etc/default/apport
       132   /etc/default/speech-dispatcher

Comments Off on Go Format Output Column Style
comments

Aug 10

Go Associative Array

Jotting down my test to implement an associative array similar to as my python test here: https://blog.ls-al.com/python-dict-for-arrays/

In python I used dict and in go I used map.

$ pwd
/home/rrosso/src/examples

$ cat maps.go 
package main
import "fmt"
type rec struct {
	lname, fname string
}

var m map[string]rec

func main() {
	m = make(map[string]rec)
	m["1"] = rec{"Sable", "Sam",}
	m["2"] = rec{"Sable", "Samantha",}
	m["3"] = rec{"Sable", "Stevie",}

	fmt.Println(m)
	fmt.Println(m["2"])
	fmt.Println(m["3"].fname)
	fmt.Println()

        //simpler example no struct
	n := map[string]int{"foo": 1, "bar": 2}
    	fmt.Println("map:", n)
	fmt.Println("val:", n["bar"])
}

Output

$ go run maps.go 
map[1:{Sable Sam} 2:{Sable Samantha} 3:{Sable Stevie}]
{Sable Samantha}
Stevie

map: map[foo:1 bar:2]
val: 2

Comments Off on Go Associative Array
comments