Skip to content

Commit

Permalink
Merge pull request #1025 from buildpacks/jkutner/new-buildpack-toml-keys
Browse files Browse the repository at this point in the history
Add buildpack create command to generate new buildpack scaffolding
  • Loading branch information
jkutner authored Mar 4, 2021
2 parents 658dec2 + 7fee3ae commit f708917
Show file tree
Hide file tree
Showing 10 changed files with 457 additions and 5 deletions.
2 changes: 1 addition & 1 deletion internal/buildpackage/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
// buildpackage metadata
h.ContentContains(`"io.buildpacks.buildpackage.metadata":"{\"id\":\"bp.1.id\",\"version\":\"bp.1.version\",\"stacks\":[{\"id\":\"stack.id.1\"},{\"id\":\"stack.id.2\"}]}"`),
// buildpack layers metadata
h.ContentContains(`"io.buildpacks.buildpack.layers":"{\"bp.1.id\":{\"bp.1.version\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"stack.id.1\"},{\"id\":\"stack.id.2\"}],\"layerDiffID\":\"sha256:a10862daec7a8a62fd04cc5d4520fdb80d4d5c07a3c146fb604a9c23c22fd5b0\"}}}"`),
h.ContentContains(`"io.buildpacks.buildpack.layers":"{\"bp.1.id\":{\"bp.1.version\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"stack.id.1\"},{\"id\":\"stack.id.2\"}],\"layerDiffID\":\"sha256:9fa0bb03eebdd0f8e4b6d6f50471b44be83dba750624dfce15dac45975c5707b\"}}`),
// image os
h.ContentContains(`"os":"linux"`),
)
Expand Down
1 change: 1 addition & 0 deletions internal/commands/buildpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func NewBuildpackCommand(logger logging.Logger, cfg config.Config, client PackCl
}

cmd.AddCommand(BuildpackPackage(logger, cfg, client, packageConfigReader))
cmd.AddCommand(BuildpackNew(logger, client))
cmd.AddCommand(BuildpackPull(logger, cfg, client))
cmd.AddCommand(BuildpackRegister(logger, cfg, client))
cmd.AddCommand(BuildpackYank(logger, cfg, client))
Expand Down
92 changes: 92 additions & 0 deletions internal/commands/buildpack_new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package commands

import (
"context"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/spf13/cobra"

"github.com/buildpacks/pack"
"github.com/buildpacks/pack/internal/build"
"github.com/buildpacks/pack/internal/dist"
"github.com/buildpacks/pack/internal/style"
"github.com/buildpacks/pack/logging"
)

// BuildpackNewFlags define flags provided to the BuildpackNew command
type BuildpackNewFlags struct {
API string
Path string
Stacks []string
Version string
}

// BuildpackCreator creates buildpacks
type BuildpackCreator interface {
NewBuildpack(ctx context.Context, options pack.NewBuildpackOptions) error
}

// BuildpackNew generates the scaffolding of a buildpack
func BuildpackNew(logger logging.Logger, client BuildpackCreator) *cobra.Command {
var flags BuildpackNewFlags
cmd := &cobra.Command{
Use: "new <id>",
Short: "Creates basic scaffolding of a buildpack.",
Args: cobra.ExactValidArgs(1),
Example: "pack buildpack new sample/my-buildpack",
Long: "buildpack new generates the basic scaffolding of a buildpack repository. It creates a new directory `name` in the current directory (or at `path`, if passed as a flag), and initializes a buildpack.toml, and two executable bash scripts, `bin/detect` and `bin/build`. ",
RunE: logError(logger, func(cmd *cobra.Command, args []string) error {
id := args[0]
idParts := strings.Split(id, "/")
dirName := idParts[len(idParts)-1]

var path string
if len(flags.Path) == 0 {
cwd, err := os.Getwd()
if err != nil {
return err
}
path = filepath.Join(cwd, dirName)
} else {
path = flags.Path
}

_, err := os.Stat(path)
if !os.IsNotExist(err) {
return fmt.Errorf("directory %s exists", style.Symbol(path))
}

var stacks []dist.Stack
for _, s := range flags.Stacks {
stacks = append(stacks, dist.Stack{
ID: s,
Mixins: []string{},
})
}

if err := client.NewBuildpack(cmd.Context(), pack.NewBuildpackOptions{
API: flags.API,
ID: id,
Path: path,
Stacks: stacks,
Version: flags.Version,
}); err != nil {
return err
}

logger.Infof("Successfully created %s", style.Symbol(id))
return nil
}),
}

cmd.Flags().StringVarP(&flags.API, "api", "a", build.SupportedPlatformAPIVersions.Latest().String(), "Buildpack API compatibility of the generated buildpack")
cmd.Flags().StringVarP(&flags.Path, "path", "p", "", "Path to generate the buildpack")
cmd.Flags().StringVarP(&flags.Version, "version", "V", "1.0.0", "Version of the generated buildpack")
cmd.Flags().StringSliceVarP(&flags.Stacks, "stacks", "s", []string{"io.buildpacks.stacks.bionic"}, "Stack(s) this buildpack will be compatible with"+multiValueHelp("stack"))

AddHelpFlag(cmd, "new")
return cmd
}
87 changes: 87 additions & 0 deletions internal/commands/buildpack_new_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package commands_test

import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/buildpacks/pack"
"github.com/buildpacks/pack/internal/dist"

"github.com/golang/mock/gomock"
"github.com/heroku/color"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
"github.com/spf13/cobra"

"github.com/buildpacks/pack/internal/commands"
"github.com/buildpacks/pack/internal/commands/testmocks"
ilogging "github.com/buildpacks/pack/internal/logging"
h "github.com/buildpacks/pack/testhelpers"
)

