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
27 changes: 27 additions & 0 deletions cmd/createtemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,33 @@ openshift:
ssh_public_key_file: "~/.ssh/id_rsa.pub" # SSH public key for 'core' user access to the OpenShift nodes
force_ocp_download: false # Set to true to force re-download of existing OpenShift artifacts

# --- ADVANCED OPENSHIFT NETWORK SETTINGS (optional, defaults shown) ---
# cluster_network_cidr: "10.128.0.0/14" # Pod network CIDR (default: 10.128.0.0/14)
# cluster_network_host_prefix: 23 # Per-node subnet prefix length (default: 23)
# service_network: "172.30.0.0/16" # Service/ClusterIP network CIDR (default: 172.30.0.0/16)

# --- OCP CLIENT DOWNLOAD OVERRIDES (optional) ---
# Leave commented out to let ShiftLaunch auto-resolve download URLs from mirror.openshift.com.
# Useful for air-gapped environments, pinned versions, or custom mirrors.
# ocp_client_config:
# ocp_client: "https://mirror.openshift.com/pub/openshift-v4/ppc64le/clients/ocp/4.21.18/openshift-client-linux.tar.gz"
# ocp_installer: "https://mirror.openshift.com/pub/openshift-v4/ppc64le/clients/ocp/4.21.18/openshift-install-linux.tar.gz"
{{- if and (eq .IsolationLevel "air-gapped") (not .ExtRegistry) (ne .ReleaseType "ci")}}
# ocp_mirror_client: "https://mirror.openshift.com/pub/openshift-v4/ppc64le/clients/ocp/4.21.18/oc-mirror.tar.gz"
{{- end}}
# checksum_url: "https://mirror.openshift.com/pub/openshift-v4/ppc64le/clients/ocp/4.21.18/sha256sum.txt"

{{- if ne .BootMethod "agent"}}
# --- RHCOS IMAGE OVERRIDES (optional) ---
# Leave commented out to let ShiftLaunch auto-resolve RHCOS image URLs.
# Required for air-gapped environments or when pinning a specific RHCOS build.
# rhcos_images:
# kernel_url: "https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/4.21/latest/rhcos-live-kernel.ppc64le"
# initramfs_url: "https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/4.21/latest/rhcos-live-initramfs.ppc64le.img"
# rootfs_url: "https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/4.21/latest/rhcos-live-rootfs.ppc64le.img"
# checksum_url: "https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/4.21/latest/sha256sum.txt"
{{- end}}

# -----------------------------------------------------------------------------
# 4. HMC CREDENTIALS (IBM Hardware Management Console)
# -----------------------------------------------------------------------------
Expand Down
64 changes: 47 additions & 17 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,26 @@ import (
"github.com/IBM/shiftlaunch/logger"
"github.com/IBM/shiftlaunch/orchestrator"
"github.com/IBM/shiftlaunch/types"
"github.com/IBM/shiftlaunch/utils"
)

// version is dynamically injected via ldflags at compile time.
// "dev-build" is the fallback if someone builds it without the Makefile.
var version = "dev-build"

// Mirror URL format strings for ppc64le OCP artefacts.
// %s is the full version (e.g. "4.17.3") for client/installer paths,
// and the major.minor (e.g. "4.17") for RHCOS dependency paths.
const (
mirrorClientFmt = "https://mirror.openshift.com/pub/openshift-v4/ppc64le/clients/ocp/%s/openshift-client-linux.tar.gz"
mirrorInstallerFmt = "https://mirror.openshift.com/pub/openshift-v4/ppc64le/clients/ocp/%s/openshift-install-linux.tar.gz"
mirrorOCMirrorFmt = "https://mirror.openshift.com/pub/openshift-v4/ppc64le/clients/ocp/%s/oc-mirror.tar.gz"
mirrorKernelFmt = "https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/%s/latest/rhcos-live-kernel.ppc64le"
mirrorInitramfsFmt = "https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/%s/latest/rhcos-live-initramfs.ppc64le.img"
mirrorRootfsFmt = "https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/%s/latest/rhcos-live-rootfs.ppc64le.img"
mirrorChecksumFmt = "https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/%s/latest/sha256sum.txt"
)

