Skip to content

feat: preflight storage needed to perform airgap install #2336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0b06490
Add airgap bundle size to Installation CRD
banjoh Jun 18, 2025
e270d49
Generate operator CRDs
banjoh Jun 18, 2025
d54331b
Create airgapinfo package to parse airgap bundle metadata
banjoh Jun 18, 2025
2143149
Store airgap bundle size in Installation custom resource
banjoh Jun 19, 2025
668b930
Add airgap storage space to host preflights for controller nodes
banjoh Jun 19, 2025
473f5e1
Add controller airgap storage space calculation
banjoh Jun 19, 2025
46ca2d3
Separate airgap storage space preflight for controller and worker nodes
banjoh Jun 19, 2025
b88f435
Extract airgap.yaml once
banjoh Jun 19, 2025
5e53eca
Fix failing tests
banjoh Jun 19, 2025
9911048
Add airgap bundle size to Installation CRD
banjoh Jun 18, 2025
3fff57f
Generate operator CRDs
banjoh Jun 18, 2025
a1e8816
Create airgapinfo package to parse airgap bundle metadata
banjoh Jun 18, 2025
a3d3847
Store airgap bundle size in Installation custom resource
banjoh Jun 19, 2025
367c093
Add airgap storage space to host preflights for controller nodes
banjoh Jun 19, 2025
69038ae
Add controller airgap storage space calculation
banjoh Jun 19, 2025
a1e0ee4
Separate airgap storage space preflight for controller and worker nodes
banjoh Jun 19, 2025
ee7964f
Extract airgap.yaml once
banjoh Jun 19, 2025
d93f5cf
Fix failing tests
banjoh Jun 19, 2025
8691b79
Merge branch 'evansmungai/sc-123579/take-airgap-bundle-size-into-acco…
banjoh Jun 19, 2025
fb4f1b5
Comment out CI jobs
banjoh Jun 19, 2025
05f7910
Fix imports
banjoh Jun 19, 2025
fc96cf4
Revert ci.yaml
banjoh Jun 19, 2025
30e78de
Pass airgap info to the api
banjoh Jun 20, 2025
f72e05c
Merge remote-tracking branch 'origin/main' into evansmungai/sc-123579…
banjoh Jun 23, 2025
5206aab
Move airgapInfo to preRunInstall
banjoh Jun 24, 2025
5235538
Merge remote-tracking branch 'origin/main' into evansmungai/sc-123579…
banjoh Jun 24, 2025
00f00dc
A few more refactorings
banjoh Jun 24, 2025
9f3562b
Pass airgap info to infra manager
banjoh Jun 24, 2025
a8f86ba
Merge remote-tracking branch 'origin/main' into evansmungai/sc-123579…
banjoh Jun 26, 2025
b263856
Commit missing changes
banjoh Jun 26, 2025
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
9 changes: 9 additions & 0 deletions api/controllers/linux/install/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/replicatedhq/embedded-cluster/pkg/metrics"
"github.com/replicatedhq/embedded-cluster/pkg/release"
"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
"github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -52,6 +53,7 @@ type InstallController struct {
tlsConfig types.TLSConfig
license []byte
airgapBundle string
airgapInfo *kotsv1beta1.Airgap
configValues string
endUserConfig *ecv1beta1.Config

Expand Down Expand Up @@ -126,6 +128,12 @@ func WithAirgapBundle(airgapBundle string) InstallControllerOption {
}
}

func WithAirgapInfo(airgapInfo *kotsv1beta1.Airgap) InstallControllerOption {
return func(c *InstallController) {
c.airgapInfo = airgapInfo
}
}

