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
74 changes: 50 additions & 24 deletions cmd/caib/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,11 +335,11 @@ func validateOutputRequiresPush(output, pushRef, flagName string) {
}
}

func downloadOCIArtifactIfRequested(output, exportOCI, registryUsername, registryPassword string) {
func downloadOCIArtifactIfRequested(output, exportOCI, registryUsername, registryPassword string, insecureSkipTLS bool) {
if output == "" {
return
}
if err := pullOCIArtifact(exportOCI, output, registryUsername, registryPassword); err != nil {
if err := pullOCIArtifact(exportOCI, output, registryUsername, registryPassword, insecureSkipTLS); err != nil {
handleError(fmt.Errorf("failed to download OCI artifact: %w", err))
}
}
Expand Down Expand Up @@ -526,12 +526,12 @@ Example:
buildCmd.Flags().StringVarP(&architecture, "arch", "a", getDefaultArch(), "architecture (amd64, arm64)")
buildCmd.Flags().StringVar(&containerPush, "push", "", "push bootc container to registry (optional if --disk is used)")
buildCmd.Flags().BoolVar(&buildDiskImage, "disk", false, "also build disk image from container")
buildCmd.Flags().StringVarP(&outputDir, "output", "o", "", "download disk image to file from registry (implies --disk; requires --push-disk)")
buildCmd.Flags().StringVarP(&outputDir, "output", "o", "", "download disk image to file from registry (implies --disk; requires --push-disk or --internal-registry)")
buildCmd.Flags().StringVar(
&diskFormat, "format", "", "disk image format (qcow2, raw, simg); inferred from output filename if not set",
)
buildCmd.Flags().StringVar(&compressionAlgo, "compress", "gzip", "compression algorithm (gzip, lz4, xz)")
buildCmd.Flags().StringVar(&exportOCI, "push-disk", "", "push disk image as OCI artifact to registry")
buildCmd.Flags().StringVar(&exportOCI, "push-disk", "", "push disk image as OCI artifact to registry (implies --disk)")
buildCmd.Flags().StringVar(
&automotiveImageBuilder, "aib-image",
"quay.io/centos-sig-automotive/automotive-image-builder:latest", "AIB container image",
Expand Down Expand Up @@ -696,14 +696,17 @@ func validateBootcBuildFlags() {
}

if useInternalRegistry {
if containerPush != "" || exportOCI != "" {
handleError(fmt.Errorf("--internal-registry cannot be used with --push or --push-disk"))
if exportOCI != "" {
handleError(fmt.Errorf("--internal-registry cannot be used with --push-disk"))
}
}

if outputDir != "" && !buildDiskImage {
buildDiskImage = true
}
if exportOCI != "" && !buildDiskImage {
buildDiskImage = true
}
if !useInternalRegistry {
validateOutputRequiresPush(outputDir, exportOCI, "--push-disk")
}
Expand All @@ -716,14 +719,19 @@ func validateBootcBuildFlags() {
}
}

// applyRegistryCredentialsToRequest sets registry credentials on the build request
// when not using the internal registry
// applyRegistryCredentialsToRequest sets registry credentials on the build request.
// When --internal-registry is combined with --push, both are configured so the
// container is pushed externally while the disk image uses the internal registry.
func applyRegistryCredentialsToRequest(req *buildapitypes.BuildRequest) {
if useInternalRegistry {
req.UseInternalRegistry = true
req.InternalRegistryImageName = internalRegistryImageName
req.InternalRegistryTag = internalRegistryTag
return
if containerPush == "" {
return
}
// Hybrid: fall through to also set external registry credentials
// for the container push.
}

effectiveRegistryURL, registryUsername, registryPassword := extractRegistryCredentials(containerPush, exportOCI)
Expand Down Expand Up @@ -782,14 +790,18 @@ func displayBuildResults(ctx context.Context, api *buildapiclient.Client, buildN
fmt.Printf("Disk image: %s\n", st.DiskImage)
}
if st.RegistryToken != "" {
credsFile, err := writeRegistryCredentialsFile(st.RegistryToken)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to write registry credentials file: %v\n", err)
fmt.Printf("\nRegistry credentials (valid ~4 hours):\n")
fmt.Printf(" Username: serviceaccount\n")
fmt.Printf(" Token: %s\n", st.RegistryToken)
if outputDir != "" && st.DiskImage != "" {
downloadOCIArtifactIfRequested(outputDir, st.DiskImage, "serviceaccount", st.RegistryToken, insecureSkipTLS)
} else {
fmt.Printf("\nRegistry credentials written to: %s (valid ~4 hours)\n", credsFile)
credsFile, err := writeRegistryCredentialsFile(st.RegistryToken)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to write registry credentials file: %v\n", err)
fmt.Printf("\nRegistry credentials (valid ~4 hours):\n")
fmt.Printf(" Username: serviceaccount\n")
fmt.Printf(" Token: %s\n", st.RegistryToken)
} else {
fmt.Printf("\nRegistry credentials written to: %s (valid ~4 hours)\n", credsFile)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
} else {
Expand All @@ -800,7 +812,7 @@ func displayBuildResults(ctx context.Context, api *buildapiclient.Client, buildN
fmt.Printf("Disk image pushed to: %s\n", exportOCI)
}
_, registryUsername, registryPassword := extractRegistryCredentials(containerPush, exportOCI)
downloadOCIArtifactIfRequested(outputDir, exportOCI, registryUsername, registryPassword)
downloadOCIArtifactIfRequested(outputDir, exportOCI, registryUsername, registryPassword, insecureSkipTLS)
}
}

Expand Down Expand Up @@ -972,7 +984,7 @@ func runDisk(_ *cobra.Command, args []string) {
displayBuildResults(ctx, api, resp.Name)
}

func pullOCIArtifact(ociRef, destPath, username, password string) error {
func pullOCIArtifact(ociRef, destPath, username, password string, insecureSkipTLS bool) error {
fmt.Printf("Pulling OCI artifact %s to %s\n", ociRef, destPath)

// Ensure output directory exists
Expand Down Expand Up @@ -1002,6 +1014,12 @@ func pullOCIArtifact(ociRef, destPath, username, password string) error {
// - $HOME/.config/containers/auth.json
}

// Configure TLS verification
if insecureSkipTLS {
systemCtx.OCIInsecureSkipTLSVerify = insecureSkipTLS
systemCtx.DockerInsecureSkipTLSVerify = types.OptionalBoolTrue
}

// Set up policy context (allow all)
policy := &signature.Policy{
Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()},
Expand Down Expand Up @@ -2127,15 +2145,23 @@ func runDownload(_ *cobra.Command, args []string) {
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)
// Use API-minted token if available (internal registry builds),
// otherwise fall back to environment credentials.
registryUsername := ""
registryPassword := ""
if st.RegistryToken != "" {
registryUsername = "serviceaccount"
registryPassword = st.RegistryToken
} else {
var effectiveRegistryURL string
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 {
if err := pullOCIArtifact(ociRef, outputDir, registryUsername, registryPassword, insecureSkipTLS); err != nil {
handleError(fmt.Errorf("download failed: %w", err))
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/buildapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ components:
type: string
useInternalRegistry:
type: boolean
description: Push to OpenShift internal registry (mutually exclusive with containerPush, exportOci, registryCredentials)
description: Push disk images to OpenShift internal registry (mutually exclusive with exportOci; can be combined with containerPush for hybrid builds)
internalRegistryImageName:
type: string
description: Override image name for internal registry (default is build name)
Expand Down
52 changes: 38 additions & 14 deletions internal/buildapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,7 @@ func createRegistrySecret(
return "", nil
}

secretName := fmt.Sprintf("%s-registry-auth", buildName)
secretName := fmt.Sprintf("%s-external-registry-auth", buildName)
secretData := make(map[string][]byte)

switch creds.AuthType {
Expand Down Expand Up @@ -1180,7 +1180,23 @@ func (a *APIServer) resolveRegistryForBuild(
namespace string, req *BuildRequest,
) (string, string, error) {
if req.UseInternalRegistry {
return a.setupInternalRegistryBuild(ctx, c, k8sClient, namespace, req)
_, pushSecretName, err := a.setupInternalRegistryBuild(ctx, c, k8sClient, namespace, req)
if err != nil {
return "", "", err
}

// Hybrid: container pushed to external registry, disk to internal.
// Create external registry secret for the container push workspace.
if req.ContainerPush != "" && req.RegistryCredentials != nil && req.RegistryCredentials.Enabled {
envSecretRef, err := createRegistrySecret(ctx, k8sClient, namespace, req.Name, req.RegistryCredentials)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return "", "", err
}
return envSecretRef, pushSecretName, nil
}

return pushSecretName, pushSecretName, nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

envSecretRef, pushSecretName, err := setupBuildSecrets(ctx, k8sClient, namespace, req)
Expand All @@ -1197,13 +1213,11 @@ func (a *APIServer) setupInternalRegistryBuild(
ctx context.Context, c *gin.Context, k8sClient client.Client,
namespace string, req *BuildRequest,
) (string, string, error) {
// Validate mutual exclusivity
if req.ContainerPush != "" || req.ExportOCI != "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "useInternalRegistry cannot be used with containerPush or exportOci"})
return "", "", fmt.Errorf("validation error")
}
if req.RegistryCredentials != nil && req.RegistryCredentials.Enabled {
c.JSON(http.StatusBadRequest, gin.H{"error": "useInternalRegistry cannot be used with registryCredentials"})
// Validate: internal registry handles the disk push, so exportOci must not be set.
// containerPush (and registryCredentials) MAY be set for hybrid builds where
// the bootc container is pushed to an external registry.
if req.ExportOCI != "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "useInternalRegistry cannot be used with exportOci"})
return "", "", fmt.Errorf("validation error")
}
// Resolve external route (validates registry is reachable)
Expand All @@ -1222,10 +1236,14 @@ func (a *APIServer) setupInternalRegistryBuild(
tag = req.Name
}

// Set concrete URLs based on build mode
// Set concrete URLs based on build mode.
// When ContainerPush is already set (hybrid: external container push),
// keep it and only generate internal URLs for what's missing.
externalContainerPush := req.ContainerPush != ""
if req.Mode.IsBootc() {
// Bootc: push container, optionally push disk
req.ContainerPush = generateRegistryImageRef(defaultInternalRegistryURL, namespace, imageName, tag)
if !externalContainerPush {
req.ContainerPush = generateRegistryImageRef(defaultInternalRegistryURL, namespace, imageName, tag)
}
// Flash requires a disk image
if req.FlashEnabled && !req.BuildDiskImage {
req.BuildDiskImage = true
Expand All @@ -1238,11 +1256,17 @@ func (a *APIServer) setupInternalRegistryBuild(
req.ExportOCI = generateRegistryImageRef(defaultInternalRegistryURL, namespace, imageName, tag)
}

// Pre-create ImageStream(s) so the OpenShift internal registry accepts oras pushes
imageStreams := []string{imageName}
// Pre-create ImageStream(s) for internal registry pushes only
var imageStreams []string
if !externalContainerPush {
imageStreams = append(imageStreams, imageName)
}
if req.Mode.IsBootc() && req.BuildDiskImage {
imageStreams = append(imageStreams, imageName+"-disk")
}
if !req.Mode.IsBootc() {
imageStreams = append(imageStreams, imageName)
}
for _, isName := range imageStreams {
if err := ensureImageStream(ctx, k8sClient, namespace, isName); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("error creating ImageStream: %v", err)})
Expand Down
Loading