diff --git a/cmd/state-svc/main.go b/cmd/state-svc/main.go index 0bdfb950ff..6a30ab9380 100644 --- a/cmd/state-svc/main.go +++ b/cmd/state-svc/main.go @@ -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/pkg/platform/api" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/inconshreveable/mousetrap" ) @@ -70,6 +71,8 @@ func main() { rollbar.SetupRollbar(constants.StateServiceRollbarToken) rollbar.SetConfig(cfg) + api.SetConfig(cfg) + if os.Getenv("VERBOSE") == "true" { logging.CurrentHandler().SetVerbose(true) } diff --git a/cmd/state/main.go b/cmd/state/main.go index 42d2a2f9dd..bd6656179a 100644 --- a/cmd/state/main.go +++ b/cmd/state/main.go @@ -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/pkg/platform/api" secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -91,6 +92,7 @@ func main() { return } rollbar.SetConfig(cfg) + api.SetConfig(cfg) // Configuration options // This should only be used if the config option is not exclusive to one package. diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 93185796cf..61d0155c68 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -261,6 +261,9 @@ const VulnerabilitiesAPIPath = "/v13s/v1/graphql" // HasuraInventoryAPIPath is the path used for the hasura inventory api const HasuraInventoryAPIPath = "/sv/hasura-inventory/v1/graphql" +// UpdateInfoAPIPath is the path used for the update info api +const UpdateInfoAPIPath = "/sv/state-update/api/v1" + // NotificationsInfoURL is the URL we check against to see what versions are deprecated const NotificationsInfoURL = "https://state-tool.s3.amazonaws.com/messages.json" @@ -409,6 +412,9 @@ const SecurityPromptConfig = "security.prompt.enabled" // SecurityPromptLevelConfig is the config key used to determine the level of security prompts const SecurityPromptLevelConfig = "security.prompt.level" +// APIHostConfig is the config key used to determine the api host +const APIHostConfig = "api.host" + // SvcAppName is the name we give our state-svc application const SvcAppName = "State Service" diff --git a/internal/runbits/checkout/checkout.go b/internal/runbits/checkout/checkout.go index fccb4ec9a8..dabf608edd 100644 --- a/internal/runbits/checkout/checkout.go +++ b/internal/runbits/checkout/checkout.go @@ -14,6 +14,7 @@ import ( "github.com/ActiveState/cli/internal/runbits/buildscript" "github.com/ActiveState/cli/internal/runbits/git" "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/pkg/platform/api" "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" @@ -193,6 +194,7 @@ func CreateProjectFiles(checkoutPath, cachePath, owner, name, branch, commitID, Language: language, Cache: cachePath, Portable: portable, + Host: api.HostOverride(), }) if err != nil { if osutils.IsAccessDeniedError(err) { diff --git a/internal/runbits/runtime/runtime.go b/internal/runbits/runtime/runtime.go index 326870ec22..55d4b8ea50 100644 --- a/internal/runbits/runtime/runtime.go +++ b/internal/runbits/runtime/runtime.go @@ -27,6 +27,7 @@ import ( "github.com/ActiveState/cli/internal/runbits/runtime/trigger" "github.com/ActiveState/cli/pkg/buildplan" "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/pkg/platform/api" "github.com/ActiveState/cli/pkg/platform/model" bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" "github.com/ActiveState/cli/pkg/project" @@ -264,7 +265,7 @@ func Update( // Build progress URL is of the form // https://///distributions?branch=&commitID= host := constants.DefaultAPIHost - if hostOverride := os.Getenv(constants.APIHostEnvVarName); hostOverride != "" { + if hostOverride := api.HostOverride(); hostOverride != "" { host = hostOverride } path, err := url.JoinPath(proj.Owner(), proj.Name(), constants.BuildProgressUrlPathName) diff --git a/internal/runners/initialize/init.go b/internal/runners/initialize/init.go index a01fcef004..5ad84880c9 100644 --- a/internal/runners/initialize/init.go +++ b/internal/runners/initialize/init.go @@ -25,6 +25,7 @@ import ( "github.com/ActiveState/cli/internal/runbits/runtime" "github.com/ActiveState/cli/internal/runbits/runtime/trigger" "github.com/ActiveState/cli/pkg/localcommit" + "github.com/ActiveState/cli/pkg/platform/api" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" @@ -186,6 +187,7 @@ func (r *Initialize) Run(params *RunParams) (rerr error) { Language: lang.String(), Directory: path, Private: params.Private, + Host: api.HostOverride(), } pjfile, err := projectfile.Create(createParams) diff --git a/internal/testhelpers/e2e/session.go b/internal/testhelpers/e2e/session.go index 61c590d6cd..92a0522c20 100644 --- a/internal/testhelpers/e2e/session.go +++ b/internal/testhelpers/e2e/session.go @@ -65,6 +65,7 @@ type Session struct { spawned []*SpawnedCmd ignoreLogErrors bool cache keyCache + cfg *config.Instance } type keyCache map[string]string @@ -203,6 +204,7 @@ func new(t *testing.T, retainDirs, updatePath bool, extraEnv ...string) *Session if err := cfg.Set(constants.SecurityPromptConfig, false); err != nil { require.NoError(session.T, err) } + session.cfg = cfg return session } @@ -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() } diff --git a/internal/testhelpers/tagsuite/tagsuite.go b/internal/testhelpers/tagsuite/tagsuite.go index 5001693d46..04fd9c4a5d 100644 --- a/internal/testhelpers/tagsuite/tagsuite.go +++ b/internal/testhelpers/tagsuite/tagsuite.go @@ -13,6 +13,7 @@ import ( const ( Activate = "activate" Analytics = "analytics" + API = "api" Artifacts = "artifacts" Auth = "auth" Automation = "automation" diff --git a/pkg/platform/api/settings.go b/pkg/platform/api/settings.go index 1e8482c407..947f6ff367 100644 --- a/pkg/platform/api/settings.go +++ b/pkg/platform/api/settings.go @@ -5,13 +5,19 @@ import ( "os" "strings" + configMediator "github.com/ActiveState/cli/internal/mediators/config" "github.com/ActiveState/cli/pkg/projectfile" "github.com/ActiveState/cli/internal/condition" + "github.com/ActiveState/cli/internal/config" "github.com/ActiveState/cli/internal/constants" "github.com/ActiveState/cli/internal/logging" ) +func init() { + configMediator.RegisterOption(constants.APIHostConfig, configMediator.String, "") +} + // Service records available api services type Service string @@ -49,6 +55,9 @@ const ( // ServiceHasuraInventory is the Hasura service for inventory information. ServiceHasuraInventory = "hasura-inventory" + // ServiceUpdateInfo is the service for update info + ServiceUpdateInfo = "update-info" + // TestingPlatform is the API host used by tests so-as not to affect production. TestingPlatform = ".testing.tld" ) @@ -109,6 +118,24 @@ var urlsByService = map[Service]*url.URL{ Host: constants.DefaultAPIHost, Path: constants.HasuraInventoryAPIPath, }, + ServiceUpdateInfo: { + Scheme: "https", + Host: constants.DefaultAPIHost, + Path: constants.UpdateInfoAPIPath, + }, +} + +var configuredAPIHost string + +func registerConfigListener(cfg *config.Instance) { + configMediator.AddListener(constants.APIHostConfig, func() { + configuredAPIHost = cfg.GetString(constants.APIHostConfig) + }) +} + +func SetConfig(cfg *config.Instance) { + configuredAPIHost = cfg.GetString(constants.APIHostConfig) + registerConfigListener(cfg) } // GetServiceURL returns the URL for the given service @@ -121,7 +148,7 @@ func GetServiceURL(service Service) *url.URL { serviceURL.Host = *host } - if insecure := os.Getenv(constants.APIInsecureEnvVarName); insecure == "true" { + if insecure := os.Getenv(constants.APIHostEnvVarName); insecure == "true" { if serviceURL.Scheme == "https" || serviceURL.Scheme == "wss" { serviceURL.Scheme = strings.TrimRight(serviceURL.Scheme, "s") } @@ -142,8 +169,9 @@ func GetServiceURL(service Service) *url.URL { } func getProjectHost(service Service) *string { - if apiHost := os.Getenv(constants.APIHostEnvVarName); apiHost != "" { - return &apiHost + if host := HostOverride(); host != "" { + logging.Debug("Using host override: %s", host) + return &host } if condition.InUnitTest() { @@ -164,11 +192,30 @@ func getProjectHost(service Service) *string { return &url.Host } +func getProjectHostFromConfig() string { + if configuredAPIHost != "" { + return configuredAPIHost + } + return "" +} + +func HostOverride() string { + if apiHost := os.Getenv(constants.APIHostEnvVarName); apiHost != "" { + return apiHost + } + + if apiHost := getProjectHostFromConfig(); apiHost != "" { + return apiHost + } + + return "" +} + // GetPlatformURL returns a generic Platform URL for the given path. // This is for retrieving non-service URLs (e.g. signup URL). func GetPlatformURL(path string) *url.URL { host := constants.DefaultAPIHost - if hostOverride := os.Getenv(constants.APIHostEnvVarName); hostOverride != "" { + if hostOverride := HostOverride(); hostOverride != "" { host = hostOverride } return &url.URL{ diff --git a/pkg/projectfile/create_test.go b/pkg/projectfile/create_test.go index 9f08634b7e..3bbf7b6570 100644 --- a/pkg/projectfile/create_test.go +++ b/pkg/projectfile/create_test.go @@ -38,6 +38,7 @@ func Test_Create(t *testing.T) { Project: tt.args.project, Directory: tt.args.directory, Language: tt.args.language, + Host: "test.example.com", }) assert.NoError(t, err) configFile := filepath.Join(tempDir, constants.ConfigFileName) diff --git a/pkg/projectfile/projectfile.go b/pkg/projectfile/projectfile.go index 3879d816e2..128e01d95a 100644 --- a/pkg/projectfile/projectfile.go +++ b/pkg/projectfile/projectfile.go @@ -922,6 +922,7 @@ type CreateParams struct { ProjectURL string Cache string Portable bool + Host string } // Create will create a new activestate.yaml with a projectURL for the given details @@ -943,9 +944,9 @@ func createCustom(params *CreateParams, lang language.Language) (*Project, error if params.ProjectURL == "" { // Note: cannot use api.GetPlatformURL() due to import cycle. - host := constants.DefaultAPIHost - if hostOverride := os.Getenv(constants.APIHostEnvVarName); hostOverride != "" { - host = hostOverride + host := params.Host + if host == "" { + host = constants.DefaultAPIHost } u, err := url.Parse(fmt.Sprintf("https://%s/%s/%s", host, params.Owner, params.Project)) if err != nil { diff --git a/test/integration/api_int_test.go b/test/integration/api_int_test.go index c00cf5c6a6..c92ade624d 100644 --- a/test/integration/api_int_test.go +++ b/test/integration/api_int_test.go @@ -56,6 +56,78 @@ func (suite *ApiIntegrationTestSuite) TestNoApiCallsForPlainInvocation() { suite.Assert().True(readLogFile, "did not read log file") } +func (suite *ApiIntegrationTestSuite) TestAPIHostConfig_SetBeforeInvocation() { + suite.OnlyRunForTags(tagsuite.API) + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + ts.SetConfig("api.host", "test.example.com") + suite.Assert().Equal(ts.GetConfig("api.host"), "test.example.com") + + cp := ts.SpawnWithOpts( + e2e.OptArgs("--version"), + ) + cp.ExpectExitCode(0) + ts.IgnoreLogErrors() + + correctHostCount := 0 + incorrectHostCount := 0 + for _, path := range ts.LogFiles() { + contents := string(fileutils.ReadFileUnsafe(path)) + if strings.Contains(contents, "test.example.com") { + correctHostCount++ + } + if strings.Contains(contents, "platform.activestate.com") { + incorrectHostCount++ + } + } + suite.Assert().Greater(correctHostCount, 0, "Log file should contain the configured API host 'test.example.com'") + // TODO: This is failing because the state-svc is trying to update with the default host. + // This will be addressed by CP-1054 very shortly. + // 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", "api.host", "") + cp.Expect("Successfully") + cp.ExpectExitCode(0) +} + +func (suite *ApiIntegrationTestSuite) TestAPIHostConfig_SetOnFirstInvocation() { + suite.OnlyRunForTags(tagsuite.API) + + ts := e2e.New(suite.T(), false) + defer ts.Close() + + cp := ts.Spawn("config", "set", "api.host", "test.example.com") + cp.Expect("Successfully") + cp.ExpectExitCode(0) + + cp = ts.SpawnWithOpts( + e2e.OptArgs("--version"), + e2e.OptAppendEnv("VERBOSE=true"), + ) + cp.ExpectExitCode(0) + // After setting the config, there should be no log entries for the default host. + suite.Assert().NotContains(cp.Output(), "platform.activestate.com") + + // Some state-svc log entries will contain the default host as it executed requests before + // we set the config value. + correctHostCount := 0 + for _, path := range ts.LogFiles() { + contents := string(fileutils.ReadFileUnsafe(path)) + if strings.Contains(contents, "test.example.com") { + correctHostCount++ + } + } + suite.Assert().Greater(correctHostCount, 0, "Log file should contain the configured API host 'test.example.com'") + + // Clean up - remove the config setting + cp = ts.Spawn("config", "set", "api.host", "") + cp.Expect("Successfully") + cp.ExpectExitCode(0) +} + func TestApiIntegrationTestSuite(t *testing.T) { suite.Run(t, new(ApiIntegrationTestSuite)) } diff --git a/test/integration/config_int_test.go b/test/integration/config_int_test.go index df2021309e..ea5972d39d 100644 --- a/test/integration/config_int_test.go +++ b/test/integration/config_int_test.go @@ -115,6 +115,38 @@ func (suite *ConfigIntegrationTestSuite) TestList() { suite.Require().NotContains(cp.Snapshot(), constants.AsyncRuntimeConfig) } + +func (suite *ConfigIntegrationTestSuite) TestAPIHostConfig() { + suite.OnlyRunForTags(tagsuite.Config) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + cp := ts.Spawn("config", "set", "api.host", "test.example.com", "-o", "json") + cp.ExpectExitCode(0) + AssertValidJSON(suite.T(), cp) + cp.Expect(`{"name":"api.host","value":"test.example.com"}`) + + cp = ts.Spawn("config", "get", "api.host", "-o", "json") + cp.ExpectExitCode(0) + AssertValidJSON(suite.T(), cp) + cp.Expect(`{"name":"api.host","value":"test.example.com"}`) + + cp = ts.Spawn("config") + cp.Expect("api.host") + cp.Expect("test.example.com") + cp.ExpectExitCode(0) + + cp = ts.Spawn("config", "set", "api.host", "", "-o", "json") + cp.ExpectExitCode(0) + AssertValidJSON(suite.T(), cp) + cp.Expect(`{"name":"api.host","value":""}`) + + cp = ts.Spawn("config", "get", "api.host", "-o", "json") + cp.ExpectExitCode(0) + AssertValidJSON(suite.T(), cp) + cp.Expect(`{"name":"api.host","value":""}`) +} + func TestConfigIntegrationTestSuite(t *testing.T) { suite.Run(t, new(ConfigIntegrationTestSuite)) }