diff --git a/.github/workflows/crd2go.yml b/.github/workflows/crd2go.yml new file mode 100644 index 0000000000..a79c323ec7 --- /dev/null +++ b/.github/workflows/crd2go.yml @@ -0,0 +1,74 @@ +# CRD2Go CI +name: CRD2Go CI + +on: + push: + branches: [ "main" ] + paths: + - 'tools/crd2go/**' + pull_request: + branches: [ "main" ] + paths: + - 'tools/crd2go/**' + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: ${{github.event.pull_request.head.sha}} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Build + working-directory: ./tools/crd2go + run: go build -v ./... + + - name: Test + working-directory: ./tools/crd2go + run: go test -race -cover -v ./... + + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.1 + working-directory: ./tools/crd2go + + gci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: ${{github.event.pull_request.head.sha}} + + - name: Install devbox + uses: jetify-com/devbox-install-action@v0.13.0 + with: + project-path: ./tools/crd2go + enable-cache: 'true' + + - name: gci + working-directory: ./tools/crd2go + run: devbox run -- 'mage gci' + + addlicense: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: ${{github.event.pull_request.head.sha}} + + - name: Install devbox + uses: jetify-com/devbox-install-action@v0.13.0 + with: + project-path: ./tools/crd2go + enable-cache: 'true' + + - name: Check license headers + working-directory: ./tools/crd2go + run: devbox run -- 'mage addlicense' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a01404ce46..f6ab0fd9d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,12 +8,14 @@ on: - 'main' paths-ignore: - 'docs/**' + - 'tools/**' pull_request_target: types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, labeled] branches: - '**' paths-ignore: - 'docs/**' + - 'tools/**' merge_group: workflow_dispatch: inputs: @@ -25,6 +27,9 @@ on: options: - "true" - "false" + paths-ignore: + - 'docs/**' + - 'tools/**' jobs: run-tests: diff --git a/tools/crd2go/cmd/crd2go/main.go b/tools/crd2go/cmd/crd2go/main.go index 139aa164f8..25a7819f16 100644 --- a/tools/crd2go/cmd/crd2go/main.go +++ b/tools/crd2go/cmd/crd2go/main.go @@ -22,6 +22,7 @@ import ( "os" "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/checkerr" + "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/fileinput" "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/pkg/config" "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/pkg/crd2go" ) @@ -41,7 +42,7 @@ func main() { } func generate(input, output, config string) (*config.Config, error) { - f, err := os.Open(config) + f, err := os.Open(fileinput.MustBeSafe(config)) if err != nil { return nil, fmt.Errorf("failed to open configuration file: %w", err) } diff --git a/tools/crd2go/cmd/updateSamples/main.go b/tools/crd2go/cmd/updateSamples/main.go index 7a2c1d6a11..6662714b3e 100644 --- a/tools/crd2go/cmd/updateSamples/main.go +++ b/tools/crd2go/cmd/updateSamples/main.go @@ -23,6 +23,7 @@ import ( "os" "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/checkerr" + "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/fileinput" "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/run" ) @@ -62,6 +63,8 @@ func updateSamples() error { } func downloadTo(url, filename string) (int64, error) { + // #nosec G107 URL is safe as we are just adding a token, it cannot be re-pathed + //nolint:noctx rsp, err := http.Get(url) if err != nil { return 0, fmt.Errorf("failed to download from %s: %w", url, err) @@ -69,7 +72,7 @@ func downloadTo(url, filename string) (int64, error) { if rsp.StatusCode != http.StatusOK { return 0, fmt.Errorf("failed to request %s with status: %q", url, rsp.Status) } - f, err := os.Create(filename) + f, err := os.Create(fileinput.MustBeSafe(filename)) if err != nil { return 0, fmt.Errorf("failed to create file %s: %w", filename, err) } diff --git a/tools/crd2go/devbox.json b/tools/crd2go/devbox.json index 93f8fd02a9..895757adae 100644 --- a/tools/crd2go/devbox.json +++ b/tools/crd2go/devbox.json @@ -1,13 +1,15 @@ { "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.14.0/.schema/devbox.schema.json", "packages": [ - "neovim@latest", + "git@latest", "go@latest", "vscodium@latest", "kubernetes-controller-tools@latest", "gh@latest", "gci@latest", - "golangci-lint@latest" + "golangci-lint@latest", + "addlicense@latest", + "mage@latest", ], "shell": { "init_hook": [ diff --git a/tools/crd2go/devbox.lock b/tools/crd2go/devbox.lock index 3879262f42..0d04f77084 100644 --- a/tools/crd2go/devbox.lock +++ b/tools/crd2go/devbox.lock @@ -1,6 +1,54 @@ { "lockfile_version": "1", "packages": { + "addlicense@latest": { + "last_modified": "2025-07-28T17:09:23Z", + "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#addlicense", + "source": "devbox-search", + "version": "1.1.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/r3lhc2n8xj5ns43zrfdx1d4rsk6l0zgy-addlicense-1.1.1", + "default": true + } + ], + "store_path": "/nix/store/r3lhc2n8xj5ns43zrfdx1d4rsk6l0zgy-addlicense-1.1.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/smjx8fp2r6ikxr6fv6vd9nws8j546hs3-addlicense-1.1.1", + "default": true + } + ], + "store_path": "/nix/store/smjx8fp2r6ikxr6fv6vd9nws8j546hs3-addlicense-1.1.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/gpfw7kfm0rm4gcpnz2cgkb3d4zxyn32s-addlicense-1.1.1", + "default": true + } + ], + "store_path": "/nix/store/gpfw7kfm0rm4gcpnz2cgkb3d4zxyn32s-addlicense-1.1.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/mxb6kb1b7lfklzmz020m0rdf7ppn0608-addlicense-1.1.1", + "default": true + } + ], + "store_path": "/nix/store/mxb6kb1b7lfklzmz020m0rdf7ppn0608-addlicense-1.1.1" + } + } + }, "gci@latest": { "last_modified": "2025-08-05T11:35:34Z", "resolved": "github:NixOS/nixpkgs/a683adc19ff5228af548c6539dbc3440509bfed3#gci", @@ -97,6 +145,78 @@ } } }, + "git@latest": { + "last_modified": "2025-07-28T17:09:23Z", + "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#git", + "source": "devbox-search", + "version": "2.50.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jn9byxgdjndngf0d2by0djg8gcdll7xc-git-2.50.1", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/j8djmq64ckbah7bl6jv1y6arrjr0shmv-git-2.50.1-doc" + } + ], + "store_path": "/nix/store/jn9byxgdjndngf0d2by0djg8gcdll7xc-git-2.50.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/h4pvvix6pvnvys9a6y1xj2442r1ajdhl-git-2.50.1", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/q8sicpx16gyzxnp3345a46lj4cz9wd09-git-2.50.1-doc" + }, + { + "name": "debug", + "path": "/nix/store/rpxnrnsn4nbx8wm9d2vrgj0fr5xzz5lg-git-2.50.1-debug" + } + ], + "store_path": "/nix/store/h4pvvix6pvnvys9a6y1xj2442r1ajdhl-git-2.50.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/8d1n8cvi5x1j0v61459lvhqs26vmcqbl-git-2.50.1", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/yn9cvbs7jz4dfdb17qralgr0ybi5rmjf-git-2.50.1-doc" + } + ], + "store_path": "/nix/store/8d1n8cvi5x1j0v61459lvhqs26vmcqbl-git-2.50.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/5i8zvall945kypmwgqd0y47f02pldwp4-git-2.50.1", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/l46kpjpcwwp8l7kzzr1s2dlk646r73z2-git-2.50.1-debug" + }, + { + "name": "doc", + "path": "/nix/store/d2lhlzkdziwmijik8nszfwp8srbkskb9-git-2.50.1-doc" + } + ], + "store_path": "/nix/store/5i8zvall945kypmwgqd0y47f02pldwp4-git-2.50.1" + } + } + }, "github:NixOS/nixpkgs/nixpkgs-unstable": { "last_modified": "2025-05-04T22:22:57Z", "resolved": "github:NixOS/nixpkgs/ed30f8aba41605e3ab46421e3dcb4510ec560ff8?lastModified=1746397377&narHash=sha256-5oLdRa3vWSRbuqPIFFmQBGGUqaYZBxX%2BGGtN9f%2Fn4lU%3D" @@ -245,51 +365,51 @@ } } }, - "neovim@latest": { - "last_modified": "2025-03-29T14:41:00Z", - "resolved": "github:NixOS/nixpkgs/eb0e0f21f15c559d2ac7633dc81d079d1caf5f5f#neovim", + "mage@latest": { + "last_modified": "2025-07-28T17:09:23Z", + "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#mage", "source": "devbox-search", - "version": "0.11.0", + "version": "1.15.0", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/4ch16nz6lwmzlxhwr2lwc3wqy7khxbks-neovim-0.11.0", + "path": "/nix/store/ckcxxf6r85zndgx4rh3x3g570sysczf2-mage-1.15.0", "default": true } ], - "store_path": "/nix/store/4ch16nz6lwmzlxhwr2lwc3wqy7khxbks-neovim-0.11.0" + "store_path": "/nix/store/ckcxxf6r85zndgx4rh3x3g570sysczf2-mage-1.15.0" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/8av5ivxr1x3cymnbdfvf1izbx7998iiq-neovim-0.11.0", + "path": "/nix/store/1xln1pdk907lxm25dpdvx1xmp1rh43pd-mage-1.15.0", "default": true } ], - "store_path": "/nix/store/8av5ivxr1x3cymnbdfvf1izbx7998iiq-neovim-0.11.0" + "store_path": "/nix/store/1xln1pdk907lxm25dpdvx1xmp1rh43pd-mage-1.15.0" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/5bd60mhl94s6gl1cqi2xjra0ad8v2cdp-neovim-0.11.0", + "path": "/nix/store/gwwj202lbp3smjdmnff18xr4fm439sn4-mage-1.15.0", "default": true } ], - "store_path": "/nix/store/5bd60mhl94s6gl1cqi2xjra0ad8v2cdp-neovim-0.11.0" + "store_path": "/nix/store/gwwj202lbp3smjdmnff18xr4fm439sn4-mage-1.15.0" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/97cka40y26y30mza1v4gfbvlbp5a2a42-neovim-0.11.0", + "path": "/nix/store/8jyy6nrmi505iczgi7d19gddqk00qqc9-mage-1.15.0", "default": true } ], - "store_path": "/nix/store/97cka40y26y30mza1v4gfbvlbp5a2a42-neovim-0.11.0" + "store_path": "/nix/store/8jyy6nrmi505iczgi7d19gddqk00qqc9-mage-1.15.0" } } }, diff --git a/tools/crd2go/go.mod b/tools/crd2go/go.mod index ce986a3646..fd92e141e6 100644 --- a/tools/crd2go/go.mod +++ b/tools/crd2go/go.mod @@ -4,6 +4,7 @@ go 1.24.3 require ( github.com/dave/jennifer v1.7.1 + github.com/magefile/mage v1.15.0 github.com/stretchr/testify v1.10.0 golang.org/x/text v0.24.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/tools/crd2go/go.sum b/tools/crd2go/go.sum index e11b1d1632..17085a38a6 100644 --- a/tools/crd2go/go.sum +++ b/tools/crd2go/go.sum @@ -29,6 +29,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/tools/crd2go/internal/checkerr/checkerr.go b/tools/crd2go/internal/checkerr/checkerr.go index 7b4585b859..f154f86757 100644 --- a/tools/crd2go/internal/checkerr/checkerr.go +++ b/tools/crd2go/internal/checkerr/checkerr.go @@ -23,4 +23,4 @@ func CheckErr(msg string, f funcErrs) { if err := f(); err != nil { log.Printf("%s failed: %v", msg, err) } -} \ No newline at end of file +} diff --git a/tools/crd2go/internal/fileinput/fileinput.go b/tools/crd2go/internal/fileinput/fileinput.go new file mode 100644 index 0000000000..126e4b7c95 --- /dev/null +++ b/tools/crd2go/internal/fileinput/fileinput.go @@ -0,0 +1,55 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package fileinput + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +func MustBeSafe(filename string) string { + cleanPath, err := Safe(filename) + if err != nil { + panic(err) + } + return cleanPath +} + +func Safe(filename string) (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get current working directory to sanitize path %q: %w", filename, err) + } + return SafeAt(cwd, filename) +} + +func MustBeSafeAt(allowedBase, filename string) string { + cleanPath, err := SafeAt(allowedBase, filename) + if err != nil { + panic(err) + } + return cleanPath +} + +func SafeAt(allowedBase, filename string) (string, error) { + cleanPath := filepath.Clean(filename) + if !strings.HasPrefix(cleanPath, allowedBase) { + return "", fmt.Errorf("Unsafe input path %q not in %q (clean path %q)", filename, allowedBase, cleanPath) + } + return cleanPath, nil +} diff --git a/tools/crd2go/internal/gotype/gotype.go b/tools/crd2go/internal/gotype/gotype.go index bc36fe6f55..0d300a7ec0 100644 --- a/tools/crd2go/internal/gotype/gotype.go +++ b/tools/crd2go/internal/gotype/gotype.go @@ -96,7 +96,7 @@ func NewAutoImportType(importType *config.ImportedTypeConfig) *GoType { } } -// AddImportInfo allows to attach teh import information to a type +// AddImportInfo allows to attach the import information to a type func AddImportInfo(gt *GoType, alias, packagePath string) *GoType { effectiveAlias := alias if effectiveAlias == "" { diff --git a/tools/crd2go/internal/render/jen.go b/tools/crd2go/internal/render/jen.go index 866941c32b..1342eeab4d 100644 --- a/tools/crd2go/internal/render/jen.go +++ b/tools/crd2go/internal/render/jen.go @@ -119,7 +119,7 @@ func renderDocFile(req *gotype.Request, group, version string) error { overwrite := false wc, err := req.CodeWriterFn("doc.go", overwrite) if err != nil { - return fmt.Errorf("failed to prepare doc.go for writting: %w", err) + return fmt.Errorf("failed to prepare doc.go for writing: %w", err) } if err := f.Render(wc); err != nil { return fmt.Errorf("failed to write Go code to doc.go: %w", err) @@ -153,7 +153,7 @@ func renderSchemeFile(req *gotype.Request, group, version string) error { overwrite := true wc, err := req.CodeWriterFn("scheme.go", overwrite) if err != nil { - return fmt.Errorf("failed to prepare scheme.go for writting: %w", err) + return fmt.Errorf("failed to prepare scheme.go for writing: %w", err) } if err := f.Render(wc); err != nil { return fmt.Errorf("failed to write Go code to scheme.go: %w", err) @@ -241,10 +241,7 @@ func generateField(f *jen.File, field *gotype.GoField) (jen.Code, error) { if field.GoType == nil { return nil, fmt.Errorf("field %q has no Go type", field.Name) } - typeRefCode, err := qualifyRequired(generateTypeRef(f, field.GoType), field.Required) - if err != nil { - return nil, fmt.Errorf("failed to generate field type: %w", err) - } + typeRefCode := qualifyRequired(generateTypeRef(f, field.GoType), field.Required) return fieldCode.Add(typeRefCode).Add(generateJSONTag(field)).Line(), nil } @@ -259,11 +256,11 @@ func generateFieldComment(code *jen.Statement, name, comment string) *jen.Statem } // qualifyRequired qualifies the type reference based on whether the field is required -func qualifyRequired(typeRef *jen.Statement, required bool) (*jen.Statement, error) { +func qualifyRequired(typeRef *jen.Statement, required bool) *jen.Statement { if required { - return typeRef, nil + return typeRef } - return jen.Op("*").Add(typeRef), nil + return jen.Op("*").Add(typeRef) } // generateTypeRef generates a type reference for a given GoType diff --git a/tools/crd2go/internal/run/run.go b/tools/crd2go/internal/run/run.go index 93d40d19d6..fde8524224 100644 --- a/tools/crd2go/internal/run/run.go +++ b/tools/crd2go/internal/run/run.go @@ -16,16 +16,30 @@ package run import ( + "context" "log" "os" "os/exec" + "os/signal" "strings" ) func Run(command string, args ...string) error { log.Printf("Running:\n %s %s", command, strings.Join(args, " ")) - cmd := exec.Command(command, args...) + ctx := cancellableContext() + cmd := exec.CommandContext(ctx, command, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } + +func cancellableContext() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt) + go func() { + <-sigChan + cancel() + }() + return ctx +} diff --git a/tools/crd2go/magefile.go b/tools/crd2go/magefile.go new file mode 100644 index 0000000000..d3fc157bcd --- /dev/null +++ b/tools/crd2go/magefile.go @@ -0,0 +1,75 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +//go:build mage + +package main + +import ( + "fmt" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +// CI runs all linting and validation checks. +func CI() { + mg.SerialDeps(Addlicense, GCI) +} + +// Addlicense runs the addlicense check to ensure source files have license headers. +func Addlicense() error { + fmt.Println("Running license header check...") + + // sh.RunV runs the command verbosely (streaming output) + // and returns an error if the command fails. + return sh.RunV("addlicense", + "-check", + "-l", "apache", + "-c", "MongoDB Inc", + "-ignore", "**/*.md", + "-ignore", "**/*.yaml", + "-ignore", "**/*.yml", + "-ignore", "**/*.nix", + "-ignore", ".devbox/**", + "-ignore", "magefile.go", + ".", + ) +} + +// GCI runs gci to check that Go import orders are correct. +func GCI() error { + fmt.Println("๐Ÿงน Formatting Go imports...") + if err := sh.RunV( + "gci", "write", + "--skip-generated", + "-s", "standard", + "-s", "default", + "-s", "localmodule", + ".", + ); err != nil { + return fmt.Errorf("gci write command failed: %w", err) + } + + fmt.Println("๐Ÿ” Checking for changes...") + if err := sh.Run("git", "diff-index", "--quiet", "HEAD", "--"); err != nil { + fmt.Println("โ—๏ธ Go files were not correctly formatted. The following files have changes:") + sh.RunV("git", "diff-index", "--name-only", "HEAD") + return fmt.Errorf("please run 'mage gci' and commit the changes") + } + + fmt.Println("โœ… Go imports are correctly formatted.") + return nil +} diff --git a/tools/crd2go/pkg/crd2go/crd2go.go b/tools/crd2go/pkg/crd2go/crd2go.go index db2ca81091..4008390487 100644 --- a/tools/crd2go/pkg/crd2go/crd2go.go +++ b/tools/crd2go/pkg/crd2go/crd2go.go @@ -30,6 +30,7 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/crd" "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/crd/hooks" + "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/fileinput" "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/gotype" "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/render" "github.com/mongodb/mongodb-atlas-kubernetes/tools/crd2go/internal/run" @@ -47,7 +48,7 @@ func LoadConfig(r io.Reader) (*config.Config, error) { } cfg := config.Config{} if err = yaml.Unmarshal(yml, &cfg); err != nil { - return nil, fmt.Errorf("failed to load configuration: %v", err) + return nil, fmt.Errorf("failed to load configuration: %w", err) } return &cfg, nil } @@ -55,12 +56,20 @@ func LoadConfig(r io.Reader) (*config.Config, error) { // CodeWriterAtPath creates a file writer for the given CRD at the specified directory func CodeWriterAtPath(dir string) config.CodeWriterFunc { return func(filename string, overwrite bool) (io.WriteCloser, error) { + log.Printf("filename=%q", filename) srcFile := filepath.Join(dir, filename) + log.Printf("dir=%q", dir) + log.Printf("srcFile=%q", srcFile) flags := os.O_CREATE | os.O_EXCL | os.O_WRONLY if overwrite { flags = os.O_CREATE | os.O_TRUNC | os.O_RDWR } - w, err := os.OpenFile(srcFile, flags, 0666) + safeSrcFile, err := fileinput.SafeAt(dir, srcFile) + if err != nil { + return nil, fmt.Errorf("unsafe file path %s: %w", srcFile, err) + } + // #nosec G304 gosec is confused here as SafeAt above already sanitized the input + w, err := os.OpenFile(safeSrcFile, flags, 0600) if err != nil { return nil, fmt.Errorf("failed to create file %s: %w", srcFile, err) } diff --git a/tools/crd2go/pkg/crd2go/crd2go_test.go b/tools/crd2go/pkg/crd2go/crd2go_test.go index 5c077e2454..3f7b88b135 100644 --- a/tools/crd2go/pkg/crd2go/crd2go_test.go +++ b/tools/crd2go/pkg/crd2go/crd2go_test.go @@ -169,7 +169,9 @@ imports: []`, } func TestCodeFileForCRDAtPath(t *testing.T) { - tmpDir, err := os.MkdirTemp(".", "test-code-file-for-crd-path") + cwd, err := os.Getwd() + require.NoError(t, err) + tmpDir, err := os.MkdirTemp(cwd, "test-code-file-for-crd-path") require.NoError(t, err) defer checkerr.CheckErr("removing test temp dir", func() error { return os.RemoveAll(tmpDir) }) @@ -185,6 +187,9 @@ func TestCodeFileForCRDAtPath(t *testing.T) { defer checkerr.CheckErr("closing 2nd testfile.go", w2.Close) _, err = cwFn("..", true) + assert.ErrorContains(t, err, "unsafe file path") + + _, err = cwFn("Bad/name", true) assert.ErrorContains(t, err, "failed to create file") }