Skip to content
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 pkg/shp/cmd/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func Command(p *params.Params, ioStreams *genericclioptions.IOStreams) *cobra.Co

// TODO: add support for `update` and `get` commands
command.AddCommand(
runner.NewRunner(p, ioStreams, updateCmd()).Cmd(),
runner.NewRunner(p, ioStreams, createCmd()).Cmd(),
runner.NewRunner(p, ioStreams, listCmd()).Cmd(),
runner.NewRunner(p, ioStreams, deleteCmd()).Cmd(),
Expand Down
160 changes: 160 additions & 0 deletions pkg/shp/cmd/build/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package build // nolint:revive

import (
"fmt"

buildv1beta1 "github.com/shipwright-io/build/pkg/apis/build/v1beta1"
"github.com/spf13/cobra"

k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"

"github.com/shipwright-io/cli/pkg/shp/cmd/runner"
"github.com/shipwright-io/cli/pkg/shp/flags"
"github.com/shipwright-io/cli/pkg/shp/params"
)

// UpdateCommand contains data provided by user to update an existing Build
type UpdateCommand struct {
cmd *cobra.Command

name string
buildSpec *buildv1beta1.BuildSpec
dockerfile *string
builderImage *string
}

func updateCmd() runner.SubCommand {
cmd := &cobra.Command{
Use: "update <name> [flags]",
Short: "Update an existing Build",
Args: cobra.ExactArgs(1),
}

buildSpecFlags, dockerfileFlag, builderImageFlag := flags.BuildSpecFromFlags(cmd.Flags())

return &UpdateCommand{
cmd: cmd,
buildSpec: buildSpecFlags,
dockerfile: dockerfileFlag,
builderImage: builderImageFlag,
}
}

// Cmd returns cobra command object
func (c *UpdateCommand) Cmd() *cobra.Command {
return c.cmd
}

// Complete fills object with user input data
func (c *UpdateCommand) Complete(_ *params.Params, _ *genericclioptions.IOStreams, args []string) error {
c.name = args[0]
return nil
}

// Validate checks user input data
func (c *UpdateCommand) Validate() error {
if c.name == "" {
return fmt.Errorf("name must be provided")
}
return nil
}

// Run executes the update of an existing Build instance
func (c *UpdateCommand) Run(params *params.Params, ioStreams *genericclioptions.IOStreams) error {
clientset, err := params.ShipwrightClientSet()
if err != nil {
return err
}

ns := params.Namespace()
build, err := clientset.ShipwrightV1beta1().Builds(ns).Get(c.cmd.Context(), c.name, metav1.GetOptions{})
if err != nil {
if k8serrors.IsNotFound(err) {
fmt.Fprintf(ioStreams.Out, "Build '%s' not found in namespace '%s'.\n", c.name, ns)
return nil
}
return err
}

// Update Git Source URL if flag was explicitly provided
if c.cmd.Flags().Changed(flags.SourceGitURLFlag) && c.buildSpec.Source != nil && c.buildSpec.Source.Git != nil {
if build.Spec.Source == nil {
build.Spec.Source = &buildv1beta1.Source{}
}
if build.Spec.Source.Git == nil {
build.Spec.Source.Git = &buildv1beta1.Git{}
}
build.Spec.Source.Git.URL = c.buildSpec.Source.Git.URL
build.Spec.Source.Type = buildv1beta1.GitType
}

// Update Git Revision if flag was explicitly provided
if c.cmd.Flags().Changed(flags.SourceGitRevisionFlag) && c.buildSpec.Source != nil && c.buildSpec.Source.Git != nil {
if build.Spec.Source == nil {
build.Spec.Source = &buildv1beta1.Source{}
}
if build.Spec.Source.Git == nil {
build.Spec.Source.Git = &buildv1beta1.Git{}
}
build.Spec.Source.Git.Revision = c.buildSpec.Source.Git.Revision
}

// Update ContextDir if flag was explicitly provided
if c.cmd.Flags().Changed(flags.SourceContextDirFlag) && c.buildSpec.Source != nil {
if build.Spec.Source == nil {
build.Spec.Source = &buildv1beta1.Source{}
}
build.Spec.Source.ContextDir = c.buildSpec.Source.ContextDir
}

// Update Strategy Name if flag was explicitly provided
if c.cmd.Flags().Changed(flags.StrategyNameFlag) {
build.Spec.Strategy.Name = c.buildSpec.Strategy.Name
}

// Update Strategy Kind if flag was explicitly provided
if c.cmd.Flags().Changed(flags.StrategyKindFlag) {
build.Spec.Strategy.Kind = c.buildSpec.Strategy.Kind
}

// Update Output Image if flag was explicitly provided
if c.cmd.Flags().Changed(flags.OutputImageFlag) {
build.Spec.Output.Image = c.buildSpec.Output.Image
}

// Update dockerfile param if changed
if c.dockerfile != nil && *c.dockerfile != "" && c.cmd.Flags().Changed(flags.DockerfileFlag) {
updateOrAppendParam(&build.Spec.ParamValues, "dockerfile", *c.dockerfile)
}

// Update builder-image param if changed
if c.builderImage != nil && *c.builderImage != "" && c.cmd.Flags().Changed(flags.BuilderImageFlag) {
updateOrAppendParam(&build.Spec.ParamValues, "builder-image", *c.builderImage)
}

flags.SanitizeBuildSpec(&build.Spec)

if _, err := clientset.ShipwrightV1beta1().Builds(ns).Update(c.cmd.Context(), build, metav1.UpdateOptions{}); err != nil {
return err
}

fmt.Fprintf(ioStreams.Out, "Updated build %q\n", c.name)
return nil
}

func updateOrAppendParam(paramValues *[]buildv1beta1.ParamValue, name string, val string) {
for i, p := range *paramValues {
if p.Name == name {
v := val
(*paramValues)[i].SingleValue = &buildv1beta1.SingleValue{Value: &v}
return
}
}
v := val
*paramValues = append(*paramValues, buildv1beta1.ParamValue{
Name: name,
SingleValue: &buildv1beta1.SingleValue{Value: &v},
})
}
116 changes: 116 additions & 0 deletions pkg/shp/cmd/build/update_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package build // nolint:revive

import (
"strings"
"testing"
"time"

buildv1beta1 "github.com/shipwright-io/build/pkg/apis/build/v1beta1"
shpfake "github.com/shipwright-io/build/pkg/client/clientset/versioned/fake"
"github.com/shipwright-io/cli/pkg/shp/flags"
"github.com/shipwright-io/cli/pkg/shp/params"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
kclientsetfake "k8s.io/client-go/kubernetes/fake"
)

func TestBuildUpdate_Success(t *testing.T) {
oldRevision := "v1.0"
testBuild := &buildv1beta1.Build{
ObjectMeta: metav1.ObjectMeta{
Name: "my-build",
Namespace: metav1.NamespaceDefault,
},
Spec: buildv1beta1.BuildSpec{
Source: &buildv1beta1.Source{
Git: &buildv1beta1.Git{
URL: "https://github.com/shipwright-io/old-repo",
Revision: &oldRevision,
},
},
Output: buildv1beta1.Image{
Image: "quay.io/myuser/old-app:v1.0",
},
},
}

shpClientset := shpfake.NewSimpleClientset(testBuild)
k8sClientset := kclientsetfake.NewSimpleClientset()

cmd := updateCmd()
configFlags := genericclioptions.NewConfigFlags(true)
timeout := 10 * time.Second
p := params.NewParamsForTest(k8sClientset, shpClientset, nil, configFlags, metav1.NamespaceDefault, &timeout, &timeout)

ioStreams, _, out, _ := genericclioptions.NewTestIOStreams()

if err := cmd.Complete(p, &ioStreams, []string{"my-build"}); err != nil {
t.Fatalf("unexpected error in Complete: %v", err)
}

// Set updated flag values
if err := cmd.Cmd().Flags().Set(flags.SourceGitURLFlag, "https://github.com/shipwright-io/new-repo"); err != nil {
t.Fatalf("failed to set source-git-url flag: %v", err)
}
if err := cmd.Cmd().Flags().Set(flags.SourceGitRevisionFlag, "v2.0"); err != nil {
t.Fatalf("failed to set source-git-revision flag: %v", err)
}
if err := cmd.Cmd().Flags().Set(flags.OutputImageFlag, "quay.io/myuser/new-app:v2.0"); err != nil {
t.Fatalf("failed to set output-image flag: %v", err)
}

if err := cmd.Run(p, &ioStreams); err != nil {
t.Fatalf("unexpected error in Run: %v", err)
}

output := out.String()
expectedMsg := `Updated build "my-build"`
if !strings.Contains(output, expectedMsg) {
t.Errorf("expected output to contain %q, but got:\n%s", expectedMsg, output)
}

// Verify object fields were updated in client
updatedBuild, err := shpClientset.ShipwrightV1beta1().Builds(metav1.NamespaceDefault).Get(cmd.Cmd().Context(), "my-build", metav1.GetOptions{})
if err != nil {
t.Fatalf("failed to get updated build: %v", err)
}

if updatedBuild.Spec.Source.Git.URL != "https://github.com/shipwright-io/new-repo" {
t.Errorf("expected updated URL 'https://github.com/shipwright-io/new-repo', got %q", updatedBuild.Spec.Source.Git.URL)
}

if *updatedBuild.Spec.Source.Git.Revision != "v2.0" {
t.Errorf("expected updated revision 'v2.0', got %q", *updatedBuild.Spec.Source.Git.Revision)
}

if updatedBuild.Spec.Output.Image != "quay.io/myuser/new-app:v2.0" {
t.Errorf("expected updated output image 'quay.io/myuser/new-app:v2.0', got %q", updatedBuild.Spec.Output.Image)
}
}

func TestBuildUpdate_NotFound(t *testing.T) {
shpClientset := shpfake.NewSimpleClientset()
k8sClientset := kclientsetfake.NewSimpleClientset()

cmd := updateCmd()
configFlags := genericclioptions.NewConfigFlags(true)
timeout := 10 * time.Second
p := params.NewParamsForTest(k8sClientset, shpClientset, nil, configFlags, metav1.NamespaceDefault, &timeout, &timeout)

ioStreams, _, out, _ := genericclioptions.NewTestIOStreams()

if err := cmd.Complete(p, &ioStreams, []string{"nonexistent-build"}); err != nil {
t.Fatalf("unexpected error in Complete: %v", err)
}

if err := cmd.Run(p, &ioStreams); err != nil {
t.Fatalf("unexpected error in Run: %v", err)
}

output := out.String()
expectedMsg := "Build 'nonexistent-build' not found in namespace 'default'."
if !strings.Contains(output, expectedMsg) {
t.Errorf("expected output to contain %q, but got:\n%s", expectedMsg, output)
}
}