Skip to content

Commit 2143149

Browse files
committed
Store airgap bundle size in Installation custom resource
Signed-off-by: Evans Mungai <evans@replicated.com>
1 parent d54331b commit 2143149

File tree

4 files changed

+231
-24
lines changed

4 files changed

+231
-24
lines changed

api/internal/managers/infra/install.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ func (m *infraManager) initComponentsList(license *kotsv1beta1.License, rc runti
100100
}
101101

102102
func (m *infraManager) install(ctx context.Context, license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) (finalErr error) {
103+
// extract airgap info if airgap bundle is provided
104+
var airgapInfo *kotsv1beta1.Airgap
105+
if m.airgapBundle != "" {
106+
var err error
107+
airgapInfo, err = airgap.AirgapInfoFromPath(m.airgapBundle)
108+
if err != nil {
109+
return fmt.Errorf("failed to get airgap info: %w", err)
110+
}
111+
}
112+
103113
defer func() {
104114
if r := recover(); r != nil {
105115
finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack()))
@@ -136,7 +146,7 @@ func (m *infraManager) install(ctx context.Context, license *kotsv1beta1.License
136146
}
137147
defer hcli.Close()
138148

139-
in, err := m.recordInstallation(ctx, kcli, license, rc)
149+
in, err := m.recordInstallation(ctx, kcli, license, rc, airgapInfo)
140150
if err != nil {
141151
return fmt.Errorf("record installation: %w", err)
142152
}
@@ -231,21 +241,28 @@ func (m *infraManager) installK0s(ctx context.Context, rc runtimeconfig.RuntimeC
231241
return k0sCfg, nil
232242
}
233243

234-
func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Client, license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) (*ecv1beta1.Installation, error) {
244+
func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Client, license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig, airgapInfo *kotsv1beta1.Airgap) (*ecv1beta1.Installation, error) {
235245
logFn := m.logFn("metadata")
236246

237247
// get the configured custom domains
238248
ecDomains := utils.GetDomains(m.releaseData)
239249

250+
// extract airgap uncompressed size if airgap info is provided
251+
var airgapUncompressedSize int64
252+
if airgapInfo != nil {
253+
airgapUncompressedSize = airgapInfo.Spec.UncompressedSize
254+
}
255+
240256
// record the installation
241257
logFn("recording installation")
242258
in, err := kubeutils.RecordInstallation(ctx, kcli, kubeutils.RecordInstallationOptions{
243-
IsAirgap: m.airgapBundle != "",
244-
License: license,
245-
ConfigSpec: m.getECConfigSpec(),
246-
MetricsBaseURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain),
247-
RuntimeConfig: rc.Get(),
248-
EndUserConfig: m.endUserConfig,
259+
IsAirgap: m.airgapBundle != "",
260+
License: license,
261+
ConfigSpec: m.getECConfigSpec(),
262+
MetricsBaseURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain),
263+
RuntimeConfig: rc.Get(),
264+
EndUserConfig: m.endUserConfig,
265+
AirgapUncompressedSize: airgapUncompressedSize,
249266
})
250267
if err != nil {
251268
return nil, fmt.Errorf("record installation: %w", err)

cmd/installer/cli/install.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command {
129129
installReporter.ReportSignalAborted(ctx, sig)
130130
})
131131