func WithConfigValues(configValues string) InstallControllerOption {
return func(c *InstallController) {
c.configValues = configValues
Expand Down Expand Up @@ -218,6 +226,7 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController,
infra.WithTLSConfig(controller.tlsConfig),
infra.WithLicense(controller.license),
infra.WithAirgapBundle(controller.airgapBundle),
infra.WithAirgapInfo(controller.airgapInfo),
infra.WithConfigValues(controller.configValues),
infra.WithReleaseData(controller.releaseData),
infra.WithEndUserConfig(controller.endUserConfig),
Expand Down
20 changes: 14 additions & 6 deletions api/controllers/linux/install/hostpreflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight"
"github.com/replicatedhq/embedded-cluster/api/internal/utils"
"github.com/replicatedhq/embedded-cluster/api/types"
"github.com/replicatedhq/embedded-cluster/pkg-new/preflights"
"github.com/replicatedhq/embedded-cluster/pkg/netutils"
)

Expand All @@ -33,14 +34,21 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP
// Get the configured custom domains
ecDomains := utils.GetDomains(c.releaseData)

// Calculate airgap storage space requirement (2x uncompressed size for controller nodes)
var controllerAirgapStorageSpace string
if c.airgapInfo != nil {
controllerAirgapStorageSpace = preflights.CalculateAirgapStorageSpace(c.airgapInfo.Spec.UncompressedSize, true)
}

// Prepare host preflights
hpf, err := c.hostPreflightManager.PrepareHostPreflights(ctx, c.rc, preflight.PrepareHostPreflightOptions{
ReplicatedAppURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain),
ProxyRegistryURL: netutils.MaybeAddHTTPS(ecDomains.ProxyRegistryDomain),
HostPreflightSpec: c.releaseData.HostPreflights,
EmbeddedClusterConfig: c.releaseData.EmbeddedClusterConfig,
IsAirgap: c.airgapBundle != "",
IsUI: opts.IsUI,
ReplicatedAppURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain),
ProxyRegistryURL: netutils.MaybeAddHTTPS(ecDomains.ProxyRegistryDomain),
HostPreflightSpec: c.releaseData.HostPreflights,
EmbeddedClusterConfig: c.releaseData.EmbeddedClusterConfig,
IsAirgap: c.airgapBundle != "",
IsUI: opts.IsUI,
ControllerAirgapStorageSpace: controllerAirgapStorageSpace,
})
if err != nil {
return fmt.Errorf("failed to prepare host preflights: %w", err)
Expand Down
1 change: 1 addition & 0 deletions api/internal/handlers/linux/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func New(cfg types.APIConfig, opts ...Option) (*Handler, error) {
install.WithTLSConfig(h.cfg.TLSConfig),
install.WithLicense(h.cfg.License),
install.WithAirgapBundle(h.cfg.AirgapBundle),
install.WithAirgapInfo(h.cfg.AirgapInfo),
install.WithConfigValues(h.cfg.ConfigValues),
install.WithEndUserConfig(h.cfg.EndUserConfig),
install.WithAllowIgnoreHostPreflights(h.cfg.AllowIgnoreHostPreflights),
Expand Down
19 changes: 13 additions & 6 deletions api/internal/managers/infra/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,15 +222,22 @@ func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Clien
// get the configured custom domains
ecDomains := utils.GetDomains(m.releaseData)

// extract airgap uncompressed size if airgap info is provided
var airgapUncompressedSize int64
if m.airgapInfo != nil {
airgapUncompressedSize = m.airgapInfo.Spec.UncompressedSize
}

// record the installation
logFn("recording installation")
in, err := kubeutils.RecordInstallation(ctx, kcli, kubeutils.RecordInstallationOptions{
IsAirgap: m.airgapBundle != "",
License: license,
ConfigSpec: m.getECConfigSpec(),
MetricsBaseURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain),
RuntimeConfig: rc.Get(),
EndUserConfig: m.endUserConfig,
IsAirgap: m.airgapBundle != "",
License: license,
ConfigSpec: m.getECConfigSpec(),
MetricsBaseURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain),
RuntimeConfig: rc.Get(),
EndUserConfig: m.endUserConfig,
AirgapUncompressedSize: airgapUncompressedSize,
})
if err != nil {
return nil, fmt.Errorf("record installation: %w", err)
Expand Down
8 changes: 8 additions & 0 deletions api/internal/managers/infra/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/replicatedhq/embedded-cluster/pkg/helm"
"github.com/replicatedhq/embedded-cluster/pkg/release"
"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
"github.com/sirupsen/logrus"
"k8s.io/client-go/metadata"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -33,6 +34,7 @@ type infraManager struct {
tlsConfig types.TLSConfig
license []byte
airgapBundle string
airgapInfo *kotsv1beta1.Airgap
configValues string
releaseData *release.ReleaseData
endUserConfig *ecv1beta1.Config
Expand Down Expand Up @@ -84,6 +86,12 @@ func WithAirgapBundle(airgapBundle string) InfraManagerOption {
}
}

func WithAirgapInfo(airgapInfo *kotsv1beta1.Airgap) InfraManagerOption {
return func(c *infraManager) {
c.airgapInfo = airgapInfo
}
}

func WithConfigValues(configValues string) InfraManagerOption {
return func(c *infraManager) {
c.configValues = configValues
Expand Down
52 changes: 28 additions & 24 deletions api/internal/managers/preflight/hostpreflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ import (
)

type PrepareHostPreflightOptions struct {
ReplicatedAppURL string
ProxyRegistryURL string
HostPreflightSpec *troubleshootv1beta2.HostPreflightSpec
EmbeddedClusterConfig *ecv1beta1.Config
TCPConnectionsRequired []string
IsAirgap bool
IsJoin bool
IsUI bool
ReplicatedAppURL string
ProxyRegistryURL string
HostPreflightSpec *troubleshootv1beta2.HostPreflightSpec
EmbeddedClusterConfig *ecv1beta1.Config
TCPConnectionsRequired []string
IsAirgap bool
IsJoin bool
IsUI bool
ControllerAirgapStorageSpace string
WorkerAirgapStorageSpace string
}

type RunHostPreflightOptions struct {
Expand All @@ -38,22 +40,24 @@ func (m *hostPreflightManager) PrepareHostPreflights(ctx context.Context, rc run

// Use the shared Prepare function to prepare host preflights
prepareOpts := preflights.PrepareOptions{
HostPreflightSpec: opts.HostPreflightSpec,
ReplicatedAppURL: opts.ReplicatedAppURL,
ProxyRegistryURL: opts.ProxyRegistryURL,
AdminConsolePort: rc.AdminConsolePort(),
LocalArtifactMirrorPort: rc.LocalArtifactMirrorPort(),
DataDir: rc.EmbeddedClusterHomeDirectory(),
K0sDataDir: rc.EmbeddedClusterK0sSubDir(),
OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(),
Proxy: rc.ProxySpec(),
PodCIDR: rc.PodCIDR(),
ServiceCIDR: rc.ServiceCIDR(),
NodeIP: nodeIP,
IsAirgap: opts.IsAirgap,
TCPConnectionsRequired: opts.TCPConnectionsRequired,
IsJoin: opts.IsJoin,
IsUI: opts.IsUI,
HostPreflightSpec: opts.HostPreflightSpec,
ReplicatedAppURL: opts.ReplicatedAppURL,
ProxyRegistryURL: opts.ProxyRegistryURL,
AdminConsolePort: rc.AdminConsolePort(),
LocalArtifactMirrorPort: rc.LocalArtifactMirrorPort(),
DataDir: rc.EmbeddedClusterHomeDirectory(),
K0sDataDir: rc.EmbeddedClusterK0sSubDir(),
OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(),
Proxy: rc.ProxySpec(),
PodCIDR: rc.PodCIDR(),
ServiceCIDR: rc.ServiceCIDR(),
NodeIP: nodeIP,
IsAirgap: opts.IsAirgap,
TCPConnectionsRequired: opts.TCPConnectionsRequired,
IsJoin: opts.IsJoin,
IsUI: opts.IsUI,
ControllerAirgapStorageSpace: opts.ControllerAirgapStorageSpace,
WorkerAirgapStorageSpace: opts.WorkerAirgapStorageSpace,
}
if cidr := rc.GlobalCIDR(); cidr != "" {
prepareOpts.GlobalCIDR = &cidr
Expand Down
2 changes: 2 additions & 0 deletions api/types/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
"github.com/replicatedhq/embedded-cluster/pkg/release"
"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
)

// APIConfig holds the configuration for the API server
Expand All @@ -13,6 +14,7 @@ type APIConfig struct {
TLSConfig TLSConfig
License []byte
AirgapBundle string
AirgapInfo *kotsv1beta1.Airgap
ConfigValues string
ReleaseData *release.ReleaseData
EndUserConfig *ecv1beta1.Config
Expand Down
3 changes: 3 additions & 0 deletions cmd/installer/cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/replicatedhq/embedded-cluster/pkg/release"
"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
"github.com/replicatedhq/embedded-cluster/web"
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
"github.com/sirupsen/logrus"
)

Expand All @@ -37,6 +38,7 @@ type apiOptions struct {
ManagerPort int
License []byte
AirgapBundle string
AirgapInfo *kotsv1beta1.Airgap
ConfigValues string
ReleaseData *release.ReleaseData
EndUserConfig *ecv1beta1.Config
Expand Down Expand Up @@ -87,6 +89,7 @@ func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate,
TLSConfig: opts.TLSConfig,
License: opts.License,
AirgapBundle: opts.AirgapBundle,
AirgapInfo: opts.AirgapInfo,
ConfigValues: opts.ConfigValues,
ReleaseData: opts.ReleaseData,
EndUserConfig: opts.EndUserConfig,
Expand Down
48 changes: 28 additions & 20 deletions cmd/installer/cli/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type InstallCmdFlags struct {
adminConsolePassword string
adminConsolePort int
airgapBundle string
airgapInfo *kotsv1beta1.Airgap
isAirgap bool
licenseFile string
assumeYes bool
Expand Down Expand Up @@ -414,6 +415,13 @@ func preRunInstallCommon(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimec
}

flags.isAirgap = flags.airgapBundle != ""
if flags.airgapBundle != "" {
var err error
flags.airgapInfo, err = airgap.AirgapInfoFromPath(flags.airgapBundle)
if err != nil {
return fmt.Errorf("failed to get airgap info: %w", err)
}
}

proxy, err := proxyConfigFromCmd(cmd, flags.assumeYes)
if err != nil {
Expand Down Expand Up @@ -615,6 +623,7 @@ func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc
ManagerPort: flags.managerPort,
License: flags.licenseBytes,
AirgapBundle: flags.airgapBundle,
AirgapInfo: flags.airgapInfo,
ConfigValues: flags.configValues,
ReleaseData: release.GetReleaseData(),
EndUserConfig: eucfg,
Expand Down Expand Up @@ -792,9 +801,9 @@ func verifyAndPrompt(ctx context.Context, name string, flags InstallCmdFlags, pr
if err != nil {
return err
}
if flags.isAirgap {
if flags.airgapInfo != nil {
logrus.Debugf("checking airgap bundle matches binary")
if err := checkAirgapMatches(flags.airgapBundle); err != nil {
if err := checkAirgapMatches(flags.airgapInfo); err != nil {
return err // we want the user to see the error message without a prefix
}
}
Expand Down Expand Up @@ -1103,23 +1112,15 @@ func installExtensions(ctx context.Context, hcli helm.Client) error {
return nil
}

func checkAirgapMatches(airgapBundle string) error {
func checkAirgapMatches(airgapInfo *kotsv1beta1.Airgap) error {
rel := release.GetChannelRelease()
if rel == nil {
return fmt.Errorf("airgap bundle provided but no release was found in binary, please rerun without the airgap-bundle flag")
}

// read file from path
rawfile, err := os.Open(airgapBundle)
if err != nil {
return fmt.Errorf("failed to open airgap file: %w", err)
}
defer rawfile.Close()

appSlug, channelID, airgapVersion, err := airgap.ChannelReleaseMetadata(rawfile)
if err != nil {
return fmt.Errorf("failed to get airgap bundle versions: %w", err)
}
appSlug := airgapInfo.Spec.AppSlug
channelID := airgapInfo.Spec.ChannelID
airgapVersion := airgapInfo.Spec.VersionLabel

// Check if the airgap bundle matches the application version data
if rel.AppSlug != appSlug {
Expand Down Expand Up @@ -1272,14 +1273,21 @@ func recordInstallation(
return nil, fmt.Errorf("process overrides file: %w", err)
}

// extract airgap uncompressed size if airgap info is provided
var airgapUncompressedSize int64
if flags.airgapInfo != nil {
airgapUncompressedSize = flags.airgapInfo.Spec.UncompressedSize
}

// record the installation
installation, err := kubeutils.RecordInstallation(ctx, kcli, kubeutils.RecordInstallationOptions{
IsAirgap: flags.isAirgap,
License: license,
ConfigSpec: cfgspec,
MetricsBaseURL: replicatedAppURL(),
RuntimeConfig: rc.Get(),
EndUserConfig: eucfg,
IsAirgap: flags.isAirgap,
License: license,
ConfigSpec: cfgspec,
MetricsBaseURL: replicatedAppURL(),
RuntimeConfig: rc.Get(),
EndUserConfig: eucfg,
AirgapUncompressedSize: airgapUncompressedSize,
})
if err != nil {
return nil, fmt.Errorf("record installation: %w", err)
Expand Down
33 changes: 20 additions & 13 deletions cmd/installer/cli/install_runpreflights.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,27 @@ func runInstallPreflights(ctx context.Context, flags InstallCmdFlags, rc runtime
return fmt.Errorf("unable to find first valid address: %w", err)
}

// Calculate airgap storage space requirement (2x uncompressed size for controller nodes)
var controllerAirgapStorageSpace string
if flags.airgapInfo != nil {
controllerAirgapStorageSpace = preflights.CalculateAirgapStorageSpace(flags.airgapInfo.Spec.UncompressedSize, true)
}

opts := preflights.PrepareOptions{
HostPreflightSpec: release.GetHostPreflights(),
ReplicatedAppURL: replicatedAppURL,
ProxyRegistryURL: proxyRegistryURL,
AdminConsolePort: rc.AdminConsolePort(),
LocalArtifactMirrorPort: rc.LocalArtifactMirrorPort(),
DataDir: rc.EmbeddedClusterHomeDirectory(),
K0sDataDir: rc.EmbeddedClusterK0sSubDir(),
OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(),
Proxy: rc.ProxySpec(),
PodCIDR: rc.PodCIDR(),
ServiceCIDR: rc.ServiceCIDR(),
NodeIP: nodeIP,
IsAirgap: flags.isAirgap,
HostPreflightSpec: release.GetHostPreflights(),
ReplicatedAppURL: replicatedAppURL,
ProxyRegistryURL: proxyRegistryURL,
AdminConsolePort: rc.AdminConsolePort(),
LocalArtifactMirrorPort: rc.LocalArtifactMirrorPort(),
DataDir: rc.EmbeddedClusterHomeDirectory(),
K0sDataDir: rc.EmbeddedClusterK0sSubDir(),
OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(),
Proxy: rc.ProxySpec(),
PodCIDR: rc.PodCIDR(),
ServiceCIDR: rc.ServiceCIDR(),
NodeIP: nodeIP,
IsAirgap: flags.isAirgap,
ControllerAirgapStorageSpace: controllerAirgapStorageSpace,
}
if globalCIDR := rc.GlobalCIDR(); globalCIDR != "" {
opts.GlobalCIDR = &globalCIDR
Expand Down
Loading
Loading