diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index 36490acd1..df7317c97 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/BurntSushi/toml" "github.com/google/go-cmp/cmp" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -246,6 +247,34 @@ func testPack(t *testing.T, when spec.G, it spec.S) { } }) }, spec.Parallel(), spec.Report(report.Terminal{})) + + when("add-stack", func() { + it("adds a custom stack to ~/.pack/config.toml", func() { + cmd := exec.Command(pack, "add-stack", "my.custom.stack", "--run-image", "my-org/run", "--build-image", "my-org/build") + cmd.Env = append(os.Environ(), "HOME="+homeDir) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("add-stack command failed: %s: %s", output, err) + } + + assertEq(t, string(output), "my.custom.stack successfully added\n") + + var config struct { + Stacks []struct { + ID string `toml:"id"` + BuildImages []string `toml:"build-images"` + RunImages []string `toml:"run-images"` + } `toml:"stacks"` + } + _, err = toml.DecodeFile(filepath.Join(homeDir, ".pack", "config.toml"), &config) + assertNil(t, err) + + stack := config.Stacks[len(config.Stacks)-1] + assertEq(t, stack.ID, "my.custom.stack") + assertEq(t, stack.BuildImages, []string{"my-org/build"}) + assertEq(t, stack.RunImages, []string{"my-org/run"}) + }) + }, spec.Parallel(), spec.Report(report.Terminal{})) } func run(t *testing.T, cmd *exec.Cmd) string { diff --git a/cmd/pack/main.go b/cmd/pack/main.go index 985b184d4..6f014e4e8 100644 --- a/cmd/pack/main.go +++ b/cmd/pack/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log" "os" "path/filepath" @@ -18,10 +19,12 @@ import ( func main() { buildCmd := buildCommand() createBuilderCmd := createBuilderCommand() + addStackCmd := addStackCommand() rootCmd := &cobra.Command{Use: "pack"} rootCmd.AddCommand(buildCmd) rootCmd.AddCommand(createBuilderCmd) + rootCmd.AddCommand(addStackCmd) if err := rootCmd.Execute(); err != nil { os.Exit(1) } @@ -90,3 +93,32 @@ func createBuilderCommand() *cobra.Command { createBuilderCommand.Flags().StringVarP(&flags.StackID, "stack", "s", "", "stack ID") return createBuilderCommand } + +func addStackCommand() *cobra.Command { + flags := struct { + BuildImage string + RunImage string + }{} + addStackCommand := &cobra.Command{ + Use: "add-stack --run-image= --build-image=", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := config.New(filepath.Join(os.Getenv("HOME"), ".pack")) + if err != nil { + return err + } + if err := cfg.Add(config.Stack{ + ID: args[0], + BuildImages: []string{flags.BuildImage}, + RunImages: []string{flags.RunImage}, + }); err != nil { + return err + } + fmt.Printf("%s successfully added\n", args[0]) + return nil + }, + } + addStackCommand.Flags().StringVar(&flags.BuildImage, "build-image", "", "build image to be used for bulder images built with the stack") + addStackCommand.Flags().StringVar(&flags.RunImage, "run-image", "", "run image to be used for runnable images built with the stack") + return addStackCommand +} diff --git a/config/config.go b/config/config.go index 016f362da..20c9be592 100644 --- a/config/config.go +++ b/config/config.go @@ -13,6 +13,7 @@ import ( type Config struct { Stacks []Stack `toml:"stacks"` DefaultStackID string `toml:"default-stack-id"` + configPath string } type Stack struct { @@ -37,20 +38,25 @@ func New(path string) (*Config, error) { RunImages: []string{"packs/run"}, }) - if err := os.MkdirAll(filepath.Dir(configPath), 0777); err != nil { + config.configPath = configPath + if err := config.save(); err != nil { return nil, err } - w, err := os.OpenFile(configPath, os.O_CREATE|os.O_RDWR, 0644) + + return config, nil +} + +func (c *Config) save() error { + if err := os.MkdirAll(filepath.Dir(c.configPath), 0777); err != nil { + return err + } + w, err := os.OpenFile(c.configPath, os.O_CREATE|os.O_RDWR, 0644) if err != nil { - return nil, err + return err } defer w.Close() - if err := toml.NewEncoder(w).Encode(config); err != nil { - return nil, err - } - - return config, nil + return toml.NewEncoder(w).Encode(c) } func previousConfig(path string) (*Config, error) { @@ -84,6 +90,14 @@ func (c *Config) Get(stackID string) (*Stack, error) { return nil, fmt.Errorf(`Missing stack: stack with id "%s" not found in pack config.toml`, stackID) } +func (c *Config) Add(stack Stack) error { + if _, err := c.Get(stack.ID); err == nil { + return fmt.Errorf(`stack "%s" already exists`, stack.ID) + } + c.Stacks = append(c.Stacks, stack) + return c.save() +} + func ImageByRegistry(registry string, images []string) (string, error) { if len(images) == 0 { return "", errors.New("empty images") diff --git a/config/config_test.go b/config/config_test.go index a0def74b0..adb4ba5d8 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -201,6 +201,71 @@ default-stack-id = "my.stack" }) }) + when("Config#Add", func() { + var subject *config.Config + it.Before(func() { + assertNil(t, ioutil.WriteFile(filepath.Join(tmpDir, "config.toml"), []byte(` +default-stack-id = "my.stack" +[[stacks]] + id = "stack-1" +[[stacks]] + id = "my.stack" +[[stacks]] + id = "stack-3" +`), 0666)) + var err error + subject, err = config.New(tmpDir) + assertNil(t, err) + }) + + when("stack to be added is new", func() { + it("adds the stack and writes to file", func() { + err := subject.Add(config.Stack{ + ID: "new-stack", + BuildImages: []string{"neworg/build"}, + RunImages: []string{"neworg/run"}, + }) + assertNil(t, err) + + stack, err := subject.Get("new-stack") + assertNil(t, err) + assertEq(t, stack.ID, "new-stack") + assertEq(t, stack.BuildImages, []string{"neworg/build"}) + assertEq(t, stack.RunImages, []string{"neworg/run"}) + + b, err := ioutil.ReadFile(filepath.Join(tmpDir, "config.toml")) + assertNil(t, err) + assertContains(t, string(b), "new-stack") + assertContains(t, string(b), "neworg/build") + assertContains(t, string(b), "neworg/run") + }) + }) + + when("stack to be added is already in file", func() { + it("errors and leaves file unchanged", func() { + stat, err := os.Stat(filepath.Join(tmpDir, "config.toml")) + assertNil(t, err) + origSize := stat.Size() + + err = subject.Add(config.Stack{ + ID: "my.stack", + BuildImages: []string{"neworg/build"}, + RunImages: []string{"neworg/run"}, + }) + assertNotNil(t, err) + assertEq(t, err.Error(), `stack "my.stack" already exists`) + + stack, err := subject.Get("my.stack") + assertNil(t, err) + assertEq(t, stack.BuildImages, []string(nil)) + + stat, err = os.Stat(filepath.Join(tmpDir, "config.toml")) + assertNil(t, err) + assertEq(t, stat.Size(), origSize) + }) + }) + }) + when("ImageByRegistry", func() { var images []string it.Before(func() {