Introduction

In this challenge, you will create a simple Go program that will ask users for some input and then will store all the information in Vault. By the end of this challenge, you should have a very good understanding of many Golang concepts and should have enough knowledge to start developing Golang programs on your own or contribute to existing open source projects. This is not, however, a Dojo on Vault, which means that even though you will learn some of the basics, we will not dive in as much.

Disclaimer For Golang Professionals

In this challenge, you will probably come across some code design decisions which might not necessarily follow industry best practices. Since we are focusing more on introducing Golang concepts rather than building robust systems, sometimes we might need to take an easier approach to systems design so people can learn more efficiently.

How the Challenge is Structured

First, we will spend some time (not much, though!) setting up your environment. Then, there will be number of phases where each one of them will go over one or more Golang concepts. And to make sure you're on the right path, there will be Definition of Done sections to show you how to test your solution. Finally, you will see some For discussion sections (which are optional and can be skipped). The goal of these sections is to create a discussion between the team members and the organizers about a certain topic.

Getting Started

Before you start developing any code, let's set up our Golang and Vault environments. You will not need to install anything in your machine apart from Docker. As a first step, create a folder called golang-dojo anywhere under your user's directory. This will be where you will develop the code.

Next, run the command (<HASH> is not part of the command, that's just the output of the docker command):

$ docker run --rm -d -v <FULL-PATH-TO-GOLANG-FOLDER>:/go/src/golang-dojo slalomdojo/env:golang
<HASH>

PS 1: you need to specify /go/src/golang-dojo as the destination inside the container so your code can be compiled according to this challenge's intructions

PS 2: if you are on Windows and the directory mounting is not working, make sure you have configured Docker to allow sharing the C: drive with containers. If it still doesn't work, reach out to one of the organizers of the event so we can discuss options.

The command above will give you back a hash that can be used to jump into the container:

docker exec -it <HASH> sh

Once inside the container, you should be able to run go and get the following:

/go # go
Go is a tool for managing Go source code.

Usage:

    go <command> [arguments]

The commands are:

    bug         start a bug report
    build       compile packages and dependencies
    clean       remove object files and cached files
    doc         show documentation for package or symbol
    env         print Go environment information
    fix         update packages to use new APIs
    fmt         gofmt (reformat) package sources
    generate    generate Go files by processing source
    get         add dependencies to current module and install them
    install     compile and install packages and dependencies
    list        list packages or modules
    mod         module maintenance
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

(...)

Now let's export two environment variables which will be needed later when connecting to the Vault server. Open a new terminal, and run docker logs to retrieve the logs from the container you just ran:

docker logs <HASH>

That will output quite a bit of log lines generated by Vault:

==> Vault server configuration:

             Api Address: http://127.0.0.1:8200
                     Cgo: disabled
         Cluster Address: https://127.0.0.1:8201
              Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
               Log Level: info
                   Mlock: supported: true, enabled: false
           Recovery Mode: false
                 Storage: inmem
                 Version: Vault v1.3.0

WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.

You may need to set the following environment variable:

    $ export VAULT_ADDR='http://127.0.0.1:8200'

The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.

Unseal Key: RBBbPkKAS5r9MOqSorBkfVCdd9GZeoTILZdm0i+b5ZI=
Root Token: <ROOT TOKEN>

Development mode should NOT be used in production installations!

==> Vault server started! Log data will stream in below:

2019-11-22T18:45:27.098Z [INFO]  proxy environment: http_proxy= https_proxy= no_proxy=
2019-11-22T18:45:27.098Z [WARN]  no `api_addr` value specified in config or in VAULT_API_ADDR; falling back to detection if possible, but this value should be manually set
2019-11-22T18:45:27.106Z [ERROR] core: no seal config found, can't determine if legacy or new-style shamir
2019-11-22T18:45:27.106Z [ERROR] core: no seal config found, can't determine if legacy or new-style shamir
2019-11-22T18:45:27.106Z [INFO]  core: security barrier not initialized
2019-11-22T18:45:27.107Z [INFO]  core: security barrier initialized: stored=1 shares=1 threshold=1
2019-11-22T18:45:27.112Z [INFO]  core: post-unseal setup starting
2019-11-22T18:45:27.123Z [INFO]  core: loaded wrapping token key
2019-11-22T18:45:27.123Z [INFO]  core: successfully setup plugin catalog: plugin-directory=
2019-11-22T18:45:27.123Z [INFO]  core: no mounts; adding default mount table
2019-11-22T18:45:27.126Z [INFO]  core: successfully mounted backend: type=cubbyhole path=cubbyhole/
2019-11-22T18:45:27.127Z [INFO]  core: successfully mounted backend: type=system path=sys/
2019-11-22T18:45:27.127Z [INFO]  core: successfully mounted backend: type=identity path=identity/
2019-11-22T18:45:27.129Z [INFO]  core: successfully enabled credential backend: type=token path=token/
2019-11-22T18:45:27.129Z [INFO]  core: restoring leases
2019-11-22T18:45:27.130Z [INFO]  rollback: starting rollback manager
2019-11-22T18:45:27.130Z [INFO]  expiration: lease restore complete
2019-11-22T18:45:27.131Z [INFO]  identity: entities restored
2019-11-22T18:45:27.131Z [INFO]  identity: groups restored
2019-11-22T18:45:27.131Z [INFO]  core: post-unseal setup complete
(...)

You will need two things from the log output: the Root Token and the Vault Address (e.g. http://127.0.0.1:8200). Go back to the container you span up and export two environment variables:

/go # export VAULT_ADDR='http://127.0.0.1:8200'
/go # export TOKEN=<ROOT TOKEN>

Now we're ready to start!

Understanding The Go Workspace

Whenever you're developing Go code, the code should be part of a workspace. And to quote the Go documentation, a workspace is a directory hierarchy with two directories at its root:

src contains Go source files and bin contains executable commands.

The go tool builds and installs binaries to the bin directory.

The src subdirectory typically contains multiple version control repositories (such as for Git or Mercurial) that track the development of one or more source packages.

Here's an example:

bin/
    hello                          # command executable
    outyet                         # command executable
src/
    github.com/golang/example/
        .git/                      # Git repository metadata
    hello/
        hello.go               # command source
    outyet/
        main.go                # command source
        main_test.go           # test source
    stringutil/
        reverse.go             # package source
        reverse_test.go        # test source
    golang.org/x/image/
        .git/                      # Git repository metadata
    bmp/
        reader.go              # package source
        writer.go              # package source
    ... (many more repositories and packages omitted) ...

How does the go compiler know where in your system is the "Go Workspace"? Through an environment variable called GOPATH. It defaults to a directory named go inside your home directory, so $HOME/go on Unix/Mac, and %USERPROFILE%\go (usually C:\Users\YourName\go) on Windows.

If you'd like to see which environment variables the Go compiler uses, run go env.

Next, let's list all directories under /go:

/go # ls -l /go
total 8
drwxrwxrwx    2 root     root          4096 Nov  1 20:22 bin
drwxrwxrwx    1 root     root          4096 Nov 22 18:45 src
bin        src

You will see that we already have a bin and src directory, which means you do not have to create anything at this point. Next, you will learn how to leverage this folder structure to compile code and install binaries

Phase 1: The Go CLI & Hello World

How can we learn a new programming language without creating a Hello World program? Let's start with that.

If you were able to mount a volume into the container successfully, you should see a folder called golang-dojo inside src:

/go # ls /go/src
golang-dojo

That's where you will be able to find your code. But again! You will not develop the code inside the container. Since this directory exists in your local computer, that's where you should develop the code. In your local computer, open a text editor or IDE in the golang-dojo folder.

However, if you wish to develop code inside the container, install vim or nano with the command: apk add --no-cache <vim/nano>.

Now that you have the golang-dojo folder open in a Text Editor or IDE (or vim inside the container), create a file called main.go.

Paste the following code:

package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}

Before we compile it, let's understand what's all that. If you already understand, skip the next 4 sections.

package main

In Go, every single go file belongs to a package. In this case, our main.go belongs to the main package. The main package is a special package because it indicates that the code there is an executable. The Go CLI will not run programs that do not contain a package main statement at the very top. If we were to call this package something else, we'd get an error like:

go run: cannot run non-main package

Another thing that is worth noting is that the very first line of a Go code should be the package to which the code belongs. If you were to start this Hello World program with import "fmt", the Go CLI would throw an error saying:

main.go:1:1: expected 'package', found 'import'

So, to summarize: regardless if you're developing a one-file program or a complex Golang API, the very first instructions that you want your program to run should be in the main package.

import "fmt"

As the name implies, the keyword import is used to import other packages into your program. Here we are importing a package called "fmt", which contains tons of functions related to formatting I/O (with functions analogous to C's printf and scanf).

We will use one of the functions of this package to be able to print in the terminal.

func main()

In Go, that's how a very basic function declaration looks like: the keyword func followed by the function name and any parameters it might take (no parameters needed in this case) and the return value (which is non-existent here for the main function).

And as you might've guessed, main() is a special function (the same way package main is): it is the entry point of an executable program.

fmt.Println("Hello World!")

As mentioned before, fmt is a package that contains functions related to I/O. If you'd like to use a function defined in a package, you prefix that function name with <package-name>.. So, to use a function called Println which has been defined in the package fmt, the syntax is fmt.Println()

You will see, however, that it is possible to invoke a function without specifying the package name, but we'll get to the nitty-gritty of packages/imports soon when we develop our own packages.

Compiling the code

Ok! Now that you have an understanding of this Hello World program, it's time to run it.

First, to run this program without generating a binary, use the go run command:

/go/src/golang-dojo # go run main.go
Hello World!

Next, let's generate a binary so we can execute it. The command below compiles main.go into a binary called dojo:

/go/src/golang-dojo # go build -o dojo main.go

Now, run ./dojo - you should get the same output.

Finally, remember that bin directory? If you'd like to compile and send the binary to the bin folder, use the go install command without specifying main.go:

/go/src/golang-dojo # go install

This command will compile main.go and generate a binary that has the same name as the folder where main.go is - golang-dojo. If you run ls /go/bin, you should see a golang-dojo binary there. Execute that binary and you should have the same "Hello World" output.

Phase 2: Reading from Standard Input (stdin)

The "Hello World" program was purely a warm-up! Things will start getting more serious now...

As mentioned at the beginning, in this challenge you will write a Go program that asks users for some input and saves that information. So let's focus first on getting user input from the terminal.

Here's the information you will ask the user:

  1. First Name
  2. Last Name
  3. Date of Birth
  4. Nationality
  5. Email

As a first step, let's create variables to store all this information.

A very useful website when learning Go is Go By Example. In this website, there are tons of annoted example programs that explain how to use many of the Golang concepts. To learn how to declare variables, check the Variables page.

You should declare 5 variables: firstName, lastName, dob, nationality and email. All of these variables should be of type string.

Now, when it comes to reading input from Standard Input, things can get a little tricker. There are many ways to read input directly from the terminal, but let's try to keep it easy for now. The following snippet of code reads characters from the terminal (including white spaces) until it finds a newline character:

var reader = bufio.NewReader(os.Stdin)


myVar, _ = reader.ReadString('\n')
myVar = strings.Replace(myVar, "\n", "", -1)

Don't worry too much for now what this is doing, but use the code above to get all the information required from the user and save the data in those 5 variables you declared.

PS: to make you program more human-friendly, use fmt.Println() to print the questions in the terminal.

After reading all the variables, print the following:

Here is what we know about you:

First name:  <FIRST NAME> # use firstName variable
Last name:  <LAST NAME> # use lastName variable
Date of Birth:  <DATE OF BIRTH> # etc...
Nationality:  <NATIONALITY>
Email:  <EMAIL>

Before you try to compile the code, note that the snippet above uses 3 packages: bufio, os and strings. You will have to import those packages.

Definition of Done

Here's how your program should look like at the end of Phase 2:

/go/src/golang-dojo # go run main.go 
Tell us about yourself:

First Name? John 
Last Name? Doe
Date of Birth? 13th September 1980
Nationality? Canadian
Email? johndoe@gmail.com

Here is what we know about you:

First name:  John
Last name:  Doe
Date of Birth:  13th September 1980
Nationality:  Canadian
Email:  johndoe@gmail.com

The exact wording doesn't matter, as long as you were able to read characters from terminal, store them in variables and use those variables to print the information gathered.

Phase 3: Packages & Maps

Naturally, whenever you develop a complex program, there will be tons and tons of packages. Packages that are native to the language and packages that you will have to create yourself.

Here's what you need to do next:

  1. Create a folder called internal at the root level of golang-dojo
  2. Inside the internal folder, create a folder called person
  3. Inside the person folder, create a file called person.go.

The goal now is to get all that code that reads characters from the terminal and put it into a package called person. In person.go, declare a package called person.

Next, declare a function called GetPersonInfo(). Note that the very first letter needs to be uppercase. We'll explain why in a bit. Then, copy and paste all the code used to read input from the terminal into this new function - but leave out the code that prints the information in the terminal.

At this point, your function should look something similar to the following:

func GetPersonInfo() {
    <variable declaration>

    var reader = bufio.NewReader(os.Stdin)

    <read first name>
    <read last name>
    <read dob>
    <read nationality>
    <read email>
}

Since the code to print the information to the terminal is not present in this function, if we call this function from main(), we will not get anything in return and therefore we will not be able to print anything. Let's work on that a little bit.

First, let's learn about maps. map works the same way as other programming languages: it maps keys to values. Take at look at this example: https://gobyexample.com/maps.

At the beginning of the GetPersonInfo() function, declare a map where both keys and values will be of type string. Now that you have a map, you don't need variables anymore. Use the map to store the input from the terminal.

Once you've changed the code to use maps, return that map so whoever is calling this function can have access to the information gathered.

PS: if you return a value, you need to indicate in the function declaration what's the type of the information being returned. Take a look at this example.

As a last step, go back to main.go. The first thing you need to do is to import the new package (person) that you created.

Read the following links to understand how to import your package in main.go:

Once you were able to import your package, declare a variable that will receive the returned data from GetPersonInfo(). Then, print the information in the terminal.

For Discussion

What would happen if the name of the function was getPersonInfo() instead? (lower case g)

Definition of Done

The definition of done is exactly the same as Phase 2.

Phase 4: Structs

So far, we've been asking for the user's first and last name, date of birth, nationality and email. All this information is of type string. But what if we were to introduce something of type int(eger)? Like age? Then, in that case, we should avoid using a map because we would have values of 2 different types: string and int (it's still possible to use a single map with different value types, but that's an advanced concept which we won't get into at this moment).

Leaving advanced concepts aside, if you wanted to use a map, you'd have to have a separate map or variable for the age. And that would not be the best approach here. Therefore, let's learn about Structs.

Based on the example above (see the Structs link), create a Struct called Person with 6 fields (including the age). Then, create a NewPerson() function that returns a Person. The function signature should be the following:

func NewPerson(firstName string, lastName string, age int, dob string, nationality string, email string) *Person

PS 1: By convention, the function that returns a struct is named New[Struct-Name], so in this case NewPerson

PS 2: You will notice that at the end, we're returning *Person instead of just Person. Learn here the difference between pass by reference and pass by value

Once you're done with the NewPerson() function, create a Print function that will print the information gathered.

Attention: If you create a function with the following signature:

func Print()

You won't be able to access the struct's fields. Give a quick read to this article to learn how to create "method" for the struct. Then, create a Print() method that will print all of the struct's fields in the same format as before.

Now, how about that GetPersonInfo() function that we created before? We won't need it anymore. That was just to introduce you to packages. You can move the code back to main().

Before you're done with Phase 4, in main() you will need to read the age of the user, which is an integer. Therefore, the code you were using to read strings will not work. Use the snippet below to read integers:

_, err := fmt.Scanf("%d\n", &age)

if err != nil {
    panic("<INSERT AN ERROR MESSAGE HERE LETTING THE USER KNOW THAT IT WASN'T POSSIBLE TO READ THEIR AGE>")
}

The code above uses fmt.Scanf() to read the age. If you try to use Scanf to read strings, beware that it will not read a string that contains space. So, to make things simple here, use the bufio library to read strings with spaces and the Scanf function to read integers.

Once you've read all the input from the terminal, create a Person and tell it to print it's personal information.

For Discussion

  1. When and why would you use a struct by value instead of by reference? Read this article.

  2. What does the syntax _, err := fmt.Scanf("%d\n", &age) mean?

Definition of Done

The definition of done is the same as Phase 2.

Phase 5: Adding One More Struct

Instead of just asking for the user's personal information like name and email, let's also ask about the user's car. Here's the information that you should get from the user: car's make, model, year and colour.

The approach you will take here should be similar to what you've done before in Phase 4: create a Struct called Car, declare all the fields and create the same methods you created for the Person struct. You do not need to create these structs in the same file. In fact, you should really create separate files and separate packages. Organize your directories/files in the following way:

internal/
├── car
│   └── car.go
└── person
    └── person.go

person.go should belong to the person package and car.go should belong to the car package.

Definition of Done

Here's how you program should work:

Tell us about yourself:
First Name? John
Last Name? Doe
Age? 30
Date of Birth? 13th September 1980
Nationality? Canadian
Email? johndoe@gmail.com

Here is what we know about you:

First name:  John
Last name:  Doe
Age:  30
Date of Birth:  13th September 1980
Nationality:  Canadian
Email:  johndoe@gmail.com

Now tell us about your car:
Make? Cadillac
Model? XTS
Year? 2020
Color? Red

Here is what we know about your car:

Make:  Cadillac
Model:  XTS
Year:  2020
Color:  Red

Now that we are able to collect some information from our user, let's work on saving all this data to Vault.

Phase 6: Interfaces & Vault

Enabling a New Secret Engine

Before we get started, we need to enable a custom kv Secret Engine on Vault.

PS: If you feel you'd benefit from a short tutorial on Vault, here's the official getting started documentation

In the container, run the following command:

# vault secrets list
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_9804bbe6    per-token private secret storage
identity/     identity     identity_0191b96f     identity store
secret/       kv           kv_b05336d2           key/value secret storage
sys/          system       system_50ef7af2       system endpoints used for control, policy and debugging

PS: If you got the following error: Error listing secrets engines: Get https://127.0.0.1:8200/v1/sys/mounts: http: server gave HTTP response to HTTPS client, that means you probably haven't exported VAULT_ADDR and TOKEN. Go back to the bottom of the Getting Started section and export these variables.

If you take a look at the Type column, you will see that the third line is a kv secret engine. This is the default kv secret engine that comes with Vault. What we'll do is to enable a custom one for this challenge and we will not use the default one.

To enable a custom kv secret engine, run the command vault secrets enable -path=dojo/ kv. Then, if you run vault secrets list one more time, you should get this:

# vault secrets list
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_9804bbe6    per-token private secret storage
dojo/         kv           kv_2af64e61           n/a
identity/     identity     identity_0191b96f     identity store
secret/       kv           kv_b05336d2           key/value secret storage
sys/          system       system_50ef7af2       system endpoints used for control, policy and debugging

You should have a kv secret engine at the path dojo/.

Creating a Vault Client

There are 3 main ways throught which we can interact with Vault:

  1. The Vault CLI (which we just tried)
  2. The Vault API (HTTP)
  3. The Golang Vault SDK

Since this is a Golang Dojo, it makes sense we learn how to use the Golang Vault SDK as well. Here's the documentation

First, let's create a client struct that will be responsible for interacting with Vault. Here's what you need to do to get started:

  1. Under the internal folder, create a folder called vault
  2. Under the vault folder, create a client.go file
  3. In client.go, create a struct called VaultClient and a function called NewVaultClient()
  4. The VaultClient struct should have 1 field only - and I'll leave it up to you to decide which one.
  5. The NewVaultClient() function should receive as parameters: vaultAddress and token (both strings) and should return multiple values: *VaultClient and error (learn here how to return multiple values in a single function)

The function NewVaultClient() is not as straightforward as just returning a new VaultClient struct, so let's discuss it a little bit.

NewVaultClient()

To have an idea of what needs to happen in the NewVaultClient() function, have a look at the InitVault() function in this StackOverflow answer.

You will just have to modify the code slightly to return *VaultClient and error. Also, the StackOverflow answer declares a global variable var VClient *api.Client. Do not use a global variable. Think how you can leverage the struct for that.

Method to Save Secret

At this point, your Vault client is able to initialize a connection with Vault, but it doesn't save any data just yet. Let's work on that next.

Create a method for the VaultClient struct called SaveSecret() that returns an error if it wasn't possble to save the secret. This method should receive one parameter only - a Secret.

Ok, hold on. So far, we have created 3 structs: Person, Car and VaultClient. What is this Secret?

Let's think about it together. You want VaultClient to be able to save secrets to Vault. Now, do you think VaultClient should care about the secret type? If you define a function like so:

func (vc *VaultClient) SaveSecret(p Person) error

that means we'll only be able to save a secret of type Person, but not Car. So, the idea is to create a new type called Secret, that will be the base for multiple secret types (like Person and Car).

Enter Interfaces

If you come from the Object-Oriented world, you probably heard of Interfaces:

An interface is a programming structure/syntax that allows the computer to enforce certain properties on an object (class). For example, say we have a car class and a scooter class and a truck class. Each of these three classes should have a start_engine() action. How the "engine is started" for each vehicle is left to each particular class, but the fact that they must have a start_engine action is the domain of the interface.

Even though Golang is not Object-Oriented like Java, for example, the concept still applies, although a bit differently. In essence, what you need to do is to create an Interface named Secret, define function(s) within that interface (which we will talk about soon), then modify both Person and Car structs to "conform" to this interface (while in Java you need the keyword implements to say that a class implements an interface, in Go as long as the struct implements all functions of the interface, Go automatically considers that the struct "conforms" to the interface - you don't need to explicitly use a keyword like in Java).

Take a look at this example to understand how interfaces work.

Interface functions

Your Secret interface should declare 3 functions:

PrintSecret()
GetData() map[string]interface{}
Type() string

PrintSecret() - Remember that our Person and Car structs had a function called Print()? Let's rename that and call it PrintSecret().

GetData() map[string]interface{} - The function GetData should return a map[string]interface{} (this is a map where the keys are strings and the values are of type interface{} - don't worry about that for now). This map should use all the struct's fields as keys. For example, for the struct Person, this is how it would look like:

map[string]interface{}{
    "firstName": <first-name>,
    "lastName": <last-name>,
    (...)
}

How about age? age is an integer and not a string like all other fields. Can you create a map[string]interface{} mixing string and integers? Yes, you can. Find out why here.

Type() string - This function should return either "person" or "car" (note that we're talking about strings here, not structs), depending on the secret type. This will be useful when we save the secret in Vault.

Back to SaveSecret()

Now that you created the Secret interface and implemented the 3 methods above in both Person and Car structs, let's go back to the implementation of the SaveSecret() function.

So, again, declare a function for the VaultClient struct called SaveSecret() that receives a single parameter of type Secret (interface) and returns an error.

I will leave it up to you to figure out how to save a secret in Vault. These are the requirements your SaveSecret() function need to comply with:

  • If you're saving a Person, the path for the secret should be /dojo/person
  • If you're saving a Car, the path for the secret should be /dojo/car
  • After saving the secret, print to the screen the following message: Secret of type [person/car] saved succesfully!

Downloading the Vault SDK

Before you compile, you will have to download the Vault SDK. This is the GitHub URL for the SDK: github.com/hashicorp/vault/api. Now, to download it, it's very simpe. Run the command:

# go get github.com/hashicorp/vault/api

When it's done, you should be able to compile your program.

Definition of Done

This is how the interaction with the program should look like:

Tell us about yourself:
First Name? John
Last Name? Doe
Age? 30
Date of Birth? 13th September 1980
Nationality? Canadian
Email? johndoe@gmail.com

Here is what we know about you:
First name:  John
Last name:  Doe
Age:  30
Date of Birth:  13th September 1980
Nationality:  Canadian
Email:  johndoe@gmail.com

Now tell us about your car:
Make? Cadillac
Model? XTS
Year? 2020
Color? Red

Here is what we know about your car:
Make:  Cadillac
Model:  XTS
Year:  2020
Color:  Red

Secret of type person saved succesfully!
Secret of type car saved succesfully!

Once both secrets are saved successfully in Vault, you should be able to see them:

# vault kv get /dojo/person
======= Data =======
Key            Value
---            -----
age            30
dob            13th September 1980
email          johndoe@gmail.com
firstName      John
lastName       Doe
nationality    Canadian

# vault kv get /dojo/car
==== Data ====
Key      Value
---      -----
color    Red
make     Cadillac
model    XTS
year     2020

If you've made it at this point, congrats!!! But we're not done yet! Let's look into one more Golang concept: Go Routines!

Phase 7: Go Routines!

So far, all of your code has been executing in a sequence. When you call the SaveSecret() function twice to save the Person and Car secrets, the go runtime will execute each call at a time. This is probably a bit overkill for this use case, but what we are going to do now is to execute those 2 function calls concurrently/in parallel. If you not sure about the difference between concurrency and parallelism, read this short answer on StackOverflow.

Whether your code will be executing concurrently or in parallel, that's up to Go and your computer's hardware. The code you will be developing is exactly the same for both cases. So let's get to it.

What are Go Routines?

Whenever you run a Go program, you're creating a process. You can think of Go Routine as an engine inside this process that executes code. When you launch a program and Go executes the main() function, that's a Go Routine. So whenever a Go program executes, there's at least 1 Go Routine running - the main() function.

To create more Go Routine, it is as easy as using the go keyword. Take a look at the examples below.

The code below will execute each function one by one:

getUrlContents("https://www.startpage.com/")
getUrlContents("https://protonmail.com/")
getUrlContents("https://nordvpn.com/")

If the first function gets stuck for some reason, the second and third function will not be executed and will be waiting for the first one to finish.

Now, take a look at the code below:

go getUrlContents("https://www.startpage.com/")
go getUrlContents("https://protonmail.com/")
go getUrlContents("https://nordvpn.com/")

By simply introducing the go keyword, you will be creating 3 Go Routines (one for each function call). If you do not have a multicore computer, these functions will be executing concurrently, which means that if the first one gets stuck (waiting for a response from www.startpage.com), go will start executing the second one.

For this challenge, your goal is to spin up multiple Go routines when saving secrets to Vault. Obviously, you will not notice any difference in execution time becase it's a very simple example, but if you're executing a program that does a lot of heavy processing, Go Routines can make a difference.

Since this is the last part of the Dojo, it only makes sense to be the most challenging one as well :)

Before you start racking your brain... :) Here are some tips:

  • As you will probably notice, simply using the go keyword in front of function will not be enough
  • Use channels to help with the communication between Go Routines (see section below for a list of links that explain what channels are)
  • In main(), after you make 2 calls to SaveSecret(), you should read twice from the channel you created.

Good luck with Phase 7 and don't hesitate to contact one of the organizers should you have any questions!

Links to learn about channels

https://tour.golang.org/concurrency/2

https://medium.com/rungo/anatomy-of-channels-in-go-concurrency-in-go-1ec336086adb

https://www.sohamkamani.com/blog/2017/08/24/golang-channels-explained/

https://tutorialedge.net/golang/go-channels-tutorial/

https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8

Definition of Done

The Definition of Done is the same as Phase 6.