Skip to content

Commit e39442a

Browse files
authored
Support airgap updates from the CLI (#526)
* Support airgap updates from the CLI * update usage * address feedback
1 parent 99a1c21 commit e39442a

File tree

4 files changed

+273
-141
lines changed

4 files changed

+273
-141
lines changed

cmd/embedded-cluster/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func main() {
3434
joinCommand,
3535
resetCommand,
3636
materializeCommand,
37+
updateCommand,
3738
},
3839
}
3940
if err := app.RunContext(ctx, os.Args); err != nil {

cmd/embedded-cluster/update.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/sirupsen/logrus"
8+
"github.com/urfave/cli/v2"
9+
10+
"github.com/replicatedhq/embedded-cluster/pkg/defaults"
11+
"github.com/replicatedhq/embedded-cluster/pkg/kotscli"
12+
"github.com/replicatedhq/embedded-cluster/pkg/release"
13+
)
14+
15+
var updateCommand = &cli.Command{
16+
Name: "update",
17+
Usage: fmt.Sprintf("Update %s", binName),
18+
Hidden: true,
19+
Flags: []cli.Flag{
20+
&cli.StringFlag{
21+
Name: "airgap-bundle",
22+
Usage: "Path to the airgap bundle.",
23+
Required: true,
24+
},
25+
},
26+
Before: func(c *cli.Context) error {
27+
if os.Getuid() != 0 {
28+
return fmt.Errorf("update command must be run as root")
29+
}
30+
os.Setenv("KUBECONFIG", defaults.PathToKubeConfig())
31+
return nil
32+
},
33+
Action: func(c *cli.Context) error {
34+
if c.String("airgap-bundle") != "" {
35+
logrus.Debugf("checking airgap bundle matches binary")
36+
if err := checkAirgapMatches(c); err != nil {
37+
return err // we want the user to see the error message without a prefix
38+
}
39+
}
40+
41+
rel, err := release.GetChannelRelease()
42+
if err != nil {
43+
return fmt.Errorf("unable to get channel release: %w", err)
44+
}
45+
if rel == nil {
46+
return fmt.Errorf("no channel release found")
47+
}
48+
49+
if err := kotscli.UpstreamUpgrade(kotscli.UpstreamUpgradeOptions{
50+
AppSlug: rel.AppSlug,
51+
Namespace: defaults.KotsadmNamespace,
52+
AirgapBundle: c.String("airgap-bundle"),
53+
}); err != nil {
54+
return err
55+
}
56+
57+
return nil
58+
},
59+
}

pkg/addons/adminconsole/adminconsole.go

