I am just starting with Go (golang) and want to get a new project folder structure set up for a project that will be built with Gradle and deployed to a Docker image. I'm struggling to determine what this project structure might look like, primarily because of the GOPATH structure and the fact that the Go language tooling seems to be antithetical to using Gradle or to configuring a project that can be cloned (Git).
The project will eventually contain various server-side code written in Go, client side code written in HTML and JavaScript, so I need a project structure that works well for Gradle to build and package all of these kinds of pieces.
Does anyone have a good working structure and tooling recommendations for this?
When I started with Go, I fiddled with a rather wide variety of build tools, from maven to gulp.
It turned out that at least for me, they were doing more harm than good, so I started to use Go's seemingly unimposing, but really well thought out features. One of them isgo generate. Add simple shell scripts or occasionally Makefiles for automation.
Sample project
I have put together a sample project to make this more clear
/Users/you/go/src/bitbucket.org/you/hello/
├── Dockerfile
├── Makefile
├── _templates
│ └── main.html
└── main.go
main.go
This is a simple web server which serves "Hello, World!" using a template which is embedded into the binary using the excellent go.rice tool:
//go:generate rice embed-go
package main
import (
"html/template"
"log"
"net/http"
rice "github.com/GeertJohan/go.rice"
)
func main() {
templateBox, err := rice.FindBox("_templates")
if err != nil {
log.Fatal(err)
}
// get file contents as string
templateString, err := templateBox.String("main.html")
if err != nil {
log.Fatal(err)
}
// parse and execute the template
tmplMessage, err := template.New("message").Parse(templateString)
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if err := tmplMessage.Execute(w, map[string]string{"Greeting": "Hello, world!"}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
Note the line
//go:generate rice embed-go
When you call go generate your source file will be scanned for such lines and the according commands will be executed. In this case, a file called rice-embed.go will be generated and your directory will look like this:
/Users/you/go/src/bitbucket.org/you/hello/
├── Dockerfile
├── Makefile
├── _templates
│ └── main.html
├── main.go
└── rice-box.go
You could call webpack in a //go generate for example, to get your stuff together and another generate to create a rice-box.go from the result. This way, all your stuff would be embedded in your binary and would become a breeze to deploy.
Dockerfile
I have used a rather simple Dockerfile for this example:
FROM alpine:latest
MAINTAINER You <you#example.com>
COPY hello /usr/bin
EXPOSE 8080
CMD ["/usr/bin/hello"]
However this brings us to a problem: We can not use go:generate to produce the docker image, as of the time when we need to call go:generate, the new binary is not build yet. This would make us do ugly things like
go generate && go build && go generate
leading to the docker image build twice and whatnot. So, we need a different solution
Solution A: A shell script
We could of course come up with something like:
#!/bin/bash
# Checks for existence omitted for brevity
GO=$(which go)
DOCKER=$(which docker)
$GO generate
$GO test
$GO build
$DOCKER -t you/hello .
However, this comes with a problem: you will always do the whole sequence using the shell script. Even when you just want to run the tests, you would end up building the docker image. Over time, this adds up. In such situations I tend to use
Solution B: a Makefile
A Makefile is a configuration file for GNU make
CC = $(shell which go 2>/dev/null)
DOCKER = $(shell which docker 2>/dev/null)
ifeq ($(CC),)
$(error "go is not in your system PATH")
else
$(info "go found")
endif
ifeq ($(DOCKER),)
$(error "docker not in your system path")
else
$(info "docker found")
endif
.PHONY: clean generate tests docker all
all: clean generate tests hello docker
clean:
$(RM) hello rice-box.go cover.out
generate:
$(CC) generate
tests: generate
$(CC) test -coverprofile=cover.out
hello: tests
$(CC) build
docker: hello
$(DOCKER) build -t sosample/hello .
A full explanation is beyond the scope of this answer, but what you can basically do here is that you can call make and the all target will be built: files from the old build are removed (clean), a new rice-box.go is generated (generate) and so on. But in case you only want to run the tests for example, calling make test would only execute the targets clean, generate and tests.
You can take a look to my approach to structure your project https://github.com/alehano/gobootstrap
It's a web framework.
Related
[EDIT - added clarity]
Here is my current env setup :
$GOPATH = /home/fzd/go
projectDir = /home/fzd/go/src/github.com/fzd/amazingo
amazingo has a go.mod file that lists several (let's say thousands) dependencies.
So far, I used to go build -t bin/amazingo cmd/main.go, but I want to share this with other people and have a build command that is environment-independent. Using go build has the advantage of downloading each dependency once -- and then using those in ${GOPATH}/pkg/mod, which saves time and bandwidth.
I want to build in a multistage docker image, so I go with
> cat /home/fzd/go/src/github.com/fzd/amazingo/Dockerfile
FROM golang:1.17 as builder
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/amazingo cmd/main.go
FROM alpine:latest
COPY --from=builder /bin/amazingo /amazingo
ENTRYPOINT ["/amazingo"]
As you can expect it, the builder is "naked" when I start it, so it has to download all my dependencies when I docker build -t amazingo:0.0.1 . . But it will do so everytime I call it, which can be several times a day.
Fortunately, I already have most of these dependencies on my disk. I would be happy to share these files (that are located in my $GOPATH/pkg/mod) with the builder, and help it build faster on my machine.
So the question is: how can I share my ${GOPATH} (or ${GOPATH}/mod/pkg) with the builder ?
I tried adding the following to the builder
ARG SRC_GOPATH
COPY ${SRC_GOPATH} /go
and call docker build --build-arg SRC_GOPATH=${GOPATH} -o amazingo:0.0.1 ., but it wasn't good enough - I got an error (COPY failed: file not found in build context or excluded by .dockerignore: stat home/fzd/go: file does not exist)
I hope this update brings a bit more clarity to the problem.
=======
I have a project with a go.mod file.
I want to build that project using a multistage docker image.
(this article is a perfect example)
The issue is that I have "lots" of dependencies, and each of them will be downloaded inside my Docker builder stage.
Is there a way to "share" my GOPATH/pkg/mod with the docker build... command (in some ways, having a local cache) ?
Your end goal isn't completely clear, but the way that I use a multistage build would look something like this for a (dirt-simple) go app, assuming that you ultimately want the docker container to run your go app. You will need to get your source into the build container somehow as well - that is not shown here:
FROM golang:1.17.2-alpine3.14 as builder
WORKDIR /my/app/source/dir
RUN go get && go build -o /path/to/my/app/binary
FROM alpine3.14 AS release
# install runtime deps, if any
# create necessary files and folders, if any
COPY --from=builder /path/to/my/app/binary /usr/local/bin
ENTRYPOINT /usr/local/bin/binary --options
In this way, the source of your application and all dependencies will not be present in the released image, only the compiled binary.
Of course you don't have to specify an output path for that, I think it just makes it a little clearer in this example. And of course you can use whatever base image/images you want to - I'm treating this as though you don't need the go runtime on your release image.
I have this run step in my circle.yaml file with no checkout or working directory set:
- run:
name: Running dataloader tests
command: venv/bin/python3 -m unittest discover -t dataloader tests
The problem with this is that the working directory from the -t flag does not get set. I have moduleNotFound Errors when trying to find an assertions folder inside the dataloader class.
My tree:
├── dataloader
│ ├── Dockerfile
│ ├── Makefile
│ ├── README.md
│ ├── __pycache__
│ ├── assertions
But this works:
version: 2
defaults: &defaults
docker:
- image: circleci/python:3.6
jobs:
dataloader_tests:
working_directory: ~/dsys-2uid/dataloader
steps:
- checkout:
path: ~/dsys-2uid
...
- run:
name: Running dataloader tests
command: venv/bin/python3 -m unittest discover -t ~/app/dataloader tests
Any idea as to what might be going on?
Why doesn't the first one work with just using the -t flag?
What does working directory and checkout with a path actually do? I don't even know why my solution works.
The exact path to the tests folder from the top has to be specified for 'discovery' to work. For example:'python -m unittest discover src/main/python/tests'. That must be why its working in the second case.
Its most likely a bug with 'unittest discovery' where discovery works when you explicitly specify namespace package as a target for discovery.But it does not recurse into any namespace packages inside namespace_pkg. So when you simply run 'python3 -m unittest discover' it doesn't go under all namespace packages (basically folders) in cwd.
Some PRs are underway(for example:issue35617) to fix this, but are yet to be released
checkout = Special step used to check out source code to the configured path (defaults to the working_directory). The reason this is a special step is because it is more of a helper function designed to make checking out code easy for you. If you require doing git over HTTPS you should not use this step as it configures git to checkout over ssh.
working_directory = In which directory to run the steps. Default: ~/project (where project is a literal string, not the name of your specific project). Processes run during the job can use the $CIRCLE_WORKING_DIRECTORY environment variable to refer to this directory. Note: Paths written in your YAML configuration file will not be expanded; if your store_test_results.path is $CIRCLE_WORKING_DIRECTORY/tests, then CircleCI will attempt to store the test subdirectory of the directory literally named $CIRCLE_WORKING_DIRECTORY, dollar sign $ and all.
I would like for this very simple Go package to run in a Docker container using Scratch (or minimal) image.
package main
import (
"fmt"
"os/exec"
)
func main() {
cmd := "host"
args := []string{"-t", "ns", "google.com"}
output, err := exec.Command(cmd, args...).Output()
if err != nil {
fmt.Println(err)
}
fmt.Println(string(output))
}
My original Dockerfile is as follows:
FROM scratch
ADD gohost /
CMD ["/gohost"]
This results in exit code 0: exec: "host": executable file not found in $PATH
I figure this means I need to ADD the /usr/bin/host and set ENV on the added host.. but all the random combinations I've tried of this failed..
I've also tried to simply change cmd := "host" to point to the host binary I added (cmd := "/host"), but seems that isn't a possibility either.
Also, I'm not sure if it's relevant, but it's important the Go binary is built with env GOOS=linux GOARCH=arm64 go build.
If you're depending on calling external binaries, either you also need to make sure they're statically-compiled and included in your Dockerfile, or you can't use a FROM scratch image. A merely very small base image might work better.
In many cases there is a Go-native library to do what you need, and you might see if you can change your code to not need an external process. For instance, instead of calling host as an external process, you might call net.LookupHost instead.
Some common tools are available in busybox (image), including an nslookup but not a host or a dig, but usually these are the sorts of things where an in-process library call will do better. Otherwise you need a lightweight base Linux distribution like alpine where you can add software; your Dockerfile would be
FROM alpine
RUN apk add bind-tools
COPY gohost /bin
CMD ["gohost"]
The Alpine base image advertises itself as being only 5 MB, so your image will be bigger than a FROM scratch image but still not huge.
I have a big tar/executable (over 30GB) I COPY/ADD it but this is used only for the installation. Once the application is installed I don't need it anymore.
How can I do? I am trying to use it but:
Everytime I run a build, it takes minutes to define the build context.
I'd like to share this image, if I create a tar with docker save, Is the final version or each layer included in it?
I found some solutions that said I can use RUN wget tar ... && rm tar but I don't want to create webserver for that.
Why isn't possible to mount a volume during build process?! It would be very useful.
Use Docker's multi-stage builds. This mechanism allows you to drop intermediate artifacts and therefore achieve a lightweight image.
Example:
FROM alpine:latest as build
# copy large file
# build
FROM alpine:latest as output
# copy necessary files built in the previous stage
COPY --from=build app /app
Anything built in the build stage will not be included in the final image, unless you explicitly COPY them.
Docs: https://docs.docker.com/develop/develop-images/multistage-build/
This is solvable using 2 different context.
Please follow these steps as mentioned below.
Objective is to create a
docker image that will have you large-build file.
docker image that will have you real codebase/executables.
For this you have to create 2 folders (Build & CodeBase) as follow.
Application<br/>
|---> BUILD <br/>
|======|--->Large-File<br/>
|======|--->Dockerfile<br/>
|--->CodeBase<br/>
|======|--->SRC+Other stuff<br/>
|======|--->Dockerfile<br/>
Build & Codebase both folders will have individual Dockerfile and arrange files accordingly.
Dockerfile(Build)
FROM **Base-Image**
COPY Large-File /tmp/Large-File
Build this and tag it with some name like (base-build-app-image)
#>cd Application <==Application root folder as mentioned above==>
#>docker build -t base-build-app-image BUILD <==path of your build-folder==>
Dockerfile(Codebase)
FROM base-build-app-image
RUN *****
CMD *****
RUN rm -f **/tmp/Large-File**
RUN rm -f **Remove installation files that is not required**
ENTRYPOINT *****
Build this-code-base and base-build-app-image is already in your local docker-repository and your large iso file is not in the current-buid-context
#>cd Application <==Application root folder as mentioned above==>
#>docker build CodeBase <==path of your code-base==>
This time since the context size is only your code base and since this doesn't include that Large file - it will definitely reduce your build time.
You can also take an advance of using docker-compose to do both operations together so you will not have to execute 2 separate commands.
If you need help on preparing this docker-compose file then do let me know in comments.
If anything is not clear then leave a comment or come over chat to fix this issue.
I've seen many dockerfiles include all build steps in a RUN statement, like:
RUN echo "Hello" &&
cd /tmp &&
mv a.txt b.txt &&
...
and so on...
My question is: what's the benefits/drawbacks on replace these instructions by a single bash script that gives me highlight syntax, loop capabilities, etc?
Something like:
COPY ./script.sh /tmp
RUN bash /tmp/script.sh
and then
#!/bin/bash
echo "hello" ;
cd /tmp ;
mv a.txt b.txt ;
...
Thanks!
The primary difference is that when you COPY the bash script into the image it will be available for inspection in the running container, whereas the RUN command is a little more opaque. Putting your commands in a file like that is arguably more manageable for other reasons: changes in your VCS history will be a little more clear, and for longer or more complex scripts you will probably find it easier to format things cleanly with the script in a separate file rather than embedded in your Dockerfile in a RUN command.
Otherwise the result is the same (in both cases, you are executing the same set of commands), although the COPY and RUN will result in an extra image layer (vs. just the RUN by itself).
I guess running it off as a shell script gives you more control.
For instance, you can do if-else statements to check whether a command has failed or not and provide a code path to handle it. Whereas RUN is more straight forward and when the return code is not 0 it fails the build immediately.
Obviously the case you have there is a relatively simple one and it would not have had a huge difference. The only impact I can see here is the code readability aspect. Someone would have to read the shell script to know what is happening, comparing to having everything on a single file.
I guess it all comes down to using the right tool for the right job. If it is a simple command and you don't need complex logic handling then do RUN.