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
81 changes: 75 additions & 6 deletions cmd/caib/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ import (
)

const (
archAMD64 = "amd64"
archARM64 = "arm64"
phaseFailed = "Failed"
archAMD64 = "amd64"
archARM64 = "arm64"
phaseCompleted = "Completed"
phaseFailed = "Failed"
)

// getDefaultArch returns the current system architecture in caib format
Expand Down Expand Up @@ -402,6 +403,25 @@ Examples:
Run: runList,
}

downloadCmd := &cobra.Command{
Use: "download <build-name>",
Short: "Download disk image artifact from a completed build",
Long: `Download retrieves the disk image artifact from a completed build.

The build must have pushed a disk image to an OCI registry (via --push-disk
or --push on disk/build-dev commands). The artifact is pulled from the
registry to a local file.

Examples:
# Download disk image from a completed build
caib download my-build -o ./disk.qcow2

# Download to a directory (multi-layer artifacts extract here)
caib download my-build -o ./output/`,
Args: cobra.ExactArgs(1),
Run: runDownload,
}

loginCmd := &cobra.Command{
Use: "login [server-url]",
Short: "Save server endpoint and authenticate for subsequent commands",
Expand Down Expand Up @@ -509,6 +529,11 @@ Example:
buildDevCmd.Flags().StringVar(&jumpstarterClient, "client", "", "path to Jumpstarter client config file (required for --flash)")
buildDevCmd.Flags().StringVar(&leaseDuration, "lease", "03:00:00", "device lease duration for flash (HH:MM:SS)")

// download command flags
downloadCmd.Flags().StringVar(&serverURL, "server", config.DefaultServer(), "REST API server base URL")
downloadCmd.Flags().StringVar(&authToken, "token", os.Getenv("CAIB_TOKEN"), "Bearer token for authentication")
downloadCmd.Flags().StringVarP(&outputDir, "output", "o", "", "destination file or directory for the artifact")

// flash command flags
flashCmd.Flags().StringVar(&serverURL, "server", config.DefaultServer(), "REST API server base URL")
flashCmd.Flags().StringVar(&authToken, "token", os.Getenv("CAIB_TOKEN"), "Bearer token for authentication")
Expand All @@ -522,7 +547,7 @@ Example:
_ = flashCmd.MarkFlagRequired("client")

// Add all commands
rootCmd.AddCommand(buildCmd, diskCmd, buildDevCmd, listCmd, flashCmd, loginCmd, catalog.NewCatalogCmd())
rootCmd.AddCommand(buildCmd, diskCmd, buildDevCmd, listCmd, downloadCmd, flashCmd, loginCmd, catalog.NewCatalogCmd())
// Add deprecated aliases for backwards compatibility
rootCmd.AddCommand(buildBootcAliasCmd, buildLegacyAliasCmd, buildTraditionalAliasCmd)

Expand Down Expand Up @@ -1317,7 +1342,7 @@ func waitForBuildCompletion(ctx context.Context, api *buildapiclient.Client, nam
}

// Handle terminal build states
if st.Phase == "Completed" {
if st.Phase == phaseCompleted {
flashWasExecuted := strings.Contains(st.Message, "flash")
if flashWasExecuted {
fmt.Println("\n" + strings.Repeat("=", 50))
Expand Down Expand Up @@ -1744,6 +1769,50 @@ func runList(_ *cobra.Command, _ []string) {
}
}

func runDownload(_ *cobra.Command, args []string) {
ctx := context.Background()
downloadBuildName := args[0]

if serverURL == "" {
handleError(fmt.Errorf("--server is required (or set CAIB_SERVER, or run 'caib login <server-url>')"))
}

if outputDir == "" {
handleError(fmt.Errorf("--output / -o is required"))
}

var st *buildapitypes.BuildResponse
err := executeWithReauth(serverURL, &authToken, func(api *buildapiclient.Client) error {
var err error
st, err = api.GetBuild(ctx, downloadBuildName)
return err
})
if err != nil {
handleError(fmt.Errorf("error getting build %s: %w", downloadBuildName, err))
}

if st.Phase != phaseCompleted {
handleError(fmt.Errorf("build %s is not completed (phase: %s), cannot download artifacts", downloadBuildName, st.Phase))
}

ociRef := st.DiskImage
if ociRef == "" {
handleError(fmt.Errorf("build %s has no disk image artifact to download (no OCI export was configured)", downloadBuildName))
}

// Extract registry credentials from environment
effectiveRegistryURL, registryUsername, registryPassword := extractRegistryCredentials(ociRef, "")

if err := validateRegistryCredentials(effectiveRegistryURL, registryUsername, registryPassword); err != nil {
handleError(err)
}

fmt.Printf("Downloading disk image from %s\n", ociRef)
if err := pullOCIArtifact(ociRef, outputDir, registryUsername, registryPassword); err != nil {
handleError(fmt.Errorf("download failed: %w", err))
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func loadTokenFromKubeconfig() (string, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// First, ask client-go to build a client config. This will execute any exec credential plugins
Expand Down Expand Up @@ -1912,7 +1981,7 @@ func waitForFlashCompletion(ctx context.Context, api *buildapiclient.Client, nam
}

// Handle terminal states
if st.Phase == "Completed" {
if st.Phase == phaseCompleted {
fmt.Println("Flash completed successfully!")
return
}
Expand Down
4 changes: 3 additions & 1 deletion internal/buildapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1277,7 +1277,9 @@ func getBuild(c *gin.Context, name string) {
}
return ""
}(),
Jumpstarter: jumpstarterInfo,
ContainerImage: build.Spec.GetContainerPush(),
DiskImage: build.Spec.GetExportOCI(),
Jumpstarter: jumpstarterInfo,
})
}

Expand Down
2 changes: 2 additions & 0 deletions internal/buildapi/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ type BuildResponse struct {
RequestedBy string `json:"requestedBy,omitempty"`
StartTime string `json:"startTime,omitempty"`
CompletionTime string `json:"completionTime,omitempty"`
ContainerImage string `json:"containerImage,omitempty"`
DiskImage string `json:"diskImage,omitempty"`
Jumpstarter *JumpstarterInfo `json:"jumpstarter,omitempty"`
}

Expand Down
19 changes: 8 additions & 11 deletions internal/controller/operatorconfig/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,9 @@ func (r *OperatorConfigReconciler) buildBuildAPIContainers(isOpenShift bool) []c
}
containers := []corev1.Container{
{
Name: "build-api",
Image: getOperatorImage(),
ImagePullPolicy: corev1.PullIfNotPresent,
Command: []string{"/build-api"},
Name: "build-api",
Image: getOperatorImage(),
Command: []string{"/build-api"},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
Expand Down Expand Up @@ -108,9 +107,8 @@ func (r *OperatorConfigReconciler) buildBuildAPIContainers(isOpenShift bool) []c
// Only add oauth-proxy on OpenShift
if isOpenShift {
containers = append(containers, corev1.Container{
Name: "oauth-proxy",
Image: "registry.redhat.io/openshift4/ose-oauth-proxy:latest",
ImagePullPolicy: corev1.PullIfNotPresent,
Name: "oauth-proxy",
Image: "registry.redhat.io/openshift4/ose-oauth-proxy:latest",
Args: []string{
"--provider=openshift",
"--https-address=",
Expand Down Expand Up @@ -199,10 +197,9 @@ func (r *OperatorConfigReconciler) buildBuildAPIDeployment(isOpenShift bool) *ap
ServiceAccountName: "ado-controller-manager",
InitContainers: []corev1.Container{
{
Name: "init-secrets",
Image: getOperatorImage(),
ImagePullPolicy: corev1.PullIfNotPresent,
Command: []string{"/init-secrets"},
Name: "init-secrets",
Image: getOperatorImage(),
Command: []string{"/init-secrets"},
Env: []corev1.EnvVar{
{
Name: "POD_NAMESPACE",
Expand Down
Loading