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
3 changes: 2 additions & 1 deletion docs/appliance-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
| userCorePass | | Yes | string | Password of user 'core' for connecting from console. |
| imageRegistry | | Yes | | Local image registry details (used when building the appliance) |
| imageRegistry.uri | | Yes | string | The URI for the image. |
| imageRegistry.port | 5005 | Yes | integer | The image registry container TCP port to bind. A valid port number is between 1024 and 65535. |
| imageRegistry.port | 5005 | Yes | integer | The image registry container TCP port to bind. A valid port number is between 1024 and 65535. |
| mirrorPath | | Yes | string | Path to pre-mirrored images from oc-mirror workspace. When provided, skips image mirroring and uses the pre-mirrored registry data. The path should point to an oc-mirror workspace directory containing a 'data' subdirectory. |
| stopLocalRegistry | false | Yes | bool | Stop the local registry post cluster installation. Note that additional images and operators won't be available when stopped. |
| createPinnedImageSets | false | Yes | bool | Create PinnedImageSets for both the master and worker MCPs. The PinnedImageSets will include all the images included in the appliance disk image. Requires openshift version 4.16 or above. **WARNING:** As of 4.18, PinnedImageSets feature is still not GA. Thus, enabling it will set the cluster to tech preview, which means the cluster cannot be upgraded (i.e. should only be used for testing purposes). |
| enableDefaultSources | false | Yes | bool | Enable all default CatalogSources (on openshift-marketplace namespace). Should be disabled for disconnected environments. |
Expand Down
21 changes: 21 additions & 0 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,27 @@ metadata:
namespace: "openshift-cnv"
```

### Using pre-mirrored images (Optional)

If you have already mirrored OCP release images using `oc-mirror` on another system, you can skip the mirroring step during the build by providing the path to the oc-mirror workspace.

Add the `mirrorPath` field to your `appliance-config.yaml`:
```yaml
apiVersion: v1beta1
kind: ApplianceConfig
ocpRelease:
version: 4.14
channel: candidate
cpuArchitecture: x86_64
diskSizeGB: 200
pullSecret: '{"auths":{<redacted>}}'
sshKey: <redacted>
# Path to oc-mirror workspace directory containing mirrored images
mirrorPath: /path/to/mirror/workspace
```

The `mirrorPath` should point to the directory created by `oc-mirror` that contains a `data` subdirectory with the mirrored registry data.

### Build the disk image
* Make sure you have enough free disk space.
* The amount of space needed is defined by the configured `diskSizeGB` value mentioned above, which is at least 150GiB.
Expand Down
48 changes: 47 additions & 1 deletion pkg/asset/config/appliance_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ pullSecret: pull-secret
# [Optional]
# useBinary: %t

# Path to pre-mirrored images from oc-mirror workspace.
# When provided, skips image mirroring and uses the pre-mirrored registry data.
# The path should point to an oc-mirror workspace directory containing a 'data' subdirectory.
# [Optional]
# mirrorPath: /path/to/mirror/workspace

# Enable all default CatalogSources (on openshift-marketplace namespace).
# Should be disabled for disconnected environments.
# Default: false
Expand Down Expand Up @@ -378,7 +384,7 @@ func (a *ApplianceConfig) GetRelease() (string, string, error) {
return "", "", nil
}
releaseDigest = strings.Trim(releaseDigest, "'")
releaseImage = fmt.Sprintf("%s@%s", strings.Split(releaseImage, ":")[0], releaseDigest)
releaseImage = fmt.Sprintf("%s@%s", releaseImage, releaseDigest)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this change needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was to handle this error:
FATAL failed to fetch Appliance live ISO: failed to fetch dependency of "Appliance live ISO": failed to generate asset "Base ISO (CoreOS)": Failed to execute cmd (/usr/local/bin/oc adm release info --image-for=machine-os-images --insecure=true virthost.ostest.test.metalkube.org@sha256:dc120ba1d210d398da3bcd712c9a7bccf0086c1b7fde42fca8b186d7d16f3fa8): error: unable to connect to image repository : invalid reference format
The problem is the custom release image has a url of virthost.ostest.test.metalkube.org:5000/localimages/local-release-image:latest which contains two colons. The original code takes the first part losing the port number.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, so maybe do this change only when using a custom release image?
I mean to avoid breaking the flow for other images without port...

}
logrus.Debugf("Release image: %s", releaseImage)
}
Expand Down Expand Up @@ -430,6 +436,11 @@ func (a *ApplianceConfig) validateConfig(f asset.FileFetcher) field.ErrorList {
}
}

// Validate mirrorPath
if err := a.validateMirrorPath(); err != nil {
allErrs = append(allErrs, err...)
}

return allErrs
}

Expand Down Expand Up @@ -553,6 +564,41 @@ func (a *ApplianceConfig) validatePinnedImageSet() error {
return nil
}

func (a *ApplianceConfig) validateMirrorPath() field.ErrorList {
allErrs := field.ErrorList{}

if a.Config.MirrorPath != nil {
mirrorPath := swag.StringValue(a.Config.MirrorPath)
if mirrorPath != "" {
// Validate mirror path exists and is a directory
info, err := os.Stat(mirrorPath)
if err != nil {
if os.IsNotExist(err) {
allErrs = append(allErrs, field.Invalid(field.NewPath("mirrorPath"),
mirrorPath, "mirror path does not exist"))
} else {
allErrs = append(allErrs, field.Invalid(field.NewPath("mirrorPath"),
mirrorPath, fmt.Sprintf("failed to access mirror path: %v", err)))
}
} else if !info.IsDir() {
allErrs = append(allErrs, field.Invalid(field.NewPath("mirrorPath"),
mirrorPath, "mirror path must be a directory"))
} else {
// Validate data subdirectory exists
dataDir := filepath.Join(mirrorPath, "data")
if _, err := os.Stat(dataDir); err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("mirrorPath"),
mirrorPath, "mirror path must contain a 'data' subdirectory (expected oc-mirror workspace structure)"))
}
}

logrus.Infof("Using pre-mirrored images from: %s", mirrorPath)
}
}

return allErrs
}

func (a *ApplianceConfig) storePullSecret() error {
// Get home dir (~)
homeDir, err := os.UserHomeDir()
Expand Down
35 changes: 34 additions & 1 deletion pkg/asset/data/data_iso.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/go-openapi/swag"
"github.com/openshift/appliance/pkg/asset/config"
"github.com/openshift/appliance/pkg/consts"
"github.com/openshift/appliance/pkg/executer"
"github.com/openshift/appliance/pkg/genisoimage"
"github.com/openshift/appliance/pkg/log"
"github.com/openshift/appliance/pkg/registry"
Expand Down Expand Up @@ -122,7 +123,39 @@ func (a *DataISO) Generate(dependencies asset.Parents) error {
)
spinner.FileToMonitor = dataIsoName
imageGen := genisoimage.NewGenIsoImage(nil)
if err = imageGen.GenerateImage(envConfig.CacheDir, dataIsoName, filepath.Join(envConfig.TempDir, dataDir), dataVolumeName); err != nil {

// When mirror-path is provided, copy the Docker registry data from mirror-path/data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this Generate func is getting a bit too long:)
can you extract the new logic to another func?

// to temp/data so it's in the same location as the registry container image (images/registry/registry.tar)
registryDataSourcePath := filepath.Join(envConfig.TempDir, dataDir)
if applianceConfig.Config.MirrorPath != nil && swag.StringValue(applianceConfig.Config.MirrorPath) != "" {
mirrorDataPath := filepath.Join(swag.StringValue(applianceConfig.Config.MirrorPath), dataDir)
dockerSrcPath := filepath.Join(mirrorDataPath, "docker")
dockerDstPath := filepath.Join(registryDataSourcePath, "docker")

logrus.Infof("Copying Docker registry data from %s to %s", dockerSrcPath, dockerDstPath)

// Validate source directory exists
if _, err := os.Stat(dockerSrcPath); err != nil {
return log.StopSpinner(spinner, fmt.Errorf("docker registry data not found at %s (mirror-path may be invalid): %w", dockerSrcPath, err))
}

// Create destination directory
if err := os.MkdirAll(registryDataSourcePath, os.ModePerm); err != nil {
return log.StopSpinner(spinner, fmt.Errorf("failed to create directory for Docker registry data: %w", err))
}

// Copy directory recursively using cp command
// Note: Paths are safe here as they're program-generated from validated inputs
cpCmd := fmt.Sprintf("cp -r %s %s", dockerSrcPath, dockerDstPath)
exec := executer.NewExecuter()
if _, err := exec.Execute(cpCmd); err != nil {
return log.StopSpinner(spinner, fmt.Errorf("failed to copy Docker registry data from %s to %s: %w", dockerSrcPath, dockerDstPath, err))
}

logrus.Infof("Successfully copied Docker registry data")
}

if err = imageGen.GenerateImage(envConfig.CacheDir, dataIsoName, registryDataSourcePath, dataVolumeName); err != nil {
return log.StopSpinner(spinner, err)
}
return log.StopSpinner(spinner, a.updateAsset(envConfig))
Expand Down
54 changes: 34 additions & 20 deletions pkg/release/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,35 +138,49 @@ func (r *release) execute(command string) (string, error) {
}

func (r *release) mirrorImages(imageSetFile, blockedImages, additionalImages, operators string) error {
if err := templates.RenderTemplateFile(
imageSetFile,
templates.GetImageSetTemplateData(r.ApplianceConfig, blockedImages, additionalImages, operators),
r.EnvConfig.TempDir); err != nil {
return err
}
var tempDir string

imageSetFilePath, err := filepath.Abs(templates.GetFilePathByTemplate(imageSetFile, r.EnvConfig.TempDir))
if err != nil {
return err
// If a mirror path is provided in appliance-config, use it directly instead of running oc-mirror
var mirrorPath string
if r.ApplianceConfig.Config.MirrorPath != nil {
mirrorPath = *r.ApplianceConfig.Config.MirrorPath
}

tempDir := filepath.Join(r.EnvConfig.TempDir, "oc-mirror")
registryPort := swag.IntValue(r.ApplianceConfig.Config.ImageRegistry.Port)
cmd := fmt.Sprintf(ocMirror, imageSetFilePath, registryPort, tempDir)
if mirrorPath != "" {
logrus.Infof("Using pre-mirrored images from: %s", mirrorPath)
tempDir = mirrorPath
} else {
// Normal mirroring flow - run oc-mirror
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you swap the condition? just for keeping convention.
i.e. check mirrorPath == "" (normal flow first)

if err := templates.RenderTemplateFile(
imageSetFile,
templates.GetImageSetTemplateData(r.ApplianceConfig, blockedImages, additionalImages, operators),
r.EnvConfig.TempDir); err != nil {
return err
}

logrus.Debugf("Fetching image from OCP release (%s)", cmd)
result, err := r.execute(cmd)
logrus.Debugf("mirroring result: %s", result)
if err != nil {
return err
imageSetFilePath, err := filepath.Abs(templates.GetFilePathByTemplate(imageSetFile, r.EnvConfig.TempDir))
if err != nil {
return err
}

tempDir = filepath.Join(r.EnvConfig.TempDir, "oc-mirror")
registryPort := swag.IntValue(r.ApplianceConfig.Config.ImageRegistry.Port)
cmd := fmt.Sprintf(ocMirror, imageSetFilePath, registryPort, tempDir)

logrus.Debugf("Fetching image from OCP release (%s)", cmd)
result, err := r.execute(cmd)
logrus.Debugf("mirroring result: %s", result)
if err != nil {
return err
}
}

// Copy generated yaml files to cache dir
if err = r.copyOutputYamls(tempDir); err != nil {
// Copy generated yaml files to cache dir (works for both mirror path and oc-mirror output)
if err := r.copyOutputYamls(tempDir); err != nil {
return err
}

return err
return nil
}

func (r *release) copyOutputYamls(ocMirrorDir string) error {
Expand Down
1 change: 1 addition & 0 deletions pkg/types/appliance_config_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type ApplianceConfig struct {
SshKey *string `json:"sshKey"`
UserCorePass *string `json:"userCorePass"`
ImageRegistry *ImageRegistry `json:"imageRegistry"`
MirrorPath *string `json:"mirrorPath,omitempty"`
EnableDefaultSources *bool `json:"enableDefaultSources"`
EnableFips *bool `json:"enableFips"`
StopLocalRegistry *bool `json:"stopLocalRegistry"`
Expand Down