Repository: appvia/kev
Pull request #382 (2021-02-16 12:33:42)

 Refactor init to reduce logic in CLI interface


Patch:
diff --git a/cmd/kev/cmd/dev.go b/cmd/kev/cmd/dev.go
index 15e477b..601c3fa 100644
--- a/cmd/kev/cmd/dev.go
+++ b/cmd/kev/cmd/dev.go
@@ -181,5 +181,4 @@ func runDevCmd(cmd *cobra.Command, args []string) error {
 	kev.DisplaySkaffoldInfo(opts)
 
 	return kev.Dev(opts, workDir, preRunCmds, errHandler, changeHandler)
-
 }
diff --git a/cmd/kev/cmd/init.go b/cmd/kev/cmd/init.go
index a28756b..a46d80a 100644
--- a/cmd/kev/cmd/init.go
+++ b/cmd/kev/cmd/init.go
@@ -17,9 +17,7 @@
 package cmd
 
 import (
-	"fmt"
 	"os"
-	"path"
 
 	"github.com/appvia/kev/pkg/kev"
 	"github.com/spf13/cobra"
@@ -58,12 +56,6 @@ var initCmd = &cobra.Command{
 	},
 }
 
-type skippableFile struct {
-	FileName string
-	Skipped  bool
-	Updated  bool
-}
-
 func init() {
 	flags := initCmd.Flags()
 	flags.SortFlags = false
@@ -96,104 +88,25 @@ func runInitCmd(cmd *cobra.Command, _ []string) error {
 
 	displayCmdStarted(cmdName)
 
-	workingDirAbs, err := os.Getwd()
-	if err != nil {
-		return displayError(err)
-	}
-
-	manifestPath := path.Join(workingDirAbs, kev.ManifestName)
-	if manifestExistsForPath(manifestPath) {
-		err := fmt.Errorf("kev.yaml already exists at: %s", manifestPath)
-		return displayError(err)
+	opts := kev.InitOptions{
+		ComposeSources: files,
+		Envs:           envs,
+		Skaffold:       skaffold,
 	}
 
-	manifest, err := kev.Init(files, envs, ".")
+	// The working directory is always the current directory.
+	// This ensures created manifest yaml entries are portable between users and require no path fixing.
+	wd := "."
+	results, err := kev.InitProjectWithOptions(wd, opts)
 	if err != nil {
 		return displayError(err)
 	}
 
-	var results []skippableFile
-
-	for _, environment := range manifest.Environments {
-		envPath := path.Join(workingDirAbs, environment.File)
-
-		if err := kev.WriteTo(envPath, environment); err != nil {
-			return displayError(err)
-		}
-
-		results = append(results, skippableFile{
-			FileName: environment.File,
-		})
-	}
-
-	if skaffold {
-		skaffoldManifestPath := path.Join(workingDirAbs, kev.SkaffoldFileName)
-
-		// set skaffold path in kev manifest
-		manifest.Skaffold = kev.SkaffoldFileName
-
-		project, err := manifest.SourcesToComposeProject()
-		if err != nil {
-			return displayError(err)
-		}
-
-		if err := createOrUpdateSkaffoldManifest(skaffoldManifestPath, manifest.GetEnvironmentsNames(), project, &results); err != nil {
-			return displayError(err)
-		}
-	}
-
-	if err := kev.WriteTo(manifestPath, manifest); err != nil {
-		return displayError(err)
+	if err := results.Write(); err != nil {
+		return err
 	}
 
-	results = append([]skippableFile{{
-		FileName: kev.ManifestName,
-	}}, results...)
-
 	displayInitSuccess(os.Stdout, results)
 
 	return nil
 }
-
-func manifestExistsForPath(manifestPath string) bool {
-	_, err := os.Stat(manifestPath)
-	return err == nil
-}
-
-func createOrUpdateSkaffoldManifest(path string, envs []string, project *kev.ComposeProject, results *[]skippableFile) error {
-	if manifestExistsForPath(path) {
-		// Skaffold manifest already present - add additional profiles to it!
-		// Note: kev will skip profiles with names matching those of existing
-		// profile names defined in Skaffold to avoid profile "hijack".
-
-		updatedSkaffold, err := kev.AddProfiles(path, envs, true)
-		if err != nil {
-			return displayError(err)
-		}
-		if err := kev.WriteTo(path, updatedSkaffold); err != nil {
-			return displayError(err)
-		}
-
-		*results = append(*results, skippableFile{
-			FileName: kev.SkaffoldFileName,
-			Updated:  true,
-		})
-
-	} else {
-
-		skaffoldManifest, err := kev.NewSkaffoldManifest(envs, project)
-		if err != nil {
-			return displayError(err)
-		}
-
-		if err := kev.WriteTo(kev.SkaffoldFileName, skaffoldManifest); err != nil {
-			return displayError(err)
-		}
-
-		*results = append(*results, skippableFile{
-			FileName: kev.SkaffoldFileName,
-		})
-	}
-
-	return nil
-}
diff --git a/cmd/kev/cmd/reconcile.go b/cmd/kev/cmd/reconcile.go
index 296ba9b..19ca2cc 100644
--- a/cmd/kev/cmd/reconcile.go
+++ b/cmd/kev/cmd/reconcile.go
@@ -19,7 +19,6 @@ package cmd
 import (
 	"fmt"
 	"os"
-	"path"
 
 	"github.com/appvia/kev/pkg/kev"
 	"github.com/spf13/cobra"
@@ -43,11 +42,8 @@ func runReconcileCmd(cmd *cobra.Command, _ []string) error {
 		return displayError(err)
 	}
 
-	for _, environment := range manifest.Environments {
-		filePath := path.Join(workingDir, environment.File)
-		if err := kev.WriteTo(filePath, environment); err != nil {
-			return displayError(err)
-		}
+	if err := manifest.Environments.Write(); err != nil {
+		return displayError(err)
 	}
 
 	os.Stdout.Write([]byte("\n"))
diff --git a/cmd/kev/cmd/render.go b/cmd/kev/cmd/render.go
index ba372be..f9994a3 100644
--- a/cmd/kev/cmd/render.go
+++ b/cmd/kev/cmd/render.go
@@ -112,7 +112,7 @@ func runRenderCmd(cmd *cobra.Command, _ []string) error {
 
 	log.DebugTitlef("Output format: %s", format)
 	if err := kev.Render(workingDir, format, singleFile, dir, envs, nil); err != nil {
-		return err
+		return displayError(err)
 	}
 
 	os.Stdout.Write([]byte("\n"))
diff --git a/cmd/kev/cmd/ux.go b/cmd/kev/cmd/ux.go
index 69e066b..9b16c94 100644
--- a/cmd/kev/cmd/ux.go
+++ b/cmd/kev/cmd/ux.go
@@ -21,6 +21,7 @@ import (
 	"io"
 	"os"
 
+	"github.com/appvia/kev/pkg/kev"
 	"github.com/appvia/kev/pkg/kev/log"
 	"github.com/sirupsen/logrus"
 )
@@ -46,16 +47,17 @@ func displayError(err error) error {
 	return err
 }
 
-func displayInitSuccess(w io.Writer, files []skippableFile) {
-	for _, file := range files {
-		msg := fmt.Sprintf(" → Creating %s ... Done\n", file.FileName)
+// func displayInitSuccess(w io.Writer, files []skippableFile) {
+func displayInitSuccess(w io.Writer, results kev.WritableResults) {
+	for _, result := range results {
+		msg := fmt.Sprintf(" → Creating %s ... Done\n", result.Filename())
 
-		if file.Updated {
-			msg = fmt.Sprintf(" → Updating %s ... Done\n", file.FileName)
+		if result.Updated {
+			msg = fmt.Sprintf(" → Updating %s ... Done\n", result.Filename())
 		}
 
-		if file.Skipped {
-			msg = fmt.Sprintf(" → %s already exists ... Skipping\n", file.FileName)
+		if result.Skipped {
+			msg = fmt.Sprintf(" → %s already exists ... Skipping\n", result.Filename())
 		}
 		_, _ = w.Write([]byte(msg))
 	}
diff --git a/pkg/kev/compose.go b/pkg/kev/compose.go
index e2940bf..aefbf5f 100644
--- a/pkg/kev/compose.go
+++ b/pkg/kev/compose.go
@@ -24,7 +24,6 @@ import (
 
 	"github.com/appvia/kev/pkg/kev/log"
 	"github.com/compose-spec/compose-go/cli"
-	"github.com/compose-spec/compose-go/errdefs"
 	composego "github.com/compose-spec/compose-go/types"
 	"github.com/pkg/errors"
 	"gopkg.in/yaml.v3"
@@ -147,19 +146,12 @@ func findDefaultComposeIn(workingDir string) (string, error) {
 		pwd = wd
 	}
 
-	for {
-		found := findFirstFileFromFilesInDir(defaultComposeFileNames, pwd)
-		if len(found) > 0 {
-			return found, nil
-		}
-
-		parent := filepath.Dir(pwd)
-		noParents := parent == pwd
-		if noParents {
-			return "", errors.Wrap(errdefs.ErrNotFound, "can't find a suitable configuration file in this directory or any parent")
-		}
-		pwd = parent
+	found := findFirstFileFromFilesInDir(defaultComposeFileNames, pwd)
+	if len(found) > 0 {
+		return found, nil
 	}
+
+	return "", errors.New("can't find any docker compose file in this directory")
 }
 
 func findOptionalOverrideComposeIn(composeFileDir string) string {
diff --git a/pkg/kev/envs.go b/pkg/kev/envs.go
index 6c9aa4f..4df64e5 100644
--- a/pkg/kev/envs.go
+++ b/pkg/kev/envs.go
@@ -57,6 +57,23 @@ func (e Environments) MarshalJSON() ([]byte, error) {
 	return json.MarshalIndent(data, "", "  ")
 }
 
+// toWritableResults returns the environments as WritableResults.
+func (e Environments) toWritableResults() WritableResults {
+	var out []WritableResult
+	for _, environment := range e {
+		out = append(out, WritableResult{
+			WriterTo: environment,
+			FilePath: environment.File,
+		})
+	}
+	return out
+}
+
+// Write is a convenience method to write out the environment's overrides to disk
+func (e Environments) Write() error {
+	return e.toWritableResults().Write()
+}
+
 // GetVersion gets the environment's override version.
 func (e *Environment) GetVersion() string {
 	return e.override.Version
diff --git a/pkg/kev/kev.go b/pkg/kev/kev.go
index 1a7f9c3..17261fa 100644
--- a/pkg/kev/kev.go
+++ b/pkg/kev/kev.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"path"
 
 	"github.com/appvia/kev/pkg/kev/config"
 	"github.com/appvia/kev/pkg/kev/converter"
@@ -34,10 +35,47 @@ const (
 	SandboxEnv   = "dev"
 )
 
-// Init initialises a kev manifest including source compose files and environments.
+// InitProjectWithOptions initialises a kev project using provided options
+func InitProjectWithOptions(workingDir string, opts InitOptions) (WritableResults, error) {
+	var out []WritableResult
+
+	m, err := InitBase(workingDir, opts.ComposeSources, opts.Envs)
+	if err != nil {
+		return nil, err
+	}
+
+	out = append(out, WritableResult{
+		WriterTo: m,
+		FilePath: path.Join(workingDir, ManifestName),
+	})
+	out = append(out, m.Environments.toWritableResults()...)
+
+	if !opts.Skaffold {
+		return out, nil
+	}
+
+	m.Skaffold = SkaffoldFileName
+	project, err := m.SourcesToComposeProject()
+	if err != nil {
+		return nil, err
+	}
+
+	results, err := CreateOrUpdateSkaffoldManifest(path.Join(workingDir, SkaffoldFileName), m.GetEnvironmentsNames(), project)
+	if err != nil {
+		return nil, err
+	}
+
+	return append(out, results...), nil
+}
+
+// InitBase initialises a kev manifest including source compose files and environments.
 // If no composeSources are provided, the working directory is introspected for valid compose files to act as sources.
 // Also, an implicit sandbox environment will always be created.
-func Init(composeSources, envs []string, workingDir string) (*Manifest, error) {
+func InitBase(workingDir string, composeSources, envs []string) (*Manifest, error) {
+	if err := EnsureFirstInit(workingDir); err != nil {
+		return nil, err
+	}
+
 	m, err := NewManifest(composeSources, workingDir)
 	if err != nil {
 		return nil, err
diff --git a/pkg/kev/manifest.go b/pkg/kev/manifest.go
index 635d83f..9c39773 100644
--- a/pkg/kev/manifest.go
+++ b/pkg/kev/manifest.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"os"
 	"path"
 	"path/filepath"
 	"strings"
@@ -279,3 +280,17 @@ func (m *Manifest) getSourcesOverride() *composeOverride {
 func (m *Manifest) SourcesToComposeProject() (*ComposeProject, error) {
 	return m.Sources.toComposeProject()
 }
+
+func ManifestExistsForPath(manifestPath string) bool {
+	_, err := os.Stat(manifestPath)
+	return err == nil
+}
+
+func EnsureFirstInit(wd string) error {
+	manifestPath := path.Join(wd, ManifestName)
+	if ManifestExistsForPath(manifestPath) {
+		err := fmt.Errorf("%s already exists at: %s", ManifestName, manifestPath)
+		return err
+	}
+	return nil
+}
diff --git a/pkg/kev/skaffold.go b/pkg/kev/skaffold.go
index c0a1aa6..a20bb07 100644
--- a/pkg/kev/skaffold.go
+++ b/pkg/kev/skaffold.go
@@ -84,6 +84,36 @@ var (
 	enabled  = true
 )
 
+// CreateOrUpdateSkaffoldManifest creates a skaffold manifest or updates an existing one.
+func CreateOrUpdateSkaffoldManifest(path string, envs []string, project *ComposeProject) (WritableResults, error) {
+	var out []WritableResult
+	var skManifest *SkaffoldManifest
+	var err error
+	var updated bool
+
+	switch ManifestExistsForPath(path) {
+	case true:
+		// Skaffold manifest already present - add additional profiles to it!
+		// Note: kev will skip profiles with names matching those of existing
+		// profile names defined in Skaffold to avoid profile "hijack".
+		if skManifest, err = AddProfiles(path, envs, true); err != nil {
+			return nil, err
+		}
+		updated = true
+	case false:
+		if skManifest, err = NewSkaffoldManifest(envs, project); err != nil {
+			return nil, err
+		}
+	}
+
+	out = append(out, WritableResult{
+		WriterTo: skManifest,
+		FilePath: path,
+		Updated:  updated,
+	})
+	return out, nil
+}
+
 // NewSkaffoldManifest returns a new SkaffoldManifest struct.
 func NewSkaffoldManifest(envs []string, project *ComposeProject) (*SkaffoldManifest, error) {
 
diff --git a/pkg/kev/types.go b/pkg/kev/types.go
index c7f1314..27d70de 100644
--- a/pkg/kev/types.go
+++ b/pkg/kev/types.go
@@ -17,6 +17,8 @@
 package kev
 
 import (
+	"io"
+
 	composego "github.com/compose-spec/compose-go/types"
 )
 
@@ -111,6 +113,13 @@ type ChangeHandler func(string) error
 // RunFunc is a callback function expected to run before/after the current command
 type RunFunc func() error
 
+// InitOptions contains parameters required to initialise a kev project
+type InitOptions struct {
+	ComposeSources []string
+	Envs           []string
+	Skaffold       bool
+}
+
 // DevOptions contains parameters required for Dev loop
 type DevOptions struct {
 	Skaffold      bool
@@ -121,3 +130,14 @@ type DevOptions struct {
 	ManualTrigger bool
 	Verbose       bool
 }
+
+// WritableResults is a collection of WritableResult
+type WritableResults []WritableResult
+
+// WritableResult used to return results that can be written out to disk
+type WritableResult struct {
+	WriterTo io.WriterTo
+	FilePath string
+	Skipped  bool
+	Updated  bool
+}
diff --git a/pkg/kev/writables.go b/pkg/kev/writables.go
new file mode 100644
index 0000000..3449b4b
--- /dev/null
+++ b/pkg/kev/writables.go
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2020 Appvia Ltd <info@appvia.io>
+ *
+ * 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 kev
+
+import (
+	"path/filepath"
+)
+
+// Write writes the results
+func (results WritableResults) Write() error {
+	for _, result := range results {
+		if err := result.Write(); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Filename returns the filename for the writable result
+func (r WritableResult) Filename() string {
+	if len(r.FilePath) == 0 {
+		return ""
+	}
+	return filepath.Base(r.FilePath)
+}
+
+// Write writes the WriterTo to the filepath
+func (r WritableResult) Write() error {
+	absPath, err := filepath.Abs(r.FilePath)
+	if err != nil {
+		return err
+	}
+	return WriteTo(absPath, r.WriterTo)
+}


Test patch:
diff --git a/pkg/kev/kev_init_test.go b/pkg/kev/kev_init_test.go
index 0087140..8572d19 100644
--- a/pkg/kev/kev_init_test.go
+++ b/pkg/kev/kev_init_test.go
@@ -25,7 +25,7 @@ import (
 	. "github.com/onsi/gomega"
 )
 
-var _ = Describe("Init", func() {
+var _ = Describe("InitBase", func() {
 	var (
 		workingDir string
 		manifest   *kev.Manifest
@@ -35,7 +35,7 @@ var _ = Describe("Init", func() {
 	)
 
 	JustBeforeEach(func() {
-		manifest, mErr = kev.Init([]string{}, envs, workingDir)
+		manifest, mErr = kev.InitBase(workingDir, []string{}, envs)
 		if mErr == nil {
 			env, _ = manifest.GetEnvironment("dev")
 		}
diff --git a/pkg/kev/kev_test.go b/pkg/kev/kev_test.go
index c7db486..247ebbd 100644
--- a/pkg/kev/kev_test.go
+++ b/pkg/kev/kev_test.go
@@ -28,7 +28,7 @@ import (
 
 func TestInitProvidesEnvironmentConfig(t *testing.T) {
 	files := []string{"testdata/in-cluster-wordpress/docker-compose.yaml"}
-	manifest, err := kev.Init(files, []string{}, "")
+	manifest, err := kev.InitBase("", files, []string{})
 	if err != nil {
 		t.Fatalf("Unexpected error:\n%s", err)
 	}