Skip to content

Commit e4bfa82

Browse files
emosbaughajp-io
andauthored
feat: prompt in the install command if newer app release version is available (#1490)
* feat: prompt in the install command if new app release version is available * f * f * Revert "f" This reverts commit 92cb07b. * f * f * f * f * feedback * Update vendorapp.go * feedback * feedback * do not reverse the default with no-prompt * document interface * Trigger Build * use httptest * use httptest * explain in comments * use updated response struct * fix tests * no options for decorative prompts * no skip app update prompts env var --------- Co-authored-by: Alex Parker <[email protected]>
1 parent eb9b6dd commit e4bfa82

File tree

7 files changed

+520
-22
lines changed

7 files changed

+520
-22
lines changed

pkg/cmd/install.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"path/filepath"
@@ -9,12 +10,13 @@ import (
910
"time"
1011

1112
k0sconfig "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1"
12-
ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
1313
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
14+
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
1415
"github.com/sirupsen/logrus"
1516
"github.com/urfave/cli/v2"
1617
k8syaml "sigs.k8s.io/yaml"
1718

19+
ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
1820
"github.com/replicatedhq/embedded-cluster/pkg/addons"
1921
"github.com/replicatedhq/embedded-cluster/pkg/airgap"
2022
"github.com/replicatedhq/embedded-cluster/pkg/config"
@@ -30,7 +32,6 @@ import (
3032
"github.com/replicatedhq/embedded-cluster/pkg/release"
3133
"github.com/replicatedhq/embedded-cluster/pkg/spinner"
3234
"github.com/replicatedhq/embedded-cluster/pkg/support"
33-
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
3435
)
3536

3637
// Minimum character length for the Admin Console password
@@ -260,7 +261,7 @@ func runHostPreflights(c *cli.Context, provider *defaults.Provider, hpf *v1beta2
260261
}
261262
pb.CloseWithError()
262263
output.PrintTableWithoutInfo()
263-
if !prompts.New().Confirm("Do you want to continue ?", false) {
264+
if !prompts.New().Confirm("Do you want to continue?", false) {
264265
return fmt.Errorf("user aborted")
265266
}
266267
return nil
@@ -805,10 +806,22 @@ func installCommand() *cli.Command {
805806
return err // we want the user to see the error message without a prefix
806807
}
807808
}
809+
810+
if err := maybePromptForAppUpdate(c, prompts.New(), license); err != nil {
811+
if errors.Is(err, ErrNothingElseToAdd) {
812+
metrics.ReportApplyFinished(c, err)
813+
return err
814+
}
815+
// If we get an error other than ErrNothingElseToAdd, we warn and continue as
816+
// this check is not critical.
817+
logrus.Debugf("WARNING: Failed to check for newer app versions: %v", err)
818+
}
819+
808820
if err := preflights.ValidateApp(); err != nil {
809821
metrics.ReportApplyFinished(c, err)
810822
return err
811823
}
824+
812825
adminConsolePwd, err := maybeAskAdminConsolePassword(c)
813826
if err != nil {
814827
metrics.ReportApplyFinished(c, err)
@@ -825,6 +838,7 @@ func installCommand() *cli.Command {
825838
metrics.ReportApplyFinished(c, err)
826839
return err
827840
}
841+
828842
logrus.Debugf("running host preflights")
829843
var replicatedAPIURL, proxyRegistryURL string
830844
if license != nil {

pkg/cmd/vendorapp.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
"net/url"
11+
"time"
12+
13+
"github.com/replicatedhq/embedded-cluster/pkg/metrics"
14+
"github.com/replicatedhq/embedded-cluster/pkg/prompts"
15+
"github.com/replicatedhq/embedded-cluster/pkg/release"
16+
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
17+
"github.com/sirupsen/logrus"
18+
"github.com/urfave/cli/v2"
19+
)
20+
21+
// maybePromptForAppUpdate warns the user if the embedded release is not the latest for the current
22+
// channel. If prompts are enabled, it will prompt the user to continue installing the out-of-date
23+
// release and return an error if the user chooses not to continue.
24+
func maybePromptForAppUpdate(c *cli.Context, prompt prompts.Prompt, license *kotsv1beta1.License) error {
25+
// It is not possible to check for app updates in airgap mode.
26+
if isAirgap := c.String("airgap-bundle") != ""; isAirgap {
27+
return nil
28+
}
29+
30+
channelRelease, err := release.GetChannelRelease()
31+
if err != nil {
32+
return fmt.Errorf("unable to get channel release: %w", err)
33+
} else if channelRelease == nil {
34+
// It is possible to install without embedding the release data. In this case, we cannot
35+
// check for app updates.
36+
return nil
37+
}
38+
39+
if license == nil {
40+
return errors.New("license required")
41+
}
42+
43+
logrus.Debugf("Checking for pending app releases")
44+
45+
currentRelease, err := getCurrentAppChannelRelease(c.Context, license, channelRelease.ChannelID)
46+
if err != nil {
47+
return fmt.Errorf("get current app channel release: %w", err)
48+
}
49+
50+
// In the dev and test environments, the channelSequence is set to 0 for all releases.
51+
if channelRelease.VersionLabel == currentRelease.VersionLabel {
52+
logrus.Debugf("Current app release is up-to-date")
53+
return nil
54+
}
55+
logrus.Debugf("Current app release is out-of-date")
56+
57+
apiURL := metrics.BaseURL(license)
58+
releaseURL := fmt.Sprintf("%s/embedded/%s/%s", apiURL, channelRelease.AppSlug, channelRelease.ChannelSlug)
59+
logrus.Warnf("A newer version %s is available.", currentRelease.VersionLabel)
60+
logrus.Infof(
61+
"To download it, run:\n curl -fL \"%s\" \\\n -H \"Authorization: %s\" \\\n -o %s-%s.tgz",
62+
releaseURL,
63+
license.Spec.LicenseID,
64+
channelRelease.AppSlug,
65+
channelRelease.ChannelSlug,
66+
)
67+
68+
// if no-prompt is true, we don't prompt the user and continue by default.
69+
if !c.Bool("no-prompt") {
70+
text := fmt.Sprintf("Do you want to continue installing %s anyway?", channelRelease.VersionLabel)
71+
if !prompt.Confirm(text, true) {
72+
return ErrNothingElseToAdd
73+
}
74+
}
75+
return nil
76+
}
77+
78+
type apiChannelRelease struct {
79+
ChannelID string `json:"channelId"`
80+
ChannelSequence int64 `json:"channelSequence"`
81+
ReleaseSequence int64 `json:"releaseSequence"`
82+
VersionLabel string `json:"versionLabel"`
83+
IsRequired bool `json:"isRequired"`
84+
SemVer string `json:"semver,omitempty"`
85+
CreatedAt string `json:"createdAt"`
86+
ReleaseNotes string `json:"releaseNotes"`
87+
ReplicatedRegistryDomain string `json:"replicatedRegistryDomain"`
88+
ReplicatedProxyDomain string `json:"replicatedProxyDomain"`
89+
}
90+
91+
func getCurrentAppChannelRelease(ctx context.Context, license *kotsv1beta1.License, channelID string) (*apiChannelRelease, error) {
92+
query := url.Values{}
93+
query.Set("selectedChannelId", channelID)
94+
query.Set("channelSequence", "") // sending an empty string will return the latest channel release
95+
query.Set("isSemverSupported", "true")
96+
97+
apiURL := metrics.BaseURL(license)
98+
url := fmt.Sprintf("%s/release/%s/pending?%s", apiURL, license.Spec.AppSlug, query.Encode())
99+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
100+
if err != nil {
101+
return nil, fmt.Errorf("create request: %w", err)
102+
}
103+
104+
auth := fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", license.Spec.LicenseID, license.Spec.LicenseID))))
105+
req.Header.Set("Authorization", auth)
106+
107+
// This will use the proxy from the environment if set by the cli command.
108+
client := &http.Client{
109+
Timeout: 10 * time.Second,
110+
}
111+
resp, err := client.Do(req)
112+
if err != nil {
113+
return nil, fmt.Errorf("get pending app releases: %w", err)
114+
}
115+
defer resp.Body.Close()
116+
117+
if resp.StatusCode != http.StatusOK {
118+
return nil, fmt.Errorf("unexpected status code %s", resp.Status)
119+
}
120+
121+
var releases struct {
122+
ChannelReleases []apiChannelRelease `json:"channelReleases"`
123+
}
124+
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
125+
return nil, fmt.Errorf("decode pending app releases: %w", err)
126+
}
127+
128+
if len(releases.ChannelReleases) == 0 {
129+
return nil, errors.New("no app releases found")
130+
}
131+
132+
return &releases.ChannelReleases[0], nil
133+
}

0 commit comments

Comments
 (0)