Skip to content

Commit

Permalink
recreate
Browse files Browse the repository at this point in the history
  • Loading branch information
lobz1g committed Apr 7, 2021
0 parents commit 55f2def
Show file tree
Hide file tree
Showing 7 changed files with 749 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 lobz1g

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
106 changes: 106 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/lobz1g/docen)](https://goreportcard.com/report/github.com/lobz1g/docen)

# Docen

Simple utility for generating Dockerfile for golang projects.

## Installation

```shell
go get github.com/lobz1g/docen
```

## Usage

```go
package main

import (
"log"

"github.com/lobz1g/docen"
)

func main() {
err := docen.New().
SetGoVersion("1.14.9").
SetPort("3000").
SetTimezone("Europe/Moscow").
SetTestMode(true).
SetAdditionalFolder("my-folder/some-files").
SetAdditionalFile("another-folder/some-files/file").
GenerateDockerfile()
if err != nil {
log.Fatal(err)
}
}
```

Dockerfile will be created in the root dir of the project.

```dockerfile
FROM golang:1.14.9-alpine as builder
RUN apk update && apk add --no-cache git ca-certificates tzdata && update-ca-certificates
RUN adduser -D -g '' appuser
RUN mkdir -p /docen
RUN mkdir -p /docen/my-folder/some-files
RUN mkdir -p /docen/another-folder/some-files
COPY . /docen
WORKDIR /docen
RUN CGO_ENABLED=0 go test ./...
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /docen
FROM scratch
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
ENV TZ=Europe/Moscow
COPY --from=builder /docen /docen
COPY --from=builder /docen/my-folder/some-files /docen/my-folder/some-files
COPY --from=builder /docen/another-folder/some-files /docen/another-folder/some-files
COPY --from=builder /docen/another-folder/some-files/file /docen/another-folder/some-files/file
USER appuser
EXPOSE 3000
ENTRYPOINT ["/docen"]
```

## Options

All methods are optional. That means there are default values for success creating Dockerfile without any settings.

Just use below code for generating Dockerfile with default values:

```go
err := docen.New().GenerateDockerfile()
if err != nil {
log.Fatal(err)
}
```

### Image

By default, the image name is `golang` and the tag is `{your_golang_version}-aplpine`. If the `runtime.Version` method
returns wrong information about your golang version it will be just `alpine` image tag. You can set the version by
method `SetGoVersion` without any settings.

### Expose port

By default, Dockerfile will be without the expose port field. You can set the port by method `SetPort`. The argument can
be a single value of port, for example `3000`, or a range of values, for example `3000-4000`.

### Timezone

By default, Dockerfile will be without the timezone env field. You can set the timezone by method `SetTimezone`.

### Testing

The image is built without testing, but you can test the app before build. Use the method `SetTestMode` for it.

### Additional folders to image

Such folders as `assets`, `config`, `static` and `templates` are added to the image. You can add additional folders by
method `SetAdditionalFolder`.

### Additional files to image

You can set additional files which should be added to the image. Use the `SetAdditionalFile` method for it. It also adds
additional folders for these files.
250 changes: 250 additions & 0 deletions docen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package docen

import (
"bufio"
"fmt"
"io"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
)

const (
defaultAppName = "app"
defaultTagVersion = "alpine"
)

var (
goModFile = "go.mod"
vendorFolderName = "vendor"
additionalFolders = map[string]bool{
"static": true,
"assets": true,
"templates": true,
"config": true,
}

// readDir used for unit testing
readDir = ioutil.ReadDir
// runVer used for unit testing
runVer = runtime.Version
// openFile used for unit testing
openFile = os.Open
)

type (
docener interface {
New() *Docen
}

additionalInfo map[string]bool

Docen struct {
timezone string
version string
port string
additionFolders additionalInfo
additionFiles additionalInfo
isTestMode bool
}
)

// New method creates new instance of generator.
// By default, the golang version is taken from runtime.Version
// By default, additional folders are `static`, `templates`, `config` and `assets`.
func New() *Docen {
d := &Docen{
version: getVersion(),
additionFolders: getAdditionalFolders(),
additionFiles: newAdditionalInfo(),
}
return d
}

// SetGoVersion method allows you to set a specific version of golang.
func (d *Docen) SetGoVersion(version string) *Docen {
d.version = fmt.Sprintf("%s-%s", version, defaultTagVersion)
return d
}

// SetPort method allows you to set an exposed port. It can be as single port as a range of ports.
func (d *Docen) SetPort(port string) *Docen {
d.port = port
return d
}

// SetTimezone method allows you to set a specific timezone.
func (d *Docen) SetTimezone(timezone string) *Docen {
d.timezone = timezone
return d
}

// SetAdditionalFolder method allows you to set additional folders which will be added to a container.
func (d *Docen) SetAdditionalFolder(path string) *Docen {
d.additionFolders.set(path)
return d
}

// SetAdditionalFolder method allows you to set additional files which will be added to a container.
func (d *Docen) SetAdditionalFile(path string) *Docen {
d.additionFiles.set(path)
d.SetAdditionalFolder(filepath.Dir(path))
return d
}

// SetTestMode method allows you to enable testing before starting to build the app.
func (d *Docen) SetTestMode(mode bool) *Docen {
d.isTestMode = mode
return d
}

// GenerateDockerfile method creates Dockerfile file.
// If vendor mode is enabled then building will be with `-mod=vendor` tag.
func (d *Docen) GenerateDockerfile() error {
packageName := getPackageName()

var data strings.Builder
data.WriteString(fmt.Sprintf("FROM golang:%s as builder\n", d.version))
data.WriteString("RUN apk update && apk add --no-cache git ca-certificates tzdata && update-ca-certificates\n")
data.WriteString("RUN adduser -D -g '' appuser\n")

data.WriteString(fmt.Sprintf("RUN mkdir -p /%s\n", packageName))
for v := range d.additionFolders {
data.WriteString(fmt.Sprintf("RUN mkdir -p /%s/%s\n", packageName, v))
}
data.WriteString(fmt.Sprintf("COPY . /%s\n", packageName))
data.WriteString(fmt.Sprintf("WORKDIR /%s\n", packageName))
if d.isTestMode {
data.WriteString("RUN CGO_ENABLED=0 go test ./...\n")
}

var vendorTag string
if isVendorMode() {
vendorTag = "-mod=vendor"
}
data.WriteString(
fmt.Sprintf(
"RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build %s -ldflags=\"-w -s\" -o /%s\n",
vendorTag, packageName,
),
)

data.WriteString("FROM scratch\n")
data.WriteString("COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo\n")
data.WriteString("COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n")
data.WriteString("COPY --from=builder /etc/passwd /etc/passwd\n")
if d.timezone != "" {
data.WriteString(fmt.Sprintf("ENV TZ=%s\n", d.timezone))
}
data.WriteString(fmt.Sprintf("COPY --from=builder /%s /%s\n", packageName, packageName))
for v := range d.additionFolders {
data.WriteString(fmt.Sprintf("COPY --from=builder /%s/%s /%s/%s\n", packageName, v, packageName, v))
}
for v := range d.additionFiles {
data.WriteString(fmt.Sprintf("COPY --from=builder /%s/%s /%s/%s\n", packageName, v, packageName, v))
}

data.WriteString("USER appuser\n")
if d.port != "" {
data.WriteString(fmt.Sprintf("EXPOSE %s\n", d.port))
}
data.WriteString(fmt.Sprintf("ENTRYPOINT [\"/%s\"]\n", packageName))

err := createDockerfile(data.String())

return err
}

func getVersion() string {
v := runVer()
re := regexp.MustCompile("[0-9.]+")
version := re.FindAllString(v, -1)
if len(version) == 0 {
return defaultTagVersion
}
return fmt.Sprintf("%s-%s", strings.Join(version, ""), defaultTagVersion)
}

func getPackageName() string {
file, err := openFile(goModFile)
if err != nil {
return defaultAppName
}
defer file.Close()

return parsePackageName(file)
}

func parsePackageName(r io.Reader) string {
reader := bufio.NewReader(r)
data, _, err := reader.ReadLine()
if err != nil {
return defaultAppName
}

module := strings.ReplaceAll(string(data), "module ", "")
if string(module[0]) == "\"" {
module = strings.ReplaceAll(module, "\"", "")
}

name := strings.Split(module, "/")

return strings.ReplaceAll(name[len(name)-1], ".", "_")
}

func getAdditionalFolders() additionalInfo {
folders := newAdditionalInfo()

files, err := getProjectFiles()
if err != nil {
return folders
}

for _, f := range files {
if f.IsDir() && additionalFolders[f.Name()] {
folders.set(f.Name())
}
}

return folders
}

func isVendorMode() bool {
files, err := getProjectFiles()
if err != nil {
return false
}

for _, f := range files {
if f.IsDir() && f.Name() == vendorFolderName {
return true
}
}

return false
}

func getProjectFiles() ([]fs.FileInfo, error) {
files, err := readDir("./")
if err != nil {
return nil, err
}

return files, nil
}

func createDockerfile(data string) error {
return os.WriteFile("Dockerfile", []byte(data), 0644)
}

func newAdditionalInfo() additionalInfo {
return map[string]bool{}
}

func (a additionalInfo) set(e string) {
a[e] = true
}
Loading

0 comments on commit 55f2def

Please sign in to comment.