Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ linters:
# Tool for code clone detection
# - dupl
# Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
- errcheck
# - errcheck
# Tool for detection of long functions
# - funlen
# Checks that no globals are present in Go code
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ lint: ## Run linter
command -v golangci-lint >/dev/null || go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59
golangci-lint run --timeout=10m --verbose

.PHONY: lint-fix
lint-fix: ## Run linter fixes
command -v golangci-lint >/dev/null || go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52
golangci-lint run --fix

.PHONY: generate
generate: ## Generate mock data
command -v mockgen >/dev/null || go install github.com/golang/mock/mockgen@latest
Expand Down
18 changes: 15 additions & 3 deletions commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@ package commands
import (
"context"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/platformsh/platformify/vendorization"
)

// Execute executes the ify command and sets flags appropriately.
func Execute(assets *vendorization.VendorAssets) error {
cmd := NewPlatformifyCmd(assets)
rootCmd := NewPlatformifyCmd(assets)
validateCmd := NewValidateCommand(assets)
cmd.AddCommand(validateCmd)
return cmd.ExecuteContext(vendorization.WithVendorAssets(context.Background(), assets))
rootCmd.AddCommand(validateCmd)

rootCmd.PersistentFlags().Bool("no-interaction", false, "Disable interactive prompts")
viper.BindPFlag("no-interaction", rootCmd.PersistentFlags().Lookup("no-interaction"))

rootCmd.PreRun = func(cmd *cobra.Command, _ []string) {
ctx := context.WithValue(cmd.Context(), NoInteractionKey, viper.GetBool("no-interaction"))
cmd.SetContext(ctx)
}

return rootCmd.ExecuteContext(vendorization.WithVendorAssets(context.Background(), assets))
}
86 changes: 69 additions & 17 deletions commands/platformify.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"

"github.com/spf13/cobra"

Expand All @@ -19,34 +22,48 @@ import (
type contextKey string

var FlavorKey contextKey = "flavor"
var NoInteractionKey contextKey = "no-interaction"
var FSKey contextKey = "fs"

func NewPlatformifyCmd(assets *vendorization.VendorAssets) *cobra.Command {
var noInteraction bool
cmd := &cobra.Command{
Use: assets.Use,
Aliases: []string{"ify"},
Short: fmt.Sprintf("Creates starter YAML files for your %s project", assets.ServiceName),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return Platformify(cmd.Context(), cmd.OutOrStderr(), cmd.ErrOrStderr(), assets)
return Platformify(
cmd.Context(),
cmd.OutOrStderr(),
cmd.ErrOrStderr(),
noInteraction,
assets,
)
},
}

cmd.Flags().BoolVar(&noInteraction, "no-interaction", false, "Disable interactive prompts")
return cmd
}

