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 }