From 4eccb1b2d919679975f4967d4b17f011a0037d20 Mon Sep 17 00:00:00 2001 From: Kaan Yagci <9104546+kaanyagci@users.noreply.github.com> Date: Wed, 5 Jun 2024 22:42:01 +0200 Subject: [PATCH] refactor: support for go library use case (#7) * refactor: support for go library use case Signed-off-by: Kaan Yagci * test: add expected rendered Dockerfile Signed-off-by: Kaan Yagci * test(core): add render tests Signed-off-by: Kaan Yagci * chore: add test makefile target Signed-off-by: Kaan Yagci * chore(gha): add a workflow to build and test Signed-off-by: Kaan Yagci --------- Signed-off-by: Kaan Yagci --- .github/workflows/build_and_test.yaml | 72 ++++++++++++++ Makefile | 4 + cli/wharf.go | 4 +- core/render.go | 50 +++------- core/render_test.go | 130 +++++++++++++++++++++++++ core/version.go | 2 +- docker-plugins/render/docker_render.go | 45 ++++++++- example/Dockerfile.expected | 18 ++++ 8 files changed, 280 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/build_and_test.yaml create mode 100644 core/render_test.go create mode 100644 example/Dockerfile.expected diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml new file mode 100644 index 0000000..6e05bb1 --- /dev/null +++ b/.github/workflows/build_and_test.yaml @@ -0,0 +1,72 @@ +name: Ensure version increment +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + paths: + - cli/*$ + - core/** + - docker-plugins/** + - example/** +concurrency: + group: "${{ github.workflow_ref }} - ${{ github.ref }} - ${{ github.event_name }}" + cancel-in-progress: true +jobs: + build_cli: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 1 + - name: Setup Go 1.22.x + uses: actions/setup-go@v5 + with: + go-version: "1.22.x" + - name: Build the CLI + run: make build + build_plugins: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 1 + - name: Setup Go 1.22.x + uses: actions/setup-go@v5 + with: + go-version: "1.22.x" + - name: Build Docker plugins + run: make build-plugins + test_docker_plugins: + runs-on: ubuntu-latest + needs: build_plugins + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 1 + - name: Setup Go 1.22.x + uses: actions/setup-go@v5 + with: + go-version: "1.22.x" + - name: Create plugins directory + run: mkdir -p ~/.docker/cli-plugins/ + - name: Build Docker plugins + run: make install-plugins + test: + runs-on: ubuntu-latest + needs: [build_cli, build_plugins] + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 1 + - name: Setup Go 1.22.x + uses: actions/setup-go@v5 + with: + go-version: '1.22.x' + - name: Test + run: make test + + \ No newline at end of file diff --git a/Makefile b/Makefile index 613ee5d..5786a04 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,10 @@ install-plugins: clean build-plugins chmod +x ${PLUGINS_OUTPUT_FOLDER}/docker-* cp ${PLUGINS_OUTPUT_FOLDER}/* ${DOCKER_PLUGINS_PATH}/ +.PHONY: test +test: + go test -v ./core + .PHONY: clean clean: clean-output-folder diff --git a/cli/wharf.go b/cli/wharf.go index 6c8ecaf..0419f0f 100644 --- a/cli/wharf.go +++ b/cli/wharf.go @@ -3,9 +3,9 @@ package main import ( "fmt" - "github.com/Makepad-fr/wharf/core" + wharf "github.com/Makepad-fr/wharf/core" ) func main() { - fmt.Println(core.Version) + fmt.Println(wharf.Version) } diff --git a/core/render.go b/core/render.go index 8a2fd48..aa972c5 100644 --- a/core/render.go +++ b/core/render.go @@ -1,63 +1,35 @@ -package core +package wharf import ( - "errors" - "flag" "fmt" + "io" "os" "path/filepath" - "strings" "text/template" "gopkg.in/yaml.v3" ) -var defaultTemplateFileName = "Dockerfile.template" - -func init() { - v, ok := os.LookupEnv("DOCKER_RENDER_DEFAULT_TEMPLATE_FILE_NAME") - if ok { - defaultTemplateFileName = v - } -} - -// const defaultContextPath = - -func Run() error { - os.Args = os.Args[1:] - valuesFilePath := flag.String("values", "./docker-values.yaml", "The path for the values file to use") - outputFilePath := flag.String("output", "", "The path for the output file") - templateFileName := flag.String("file-name", defaultTemplateFileName, "The name of the template file") - flag.Parse() - if flag.NArg() != 1 { - return errors.New("the path for the template file is required\n") - } - contextPath := flag.Arg(0) - template, err := getTemplate(contextPath, *templateFileName) +// Render renders the template file in the given contextPath using the values files from the given path +// It writes the rendered Dockerfile to the io.Writer passed in parameters. It returns an error if something goes wrong +func Render(contextPath, templateFileName, valuesFilePath string, output io.Writer) error { + template, err := getTemplate(contextPath, templateFileName) if err != nil { return err } - values, err := readValues(contextPath, *valuesFilePath) + values, err := readValues(contextPath, valuesFilePath) if err != nil { return err } - err = render(*outputFilePath, template, values) + + err = render(template, values, output) return nil } // render renders the template and values to the file with the given path. // If something goes wrong it returns an error -func render(path string, tmpl *template.Template, values map[string]any) error { - var file *os.File = os.Stdout - var err error - if len(strings.TrimSpace(path)) > 0 { - file, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) - if err != nil { - return err - } - defer file.Close() - } - err = tmpl.Execute(file, values) +func render(tmpl *template.Template, values map[string]any, file io.Writer) error { + err := tmpl.Execute(file, values) if err != nil { return err } diff --git a/core/render_test.go b/core/render_test.go new file mode 100644 index 0000000..ed5c109 --- /dev/null +++ b/core/render_test.go @@ -0,0 +1,130 @@ +package wharf + +import ( + "io" + "os" + "strings" + "testing" +) + +const expectedRenderedDockerfile = ` +# Use an official base image +FROM ubuntu:latest + +# Set the exposed port +EXPOSE 9091 + +# Set environment variables + +ENV DEBUG="false" + +ENV LOG_LEVEL="info" + + +# Copy source code +COPY . /app + +# Run command +CMD echo Hello World +` + +func TestRenderToString(t *testing.T) { + var stringBuilder strings.Builder + err := Render("../example/", "Dockerfile.template", "docker-values.yaml", &stringBuilder) + if err != nil { + t.Error(err) + } + + generated := strings.TrimSpace(stringBuilder.String()) + expected := strings.TrimSpace(expectedRenderedDockerfile) + + if generated != expected { + t.Log("Strings are not equal") + t.Logf("Generated length: %d", len(generated)) + t.Logf("Expected length: %d", len(expected)) + + for i := 0; i < len(generated) && i < len(expected); i++ { + if generated[i] != expected[i] { + t.Errorf("Difference at char %d: '%c' != '%c'", i, generated[i], expected[i]) + break + } + } + + if len(generated) != len(expected) { + t.Error("Generated and expected strings have different lengths") + } + } +} + +func TestRenderToOutputFile(t *testing.T) { + // Render the template to a file + // Compare result with the expected one + file, err := os.CreateTemp(os.TempDir(), "Dockerfile") + if err != nil { + t.Error(err) + } + defer func() { + file.Close() + os.Remove(file.Name()) + }() + err = Render("../example", "Dockerfile.template", "docker-values.yaml", file) + if err != nil { + t.Error(err) + } + equals, err := compareFiles(file, "../example/Dockerfile.expected") + if err != nil { + t.Error(err) + } + if !equals { + t.Error("Files are not equals") + } +} + +// CompareFiles checks if the contents of two files are the same. +func compareFiles(file1 *os.File, file2 string) (bool, error) { + file1.Seek(0, io.SeekStart) + f2, err := os.Open(file2) + if err != nil { + return false, err + } + defer f2.Close() + + const bufferSize = 4096 + buf1 := make([]byte, bufferSize) + buf2 := make([]byte, bufferSize) + + for { + n1, err1 := file1.Read(buf1) + n2, err2 := f2.Read(buf2) + + if n1 != n2 || err1 != nil || err2 != nil { + if err1 == io.EOF && err2 == io.EOF { + return true, nil // End of both files reached + } + return false, nil + } + + if n1 == 0 && n2 == 0 { + break // End of both files reached + } + + if !compareChunks(buf1[:n1], buf2[:n2]) { + return false, nil + } + } + + return true, nil +} + +// compareChunks checks if two byte slices are equal. +func compareChunks(chunk1, chunk2 []byte) bool { + if len(chunk1) != len(chunk2) { + return false + } + for i := range chunk1 { + if chunk1[i] != chunk2[i] { + return false + } + } + return true +} diff --git a/core/version.go b/core/version.go index 9b10bb7..cca61ea 100644 --- a/core/version.go +++ b/core/version.go @@ -1,3 +1,3 @@ -package core +package wharf const Version = "0.0.1" diff --git a/docker-plugins/render/docker_render.go b/docker-plugins/render/docker_render.go index 208b333..b1243e4 100644 --- a/docker-plugins/render/docker_render.go +++ b/docker-plugins/render/docker_render.go @@ -1,28 +1,67 @@ package main import ( + "errors" "flag" "fmt" "os" + "strings" - "github.com/Makepad-fr/wharf/core" + wharf "github.com/Makepad-fr/wharf/core" "github.com/Makepad-fr/wharf/docker-plugins/commons" ) var metadata = commons.PluginMetadata{ SchemaVersion: "0.1.0", Vendor: "MAKEPAD", - Version: core.Version, + Version: wharf.Version, ShortDescription: "Render Dockerfiles from templates", URL: "https://github.com/Makepad-fr/wharf", Experimental: true, } +var defaultTemplateFileName = "Dockerfile.template" + +func init() { + v, ok := os.LookupEnv("DOCKER_RENDER_DEFAULT_TEMPLATE_FILE_NAME") + if ok { + defaultTemplateFileName = v + } +} + +func run() error { + os.Args = os.Args[1:] + valuesFilePath := flag.String("values", "./docker-values.yaml", "The path for the values file to use") + outputFilePath := flag.String("output", "", "The path for the output file") + templateFileName := flag.String("file-name", defaultTemplateFileName, "The name of the template file") + flag.Parse() + if flag.NArg() != 1 { + return errors.New("the path for the template file is required\n") + } + contextPath := flag.Arg(0) + // If output filepath is an + + var file *os.File = os.Stdout + + var err error + + if outputFilePath != nil && (len(strings.TrimSpace(*outputFilePath)) > 0) { + file, err = os.OpenFile(*outputFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + } + + err = wharf.Render(contextPath, *templateFileName, *valuesFilePath, file) + return err +} + func main() { if commons.ShowPluginMetaData(metadata) { return } - err := core.Run() + err := run() if err != nil { fmt.Println(err) flag.Usage() diff --git a/example/Dockerfile.expected b/example/Dockerfile.expected new file mode 100644 index 0000000..701a78a --- /dev/null +++ b/example/Dockerfile.expected @@ -0,0 +1,18 @@ +# Use an official base image +FROM ubuntu:latest + +# Set the exposed port +EXPOSE 9091 + +# Set environment variables + +ENV DEBUG="false" + +ENV LOG_LEVEL="info" + + +# Copy source code +COPY . /app + +# Run command +CMD echo Hello World