Skip to content
This repository has been archived by the owner on Nov 20, 2021. It is now read-only.
/ fishfinger Public archive

[Go] - FishFinger is a Docker-Compose lightweight programmatic library written in Go.

License

Notifications You must be signed in to change notification settings

TimTosi/fishfinger

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FishFinger

GoDoc Go Report Card

What is FishFinger ?

FishFinger is a Dockerfile and Docker-Compose lightweight programmatic library written in Go. This project provides easy to use abstractions of official Docker libraries with no other dependency than official Go library and official Docker libraries.

FishFinger also provides a one possible solution to deal with container initialization delay and makes room for further improvement through user-defined solutions.

This project is fully tested and comes with an extensive documentation.

Quickstart

First of all, you need to install and configure the latest Docker version on your machine.

Then go get this repository:

go get -d github.com/timtosi/fishfinger

Finally, check how to use the components provided:

Use a Compose File

The fishfinger.Compose component represents an abstraction of the libcompose library. It provides functions allowing the user to use a Compose file programmatically.

An extensive documentation of this component is available here.

Basic Compose Usage

Let's say you have a basic Compose file where you defined three redis services.

For instance, you could want to be able to write a test suite that will directly make use of your containers instead of simply mocking their behaviour through burdensome to write functions.

First of all, let's create a new Compose file handler by using the fishfinger.NewCompose function that takes the path to your Compose file as an argument.

func main() {
	c, err := fishfinger.NewCompose("./docker-compose.yaml")
	if err != nil {
		log.Fatal(err)
	}
}

Time to start services.

func main() {
	c, err := fishfinger.NewCompose("./docker-compose.yaml")
	if err != nil {
		log.Fatal(err)
	}
    if err := c.Start(); err != nil {
		log.Fatal(err)
	}
}

As no argument is provided, all services will be started, but Compose.Start function accepts a variable number of service name in argument and will start them in the order specified.

At this stage, you should have a neat redis cluster running. The Redis client ports you previously set for each of your services are the 6379 but you had to specify different available ports on your host machine. You can hard code the different ports OR you can use the Compose.Port function to find the correct port to use from a service name and the combination of the port exposed and the protocol used such as 6379/tcp. This function returns the full address in the following form <host>:<port>.

func main() {
	c, err := fishfinger.NewCompose("./docker-compose.yaml")
	if err != nil {
		log.Fatal(err)
	}
    if err := c.Start(); err != nil {
		log.Fatal(err)
	}
    addr, err := c.Port("redis-01", "6379/tcp")
	if err != nil {
		log.Fatal(err)
	}
}

The Compose.Port function will return the full address in the following form <host>:<port>. Now you can instanciate your redis client and do whatever stuff you want.

In the end, you maybe want to clean the containers created. Just use the Compose.Stop function just as you would use the Compose.Start function.

func main() {
	c, err := fishfinger.NewCompose("./docker-compose.yaml")
	if err != nil {
		log.Fatal(err)
	}
    if err := c.Start(); err != nil {
		log.Fatal(err)
	}
    addr, err := c.Port("redis-01", "6379/tcp")
	if err != nil {
		log.Fatal(err)
	}
    if err := c.Stop(); err != nil {
		log.Fatal(err)
	}
}

As no argument is provided, all services will be stopped.

Complete working code can be found here.

Backoff Compose Usage

When you want to use Docker in a programmatic way, you can encounter a particular issues regarding the definition of a container's ready state: the container state and the state of the software running inside the container are decoupled.

That means a container will be considered ready regardless of the software state, leading to programmatic errors. Moreover, that is not because the main dockerized software is running that you consider your container ready.

Let's say you have a basic Compose file where you defined a service running a SQL database. In this case, you are using it for testing purposes because you do not want to use a mocking driver. Basically, you could build an image with all your data preloaded but currently you do not want to rebuild your image each time you update your dataset because it's evolving frequently.

As you can see at line #40, in this example, you find a solution by using a container that mounts a volume where you put all .sql scripts you use for populating the database at initialization.

Here is a visualisation of the three ready states this Docker container has :

+--------------------+     +-------------+     +-------------------+
| CONTAINER IS READY | --> | MYSQL IS UP | --> | DATA ARE INSERTED |
+--------------------+     +-------------+     +-------------------+

You can't rely on Docker to know when your container is ready and you can't rely on a time constant because the data insertion step is variable depending on the data you will have to set.

There are several ways to tackle this problem and FishFinger allow you to resolve this easily by using the Compose.StartBackoff function.

First of all, let's create a new Compose file handler by using the fishfinger.NewCompose function that takes the path to your Compose file as an argument.

func main() {
	c, err := fishfinger.NewCompose("./docker-compose.yaml")
	if err != nil {
		log.Fatal(err)
	}
}

Now, time to start the MySQL service with the Compose.StartBackoff function.

func main() {
	c, err := fishfinger.NewCompose("./docker-compose.yaml")
	if err != nil {
		log.Fatal(err)
	}

	if err := c.StartBackoff(fishfinger.SocketBackoff, "datastore:9090/tcp"); err != nil {
		log.Fatal(err)
	}
}

This function takes two arguments: the Backoff function used and a variable number of strings composed of the service name, a port and a protocol used such as redis:6379/tcp.The Backoff function used here is the one provided by default by the Fishfinger project but you are expected to provide another one that suits your needs.

func SocketBackoff(c *Compose, service, port string) error {
	var (
		msg  string
		conn net.Conn
	)

	addr, err := c.Port(service, port)
	if err != nil {
		return err
	}
	for ; msg != "ready\n"; time.Sleep(5 * time.Second) {
		if conn, err = net.Dial("tcp", addr); err == nil {
			fmt.Fprintf(conn, "ready\n")
			msg, _ = bufio.NewReader(conn).ReadString('\n')
			conn.Close()
		}
		fmt.Printf("Retry connection in 5s.\n")
	}
	return nil
}

It's only keep trying to connect to a specific port exposed by the container. The fact is the function will not find any remote listener until all data is correctly loaded, as you can see here. In this way, you are assured everything is ready to be processed by the rest of your program.

Complete working code can be found here.

License

Every file provided here is available under the MIT License.

The logo is a derivative of "BLUE_GOPHER.png" by Ashley McNamara, used under CC BY. This logo is licensed under CC BY by Tim Tosi.

Not Good Enough ?

If you encouter any issue by using what is provided here, please let me know ! Help me to improve by sending your thoughts to timothee.tosi@gmail.com !

About

[Go] - FishFinger is a Docker-Compose lightweight programmatic library written in Go.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages