Skip to content

Commit 4278153

Browse files
chore(helm/v2-aplha): Add --input-dir flag to helm/v2-alpha plugin to allow operating on projects in different directories
Generated-by: Claude
1 parent 8c56039 commit 4278153

6 files changed

Lines changed: 449 additions & 19 deletions

File tree

docs/book/src/plugins/available/helm-v2-alpha.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,8 @@ The Makefile targets use sensible defaults extracted from your project configura
690690
| Flag | Description |
691691
|---------------------|-----------------------------------------------------------------------------|
692692
| **--manifests** | Path to YAML file containing Kubernetes manifests (default: `dist/install.yaml`) |
693-
| **--output-dir** string | Output directory for chart (default: `dist`) |
693+
| **--output-dir** | Output directory for chart (default: `dist`) |
694+
| **--input-dir** | Path to directory containing PROJECT file. All operations will use files from this directory |
694695
| **--force** | Regenerates preserved files except `Chart.yaml` (`values.yaml`, `NOTES.txt`, `_helpers.tpl`, `.helmignore`, `test-chart.yml`) |
695696

696697
<aside class="note" role="note">

pkg/cli/cli.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
log "log/slog"
2323
"os"
24+
"path/filepath"
2425
"strings"
2526

2627
"github.com/spf13/afero"
@@ -611,6 +612,71 @@ func (c CLI) Run() error {
611612
return nil
612613
}
613614

615+
// handleInputDirForEdit checks if --input-dir flag is set for edit command and changes directory if needed.
616+
// This must run BEFORE the framework loads PROJECT file.
617+
func (c *CLI) handleInputDirForEdit(cmd *cobra.Command, subcommands []keySubcommandTuple) error {
618+
// Check if --input-dir flag exists and is set
619+
inputDirFlag := cmd.Flags().Lookup("input-dir")
620+
if inputDirFlag == nil || !inputDirFlag.Changed {
621+
return nil
622+
}
623+
624+
inputDir := inputDirFlag.Value.String()
625+
if inputDir == "" {
626+
return nil
627+
}
628+
629+
// Validate that at least one plugin supports --input-dir
630+
// Currently only helm.kubebuilder.io/v2-alpha is allowed
631+
hasAllowedPlugin := false
632+
for _, tuple := range subcommands {
633+
if tuple.key == "helm.kubebuilder.io/v2-alpha" {
634+
hasAllowedPlugin = true
635+
break
636+
}
637+
}
638+
639+
if !hasAllowedPlugin {
640+
return fmt.Errorf("--input-dir flag is only supported with helm.kubebuilder.io/v2-alpha plugin")
641+
}
642+
643+
// Resolve to absolute path
644+
if !filepath.IsAbs(inputDir) {
645+
absPath, err := filepath.Abs(inputDir)
646+
if err != nil {
647+
return fmt.Errorf("failed to resolve input-dir %q: %w", inputDir, err)
648+
}
649+
inputDir = absPath
650+
}
651+
652+
// Validate directory exists
653+
if info, err := os.Stat(inputDir); err != nil {
654+
if os.IsNotExist(err) {
655+
return fmt.Errorf("input-dir does not exist: %q", inputDir)
656+
}
657+
return fmt.Errorf("failed to access input-dir %q: %w", inputDir, err)
658+
} else if !info.IsDir() {
659+
return fmt.Errorf("input-dir is not a directory: %q", inputDir)
660+
}
661+
662+
// Validate PROJECT file exists in input-dir
663+
projectFilePath := filepath.Join(inputDir, yamlstore.DefaultPath)
664+
if _, err := os.Stat(projectFilePath); err != nil {
665+
if os.IsNotExist(err) {
666+
return fmt.Errorf("input-dir does not contain %q: %q", yamlstore.DefaultPath, inputDir)
667+
}
668+
return fmt.Errorf("failed to access project file %q: %w", projectFilePath, err)
669+
}
670+
671+
// Change working directory
672+
if err := os.Chdir(inputDir); err != nil {
673+
return fmt.Errorf("failed to change to input-dir %q: %w", inputDir, err)
674+
}
675+
676+
log.Info("Operating in input directory", "path", inputDir)
677+
return nil
678+
}
679+
614680
// Command returns the underlying root command.
615681
func (c CLI) Command() *cobra.Command {
616682
return c.cmd

pkg/cli/cli_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"io"
2222
"os"
23+
"path/filepath"
2324
"strings"
2425

2526
. "github.com/onsi/ginkgo/v2"
@@ -807,4 +808,140 @@ plugins:
807808
})
808809
})
809810
})
811+
812+
Context("handleInputDirForEdit", func() {
813+
var (
814+
c *CLI
815+
cmd *cobra.Command
816+
subcommands []keySubcommandTuple
817+
tmpDir string
818+
)
819+
820+
BeforeEach(func() {
821+
c = &CLI{}
822+
cmd = &cobra.Command{}
823+
cmd.Flags().String("input-dir", "", "test input-dir flag")
824+
825+
// Create a temporary directory for testing
826+
var err error
827+
tmpDir, err = os.MkdirTemp("", "cli-input-dir-test-*")
828+
Expect(err).NotTo(HaveOccurred())
829+
})
830+
831+
AfterEach(func() {
832+
if tmpDir != "" {
833+
_ = os.RemoveAll(tmpDir)
834+
}
835+
})
836+
837+
It("should return nil when input-dir flag is not set", func() {
838+
subcommands = []keySubcommandTuple{
839+
{key: "helm.kubebuilder.io/v2-alpha"},
840+
}
841+
err := c.handleInputDirForEdit(cmd, subcommands)
842+
Expect(err).NotTo(HaveOccurred())
843+
})
844+
845+
It("should return nil when input-dir flag is empty", func() {
846+
_ = cmd.Flags().Set("input-dir", "")
847+
subcommands = []keySubcommandTuple{
848+
{key: "helm.kubebuilder.io/v2-alpha"},
849+
}
850+
err := c.handleInputDirForEdit(cmd, subcommands)
851+
Expect(err).NotTo(HaveOccurred())
852+
})
853+
854+
It("should return error when plugin is not helm/v2-alpha", func() {
855+
_ = cmd.Flags().Set("input-dir", tmpDir)
856+
subcommands = []keySubcommandTuple{
857+
{key: "go.kubebuilder.io/v4"},
858+
}
859+
err := c.handleInputDirForEdit(cmd, subcommands)
860+
Expect(err).To(HaveOccurred())
861+
Expect(err.Error()).To(ContainSubstring("only supported with helm.kubebuilder.io/v2-alpha"))
862+
})
863+
864+
It("should return error when directory does not exist", func() {
865+
_ = cmd.Flags().Set("input-dir", "/nonexistent/path/to/dir")
866+
subcommands = []keySubcommandTuple{
867+
{key: "helm.kubebuilder.io/v2-alpha"},
868+
}
869+
err := c.handleInputDirForEdit(cmd, subcommands)
870+
Expect(err).To(HaveOccurred())
871+
Expect(err.Error()).To(ContainSubstring("does not exist"))
872+
})
873+
874+
It("should return error when path is not a directory", func() {
875+
// Create a file instead of directory
876+
filePath := filepath.Join(tmpDir, "testfile")
877+
err := os.WriteFile(filePath, []byte("test"), 0o644)
878+
Expect(err).NotTo(HaveOccurred())
879+
880+
_ = cmd.Flags().Set("input-dir", filePath)
881+
subcommands = []keySubcommandTuple{
882+
{key: "helm.kubebuilder.io/v2-alpha"},
883+
}
884+
err = c.handleInputDirForEdit(cmd, subcommands)
885+
Expect(err).To(HaveOccurred())
886+
Expect(err.Error()).To(ContainSubstring("not a directory"))
887+
})
888+
889+
It("should return error when directory does not contain PROJECT file", func() {
890+
// Create directory without PROJECT file
891+
emptyDir := filepath.Join(tmpDir, "empty-dir")
892+
err := os.MkdirAll(emptyDir, 0o755)
893+
Expect(err).NotTo(HaveOccurred())
894+
895+
_ = cmd.Flags().Set("input-dir", emptyDir)
896+
subcommands = []keySubcommandTuple{
897+
{key: "helm.kubebuilder.io/v2-alpha"},
898+
}
899+
err = c.handleInputDirForEdit(cmd, subcommands)
900+
Expect(err).To(HaveOccurred())
901+
Expect(err.Error()).To(ContainSubstring("does not contain"))
902+
Expect(err.Error()).To(ContainSubstring("PROJECT"))
903+
})
904+
905+
It("should change directory when helm plugin is present and dir exists", func() {
906+
By("saving original directory")
907+
originalDir, err := os.Getwd()
908+
Expect(err).NotTo(HaveOccurred())
909+
defer func() {
910+
_ = os.Chdir(originalDir)
911+
}()
912+
913+
By("creating PROJECT file in input directory")
914+
projectPath := filepath.Join(tmpDir, "PROJECT")
915+
projectContent := `domain: example.com
916+
layout:
917+
- go.kubebuilder.io/v4
918+
projectName: test-project
919+
repo: example.com/test-project
920+
version: "3"
921+
`
922+
err = os.WriteFile(projectPath, []byte(projectContent), 0o644)
923+
Expect(err).NotTo(HaveOccurred())
924+
925+
_ = cmd.Flags().Set("input-dir", tmpDir)
926+
subcommands = []keySubcommandTuple{
927+
{key: "helm.kubebuilder.io/v2-alpha"},
928+
}
929+
930+
By("calling handleInputDirForEdit")
931+
err = c.handleInputDirForEdit(cmd, subcommands)
932+
Expect(err).NotTo(HaveOccurred())
933+
934+
By("verifying directory changed")
935+
currentDir, err := os.Getwd()
936+
Expect(err).NotTo(HaveOccurred())
937+
938+
By("resolving symlinks for comparison")
939+
expectedDir, err := filepath.EvalSymlinks(tmpDir)
940+
Expect(err).NotTo(HaveOccurred())
941+
actualDir, err := filepath.EvalSymlinks(currentDir)
942+
Expect(err).NotTo(HaveOccurred())
943+
944+
Expect(actualDir).To(Equal(expectedDir))
945+
})
946+
})
810947
})

pkg/cli/edit.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
//nolint:dupl
1817
package cli
1918

2019
import (
@@ -63,6 +62,18 @@ func (c CLI) newEditCmd() *cobra.Command {
6362

6463
c.applySubcommandHooks(cmd, subcommands, editErrorMsg, false)
6564

65+
// Wrap PreRunE to handle --input-dir flag BEFORE framework loads PROJECT
66+
frameworkPreRunE := cmd.PreRunE
67+
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
68+
if err := c.handleInputDirForEdit(cmd, subcommands); err != nil {
69+
return err
70+
}
71+
if frameworkPreRunE != nil {
72+
return frameworkPreRunE(cmd, args)
73+
}
74+
return nil
75+
}
76+
6677
// Append plugin table after metadata updates
6778
c.appendPluginTable(cmd, func(p plugin.Plugin) bool {
6879
_, isValid := p.(plugin.Edit)

pkg/plugins/optional/helm/v2alpha/edit.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type editSubcommand struct {
5252
force bool
5353
manifestsFile string
5454
outputDir string
55+
inputDir string
5556
}
5657

5758
//nolint:lll
@@ -76,6 +77,9 @@ distribution of your project. When enabled, adds Helm helpers targets to Makefil
7677
# Generate from custom manifests to custom output directory
7778
%[1]s edit --plugins=%[2]s --manifests=manifests/install.yaml --output-dir=helm-charts
7879
80+
# Work with a project in a different directory (useful for monorepos)
81+
%[1]s edit --plugins=%[2]s --input-dir=./path/to/project
82+
7983
# Typical workflow:
8084
make build-installer # Generate dist/install.yaml with latest changes
8185
%[1]s edit --plugins=%[2]s # Generate/update Helm chart in dist/chart/
@@ -105,10 +109,16 @@ func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
105109
fs.BoolVar(&p.force, "force", false, "if true, regenerates all the files")
106110
fs.StringVar(&p.manifestsFile, "manifests", DefaultManifestsFile,
107111
"path to the YAML file containing Kubernetes manifests from kustomize output")
108-
fs.StringVar(&p.outputDir, "output-dir", DefaultOutputDir, "output directory for the generated Helm chart")
112+
fs.StringVar(&p.outputDir, "output-dir", DefaultOutputDir,
113+
"output directory for the generated Helm chart")
114+
fs.StringVar(&p.inputDir, "input-dir", "",
115+
"path to directory containing PROJECT file; "+
116+
"all operations will use files from this directory (defaults to current working directory)")
109117
}
110118

111119
func (p *editSubcommand) InjectConfig(c config.Config) error {
120+
// The --input-dir flag is handled by handleInputDirForEdit in pkg/cli/cli.go
121+
// before this method is called, so the working directory is already set.
112122
p.config = c
113123
return nil
114124
}

0 commit comments

Comments
 (0)