diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 4cc96326857f59914b8ed4b566a63eacda72638d..cf9a8965716370a92b08994beea5c73ce954034c 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,4 +1,12 @@ -ARG BASE_IMAGE=alpine:3.21.3 +ARG BASE_IMAGE=docker.io/library/alpine:3.21.3 + +FROM docker.io/library/golang:1.24-alpine AS builder + +WORKDIR /app +# Copy the Go source code +COPY src/auto-define-backend /app/ +# Build the Go binary with static linking +RUN go build -o gitlab-tofu-auto-define-backend . FROM $BASE_IMAGE @@ -27,6 +35,7 @@ RUN curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opent WORKDIR / COPY --chmod=755 src/gitlab-tofu.sh /usr/bin/gitlab-tofu +COPY --from=builder --chmod=755 /app/gitlab-tofu-auto-define-backend /usr/bin/ # Override ENTRYPOINT ENTRYPOINT [] diff --git a/Dockerfile.debian b/Dockerfile.debian index 3b6949de623e45c1f4a9b8b35db8eccf8fe4e48a..7f13246391ec9369451ec68552f35e37aa779d78 100644 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -1,4 +1,12 @@ -ARG BASE_IMAGE=debian:12.9-slim +ARG BASE_IMAGE=docker.io/library/debian:12.9-slim + +FROM docker.io/library/golang:1.24-bookworm AS builder + +WORKDIR /app +# Copy the Go source code +COPY src/auto-define-backend /app/ +# Build the Go binary with static linking +RUN go build -o gitlab-tofu-auto-define-backend . FROM $BASE_IMAGE @@ -42,6 +50,7 @@ RUN curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opent WORKDIR / COPY --chmod=755 src/gitlab-tofu.sh /usr/bin/gitlab-tofu +COPY --from=builder --chmod=755 /app/gitlab-tofu-auto-define-backend /usr/bin/ # Override ENTRYPOINT ENTRYPOINT [] diff --git a/src/auto-define-backend/go.mod b/src/auto-define-backend/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..4b42fce8dd80d1f1919cfbacdbca8aab2be2ebf0 --- /dev/null +++ b/src/auto-define-backend/go.mod @@ -0,0 +1,17 @@ +module gitlab.com/components/opentofu/src/discover-http-backend + +go 1.24 + +require github.com/hashicorp/hcl/v2 v2.23.0 + +require ( + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/zclconf/go-cty v1.13.0 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.11.0 // indirect + golang.org/x/tools v0.6.0 // indirect +) diff --git a/src/auto-define-backend/go.sum b/src/auto-define-backend/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..0f49a8988a9b6f8c002159897c652004385587a7 --- /dev/null +++ b/src/auto-define-backend/go.sum @@ -0,0 +1,30 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= +github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= +github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= diff --git a/src/auto-define-backend/main.go b/src/auto-define-backend/main.go new file mode 100644 index 0000000000000000000000000000000000000000..29722106e814306f20b24afb6d4c1e45439d2d50 --- /dev/null +++ b/src/auto-define-backend/main.go @@ -0,0 +1,134 @@ +package main + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +var ErrManuallyConfigured = errors.New("HTTP backend is manually configured") +var ErrDifferentManuallyConfigured = errors.New("Backend of different type is manually configured") + +func isHTTPBackendConfigured(root *os.Root) (bool, error) { + err := fs.WalkDir(root.FS(), ".", func(path string, d fs.DirEntry, err error) error { + if path != "." && d.IsDir() { + return fs.SkipDir + } + + name := filepath.Base(path) + ext := filepath.Ext(name) + if ext == "" || strings.HasPrefix(name, ".") || strings.HasSuffix(name, "~") || (strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#")) { + return nil + } + + if ext == ".tftest.hcl" || ext == ".tftest.json" || ext == ".tofutest.hcl" || ext == ".tofutest.json" { + return nil + } + + // FIXME: should actually prefer .tofu if .tf does exist, too. + if ext != ".tf" && ext != ".tofu" { + return nil + } + + file, err := root.Open(path) + if err != nil { + return err + } + + data, err := io.ReadAll(file) + if err != nil { + return err + } + + hclFile, diags := hclsyntax.ParseConfig(data, path, hcl.InitialPos) + if diags.HasErrors() { + return diags + } + + root, _, diags := hclFile.Body.PartialContent(&hcl.BodySchema{Blocks: []hcl.BlockHeaderSchema{{Type: "terraform"}}}) + if diags.HasErrors() { + return diags + } + + for _, block := range root.Blocks { + content, _, diags := block.Body.PartialContent(&hcl.BodySchema{Blocks: []hcl.BlockHeaderSchema{{Type: "backend", LabelNames: []string{"type"}}}}) + if diags.HasErrors() { + return diags + } + for _, cb := range content.Blocks { + if cb.Type == "backend" && len(cb.Labels) == 1 { + if cb.Labels[0] == "http" { + return ErrManuallyConfigured + } else { + return ErrDifferentManuallyConfigured + } + } + } + } + return nil + }) + + if err != nil { + switch err { + case ErrManuallyConfigured: + return true, nil + case ErrDifferentManuallyConfigured: + return true, err + default: + return false, err + } + } + + return false, nil +} + +func main() { + if len(os.Args) < 2 { + fmt.Printf("gitlab-tofu: no root directory for backend discovery provided as first argument\n") + os.Exit(1) + } + root, err := os.OpenRoot(os.Args[1]) + if err != nil { + fmt.Printf("gitlab-tofu: unable to open root directory for backend discovery: %s\n", err) + os.Exit(1) + } + + configured, err := isHTTPBackendConfigured(root) + if err != nil { + fmt.Printf("gitlab-tofu: error during backend discovery: %s\n", err) + os.Exit(1) + } + + if configured { + fmt.Println("gitlab-tofu: HTTP backend manually configured, doing nothing") + os.Exit(0) + } + + file, err := root.Create("__gitlab-opentofu-backend.tf") + if err != nil { + fmt.Printf("gitlab-tofu: failed to create __gitlab-opentofu-backend.tf to automatically define HTTP backend: %s\n", err) + os.Exit(1) + } + + _, err = file.WriteString(`terraform { + backend "http" {} +}`) + if err != nil { + fmt.Printf("gitlab-tofu: failed to write __gitlab-opentofu-backend.tf to automatically define HTTP backend: %s\n", err) + os.Exit(1) + } + err = file.Close() + if err != nil { + fmt.Printf("gitlab-tofu: failed to close __gitlab-opentofu-backend.tf to automatically define HTTP backend: %s\n", err) + os.Exit(1) + } + + fmt.Printf("gitlab-tofu: automatically defining the HTTP backend in __gitlab-opentofu-backend.tf") +} diff --git a/src/gitlab-tofu.sh b/src/gitlab-tofu.sh index 8a339e09122a14d3a0a37391ee1e6fb80b1ba855..3fb85a90c15cbc58fa9427df207f4b3b8a4c239f 100644 --- a/src/gitlab-tofu.sh +++ b/src/gitlab-tofu.sh @@ -238,17 +238,7 @@ define_http_backend() { return fi - if ! grep -q '^[[:space:]]*backend[[:space:]]\+"http"[[:space:]]\+{.*$' "${abs_tf_root}" -r 2>/dev/null; then - echo "gitlab-tofu: automatically defining the HTTP backend in __gitlab-opentofu-backend.tf. If that is a mistake, please disable it with the auto_define_backend: false input." - - cat <<EOF > __gitlab-opentofu-backend.tf -terraform { - backend "http" {} -} -EOF - else - echo "gitlab-tofu: auto_define_backend is enabled, but found manually configured HTTP backend, doing nothing." - fi + gitlab-tofu-auto-define-backend "${abs_tf_root}" } # configure_variables_for_tofu sets and exports all relevant variables for subsequent `tofu` command invocations.