func Platformify(ctx context.Context, stdout, stderr io.Writer, assets *vendorization.VendorAssets) error {
answers := models.NewAnswers()
answers.Flavor, _ = ctx.Value(FlavorKey).(string)
ctx = models.ToContext(ctx, answers)
ctx = colors.ToContext(
ctx,
stdout,
stderr,
)
func Discover(
ctx context.Context,
flavor string,
noInteraction bool,
fileSystem fs.FS,
) (*platformifier.UserInput, error) {
answers, _ := models.FromContext(ctx)
if answers == nil {
answers = models.NewAnswers()
ctx = models.ToContext(ctx, answers)
}
answers.Flavor = flavor
answers.NoInteraction = noInteraction
answers.WorkingDirectory = fileSystem
q := questionnaire.New(
&question.WorkingDirectory{},
&question.FilesOverwrite{},
&question.Welcome{},
&question.Stack{},
&question.Type{},
Expand All @@ -65,23 +82,58 @@ func Platformify(ctx context.Context, stdout, stderr io.Writer, assets *vendoriz
)
err := q.AskQuestions(ctx)
if errors.Is(err, questionnaire.ErrSilent) {
return nil
return nil, nil
}

if err != nil {
fmt.Fprintln(stderr, colors.Colorize(colors.ErrorCode, err.Error()))
return err
return nil, err
}

input := answers.ToUserInput()
return answers.ToUserInput(), nil
}

func Platformify(
ctx context.Context,
stdout, stderr io.Writer,
noInteraction bool,
assets *vendorization.VendorAssets,
) error {
ctx = colors.ToContext(ctx, stdout, stderr)
ctx = models.ToContext(ctx, models.NewAnswers())
input, err := Discover(ctx, assets.ConfigFlavor, noInteraction, nil)
if err != nil {
return err
}
answers, _ := models.FromContext(ctx)
pfier := platformifier.New(input, assets.ConfigFlavor)
err = pfier.Platformify(ctx)
configFiles, err := pfier.Platformify(ctx)
if err != nil {
fmt.Fprintln(stderr, colors.Colorize(colors.ErrorCode, err.Error()))
return fmt.Errorf("could not configure project: %w", err)
}

filesToCreateUpdate := make([]string, 0, len(configFiles))
for file := range configFiles {
filesToCreateUpdate = append(filesToCreateUpdate, file)
}

filesOverwrite := question.FilesOverwrite{FilesToCreateUpdate: filesToCreateUpdate}
if err := filesOverwrite.Ask(ctx); err != nil {
return err
}

for file, contents := range configFiles {
filePath := path.Join(answers.Cwd, file)
if err := os.MkdirAll(path.Dir(filePath), os.ModeDir|os.ModePerm); err != nil {
fmt.Fprintf(stderr, "Could not create parent directories of file %s: %s\n", file, err)
continue
}

if err := os.WriteFile(filePath, contents, 0o664); err != nil {
fmt.Fprintf(stderr, "Could not write file %s: %s\n", file, err)
continue
}
}

done := question.Done{}
return done.Ask(ctx)
}
2 changes: 1 addition & 1 deletion commands/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func NewValidateCommand(assets *vendorization.VendorAssets) *cobra.Command {
return err
}

if err = validator.ValidateConfig(cwd, assets.ConfigFlavor); err != nil {
if err = validator.ValidateConfig(os.DirFS(cwd), assets.ConfigFlavor); err != nil {
fmt.Fprintf(
cmd.ErrOrStderr(),
colors.Colorize(
Expand Down
45 changes: 45 additions & 0 deletions discovery/application_root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package discovery

import (
"path"
"slices"

"github.com/platformsh/platformify/internal/utils"
)

// Returns the application root, either from memory or by discovering it on the spot
func (d *Discoverer) ApplicationRoot() (string, error) {
if applicationRoot, ok := d.memory["application_root"]; ok {
return applicationRoot.(string), nil
}

appRoot, err := d.discoverApplicationRoot()
if err != nil {
return "", err
}

d.memory["application_root"] = appRoot
return appRoot, nil
}

func (d *Discoverer) discoverApplicationRoot() (string, error) {
depManagers, err := d.DependencyManagers()
if err != nil {
return "", err
}

for _, dependencyManager := range dependencyManagersMap {
if !slices.Contains(depManagers, dependencyManager.name) {
continue
}

lockPath := utils.FindFile(d.fileSystem, "", dependencyManager.lockFile)
if lockPath == "" {
continue
}

return path.Dir(lockPath), nil
}

return "", nil
}
67 changes: 67 additions & 0 deletions discovery/application_root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package discovery

import (
"io/fs"
"testing"
"testing/fstest"
)

func TestDiscoverer_discoverApplicationRoot(t *testing.T) {
type fields struct {
fileSystem fs.FS
memory map[string]any
}
tests := []struct {
name string
fields fields
want string
wantErr bool
}{
{
name: "Simple",
fields: fields{
fileSystem: fstest.MapFS{
"package-lock.json": &fstest.MapFile{},
},
},
want: ".",
},
{
name: "No root",
fields: fields{
fileSystem: fstest.MapFS{},
},
want: "",
},
{
name: "Priority",
fields: fields{
fileSystem: fstest.MapFS{
"yarn/yarn.lock": &fstest.MapFile{},
"poetry/poetry.lock": &fstest.MapFile{},
"composer/composer-lock.json": &fstest.MapFile{},
},
},
want: "poetry",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &Discoverer{
fileSystem: tt.fields.fileSystem,
memory: tt.fields.memory,
}
if d.memory == nil {
d.memory = make(map[string]any)
}
got, err := d.discoverApplicationRoot()
if (err != nil) != tt.wantErr {
t.Errorf("Discoverer.discoverApplicationRoot() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Discoverer.discoverApplicationRoot() = %v, want %v", got, tt.want)
}
})
}
}
Loading
Loading