132-
if err := runInstall(cmd.Context(), flags, rc, installReporter); err != nil {
132+
if err := runInstall(cmd.Context(), flags, rc, installReporter, airgapInfo); err != nil {
133133
// Check if this is an interrupt error from the terminal
134134
if errors.Is(err, terminal.InterruptErr) {
135135
installReporter.ReportSignalAborted(ctx, syscall.SIGINT)
@@ -444,7 +444,7 @@ func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc
444444
return nil
445445
}
446446

447-
func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, installReporter *InstallReporter) (finalErr error) {
447+
func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, installReporter *InstallReporter, airgapInfo *kotsv1beta1.Airgap) (finalErr error) {
448448
if flags.enableManagerExperience {
449449
return nil
450450
}
@@ -479,7 +479,7 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run
479479
errCh := kubeutils.WaitForKubernetes(ctx, kcli)
480480
defer logKubernetesErrors(errCh)
481481

482-
in, err := recordInstallation(ctx, kcli, flags, rc, flags.license)
482+
in, err := recordInstallation(ctx, kcli, flags, rc, flags.license, airgapInfo)
483483
if err != nil {
484484
return fmt.Errorf("unable to record installation: %w", err)
485485
}
@@ -1048,7 +1048,7 @@ func waitForNode(ctx context.Context) error {
10481048
}
10491049

10501050
func recordInstallation(
1051-
ctx context.Context, kcli client.Client, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, license *kotsv1beta1.License,
1051+
ctx context.Context, kcli client.Client, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, license *kotsv1beta1.License, airgapInfo *kotsv1beta1.Airgap,
10521052
) (*ecv1beta1.Installation, error) {
10531053
// get the embedded cluster config
10541054
cfg := release.GetEmbeddedClusterConfig()
@@ -1063,14 +1063,21 @@ func recordInstallation(
10631063
return nil, fmt.Errorf("process overrides file: %w", err)
10641064
}
10651065

1066+
// extract airgap uncompressed size if airgap info is provided
1067+
var airgapUncompressedSize int64
1068+
if airgapInfo != nil {
1069+
airgapUncompressedSize = airgapInfo.Spec.UncompressedSize
1070+
}
1071+
10661072
// record the installation
10671073
installation, err := kubeutils.RecordInstallation(ctx, kcli, kubeutils.RecordInstallationOptions{
1068-
IsAirgap: flags.isAirgap,
1069-
License: license,
1070-
ConfigSpec: cfgspec,
1071-
MetricsBaseURL: replicatedAppURL(),
1072-
RuntimeConfig: rc.Get(),
1073-
EndUserConfig: eucfg,
1074+
IsAirgap: flags.isAirgap,
1075+
License: license,
1076+
ConfigSpec: cfgspec,
1077+
MetricsBaseURL: replicatedAppURL(),
1078+
RuntimeConfig: rc.Get(),
1079+
EndUserConfig: eucfg,
1080+
AirgapUncompressedSize: airgapUncompressedSize,
10741081
})
10751082
if err != nil {
10761083
return nil, fmt.Errorf("record installation: %w", err)

pkg/kubeutils/installation.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,13 @@ func writeInstallationStatusMessage(writer *spinner.MessageWriter, install *ecv1
121121
}
122122

123123
type RecordInstallationOptions struct {
124-
IsAirgap bool
125-
License *kotsv1beta1.License
126-
ConfigSpec *ecv1beta1.ConfigSpec
127-
MetricsBaseURL string
128-
RuntimeConfig *ecv1beta1.RuntimeConfigSpec
129-
EndUserConfig *ecv1beta1.Config
124+
IsAirgap bool
125+
License *kotsv1beta1.License
126+
ConfigSpec *ecv1beta1.ConfigSpec
127+
MetricsBaseURL string
128+
RuntimeConfig *ecv1beta1.RuntimeConfigSpec
129+
EndUserConfig *ecv1beta1.Config
130+
AirgapUncompressedSize int64
130131
}
131132

132133
func RecordInstallation(ctx context.Context, kcli client.Client, opts RecordInstallationOptions) (*ecv1beta1.Installation, error) {
@@ -162,6 +163,7 @@ func RecordInstallation(ctx context.Context, kcli client.Client, opts RecordInst
162163
ClusterID: metrics.ClusterID().String(),
163164
MetricsBaseURL: opts.MetricsBaseURL,
164165
AirGap: opts.IsAirgap,
166+
AirgapUncompressedSize: opts.AirgapUncompressedSize,
165167
Config: opts.ConfigSpec,
166168
RuntimeConfig: opts.RuntimeConfig,
167169
EndUserK0sConfigOverrides: euOverrides,

pkg/kubeutils/installation_test.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package kubeutils
22

33
import (
44
"context"
5+
"encoding/json"
56
"os"
67
"strings"
78
"testing"
@@ -12,7 +13,12 @@ import (
1213
"github.com/go-logr/logr/testr"
1314
ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
1415
"github.com/replicatedhq/embedded-cluster/pkg/crds"
16+
"github.com/replicatedhq/embedded-cluster/pkg/metrics"
17+
"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
18+
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
19+
"github.com/stretchr/testify/assert"
1520
"github.com/stretchr/testify/require"
21+
corev1 "k8s.io/api/core/v1"
1622
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1723
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1824
"k8s.io/apimachinery/pkg/runtime"
@@ -180,3 +186,178 @@ func TestEnsureInstallationCRD(t *testing.T) {
180186
})
181187
}
182188
}
189+
190+
func TestRecordInstallation(t *testing.T) {
191+
// Setup the test scheme
192+
s := runtime.NewScheme()
193+
require.NoError(t, apiextensionsv1.AddToScheme(s))
194+
require.NoError(t, ecv1beta1.AddToScheme(s))
195+
require.NoError(t, corev1.AddToScheme(s))
196+
197+
tests := []struct {
198+
name string
199+
opts RecordInstallationOptions
200+
wantErr bool
201+
validate func(t *testing.T, installation *ecv1beta1.Installation)
202+
}{
203+
{
204+
name: "online installation without airgap",
205+
opts: RecordInstallationOptions{
206+
IsAirgap: false,
207+
License: &kotsv1beta1.License{
208+
Spec: kotsv1beta1.LicenseSpec{
209+
IsDisasterRecoverySupported: true,
210+
IsEmbeddedClusterMultiNodeEnabled: false,
211+
},
212+
},
213+
ConfigSpec: &ecv1beta1.ConfigSpec{
214+
Version: "1.15.0+k8s-1.30",
215+
},
216+
MetricsBaseURL: "https://replicated.app",
217+
RuntimeConfig: &ecv1beta1.RuntimeConfigSpec{
218+
DataDir: "/var/lib/embedded-cluster",
219+
},
220+
EndUserConfig: &ecv1beta1.Config{
221+
Spec: ecv1beta1.ConfigSpec{
222+
UnsupportedOverrides: ecv1beta1.UnsupportedOverrides{
223+
K0s: "apiVersion: k0s.k0sproject.io/v1beta1\nkind: Cluster",
224+
},
225+
},
226+
},
227+
},
228+
wantErr: false,
229+
validate: func(t *testing.T, installation *ecv1beta1.Installation) {
230+
assert.False(t, installation.Spec.AirGap)
231+
assert.Equal(t, int64(0), installation.Spec.AirgapUncompressedSize)
232+
assert.Equal(t, "1.15.0+k8s-1.30", installation.Spec.Config.Version)
233+
assert.Equal(t, "https://replicated.app", installation.Spec.MetricsBaseURL)
234+
assert.Equal(t, "/var/lib/embedded-cluster", installation.Spec.RuntimeConfig.DataDir)
235+
assert.Equal(t, "apiVersion: k0s.k0sproject.io/v1beta1\nkind: Cluster", installation.Spec.EndUserK0sConfigOverrides)
236+
assert.True(t, installation.Spec.LicenseInfo.IsDisasterRecoverySupported)
237+
assert.False(t, installation.Spec.LicenseInfo.IsMultiNodeEnabled)
238+
assert.Equal(t, ecv1beta1.InstallationStateKubernetesInstalled, installation.Status.State)
239+
assert.Equal(t, "Kubernetes installed", installation.Status.Reason)
240+
},
241+
},
242+
{
243+
name: "airgap installation with uncompressed size",
244+
opts: RecordInstallationOptions{
245+
IsAirgap: true,
246+
License: &kotsv1beta1.License{
247+
Spec: kotsv1beta1.LicenseSpec{
248+
IsDisasterRecoverySupported: false,
249+
IsEmbeddedClusterMultiNodeEnabled: true,
250+
},
251+
},
252+
ConfigSpec: &ecv1beta1.ConfigSpec{
253+
Version: "1.16.0+k8s-1.31",
254+
},
255+
MetricsBaseURL: "https://staging.replicated.app",
256+
RuntimeConfig: &ecv1beta1.RuntimeConfigSpec{
257+
DataDir: "/opt/embedded-cluster",
258+
},
259+
EndUserConfig: nil,
260+
AirgapUncompressedSize: 1234567890,
261+
},
262+
wantErr: false,
263+
validate: func(t *testing.T, installation *ecv1beta1.Installation) {
264+
assert.True(t, installation.Spec.AirGap)
265+
assert.Equal(t, int64(1234567890), installation.Spec.AirgapUncompressedSize)
266+
assert.Equal(t, "1.16.0+k8s-1.31", installation.Spec.Config.Version)
267+
assert.Equal(t, "https://staging.replicated.app", installation.Spec.MetricsBaseURL)
268+
assert.Equal(t, "/opt/embedded-cluster", installation.Spec.RuntimeConfig.DataDir)
269+
assert.Empty(t, installation.Spec.EndUserK0sConfigOverrides)
270+
assert.False(t, installation.Spec.LicenseInfo.IsDisasterRecoverySupported)
271+
assert.True(t, installation.Spec.LicenseInfo.IsMultiNodeEnabled)
272+
assert.Equal(t, ecv1beta1.InstallationStateKubernetesInstalled, installation.Status.State)
273+
assert.Equal(t, "Kubernetes installed", installation.Status.Reason)
274+
},
275+
},
276+
{
277+
name: "airgap installation with large uncompressed size",
278+
opts: RecordInstallationOptions{
279+
IsAirgap: true,
280+
License: &kotsv1beta1.License{
281+
Spec: kotsv1beta1.LicenseSpec{
282+
IsDisasterRecoverySupported: false,
283+
IsEmbeddedClusterMultiNodeEnabled: false,
284+
},
285+
},
286+
ConfigSpec: &ecv1beta1.ConfigSpec{
287+
Version: "1.18.0+k8s-1.33",
288+
},
289+
MetricsBaseURL: "https://custom.replicated.app",
290+
RuntimeConfig: &ecv1beta1.RuntimeConfigSpec{
291+
DataDir: "/custom/data/dir",
292+
},
293+
EndUserConfig: nil,
294+
AirgapUncompressedSize: 9876543210,
295+
},
296+
wantErr: false,
297+
validate: func(t *testing.T, installation *ecv1beta1.Installation) {
298+
assert.True(t, installation.Spec.AirGap)
299+
assert.Equal(t, int64(9876543210), installation.Spec.AirgapUncompressedSize)
300+
assert.Equal(t, "1.18.0+k8s-1.33", installation.Spec.Config.Version)
301+
assert.Equal(t, "https://custom.replicated.app", installation.Spec.MetricsBaseURL)
302+
assert.Equal(t, "/custom/data/dir", installation.Spec.RuntimeConfig.DataDir)
303+
assert.Empty(t, installation.Spec.EndUserK0sConfigOverrides)
304+
assert.False(t, installation.Spec.LicenseInfo.IsDisasterRecoverySupported)
305+
assert.False(t, installation.Spec.LicenseInfo.IsMultiNodeEnabled)
306+
assert.Equal(t, ecv1beta1.InstallationStateKubernetesInstalled, installation.Status.State)
307+
assert.Equal(t, "Kubernetes installed", installation.Status.Reason)
308+
},
309+
},
310+
}
311+
312+
for _, tt := range tests {
313+
t.Run(tt.name, func(t *testing.T) {
314+
// Setup the test environment
315+
verbosity := 1
316+
if os.Getenv("DEBUG") != "" {
317+
verbosity = 10
318+
}
319+
log := testr.NewWithOptions(t, testr.Options{Verbosity: verbosity})
320+
ctx := logr.NewContext(context.Background(), log)
321+
322+
testEnv := &envtest.Environment{}
323+
cfg, err := testEnv.Start()
324+
require.NoError(t, err)
325+
t.Cleanup(func() { _ = testEnv.Stop() })
326+
327+
cli, err := client.New(cfg, client.Options{Scheme: s})
328+
require.NoError(t, err)
329+
330+
// Call the function being tested
331+
installation, err := RecordInstallation(ctx, cli, tt.opts)
332+
333+
if tt.wantErr {
334+
require.Error(t, err)
335+
return
336+
}
337+
require.NoError(t, err)
338+
require.NotNil(t, installation)
339+
340+
// Verify the installation was created in the cluster
341+
var resultInstallation ecv1beta1.Installation
342+
err = cli.Get(ctx, client.ObjectKey{Name: installation.Name}, &resultInstallation)
343+
require.NoError(t, err)
344+
345+
// Run custom validation
346+
if tt.validate != nil {
347+
tt.validate(t, &resultInstallation)
348+
}
349+
350+
json, err := json.MarshalIndent(resultInstallation, "", " ")
351+
require.NoError(t, err)
352+
t.Logf("resultInstallation: %s", string(json))
353+
// Verify common fields
354+
assert.NotEmpty(t, resultInstallation.Name)
355+
assert.Equal(t, "", resultInstallation.APIVersion) // I expected this to be "embeddedcluster.replicated.com/v1beta1"
356+
assert.Equal(t, "", resultInstallation.Kind) // I expected this to be "Installation"
357+
assert.Equal(t, metrics.ClusterID().String(), resultInstallation.Spec.ClusterID)
358+
assert.Equal(t, runtimeconfig.BinaryName(), resultInstallation.Spec.BinaryName)
359+
assert.Equal(t, ecv1beta1.InstallationSourceTypeCRD, resultInstallation.Spec.SourceType)
360+
assert.Equal(t, "ec-install", resultInstallation.Labels["replicated.com/disaster-recovery"])
361+
})
362+
}
363+
}

0 commit comments

Comments
 (0)