get.porter.sh/porter@v1.3.0/pkg/porter/mixins.go (about)

     1  package porter
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  
    12  	"get.porter.sh/porter/pkg/mixin"
    13  	"get.porter.sh/porter/pkg/pkgmgmt"
    14  	"get.porter.sh/porter/pkg/pkgmgmt/feed"
    15  	"get.porter.sh/porter/pkg/portercontext"
    16  	"get.porter.sh/porter/pkg/printer"
    17  )
    18  
    19  const (
    20  	SkeletorRepo = "https://github.com/getporter/skeletor"
    21  )
    22  
    23  // PrintMixinsOptions represent options for the PrintMixins function
    24  type PrintMixinsOptions struct {
    25  	printer.PrintOptions
    26  }
    27  
    28  func (p *Porter) PrintMixins(ctx context.Context, opts PrintMixinsOptions) error {
    29  	mixins, err := p.ListMixins(ctx)
    30  	if err != nil {
    31  		return err
    32  	}
    33  
    34  	switch opts.Format {
    35  	case printer.FormatPlaintext:
    36  		printMixinRow :=
    37  			func(v interface{}) []string {
    38  				m, ok := v.(mixin.Metadata)
    39  				if !ok {
    40  					return nil
    41  				}
    42  				return []string{m.Name, m.VersionInfo.Version, m.VersionInfo.Author}
    43  			}
    44  		return printer.PrintTable(p.Out, mixins, printMixinRow, "Name", "Version", "Author")
    45  	case printer.FormatJson:
    46  		return printer.PrintJson(p.Out, mixins)
    47  	case printer.FormatYaml:
    48  		return printer.PrintYaml(p.Out, mixins)
    49  	default:
    50  		return fmt.Errorf("invalid format: %s", opts.Format)
    51  	}
    52  }
    53  
    54  func (p *Porter) ListMixins(ctx context.Context) ([]mixin.Metadata, error) {
    55  	// List out what is installed on the file system
    56  	names, err := p.Mixins.List()
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	// Query each mixin and fill out their metadata
    62  	mixins := make([]mixin.Metadata, len(names))
    63  	for i, name := range names {
    64  		m, err := p.Mixins.GetMetadata(ctx, name)
    65  		if err != nil {
    66  			fmt.Fprintf(p.Err, "could not get version from mixin %s: %s\n ", name, err.Error())
    67  			continue
    68  		}
    69  
    70  		meta, _ := m.(*mixin.Metadata)
    71  		mixins[i] = *meta
    72  	}
    73  
    74  	return mixins, nil
    75  }
    76  
    77  func (p *Porter) InstallMixin(ctx context.Context, opts mixin.InstallOptions) error {
    78  	err := p.Mixins.Install(ctx, opts.InstallOptions)
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	mixin, err := p.Mixins.GetMetadata(ctx, opts.Name)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	v := mixin.GetVersionInfo()
    89  	fmt.Fprintf(p.Out, "installed %s mixin %s (%s)\n", opts.Name, v.Version, v.Commit)
    90  
    91  	return nil
    92  }
    93  
    94  func (p *Porter) UninstallMixin(ctx context.Context, opts pkgmgmt.UninstallOptions) error {
    95  	err := p.Mixins.Uninstall(ctx, opts)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	fmt.Fprintf(p.Out, "Uninstalled %s mixin", opts.Name)
   101  
   102  	return nil
   103  }
   104  
   105  func (p *Porter) GenerateMixinFeed(ctx context.Context, opts feed.GenerateOptions) error {
   106  	f := feed.NewMixinFeed(p.Context)
   107  
   108  	err := f.Generate(ctx, opts)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	return f.Save(opts)
   114  }
   115  
   116  func (p *Porter) CreateMixinFeedTemplate() error {
   117  	return feed.CreateTemplate(p.Context)
   118  }
   119  
   120  // MixinsCreateOptions represent options for Porter's mixin create command
   121  type MixinsCreateOptions struct {
   122  	MixinName      string
   123  	AuthorName     string
   124  	AuthorUsername string
   125  	DirPath        string
   126  }
   127  
   128  func (o *MixinsCreateOptions) Validate(args []string, cxt *portercontext.Context) error {
   129  	if len(args) < 1 || args[0] == "" {
   130  		return errors.New("mixin name is required")
   131  	}
   132  
   133  	if len(args) > 1 {
   134  		return fmt.Errorf("only one positional argument may be specified, the mixin name, but multiple were received: %s", args)
   135  	}
   136  
   137  	o.MixinName = args[0]
   138  
   139  	if o.AuthorName == "" {
   140  		return errors.New("must provide a value for flag --author")
   141  	}
   142  
   143  	if o.AuthorUsername == "" {
   144  		return errors.New("must provide a value for flag --username")
   145  	}
   146  
   147  	if o.DirPath == "" {
   148  		o.DirPath = cxt.Getwd()
   149  	}
   150  
   151  	if _, err := cxt.FileSystem.Stat(o.DirPath); err != nil {
   152  		return fmt.Errorf("invalid --dir: %s: %w", o.DirPath, err)
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  func (p *Porter) CreateMixin(opts MixinsCreateOptions) error {
   159  	skeletorDestPath := opts.DirPath + "/" + opts.MixinName
   160  
   161  	if err := exec.Command("git", "clone", SkeletorRepo, skeletorDestPath).Run(); err != nil {
   162  		return fmt.Errorf("failed cloning skeletor repo: %w", err)
   163  	}
   164  
   165  	err := os.Rename(skeletorDestPath+"/cmd/skeletor", skeletorDestPath+"/cmd/"+opts.MixinName)
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	err = os.Rename(skeletorDestPath+"/pkg/skeletor", skeletorDestPath+"/pkg/"+opts.MixinName)
   171  	if err != nil {
   172  		return err
   173  	}
   174  
   175  	replacementList := map[string]string{
   176  		"get.porter.sh/mixin/skeletor":       fmt.Sprintf("github.com/%s/%s", opts.AuthorUsername, opts.MixinName),
   177  		"PKG = get.porter.sh/mixin/$(MIXIN)": fmt.Sprintf("PKG = github.com/%s/%s", opts.AuthorUsername, opts.MixinName),
   178  		"skeletor":                           opts.MixinName,
   179  		"YOURNAME":                           opts.AuthorName,
   180  	}
   181  
   182  	for replaced, replacement := range replacementList {
   183  		err := replaceStringInDir(skeletorDestPath, replaced, replacement)
   184  		if err != nil {
   185  			return err
   186  		}
   187  	}
   188  
   189  	fmt.Fprintf(p.Out, "Created %s mixin\n", opts.MixinName)
   190  
   191  	return nil
   192  }
   193  
   194  // replaceStringInDir walks through all the file in a designated directory and replace any occurrence of a string with a particular replacement
   195  // while skipping specifically directory .git and file README.md
   196  func replaceStringInDir(dir, replaced, replacement string) error {
   197  	return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
   198  		if info.IsDir() && info.Name() == ".git" {
   199  			return filepath.SkipDir
   200  		}
   201  		if !info.IsDir() && info.Name() != "README.md" {
   202  			content, err := os.ReadFile(path)
   203  			if err != nil {
   204  				return err
   205  			}
   206  
   207  			err = os.WriteFile(path, bytes.Replace(content, []byte(replaced), []byte(replacement), -1), info.Mode().Perm())
   208  			if err != nil {
   209  				return err
   210  			}
   211  		}
   212  
   213  		return nil
   214  	})
   215  }