Skip to content
Merged
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
21 changes: 19 additions & 2 deletions api/v1alpha1/imagebuild_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ImageBuild phase constants for Status.Phase.
const (
ImageBuildPhasePending = "Pending"
ImageBuildPhaseUploading = "Uploading"
ImageBuildPhaseBuilding = "Building"
ImageBuildPhasePushing = "Pushing"
ImageBuildPhaseFlashing = "Flashing"
ImageBuildPhaseCompleted = "Completed"
ImageBuildPhaseFailed = "Failed"
ImageBuildPhaseCancelled = "Cancelled"
)

// IsTerminalBuildPhase reports whether phase is a final build state.
func IsTerminalBuildPhase(phase string) bool {
return phase == ImageBuildPhaseCompleted || phase == ImageBuildPhaseFailed || phase == ImageBuildPhaseCancelled
}

// ImageBuildSpec defines the desired state of ImageBuild
// +kubebuilder:printcolumn:name="StorageClass",type=string,JSONPath=`.spec.storageClass`
type ImageBuildSpec struct {
Expand Down Expand Up @@ -196,8 +213,8 @@ type ImageBuildStatus struct {
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`

// Phase represents the current phase of the build (Building, Completed, Failed)
// +kubebuilder:validation:Enum=Pending;Uploading;Building;Pushing;Flashing;Completed;Failed
// Phase represents the current phase of the build (Building, Completed, Failed, Cancelled)
// +kubebuilder:validation:Enum=Pending;Uploading;Building;Pushing;Flashing;Completed;Failed;Cancelled
Phase string `json:"phase,omitempty"`
Comment thread
bennyz marked this conversation as resolved.

// StartTime is when the build started
Expand Down
32 changes: 23 additions & 9 deletions cmd/caib/buildcmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"time"

automotivev1alpha1 "github.com/centos-automotive-suite/automotive-dev-operator/api/v1alpha1"
common "github.com/centos-automotive-suite/automotive-dev-operator/cmd/caib/common"
"github.com/centos-automotive-suite/automotive-dev-operator/cmd/caib/registryauth"
buildapitypes "github.com/centos-automotive-suite/automotive-dev-operator/internal/buildapi"
Expand All @@ -19,16 +20,19 @@ import (
)

const (
phaseCompleted = "Completed"
phaseFailed = "Failed"
phaseFlashing = "Flashing"
phasePending = "Pending"
phaseCancelled = automotivev1alpha1.ImageBuildPhaseCancelled
phaseCompleted = automotivev1alpha1.ImageBuildPhaseCompleted
phaseFailed = automotivev1alpha1.ImageBuildPhaseFailed
phaseFlashing = automotivev1alpha1.ImageBuildPhaseFlashing
phasePending = automotivev1alpha1.ImageBuildPhasePending
phaseUploading = automotivev1alpha1.ImageBuildPhaseUploading
phaseRunning = "Running"
phaseUploading = "Uploading"

errPrefixFlash = "flash"
)

var isTerminalPhase = automotivev1alpha1.IsTerminalBuildPhase

// Options wires build handlers to caller-owned state and helper functions.
type Options struct {
ServerURL *string
Expand Down Expand Up @@ -818,9 +822,19 @@ func (h *Handler) handleFileUploads(

// RunDelete handles `caib image delete`.
func (h *Handler) RunDelete(_ *cobra.Command, args []string) {
ctx := context.Background()
buildName := args[0]
h.runBuildAction(args[0], "deleted", func(ctx context.Context, api *buildapiclient.Client, name string) error {
return api.DeleteBuild(ctx, name)
})
}

// RunCancel handles `caib image cancel`.
func (h *Handler) RunCancel(_ *cobra.Command, args []string) {
h.runBuildAction(args[0], "cancelled", func(ctx context.Context, api *buildapiclient.Client, name string) error {
return api.CancelBuild(ctx, name)
})
}

func (h *Handler) runBuildAction(buildName, verb string, action func(context.Context, *buildapiclient.Client, string) error) {
if strings.TrimSpace(*h.opts.ServerURL) == "" {
h.handleError(fmt.Errorf("server URL required (use --server, CAIB_SERVER, run 'caib login <server-url>' or 'jmp login <endpoint>')"))
return
Expand All @@ -832,10 +846,10 @@ func (h *Handler) RunDelete(_ *cobra.Command, args []string) {
return
}

if err := api.DeleteBuild(ctx, buildName); err != nil {
if err := action(context.Background(), api, buildName); err != nil {
h.handleError(err)
return
}

fmt.Printf("Build %q deleted\n", buildName)
fmt.Printf("Build %q %s\n", buildName, verb)
}
7 changes: 6 additions & 1 deletion cmd/caib/buildcmd/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ func (h *Handler) waitForBuildCompletion(ctx context.Context, api *buildapiclien
}
return nil
}
if st.Phase == phaseCancelled {
pb.Clear()
fmt.Println("Build was cancelled.")
return fmt.Errorf("build cancelled")
}
Comment thread
bennyz marked this conversation as resolved.
if st.Phase == phaseFailed {
pb.Clear()
isFlashFailure := strings.Contains(strings.ToLower(st.Message), errPrefixFlash) ||
Expand Down Expand Up @@ -273,7 +278,7 @@ func (h *Handler) RunLogs(_ *cobra.Command, args []string) {
}
fmt.Printf("Build %s: %s - %s\n", name, st.Phase, st.Message)

if st.Phase == phaseCompleted || st.Phase == phaseFailed {
if isTerminalPhase(st.Phase) {
logTransport := &http.Transport{
ResponseHeaderTimeout: 30 * time.Second,
}
Expand Down
29 changes: 29 additions & 0 deletions cmd/caib/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Options struct {
RunInjectSigned func(*cobra.Command, []string)
RunToken func(*cobra.Command, []string)
RunDelete func(*cobra.Command, []string)
RunCancel func(*cobra.Command, []string)

GetDefaultArch func() string

Expand Down Expand Up @@ -107,6 +108,7 @@ func NewImageCmd(opts Options) *cobra.Command {

tokenCmd := newTokenCmd(opts)
deleteCmd := newDeleteCmd(opts)
cancelCmd := newCancelCmd(opts)

prepareResealCmd := newPrepareResealCmd(opts)
resealCmd := newResealCmd(opts)
Expand Down Expand Up @@ -282,6 +284,10 @@ func NewImageCmd(opts Options) *cobra.Command {
deleteCmd.Flags().StringVar(opts.ServerURL, "server", defaultServer, "REST API server base URL")
deleteCmd.Flags().StringVar(opts.AuthToken, "token", os.Getenv("CAIB_TOKEN"), "Bearer token for authentication")

// cancel command flags
cancelCmd.Flags().StringVar(opts.ServerURL, "server", defaultServer, "REST API server base URL")
cancelCmd.Flags().StringVar(opts.AuthToken, "token", os.Getenv("CAIB_TOKEN"), "Bearer token for authentication")

// flash command flags
flashCmd.Flags().StringVar(opts.ServerURL, "server", defaultServer, "REST API server base URL")
flashCmd.Flags().StringVar(opts.AuthToken, "token", os.Getenv("CAIB_TOKEN"), "Bearer token for authentication")
Expand Down Expand Up @@ -317,6 +323,7 @@ func NewImageCmd(opts Options) *cobra.Command {
logsCmd,
tokenCmd,
deleteCmd,
cancelCmd,
flashCmd,
prepareResealCmd,
resealCmd,
Expand Down Expand Up @@ -523,6 +530,28 @@ Examples:
}
}

func newCancelCmd(opts Options) *cobra.Command {
return &cobra.Command{
Use: "cancel <build-name>",
Short: "Cancel an in-progress build",
Long: `Cancel stops an in-progress build by cancelling its Tekton PipelineRun.
The ImageBuild resource is preserved so you can inspect its logs and status.

Only builds in Pending, Uploading, or Building phase can be cancelled.
You can only cancel builds that you created.

Examples:
# Cancel a running build
caib image cancel my-build

# List builds first, then cancel one
caib image list
caib image cancel <build-name>`,
Args: cobra.ExactArgs(1),
Run: opts.RunCancel,
}
}

func newPrepareResealCmd(opts Options) *cobra.Command {
return &cobra.Command{
Use: "prepare-reseal [source-container] [output-container]",
Expand Down
1 change: 1 addition & 0 deletions cmd/caib/runtime_wiring.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ func (s runtimeState) imageOptions(h handlerSet) image.Options {
RunInjectSigned: h.sealed.RunInjectSigned,
RunToken: h.token.RunToken,
RunDelete: h.build.RunDelete,
RunCancel: h.build.RunCancel,
GetDefaultArch: getDefaultArch,

ServerURL: s.ServerURL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ spec:
type: integer
phase:
description: Phase represents the current phase of the build (Building,
Completed, Failed)
Completed, Failed, Cancelled)
enum:
- Pending
- Uploading
Expand All @@ -322,6 +322,7 @@ spec:
- Flashing
- Completed
- Failed
- Cancelled
type: string
pipelineRunName:
description: PipelineRunName is the name of the active PipelineRun
Expand Down
14 changes: 11 additions & 3 deletions internal/buildapi/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,16 @@ func (c *Client) GetBuild(ctx context.Context, name string) (*buildapi.BuildResp

// DeleteBuild deletes an image build by name.
func (c *Client) DeleteBuild(ctx context.Context, name string) error {
endpoint := c.resolve(path.Join("/v1/builds", url.PathEscape(name)))
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
return c.doBuildAction(ctx, http.MethodDelete, path.Join("/v1/builds", url.PathEscape(name)), "delete build")
}

// CancelBuild cancels an in-progress image build by name.
func (c *Client) CancelBuild(ctx context.Context, name string) error {
return c.doBuildAction(ctx, http.MethodPost, path.Join("/v1/builds", url.PathEscape(name), "cancel"), "cancel build")
}

func (c *Client) doBuildAction(ctx context.Context, method, endpoint, action string) error {
req, err := http.NewRequestWithContext(ctx, method, c.resolve(endpoint), nil)
if err != nil {
return err
}
Expand All @@ -185,7 +193,7 @@ func (c *Client) DeleteBuild(ctx context.Context, name string) error {
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return fmt.Errorf("delete build failed: %s: %s", resp.Status, string(b))
return fmt.Errorf("%s failed: %s: %s", action, resp.Status, string(b))
}
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions internal/buildapi/container_builds.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func (a *APIServer) streamContainerBuildLogs(c *gin.Context, name string) {
// Check if build is complete AND all pod logs have been streamed
if allPodsComplete {
if err := k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, cb); err == nil {
if cb.Status.Phase == phaseCompleted || cb.Status.Phase == phaseFailed {
if isTerminalPhase(cb.Status.Phase) {
break
}
}
Expand Down Expand Up @@ -459,7 +459,7 @@ func (a *APIServer) getContainerBuild(c *gin.Context, name string) {
buildOwner := cb.Annotations["automotive.sdv.cloud.redhat.com/requested-by"]
if requester == buildOwner &&
cb.Spec.UseServiceAccountAuth &&
(cb.Status.Phase == phaseCompleted || cb.Status.Phase == phaseFailed) {
isTerminalPhase(cb.Status.Phase) {
token, _, tokenErr := a.mintRegistryToken(ctx, c, namespace)
if tokenErr != nil {
a.log.Error(tokenErr, "failed to mint registry token for container build", "build", name)
Expand Down
Loading
Loading