Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pack): add support for scaffolding new extensions #2041

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions internal/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type PackClient interface {
NewBuildpack(context.Context, client.NewBuildpackOptions) error
PackageBuildpack(ctx context.Context, opts client.PackageBuildpackOptions) error
PackageExtension(ctx context.Context, opts client.PackageBuildpackOptions) error
NewExtension(ctx context.Context, options client.NewExtensionOptions) error
Build(context.Context, client.BuildOptions) error
RegisterBuildpack(context.Context, client.RegisterBuildpackOptions) error
YankBuildpack(client.YankBuildpackOptions) error
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func NewExtensionCommand(logger logging.Logger, cfg config.Config, client PackCl
// client and packageConfigReader to be passed later on
cmd.AddCommand(ExtensionPackage(logger, cfg, client, packageConfigReader))
// client to be passed later on
cmd.AddCommand(ExtensionNew(logger))
cmd.AddCommand(ExtensionNew(logger, client))
cmd.AddCommand(ExtensionPull(logger, cfg, client))
cmd.AddCommand(ExtensionRegister(logger, cfg, client))
cmd.AddCommand(ExtensionYank(logger, cfg, client))
Expand Down
72 changes: 68 additions & 4 deletions internal/commands/extension_new.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
package commands

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

"github.com/spf13/cobra"

"github.com/buildpacks/pack/internal/style"
"github.com/buildpacks/pack/internal/target"
"github.com/buildpacks/pack/pkg/client"
"github.com/buildpacks/pack/pkg/dist"
"github.com/buildpacks/pack/pkg/logging"
)

Expand All @@ -11,25 +22,78 @@
API string
Path string
Stacks []string
Targets []string
Version string
}

// extensioncreator type to be added here and argument also to be added in the function
type ExtensionCreator interface {
NewExtension(ctx context.Context, options client.NewExtensionOptions) error
}