func TestBuildpackNewCommand(t *testing.T) {
color.Disable(true)
defer color.Disable(false)
spec.Run(t, "BuildpackNewCommand", testBuildpackNewCommand, spec.Parallel(), spec.Report(report.Terminal{}))
}

func testBuildpackNewCommand(t *testing.T, when spec.G, it spec.S) {
var (
command *cobra.Command
logger *ilogging.LogWithWriters
outBuf bytes.Buffer
mockController *gomock.Controller
mockClient *testmocks.MockPackClient
tmpDir string
)

it.Before(func() {
var err error
tmpDir, err = ioutil.TempDir("", "build-test")
h.AssertNil(t, err)

logger = ilogging.NewLogWithWriters(&outBuf, &outBuf)
mockController = gomock.NewController(t)
mockClient = testmocks.NewMockPackClient(mockController)

command = commands.BuildpackNew(logger, mockClient)
})

it.After(func() {
os.RemoveAll(tmpDir)
})

when("BuildpackNew#Execute", func() {
it("uses the args to generate artifacts", func() {
mockClient.EXPECT().NewBuildpack(gomock.Any(), pack.NewBuildpackOptions{
API: "0.4",
ID: "example/some-cnb",
Path: filepath.Join(tmpDir, "some-cnb"),
Version: "1.0.0",
Stacks: []dist.Stack{{
ID: "io.buildpacks.stacks.bionic",
Mixins: []string{},
}},
}).Return(nil).MaxTimes(1)

path := filepath.Join(tmpDir, "some-cnb")
command.SetArgs([]string{"--path", path, "example/some-cnb"})

err := command.Execute()
h.AssertNil(t, err)
})

it("stops if the directory already exists", func() {
err := os.MkdirAll(tmpDir, 0600)
h.AssertNil(t, err)

command.SetArgs([]string{"--path", tmpDir, "example/some-cnb"})
err = command.Execute()
h.AssertNotNil(t, err)
h.AssertContains(t, outBuf.String(), "ERROR: directory")
})
})
}
1 change: 1 addition & 0 deletions internal/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type PackClient interface {
InspectImage(string, bool) (*pack.ImageInfo, error)
Rebase(context.Context, pack.RebaseOptions) error
CreateBuilder(context.Context, pack.CreateBuilderOptions) error
NewBuildpack(context.Context, pack.NewBuildpackOptions) error
PackageBuildpack(ctx context.Context, opts pack.PackageBuildpackOptions) error
Build(context.Context, pack.BuildOptions) error
RegisterBuildpack(context.Context, pack.RegisterBuildpackOptions) error
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions internal/commands/testmocks/mock_pack_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions internal/dist/buildpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ func (b BuildpackInfo) Match(o BuildpackInfo) bool {
}

type Stack struct {
ID string `json:"id"`
Mixins []string `json:"mixins,omitempty"`
ID string `json:"id" toml:"id"`
Mixins []string `json:"mixins,omitempty" toml:"mixins,omitempty"`
}

// BuildpackFromBlob constructs a buildpack from a blob. It is assumed that the buildpack
Expand Down
119 changes: 119 additions & 0 deletions new_buildpack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package pack

import (
"context"
"io/ioutil"
"os"
"path/filepath"

"github.com/BurntSushi/toml"

"github.com/buildpacks/lifecycle/api"

"github.com/buildpacks/pack/internal/dist"
"github.com/buildpacks/pack/internal/style"
)

var (
bashBinBuild = `
#!/usr/bin/env bash
set -euo pipefail
layers_dir="$1"
env_dir="$2/env"
plan_path="$3"
exit 0
`
bashBinDetect = `
#!/usr/bin/env bash
exit 0
`
)

type NewBuildpackOptions struct {
// api compat version of the output buildpack artifact.
API string

// The base directory to generate assets
Path string

// The ID of the output buildpack artifact.
ID string

// version of the output buildpack artifact.
Version string

// The stacks this buildpack will work with
Stacks []dist.Stack
}

func (c *Client) NewBuildpack(ctx context.Context, opts NewBuildpackOptions) error {
api, err := api.NewVersion(opts.API)
if err != nil {
return err
}

buildpackTOML := dist.BuildpackDescriptor{
API: api,
Stacks: opts.Stacks,
Info: dist.BuildpackInfo{
ID: opts.ID,
Version: opts.Version,
},
}

if err := os.MkdirAll(opts.Path, 0755); err != nil {
return err
}

buildpackTOMLPath := filepath.Join(opts.Path, "buildpack.toml")
_, err = os.Stat(buildpackTOMLPath)
if os.IsNotExist(err) {
f, err := os.Create(buildpackTOMLPath)
if err != nil {
return err
}
if err := toml.NewEncoder(f).Encode(buildpackTOML); err != nil {
return err
}
defer f.Close()
c.logger.Infof(" %s buildpack.toml", style.Symbol("create"))
}

return createBashBuildpack(opts.Path, c)
}

func createBashBuildpack(path string, c *Client) error {
if err := createBinScript(path, "build", bashBinBuild, c); err != nil {
return err
}

if err := createBinScript(path, "detect", bashBinDetect, c); err != nil {
return err
}

return nil
}

func createBinScript(path, name, contents string, c *Client) error {
binDir := filepath.Join(path, "bin")
binFile := filepath.Join(binDir, name)

_, err := os.Stat(binFile)
if os.IsNotExist(err) {
if err := os.MkdirAll(binDir, 0755); err != nil {
return err
}

err = ioutil.WriteFile(binFile, []byte(contents), 0755)
if err != nil {
return err
}

c.logger.Infof(" %s bin/%s", style.Symbol("create"), name)
}
return nil
}
Loading

0 comments on commit f708917

Please sign in to comment.