var (
// Global flags
configFile string
Expand Down Expand Up @@ -244,25 +258,41 @@ func loadConfig(requireConfig bool) (*types.AgentConfig, *config.AgentDaemonConf
if parts := strings.Split(cfg.OpenShift.Version, "."); len(parts) >= 2 {
majorMinor = parts[0] + "." + parts[1]
}
if cfg.OpenShift.OCPClientConfig.Client == "" {
cfg.OpenShift.OCPClientConfig.Client = fmt.Sprintf("https://mirror.openshift.com/pub/openshift-v4/ppc64le/clients/ocp/%s/openshift-client-linux.tar.gz", cfg.OpenShift.Version)
}
if cfg.OpenShift.OCPClientConfig.Installer == "" {
cfg.OpenShift.OCPClientConfig.Installer = fmt.Sprintf("https://mirror.openshift.com/pub/openshift-v4/ppc64le/clients/ocp/%s/openshift-install-linux.tar.gz", cfg.OpenShift.Version)
}
// Auto-resolver for the oc-mirror plugin (required for air-gapped deployments)
if cfg.OpenShift.OCPClientConfig.MirrorClient == "" {
cfg.OpenShift.OCPClientConfig.MirrorClient = fmt.Sprintf("https://mirror.openshift.com/pub/openshift-v4/ppc64le/clients/ocp/%s/oc-mirror.tar.gz", cfg.OpenShift.Version)
}
if cfg.Nodes.BootMethod != "agent" {
if cfg.OpenShift.RHCOSImages.KernelURL == "" {
cfg.OpenShift.RHCOSImages.KernelURL = fmt.Sprintf("https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/%s/latest/rhcos-live-kernel.ppc64le", majorMinor)

// Pre-release versions (ec, rc, candidate, nightly, etc.) do not have stable
// mirror paths. Skip auto-resolving URLs and let validation enforce that the
// user explicitly provides them.
if !utils.IsPreReleaseVersion(cfg.OpenShift.Version) {
if cfg.OpenShift.OCPClientConfig.Client == "" {
cfg.OpenShift.OCPClientConfig.Client = fmt.Sprintf(mirrorClientFmt, cfg.OpenShift.Version)
}
if cfg.OpenShift.OCPClientConfig.Installer == "" {
cfg.OpenShift.OCPClientConfig.Installer = fmt.Sprintf(mirrorInstallerFmt, cfg.OpenShift.Version)
}
// Auto-resolver for the oc-mirror plugin: only needed for air-gapped managed-registry
// deployments with official releases (ci builds use 'oc adm release mirror' instead).
if cfg.OpenShift.OCPClientConfig.MirrorClient == "" &&
cfg.Network.IsolationLevel == "air-gapped" &&
cfg.Services.Registry.IsManaged() &&
cfg.OpenShift.ReleaseType != "ci" {
cfg.OpenShift.OCPClientConfig.MirrorClient = fmt.Sprintf(mirrorOCMirrorFmt, cfg.OpenShift.Version)
}
if cfg.OpenShift.RHCOSImages.InitramfsURL == "" {
cfg.OpenShift.RHCOSImages.InitramfsURL = fmt.Sprintf("https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/%s/latest/rhcos-live-initramfs.ppc64le.img", majorMinor)
if cfg.Nodes.BootMethod != "agent" {
if cfg.OpenShift.RHCOSImages.KernelURL == "" {
cfg.OpenShift.RHCOSImages.KernelURL = fmt.Sprintf(mirrorKernelFmt, majorMinor)
}
if cfg.OpenShift.RHCOSImages.InitramfsURL == "" {
cfg.OpenShift.RHCOSImages.InitramfsURL = fmt.Sprintf(mirrorInitramfsFmt, majorMinor)
}
if cfg.OpenShift.RHCOSImages.RootfsURL == "" {
cfg.OpenShift.RHCOSImages.RootfsURL = fmt.Sprintf(mirrorRootfsFmt, majorMinor)
}
if cfg.OpenShift.RHCOSImages.ChecksumURL == "" {
cfg.OpenShift.RHCOSImages.ChecksumURL = fmt.Sprintf(mirrorChecksumFmt, majorMinor)
}
}
if cfg.OpenShift.RHCOSImages.RootfsURL == "" {
cfg.OpenShift.RHCOSImages.RootfsURL = fmt.Sprintf("https://mirror.openshift.com/pub/openshift-v4/ppc64le/dependencies/rhcos/%s/latest/rhcos-live-rootfs.ppc64le.img", majorMinor)
if cfg.OpenShift.RHCOSImages.ChecksumURL == "" {
cfg.OpenShift.RHCOSImages.ChecksumURL = fmt.Sprintf(mirrorChecksumFmt, majorMinor)
}
}
}
Expand Down
15 changes: 4 additions & 11 deletions cmd/scale.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/IBM/shiftlaunch/localexec"
"github.com/IBM/shiftlaunch/services"
"github.com/IBM/shiftlaunch/types"
"github.com/IBM/shiftlaunch/utils"
)

// Day2NodesConfig represents the configuration for Day 2 nodes
Expand Down Expand Up @@ -239,7 +240,7 @@ func runScale(cmd *cobra.Command, args []string) error {
// 2. Identify added workers (Scale-Up)
for _, worker := range updatedCfg.Nodes.Workers {
readyMarker := "ready_" + worker.Hostname
if contains(state.CompletedPhases, readyMarker) {
if utils.Contains(state.CompletedPhases, readyMarker) {
continue
}

Expand All @@ -255,7 +256,7 @@ func runScale(cmd *cobra.Command, args []string) error {
scaleUpTargets = append(scaleUpTargets, worker)

// If it hasn't been powered on by the HMC yet, it needs a boot
if !contains(state.CompletedPhases, "booted_"+worker.Hostname) {
if !utils.Contains(state.CompletedPhases, "booted_"+worker.Hostname) {
pendingBoot = append(pendingBoot, worker)
}
}
Expand Down Expand Up @@ -763,7 +764,7 @@ func runScale(cmd *cobra.Command, args []string) error {
state, _ = stateManager.LoadState()
for _, w := range scaleUpTargets {
readyMarker := "ready_" + w.Hostname
if !contains(state.CompletedPhases, readyMarker) {
if !utils.Contains(state.CompletedPhases, readyMarker) {
state.CompletedPhases = append(state.CompletedPhases, readyMarker)
}
}
Expand All @@ -775,12 +776,4 @@ func runScale(cmd *cobra.Command, args []string) error {
}

// contains function is for search a string in a slice of strings
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}

24 changes: 8 additions & 16 deletions orchestrator/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/IBM/shiftlaunch/logger"
"github.com/IBM/shiftlaunch/services"
"github.com/IBM/shiftlaunch/types"
"github.com/IBM/shiftlaunch/utils"
"github.com/pterm/pterm"
"go.yaml.in/yaml/v3"
)
Expand Down Expand Up @@ -76,7 +77,7 @@ func (o *Orchestrator) saveState(phase string) {
}

o.state.CurrentPhase = phase
if !contains(o.state.CompletedPhases, phase) {
if !utils.Contains(o.state.CompletedPhases, phase) {
o.state.CompletedPhases = append(o.state.CompletedPhases, phase)
}
_ = o.stateManager.SaveState(o.state)
Expand Down Expand Up @@ -288,7 +289,7 @@ func (o *Orchestrator) Deploy(ctx context.Context, resume bool) (err error) {
}

// --- PHASE 1: DISCOVERY ---
if !resume || !contains(o.state.CompletedPhases, "discovery") {
if !resume || !utils.Contains(o.state.CompletedPhases, "discovery") {
phaseExec := o.startPhase("discovery")
o.logger.StartPhase("[Phase 1/6] Pre-Flight & HMC Discovery")

Expand Down Expand Up @@ -323,7 +324,7 @@ func (o *Orchestrator) Deploy(ctx context.Context, resume bool) (err error) {
}

// --- PHASE 2: DOWNLOADS ---
needsDownloads := !resume || !contains(o.state.CompletedPhases, "downloads")
needsDownloads := !resume || !utils.Contains(o.state.CompletedPhases, "downloads")

// --- LBYL Safety Check for Missing Binaries ---
if !needsDownloads {
Expand Down Expand Up @@ -352,7 +353,7 @@ func (o *Orchestrator) Deploy(ctx context.Context, resume bool) (err error) {
}

// --- PHASE 3: MANAGED SERVICES ---
if !resume || !contains(o.state.CompletedPhases, "services") {
if !resume || !utils.Contains(o.state.CompletedPhases, "services") {
phaseExec := o.startPhase("services")
o.logger.StartPhase("[Phase 3/6] Configuring Managed Infrastructure Services")

Expand Down Expand Up @@ -588,7 +589,7 @@ func (o *Orchestrator) Deploy(ctx context.Context, resume bool) (err error) {
}

// --- PHASE 4: IGNITION GENERATION ---
needsIgnition := !resume || !contains(o.state.CompletedPhases, "ignition")
needsIgnition := !resume || !utils.Contains(o.state.CompletedPhases, "ignition")

if !needsIgnition {
if _, err := os.Stat(filepath.Join(o.workspaceDir, "install-dir")); os.IsNotExist(err) {
Expand Down Expand Up @@ -677,7 +678,7 @@ func (o *Orchestrator) Deploy(ctx context.Context, resume bool) (err error) {
}

// --- PHASE 5: BOOT ---
if !resume || !contains(o.state.CompletedPhases, "boot") {
if !resume || !utils.Contains(o.state.CompletedPhases, "boot") {
phaseExec := o.startPhase("boot")
o.logger.StartPhase("[Phase 5/6] Initiating Cluster Boot")

Expand Down Expand Up @@ -720,7 +721,7 @@ func (o *Orchestrator) Deploy(ctx context.Context, resume bool) (err error) {
}

// --- PHASE 6: WAIT FOR INSTALLATION ---
if !resume || !contains(o.state.CompletedPhases, "wait") {
if !resume || !utils.Contains(o.state.CompletedPhases, "wait") {
phaseExec := o.startPhase("wait")
o.logger.StartPhase("[Phase 6/6] Waiting for OpenShift Installation")

Expand Down Expand Up @@ -918,15 +919,6 @@ func (o *Orchestrator) DumpConfigs(ctx context.Context) error {
return nil
}

// Helper to check if a string exists in a slice
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}

// findClusterUsingVIP searches all managed clusters to find if any is using the given VIP
func (o *Orchestrator) findClusterUsingVIP(vip string) string {
Expand Down
13 changes: 13 additions & 0 deletions services/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ func (d *Downloader) DownloadRHCOSImages(ctx context.Context, workspaceDir strin

manifestPath := filepath.Join(rhcosDir, "sha256sum.txt")

// Fetch the checksum manifest if a URL is available (auto-resolved by root.go
// when rhcos_images is omitted, or explicitly set by the user).
if urls.ChecksumURL != "" {
d.logger.Info("Integrity Mode: Fetching fresh RHCOS checksum manifest", "url", urls.ChecksumURL)
_ = os.Remove(manifestPath)
dlManifestCmd := fmt.Sprintf("curl -sSL --fail --max-time %d -o %s -- %s", timeout, shellQuote(manifestPath), shellQuote(urls.ChecksumURL))
if _, err := d.exec.Execute(ctx, dlManifestCmd); err != nil {
d.logger.Warn("Failed to fetch RHCOS checksum manifest", "error", err)
} else {
d.logger.Info("RHCOS checksum manifest downloaded")
}
}

images := []struct {
url string
filename string
Expand Down
2 changes: 1 addition & 1 deletion services/httphelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ check_ignition() {
check_rhcos() {
echo "Checking RHCOS images..."
local found=0
for file in rhcos-live-kernel-ppc64le rhcos-live-initramfs.ppc64le.img rhcos-live-rootfs.ppc64le.img; do
for file in rhcos-live-kernel.ppc64le rhcos-live-initramfs.ppc64le.img rhcos-live-rootfs.ppc64le.img; do
if [ -f "${HTTP_DIR}/rhcos/${file}" ]; then
local size=$(du -h "${HTTP_DIR}/rhcos/${file}" | awk '{print $1}')
echo " ${file} (${size})"
Expand Down
11 changes: 2 additions & 9 deletions services/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/IBM/shiftlaunch/localexec"
"github.com/IBM/shiftlaunch/logger"
"github.com/IBM/shiftlaunch/types"
"github.com/IBM/shiftlaunch/utils"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -252,7 +253,7 @@ func (r *RegistryManager) Setup(ctx context.Context, workspaceDir string) error
mirrorEventID := fmt.Sprintf("mirror_release_%s", r.cfg.OpenShift.Version)

// Check if mirroring was already completed
if r.state != nil && contains(r.state.CompletedEvents, mirrorEventID) {
if r.state != nil && utils.Contains(r.state.CompletedEvents, mirrorEventID) {
r.logger.Info("Image mirroring already completed, skipping...")
} else {
r.logger.Info("Mirroring OpenShift release images (this may take 15-30 minutes)...")
Expand Down Expand Up @@ -515,14 +516,6 @@ func (r *RegistryManager) isRegistryShared() bool {
}

// Helper function to check if a string exists in a slice
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}

// shellQuote wraps s in single quotes and escapes any literal single quotes
// inside it using the '\'' idiom, making the value safe to embed in a bash
Expand Down
1 change: 1 addition & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ type RHCOSURLs struct {
KernelURL string `yaml:"kernel_url,omitempty"`
InitramfsURL string `yaml:"initramfs_url,omitempty"`
RootfsURL string `yaml:"rootfs_url,omitempty"`
ChecksumURL string `yaml:"checksum_url,omitempty"`
}

type OCPClientConfig struct {
Expand Down
39 changes: 39 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Package utils provides small, reusable helpers shared across shiftlaunch packages.
package utils

import (
"regexp"
"strings"
)

// versionRe matches a valid OpenShift version: MAJOR.MINOR.PATCH with an optional pre-release suffix.
// Examples: 4.17.3, 4.21.0-rc.2, 4.21.0-0.nightly-2025-01-01
var versionRe = regexp.MustCompile(`^\d+\.\d+\.\d+(-\S+)?$`)

// Contains reports whether item is present in slice.
func Contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}

// IsValidVersion reports whether version matches the expected MAJOR.MINOR.PATCH[‑suffix] format.
func IsValidVersion(version string) bool {
return versionRe.MatchString(version)
}

// IsPreReleaseVersion returns true if the version string contains any pre-release
// marker that would not have a stable path on mirror.openshift.com.
// Examples: 4.21.0-ec.1, 4.21.0-rc.2, 4.21.0-candidate, 4.21.0-0.nightly-2025-01-01
func IsPreReleaseVersion(version string) bool {
lower := strings.ToLower(version)
for _, marker := range []string{"ec", "rc", "candidate", "nightly", "pre", "alpha", "beta"} {
if strings.Contains(lower, "-"+marker) {
return true
}
}
return false
}
Loading
Loading