Lines changed: 8 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ import (
66
"context"
77
"encoding/base64"
88
"fmt"
9-
"os"
109
"regexp"
11-
"strings"
1210
"time"
1311

1412
"github.com/k0sproject/dig"
@@ -23,8 +21,8 @@ import (
2321

2422
"github.com/replicatedhq/embedded-cluster/pkg/addons/registry"
2523
"github.com/replicatedhq/embedded-cluster/pkg/defaults"
26-
"github.com/replicatedhq/embedded-cluster/pkg/goods"
2724
"github.com/replicatedhq/embedded-cluster/pkg/helpers"
25+
"github.com/replicatedhq/embedded-cluster/pkg/kotscli"
2826
"github.com/replicatedhq/embedded-cluster/pkg/kubeutils"
2927
"github.com/replicatedhq/embedded-cluster/pkg/metrics"
3028
"github.com/replicatedhq/embedded-cluster/pkg/prompts"
@@ -210,99 +208,6 @@ func (a *AdminConsole) GetAdditionalImages() []string {
210208
return nil
211209
}
212210

213-
// MaskKotsOutputForOnline masks the kots cli output during online installations. For
214-
// online installations we only want to print "Finalizing" until it is done and then
215-
// print "Finished!".
216-
func (a *AdminConsole) MaskKotsOutputForOnline() spinner.MaskFn {
217-
return func(message string) string {
218-
if strings.Contains(message, "Finished") {
219-
return message
220-
}
221-
return "Finalizing"
222-
}
223-
}
224-
225-
// MaskKotsOutputForAirgap masks the kots cli output during airgap installations. This
226-
// function replaces some of the messages being printed to the user so the output looks
227-
// nicer.
228-
func (a *AdminConsole) MaskKotsOutputForAirgap() spinner.MaskFn {
229-
current := "Uploading air gap bundle"
230-
return func(message string) string {
231-
switch {
232-
case strings.Contains(message, "Pushing application images"):
233-
current = message
234-
case strings.Contains(message, "Pushing embedded cluster artifacts"):
235-
current = message
236-
case strings.Contains(message, "Waiting for Admin Console"):
237-
current = "Finalizing"
238-
case strings.Contains(message, "Finished!"):
239-
current = message
240-
}
241-
return current
242-
}
243-
}
244-
245-
// KostsOutputLineBreaker creates a line break (new spinner) when some of the messages
246-
// are printed to the user. For example: after finishing all image uploads we want to
247-
// have a new spinner for the artifacts upload.
248-
func (a *AdminConsole) KostsOutputLineBreaker() spinner.LineBreakerFn {
249-
// finished is an auxiliary function that evaluates if a message refers to a
250-
// step that has been finished. We determine that by inspected if the message
251-
// contains %d/%d and both integers are equal.
252-
finished := func(message string) bool {
253-
matches := CounterRegex.FindStringSubmatch(message)
254-
if len(matches) != 3 {
255-
return false
256-
}
257-
var counter int
258-
if _, err := fmt.Sscanf(matches[1], "%d", &counter); err != nil {
259-
return false
260-
}
261-
var total int
262-
if _, err := fmt.Sscanf(matches[2], "%d", &total); err != nil {
263-
return false
264-
}
265-
return counter == total
266-
}
267-
268-
var previous string
269-
var seen = map[string]bool{}
270-
return func(current string) (bool, string) {
271-
defer func() {
272-
previous = current
273-
}()
274-
275-
// if we have already seen this message we certainly have already assessed
276-
// if a break line as necessary or not, on this case we return false so we
277-
// do not keep breaking lines indefinitely.
278-
if _, ok := seen[current]; ok {
279-
return false, ""
280-
}
281-
seen[current] = true
282-
283-
// if the previous message evaluated does not relate to an end of a process
284-
// we don't want to break the line. i.e. we only want to break the line when
285-
// the previous evaluated message contains %d/%d and both integers are equal.
286-
if !finished(previous) {
287-
return false, ""
288-
}
289-
290-
// if we are printing a message about pushing the embedded cluster artifacts
291-
// it means that we have finished with the images and we want to break the line.
292-
if strings.Contains(current, "Pushing embedded cluster artifacts") {
293-
return true, "Application images are ready!"
294-
}
295-
296-
// if we are printing a message about the finalization of the installation it
297-
// means that the embedded cluster artifacts are ready and we want to break the
298-
// line.
299-
if current == "Finalizing" {
300-
return true, "Embedded cluster artifacts are ready!"
301-
}
302-
return false, ""
303-
}
304-
}
305-
306211
// Outro waits for the adminconsole to be ready.
307212
func (a *AdminConsole) Outro(ctx context.Context, cli client.Client) error {
308213
loading := spinner.Start()
@@ -345,59 +250,21 @@ func (a *AdminConsole) Outro(ctx context.Context, cli client.Client) error {
345250
return nil
346251
}
347252

348-
kotsBinPath, err := goods.MaterializeInternalBinary("kubectl-kots")
349-
if err != nil {
350-
return fmt.Errorf("unable to materialize kubectl-kots binary: %w", err)
351-
}
352-
defer os.Remove(kotsBinPath)
353-
354253
license, err := helpers.ParseLicense(a.licenseFile)
355254
if err != nil {
356255
loading.CloseWithError()
357256
return fmt.Errorf("unable to parse license: %w", err)
358257
}
359258

360-
var appVersionLabel string
361-
var channelSlug string
362-
if channelRelease, err := release.GetChannelRelease(); err != nil {
363-
loading.CloseWithError()
364-
return fmt.Errorf("unable to get channel release: %w", err)
365-
} else if channelRelease != nil {
366-
appVersionLabel = channelRelease.VersionLabel
367-
channelSlug = channelRelease.ChannelSlug
368-
}
369-
370-
upstreamURI := license.Spec.AppSlug
371-
if channelSlug != "" && channelSlug != "stable" {
372-
upstreamURI = fmt.Sprintf("%s/%s", upstreamURI, channelSlug)
373-
}
374-
375-
var lbreakfn spinner.LineBreakerFn
376-
maskfn := a.MaskKotsOutputForOnline()
377-
installArgs := []string{
378-
"install",
379-
upstreamURI,
380-
"--license-file",
381-
a.licenseFile,
382-
"--namespace",
383-
a.namespace,
384-
"--app-version-label",
385-
appVersionLabel,
386-
"--exclude-admin-console",
387-
}
388-
if a.airgapBundle != "" {
389-
installArgs = append(installArgs, "--airgap-bundle", a.airgapBundle)
390-
maskfn = a.MaskKotsOutputForAirgap()
391-
lbreakfn = a.KostsOutputLineBreaker()
392-
}
393-
394-
loading = spinner.Start(spinner.WithMask(maskfn), spinner.WithLineBreaker(lbreakfn))
395-
if err := helpers.RunCommandWithWriter(loading, kotsBinPath, installArgs...); err != nil {
396-
loading.CloseWithError()
397-
return fmt.Errorf("unable to install the application: %w", err)
259+
if err := kotscli.Install(kotscli.InstallOptions{
260+
AppSlug: license.Spec.AppSlug,
261+
LicenseFile: a.licenseFile,
262+
Namespace: a.namespace,
263+
AirgapBundle: a.airgapBundle,
264+
}); err != nil {
265+
return err
398266
}
399267

400-
loading.Closef("Finished!")
401268
a.printSuccessMessage(license.Spec.AppSlug)
402269
return nil
403270
}

0 commit comments

Comments
 (0)