// ExtensionNew generates the scaffolding of an extension
func ExtensionNew(logger logging.Logger) *cobra.Command {
func ExtensionNew(logger logging.Logger, creator ExtensionCreator) *cobra.Command {
var flags ExtensionNewFlags
cmd := &cobra.Command{
Use: "new <id>",
Short: "Creates basic scaffolding of an extension",
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Example: "pack extension new <example-extension>",
RunE: logError(logger, func(cmd *cobra.Command, args []string) error {
// logic will go here
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)

Check warning on line 53 in internal/commands/extension_new.go

View check run for this annotation

Codecov / codecov/patch

internal/commands/extension_new.go#L49-L53

Added lines #L49 - L53 were not covered by tests
} else {
path = flags.Path
}

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

var targets []dist.Target
if len(flags.Targets) == 0 && len(flags.Stacks) == 0 {
targets = []dist.Target{{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
}}
} else {
if targets, err = target.ParseTargets(flags.Targets, logger); err != nil {
return err
}
}

if err := creator.NewExtension(cmd.Context(), client.NewExtensionOptions{
API: flags.API,
ID: id,
Path: path,
Targets: targets,
Version: flags.Version,
}); err != nil {
return err
}

Check warning on line 83 in internal/commands/extension_new.go

View check run for this annotation

Codecov / codecov/patch

internal/commands/extension_new.go#L82-L83

Added lines #L82 - L83 were not covered by tests

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

// flags will go here
cmd.Flags().StringVarP(&flags.API, "api", "a", "0.9", "Buildpack API compatibility of the generated extension")
cmd.Flags().StringVarP(&flags.Path, "path", "p", "", "Path to generate the extension")
cmd.Flags().StringVarP(&flags.Version, "version", "V", "1.0.0", "Version of the generated extension")
cmd.Flags().StringSliceVarP(&flags.Targets, "targets", "t", nil,
`A list of platforms to target; these recorded in extension.toml. One can provide targets in the format [os][/arch][/arch-variant]:[distroname@osversion];[distroname@osversion]
- Example: '--targets "linux/amd64" --targets "linux/arm64"'
- Example (distribution version): '--targets "windows/amd64:[email protected]"'
- Example (architecture with distributed versions): '--targets "linux/arm/v6:[email protected]" --targets "linux/arm/v6:[email protected]"' `)

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

import (
"bytes"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/buildpacks/pack/pkg/client"
"github.com/buildpacks/pack/pkg/dist"
"github.com/buildpacks/pack/pkg/logging"

"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"
h "github.com/buildpacks/pack/testhelpers"
)

func TestExtensionNewCommand(t *testing.T) {
color.Disable(true)
defer color.Disable(false)
spec.Run(t, "ExtensionNewCommand", testExtensionNewCommand, spec.Parallel(), spec.Report(report.Terminal{}))
}

func testExtensionNewCommand(t *testing.T, when spec.G, it spec.S) {
var (
command *cobra.Command
logger *logging.LogWithWriters
outBuf bytes.Buffer
mockController *gomock.Controller
mockClient *testmocks.MockPackClient
tmpDir string
)
targets := []dist.Target{{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
}}

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

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

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

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

when("ExtensionNew#Execute", func() {
it("uses the args to generate artifacts", func() {
mockClient.EXPECT().NewExtension(gomock.Any(), client.NewExtensionOptions{
API: "0.9",
ID: "example/some-cnb",
Path: filepath.Join(tmpDir, "some-cnb"),
Version: "1.0.0",
Targets: targets,
}).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")
})

when("target flag is specified, ", func() {
it("it uses target to generate artifacts", func() {
mockClient.EXPECT().NewExtension(gomock.Any(), client.NewExtensionOptions{
API: "0.9",
ID: "example/targets",
Path: filepath.Join(tmpDir, "targets"),
Version: "1.0.0",
Targets: []dist.Target{{
OS: "linux",
Arch: "arm",
ArchVariant: "v6",
Distributions: []dist.Distribution{{
Name: "ubuntu",
Versions: []string{"14.04", "16.04"},
}},
}},
}).Return(nil).MaxTimes(1)

path := filepath.Join(tmpDir, "targets")
command.SetArgs([]string{"--path", path, "example/targets", "--targets", "linux/arm/v6:[email protected]@16.04"})

err := command.Execute()
h.AssertNil(t, err)
})
it("it should show error when invalid [os]/[arch] passed", func() {
mockClient.EXPECT().NewExtension(gomock.Any(), client.NewExtensionOptions{
API: "0.9",
ID: "example/targets",
Path: filepath.Join(tmpDir, "targets"),
Version: "1.0.0",
Targets: []dist.Target{{
OS: "os",
Arch: "arm",
ArchVariant: "v6",
Distributions: []dist.Distribution{{
Name: "ubuntu",
Versions: []string{"14.04", "16.04"},
}},
}},
}).Return(nil).MaxTimes(1)

path := filepath.Join(tmpDir, "targets")
command.SetArgs([]string{"--path", path, "example/targets", "--targets", "os/arm/v6:[email protected]@16.04"})

err := command.Execute()
h.AssertNotNil(t, err)
})
when("it should", func() {
it("support format [os][/arch][/variant]:[name@version@version2];[some-name@version@version2]", func() {
mockClient.EXPECT().NewExtension(gomock.Any(), client.NewExtensionOptions{
API: "0.9",
ID: "example/targets",
Path: filepath.Join(tmpDir, "targets"),
Version: "1.0.0",
Targets: []dist.Target{
{
OS: "linux",
Arch: "arm",
ArchVariant: "v6",
Distributions: []dist.Distribution{
{
Name: "ubuntu",
Versions: []string{"14.04", "16.04"},
},
{
Name: "debian",
Versions: []string{"8.10", "10.9"},
},
},
},
{
OS: "windows",
Arch: "amd64",
Distributions: []dist.Distribution{
{
Name: "windows-nano",
Versions: []string{"10.0.19041.1415"},
},
},
},
},
}).Return(nil).MaxTimes(1)

path := filepath.Join(tmpDir, "targets")
command.SetArgs([]string{"--path", path, "example/targets", "--targets", "linux/arm/v6:[email protected]@16.04;[email protected]@10.9", "-t", "windows/amd64:[email protected]"})

err := command.Execute()
h.AssertNil(t, err)
})
})
})
})
}
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.

Loading
Loading