Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions cmd/state-svc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/ActiveState/cli/internal/rollbar"
"github.com/ActiveState/cli/internal/runbits/panics"
"github.com/ActiveState/cli/internal/svcctl"
"github.com/ActiveState/cli/internal/updater"
"github.com/ActiveState/cli/pkg/platform/authentication"
"github.com/inconshreveable/mousetrap"
)
Expand Down Expand Up @@ -70,6 +71,8 @@ func main() {
rollbar.SetupRollbar(constants.StateServiceRollbarToken)
rollbar.SetConfig(cfg)

updater.SetConfig(cfg)

if os.Getenv("VERBOSE") == "true" {
logging.CurrentHandler().SetVerbose(true)
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/state/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/ActiveState/cli/internal/runbits/panics"
"github.com/ActiveState/cli/internal/subshell"
"github.com/ActiveState/cli/internal/svcctl"
"github.com/ActiveState/cli/internal/updater"
secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets"
"github.com/ActiveState/cli/pkg/platform/authentication"
"github.com/ActiveState/cli/pkg/platform/model"
Expand Down Expand Up @@ -92,6 +93,8 @@ func main() {
}
rollbar.SetConfig(cfg)

updater.SetConfig(cfg)

// Configuration options
// This should only be used if the config option is not exclusive to one package.
configMediator.RegisterOption(constants.OptinBuildscriptsConfig, configMediator.Bool, false)
Expand Down
3 changes: 3 additions & 0 deletions internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,9 @@ const SecurityPromptConfig = "security.prompt.enabled"
// SecurityPromptLevelConfig is the config key used to determine the level of security prompts
const SecurityPromptLevelConfig = "security.prompt.level"

// UpdateEndpointConfig is the config key used to determine the update endpoint to use
const UpdateEndpointConfig = "update.endpoint"

// SvcAppName is the name we give our state-svc application
const SvcAppName = "State Service"

Expand Down
10 changes: 10 additions & 0 deletions internal/testhelpers/e2e/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type Session struct {
ExecutorExe string
spawned []*SpawnedCmd
ignoreLogErrors bool
cfg *config.Instance
cache keyCache
}

Expand Down Expand Up @@ -199,6 +200,7 @@ func new(t *testing.T, retainDirs, updatePath bool, extraEnv ...string) *Session

cfg, err := config.NewCustom(dirs.Config, singlethread.New(), true)
require.NoError(session.T, err)
session.cfg = cfg

if err := cfg.Set(constants.SecurityPromptConfig, false); err != nil {
require.NoError(session.T, err)
Expand Down Expand Up @@ -790,6 +792,14 @@ func (s *Session) SetupRCFileCustom(subshell subshell.SubShell) {
require.NoError(s.T, err)
}

func (s *Session) SetConfig(key string, value interface{}) {
require.NoError(s.T, s.cfg.Set(key, value))
}

func (s *Session) GetConfig(key string) interface{} {
return s.cfg.Get(key)
}

func RunningOnCI() bool {
return condition.OnCI()
}
48 changes: 34 additions & 14 deletions internal/updater/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/ActiveState/cli/internal/logging"
"github.com/ActiveState/cli/internal/retryhttp"
"github.com/ActiveState/cli/internal/rtutils/ptr"

configMediator "github.com/ActiveState/cli/internal/mediators/config"
)

type Configurable interface {
Expand All @@ -30,32 +32,43 @@ type InvocationSource string
var (
InvocationSourceInstall InvocationSource = "install"
InvocationSourceUpdate InvocationSource = "update"

UpdateEndpointURL string
)

func init() {
configMediator.RegisterOption(constants.UpdateEndpointConfig, configMediator.String, "")
}

func registerConfigListener(cfg Configurable) {
configMediator.AddListener(constants.UpdateEndpointConfig, func() {
UpdateEndpointURL = cfg.GetString(constants.UpdateEndpointConfig)
})
}

func SetConfig(cfg Configurable) {
UpdateEndpointURL = cfg.GetString(constants.UpdateEndpointConfig)
registerConfigListener(cfg)
}

type Checker struct {
cfg Configurable
an analytics.Dispatcher
apiInfoURL string
retryhttp *retryhttp.Client
cache *AvailableUpdate
done chan struct{}
cfg Configurable
an analytics.Dispatcher
retryhttp *retryhttp.Client
cache *AvailableUpdate
done chan struct{}

InvocationSource InvocationSource
}

func NewDefaultChecker(cfg Configurable, an analytics.Dispatcher) *Checker {
infoURL := constants.APIUpdateInfoURL
if url, ok := os.LookupEnv("_TEST_UPDATE_INFO_URL"); ok {
infoURL = url
}
return NewChecker(cfg, an, infoURL, retryhttp.DefaultClient)
return NewChecker(cfg, an, retryhttp.DefaultClient)
}

func NewChecker(cfg Configurable, an analytics.Dispatcher, infoURL string, httpget *retryhttp.Client) *Checker {
func NewChecker(cfg Configurable, an analytics.Dispatcher, httpget *retryhttp.Client) *Checker {
return &Checker{
cfg,
an,
infoURL,
httpget,
nil,
make(chan struct{}),
Expand Down Expand Up @@ -83,11 +96,16 @@ func (u *Checker) infoURL(tag, desiredVersion, branchName, platform, arch string
v.Set("target-version", desiredVersion)
}

infoURL := UpdateEndpointURL
if url, ok := os.LookupEnv("_TEST_UPDATE_INFO_URL"); ok {
infoURL = url
}

if tag != "" {
v.Set("tag", tag)
}

return u.apiInfoURL + "/info?" + v.Encode()
return infoURL + "/info?" + v.Encode()
}

func (u *Checker) getUpdateInfo(desiredChannel, desiredVersion string) (*AvailableUpdate, error) {
Expand Down Expand Up @@ -118,13 +136,15 @@ func (u *Checker) getUpdateInfo(desiredChannel, desiredVersion string) (*Availab
logging.Debug("Update info 404s: %v", errs.JoinMessage(err))
label = anaConst.UpdateLabelUnavailable
msg = anaConst.UpdateErrorNotFound
info = &AvailableUpdate{}

// The request could not be satisfied or service is unavailable. This happens when Cloudflare
// blocks access, or the service is unavailable in a particular geographic location.
case resp.StatusCode == 403 || resp.StatusCode == 503:
logging.Warning("Update info request blocked or service unavailable: %v", err)
label = anaConst.UpdateLabelUnavailable
msg = anaConst.UpdateErrorBlocked
info = &AvailableUpdate{}

// If all went well.
default:
Expand Down
55 changes: 55 additions & 0 deletions test/integration/update_int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,61 @@ func (suite *UpdateIntegrationTestSuite) TestUpdateTags() {
}
}

func (suite *UpdateIntegrationTestSuite) TestUpdateHost_SetBeforeInvocation() {
suite.OnlyRunForTags(tagsuite.Update)

ts := e2e.New(suite.T(), false)
defer ts.Close()

ts.SetConfig(constants.UpdateEndpointConfig, "https://test.example.com/update")
suite.Assert().Equal(ts.GetConfig(constants.UpdateEndpointConfig), "https://test.example.com/update")

cp := ts.SpawnWithOpts(
e2e.OptArgs("--version"),
)
cp.ExpectExitCode(0)

correctHostCount := 0
incorrectHostCount := 0
for _, path := range ts.LogFiles() {
contents := string(fileutils.ReadFileUnsafe(path))
if strings.Contains(contents, "https://test.example.com/update") {
correctHostCount++
}
if strings.Contains(contents, "https://platform.activestate.com/update") {
incorrectHostCount++
}
}
suite.Assert().Greater(correctHostCount, 0, "Log file should contain the configured API host 'test.example.com'")
suite.Assert().Equal(incorrectHostCount, 0, "Log file should not contain the default API host 'platform.activestate.com'")

// Clean up - remove the config setting
cp = ts.Spawn("config", "set", constants.UpdateEndpointConfig, "")
cp.Expect("Successfully")
cp.ExpectExitCode(0)
}

func (suite *UpdateIntegrationTestSuite) TestUpdateHost() {
suite.OnlyRunForTags(tagsuite.Update)

ts := e2e.New(suite.T(), false)
defer ts.Close()

cp := ts.Spawn("config", "set", constants.UpdateEndpointConfig, "https://example.com/update")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I prefer to use something that's not an actual domain like test.tld. I can only imagine the traffic example.com sees :p

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example.com is not a valid domain. This is so you can use it in these contexts.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example.com is not a valid domain. This is so you can use it in these contexts.

I'm going to pretend that I knew that when I wrote this test 🙂

cp.Expect("Successfully set config key")
cp.ExpectExitCode(0)

cp = ts.SpawnWithOpts(
e2e.OptArgs("update"),
e2e.OptAppendEnv(suite.env(false, false)...),
e2e.OptAppendEnv("VERBOSE=true"),
)
cp.ExpectExitCode(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really gives a zero exit code? I would imagine this will fail.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checker handles some 4xx codes without throwing an error. I think that's what's happening here.


output := cp.Snapshot()
suite.Assert().Contains(output, "Getting update info: https://example.com/update/")
}

func TestUpdateIntegrationTestSuite(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode.")
Expand Down
Loading