diff options
47 files changed, 1169 insertions, 167 deletions
diff --git a/client/go/auth/auth.go b/client/go/auth/auth.go new file mode 100644 index 00000000000..397e410924d --- /dev/null +++ b/client/go/auth/auth.go @@ -0,0 +1,175 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package auth + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + audiencePath = "/api/v2/" + waitThresholdInSeconds = 3 + // SecretsNamespace namespace used to set/get values from the keychain + SecretsNamespace = "vespa-cli" +) + +var requiredScopes = []string{"openid", "offline_access"} + +type Authenticator struct { + Audience string + ClientID string + DeviceCodeEndpoint string + OauthTokenEndpoint string +} + +// SecretStore provides access to stored sensitive data. +type SecretStore interface { + // Get gets the secret + Get(namespace, key string) (string, error) + // Delete removes the secret + Delete(namespace, key string) error +} + +type Result struct { + Tenant string + Domain string + RefreshToken string + AccessToken string + ExpiresIn int64 +} + +type State struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri_complete"` + ExpiresIn int `json:"expires_in"` + Interval int `json:"interval"` +} + +// RequiredScopes returns the scopes used for login. +func RequiredScopes() []string { return requiredScopes } + +func (s *State) IntervalDuration() time.Duration { + return time.Duration(s.Interval+waitThresholdInSeconds) * time.Second +} + +// Start kicks-off the device authentication flow +// by requesting a device code from Auth0, +// The returned state contains the URI for the next step of the flow. +func (a *Authenticator) Start(ctx context.Context) (State, error) { + s, err := a.getDeviceCode(ctx) + if err != nil { + return State{}, fmt.Errorf("cannot get device code: %w", err) + } + return s, nil +} + +// Wait waits until the user is logged in on the browser. +func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) { + t := time.NewTicker(state.IntervalDuration()) + for { + select { + case <-ctx.Done(): + return Result{}, ctx.Err() + case <-t.C: + data := url.Values{ + "client_id": {a.ClientID}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + "device_code": {state.DeviceCode}, + } + r, err := http.PostForm(a.OauthTokenEndpoint, data) + if err != nil { + return Result{}, fmt.Errorf("cannot get device code: %w", err) + } + defer r.Body.Close() + + var res struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` + ExpiresIn int64 `json:"expires_in"` + TokenType string `json:"token_type"` + Error *string `json:"error,omitempty"` + ErrorDescription string `json:"error_description,omitempty"` + } + + err = json.NewDecoder(r.Body).Decode(&res) + if err != nil { + return Result{}, fmt.Errorf("cannot decode response: %w", err) + } + + if res.Error != nil { + if *res.Error == "authorization_pending" { + continue + } + return Result{}, errors.New(res.ErrorDescription) + } + + ten, domain, err := parseTenant(res.AccessToken) + if err != nil { + return Result{}, fmt.Errorf("cannot parse tenant from the given access token: %w", err) + } + + return Result{ + RefreshToken: res.RefreshToken, + AccessToken: res.AccessToken, + ExpiresIn: res.ExpiresIn, + Tenant: ten, + Domain: domain, + }, nil + } + } +} + +func (a *Authenticator) getDeviceCode(ctx context.Context) (State, error) { + data := url.Values{ + "client_id": {a.ClientID}, + "scope": {strings.Join(requiredScopes, " ")}, + "audience": {a.Audience}, + } + r, err := http.PostForm(a.DeviceCodeEndpoint, data) + if err != nil { + return State{}, fmt.Errorf("cannot get device code: %w", err) + } + defer r.Body.Close() + var res State + err = json.NewDecoder(r.Body).Decode(&res) + if err != nil { + return State{}, fmt.Errorf("cannot decode response: %w", err) + } + return res, nil +} + +func parseTenant(accessToken string) (tenant, domain string, err error) { + parts := strings.Split(accessToken, ".") + v, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "", "", err + } + var payload struct { + AUDs []string `json:"aud"` + } + if err := json.Unmarshal(v, &payload); err != nil { + return "", "", err + } + for _, aud := range payload.AUDs { + u, err := url.Parse(aud) + if err != nil { + return "", "", err + } + if u.Path == audiencePath { + parts := strings.Split(u.Host, ".") + return parts[0], u.Host, nil + } + } + return "", "", fmt.Errorf("audience not found for %s", audiencePath) +} diff --git a/client/go/auth/secrets.go b/client/go/auth/secrets.go new file mode 100644 index 00000000000..e38d8c56595 --- /dev/null +++ b/client/go/auth/secrets.go @@ -0,0 +1,24 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package auth + +import ( + "github.com/zalando/go-keyring" +) + +type Keyring struct{} + +// Set sets the given key/value pair with the given namespace. +func (k *Keyring) Set(namespace, key, value string) error { + return keyring.Set(namespace, key, value) +} + +// Get gets a value for the given namespace and key. +func (k *Keyring) Get(namespace, key string) (string, error) { + return keyring.Get(namespace, key) +} + +// Delete deletes a value for the given namespace and key. +func (k *Keyring) Delete(namespace, key string) error { + return keyring.Delete(namespace, key) +} diff --git a/client/go/auth/token.go b/client/go/auth/token.go new file mode 100644 index 00000000000..e9b90b8994e --- /dev/null +++ b/client/go/auth/token.go @@ -0,0 +1,68 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package auth + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" +) + +type TokenResponse struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` +} + +type TokenRetriever struct { + Authenticator *Authenticator + Secrets SecretStore + Client *http.Client +} + +// Delete deletes the given tenant from the secrets' storage. +func (t *TokenRetriever) Delete(tenant string) error { + return t.Secrets.Delete(SecretsNamespace, tenant) +} + +// Refresh gets a new access token from the provided refresh token, +// The request is used the default client_id and endpoint for device authentication. +func (t *TokenRetriever) Refresh(ctx context.Context, tenant string) (TokenResponse, error) { + // get stored refresh token: + refreshToken, err := t.Secrets.Get(SecretsNamespace, tenant) + if err != nil { + return TokenResponse{}, fmt.Errorf("cannot get the stored refresh token: %w", err) + } + if refreshToken == "" { + return TokenResponse{}, errors.New("cannot use the stored refresh token: the token is empty") + } + // get access token: + r, err := t.Client.PostForm(t.Authenticator.OauthTokenEndpoint, url.Values{ + "grant_type": {"refresh_token"}, + "client_id": {t.Authenticator.ClientID}, + "refresh_token": {refreshToken}, + }) + if err != nil { + return TokenResponse{}, fmt.Errorf("cannot get a new access token from the refresh token: %w", err) + } + + defer r.Body.Close() + if r.StatusCode != http.StatusOK { + b, _ := ioutil.ReadAll(r.Body) + bodyStr := string(b) + return TokenResponse{}, fmt.Errorf("cannot get a new access token from the refresh token: %s", bodyStr) + } + + var res TokenResponse + err = json.NewDecoder(r.Body).Decode(&res) + if err != nil { + return TokenResponse{}, fmt.Errorf("cannot decode response: %w", err) + } + + return res, nil +} diff --git a/client/go/cli/cli.go b/client/go/cli/cli.go new file mode 100644 index 00000000000..e1dde387b89 --- /dev/null +++ b/client/go/cli/cli.go @@ -0,0 +1,355 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package cli + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/joeshaw/envdecode" + "github.com/pkg/browser" + "github.com/vespa-engine/vespa/client/go/util" + "io/ioutil" + "net/http" + "os" + "os/signal" + "path/filepath" + "sort" + "sync" + "time" + + "github.com/lestrrat-go/jwx/jwt" + "github.com/vespa-engine/vespa/client/go/auth" +) + +const accessTokenExpThreshold = 5 * time.Minute + +var errUnauthenticated = errors.New("not logged in. Try 'vespa login'") + +type config struct { + InstallID string `json:"install_id,omitempty"` + DefaultTenant string `json:"default_tenant"` + Tenants map[string]Tenant `json:"tenants"` +} + +// Tenant is an auth0 Tenant. +type Tenant struct { + Name string `json:"name"` + Domain string `json:"domain"` + AccessToken string `json:"access_token,omitempty"` + Scopes []string `json:"scopes,omitempty"` + ExpiresAt time.Time `json:"expires_at"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} + +type Cli struct { + Authenticator *auth.Authenticator + tenant string + initOnce sync.Once + errOnce error + Path string + config config +} + +// IsLoggedIn encodes the domain logic for determining whether we're +// logged in. This might check our config storage, or just in memory. +func (c *Cli) IsLoggedIn() bool { + // No need to check errors for initializing context. + _ = c.init() + + if c.tenant == "" { + return false + } + + // Parse the access token for the tenant. + t, err := jwt.ParseString(c.config.Tenants[c.tenant].AccessToken) + if err != nil { + return false + } + + // Check if token is valid. + if err = jwt.Validate(t, jwt.WithIssuer("https://vespa-cd.auth0.com/")); err != nil { + return false + } + + return true +} + +// default to vespa-cd.auth0.com +var ( + authCfg struct { + Audience string `env:"AUTH0_AUDIENCE,default=https://vespa-cd.auth0.com/api/v2/"` + ClientID string `env:"AUTH0_CLIENT_ID,default=4wYWA496zBP28SLiz0PuvCt8ltL11DZX"` + DeviceCodeEndpoint string `env:"AUTH0_DEVICE_CODE_ENDPOINT,default=https://vespa-cd.auth0.com/oauth/device/code"` + OauthTokenEndpoint string `env:"AUTH0_OAUTH_TOKEN_ENDPOINT,default=https://vespa-cd.auth0.com/oauth/token"` + } +) + +func ContextWithCancel() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + go func() { + <-ch + defer cancel() + os.Exit(0) + }() + return ctx +} + +// Setup will try to initialize the config context, as well as figure out if +// there's a readily available tenant. +func GetCli(configPath string) (*Cli, error) { + c := Cli{} + c.Path = configPath + if err := envdecode.StrictDecode(&authCfg); err != nil { + return nil, fmt.Errorf("could not decode env: %w", err) + } + c.Authenticator = &auth.Authenticator{ + Audience: authCfg.Audience, + ClientID: authCfg.ClientID, + DeviceCodeEndpoint: authCfg.DeviceCodeEndpoint, + OauthTokenEndpoint: authCfg.OauthTokenEndpoint, + } + return &c, nil +} + +// prepareTenant loads the Tenant, refreshing its token if necessary. +// The Tenant access token needs a refresh if: +// 1. the Tenant scopes are different from the currently required scopes. +// 2. the access token is expired. +func (c *Cli) PrepareTenant(ctx context.Context) (Tenant, error) { + if err := c.init(); err != nil { + return Tenant{}, err + } + t, err := c.getTenant() + if err != nil { + return Tenant{}, err + } + + if t.ClientID != "" && t.ClientSecret != "" { + return t, nil + } + + if t.AccessToken == "" || scopesChanged(t) { + t, err = RunLogin(ctx, c, true) + if err != nil { + return Tenant{}, err + } + } else if isExpired(t.ExpiresAt, accessTokenExpThreshold) { + // check if the stored access token is expired: + // use the refresh token to get a new access token: + tr := &auth.TokenRetriever{ + Authenticator: c.Authenticator, + Secrets: &auth.Keyring{}, + Client: http.DefaultClient, + } + + res, err := tr.Refresh(ctx, t.Domain) + if err != nil { + // ask and guide the user through the login process: + fmt.Println(fmt.Errorf("failed to renew access token, %s", err)) + t, err = RunLogin(ctx, c, true) + if err != nil { + return Tenant{}, err + } + } else { + // persist the updated tenant with renewed access token + t.AccessToken = res.AccessToken + t.ExpiresAt = time.Now().Add( + time.Duration(res.ExpiresIn) * time.Second, + ) + + err = c.AddTenant(t) + if err != nil { + return Tenant{}, err + } + } + } + + return t, nil +} + +// isExpired is true if now() + a threshold is after the given date +func isExpired(t time.Time, threshold time.Duration) bool { + return time.Now().Add(threshold).After(t) +} + +// scopesChanged compare the Tenant scopes +// with the currently required scopes. +func scopesChanged(t Tenant) bool { + want := auth.RequiredScopes() + got := t.Scopes + + sort.Strings(want) + sort.Strings(got) + + if (want == nil) != (got == nil) { + return true + } + + if len(want) != len(got) { + return true + } + + for i := range t.Scopes { + if want[i] != got[i] { + return true + } + } + + return false +} + +func (c *Cli) getTenant() (Tenant, error) { + if err := c.init(); err != nil { + return Tenant{}, err + } + + t, ok := c.config.Tenants[c.tenant] + if !ok { + return Tenant{}, fmt.Errorf("unable to find tenant: %s; run 'vespa login' to configure a new tenant", c.tenant) + } + + return t, nil +} + +// AddTenant assigns an existing, or new Tenant. This is expected to be called +// after a login has completed. +func (c *Cli) AddTenant(ten Tenant) error { + _ = c.init() + + if c.config.DefaultTenant == "" { + c.config.DefaultTenant = ten.Domain + } + + // If we're dealing with an empty file, we'll need to initialize this map. + if c.config.Tenants == nil { + c.config.Tenants = map[string]Tenant{} + } + + c.config.Tenants[ten.Domain] = ten + + if err := c.persistConfig(); err != nil { + return fmt.Errorf("unexpected error persisting config: %w", err) + } + + return nil +} + +func (c *Cli) persistConfig() error { + dir := filepath.Dir(c.Path) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + } + + buf, err := json.MarshalIndent(c.config, "", " ") + if err != nil { + return err + } + + if err := ioutil.WriteFile(c.Path, buf, 0600); err != nil { + return err + } + + return nil +} + +func (c *Cli) init() error { + c.initOnce.Do(func() { + if c.errOnce = c.initContext(); c.errOnce != nil { + return + } + }) + return c.errOnce +} + +func (c *Cli) initContext() (err error) { + if _, err := os.Stat(c.Path); os.IsNotExist(err) { + return errUnauthenticated + } + + var buf []byte + if buf, err = ioutil.ReadFile(c.Path); err != nil { + return err + } + + if err := json.Unmarshal(buf, &c.config); err != nil { + return err + } + + if c.tenant == "" && c.config.DefaultTenant == "" { + return errUnauthenticated + } + + if c.tenant == "" { + c.tenant = c.config.DefaultTenant + } + + return nil +} + +// RunLogin runs the login flow guiding the user through the process +// by showing the login instructions, opening the browser. +// Use `expired` to run the login from other commands setup: +// this will only affect the messages. +func RunLogin(ctx context.Context, cli *Cli, expired bool) (Tenant, error) { + if expired { + fmt.Println("Please sign in to re-authorize the CLI.") + } + + state, err := cli.Authenticator.Start(ctx) + if err != nil { + return Tenant{}, fmt.Errorf("could not start the authentication process: %w", err) + } + + fmt.Printf("Your Device Confirmation code is: %s\n\n", state.UserCode) + fmt.Println("Press Enter to open the browser to log in or ^C to quit...") + fmt.Scanln() + + err = browser.OpenURL(state.VerificationURI) + + if err != nil { + fmt.Printf("Couldn't open the URL, please do it manually: %s.", state.VerificationURI) + } + + var res auth.Result + err = util.Spinner("Waiting for login to complete in browser", func() error { + res, err = cli.Authenticator.Wait(ctx, state) + return err + }) + + if err != nil { + return Tenant{}, fmt.Errorf("login error: %w", err) + } + + fmt.Print("\n") + fmt.Println("Successfully logged in.") + fmt.Print("\n") + + // store the refresh token + secretsStore := &auth.Keyring{} + err = secretsStore.Set(auth.SecretsNamespace, res.Domain, res.RefreshToken) + if err != nil { + // log the error but move on + fmt.Println("Could not store the refresh token locally, please expect to login again once your access token expired.") + } + + t := Tenant{ + Name: res.Tenant, + Domain: res.Domain, + AccessToken: res.AccessToken, + ExpiresAt: time.Now().Add(time.Duration(res.ExpiresIn) * time.Second), + Scopes: auth.RequiredScopes(), + } + err = cli.AddTenant(t) + if err != nil { + return Tenant{}, fmt.Errorf("could not add tenant to config: %w", err) + } + + return t, nil +} diff --git a/client/go/cmd/config.go b/client/go/cmd/config.go index d10f66c83c6..0b08a2dc28d 100644 --- a/client/go/cmd/config.go +++ b/client/go/cmd/config.go @@ -148,6 +148,10 @@ func (c *Config) ReadAPIKey(tenantName string) ([]byte, error) { return ioutil.ReadFile(c.APIKeyPath(tenantName)) } +func (c *Config) AuthConfigPath() string { + return filepath.Join(c.Home, "auth", "config.json") +} + func (c *Config) ReadSessionID(app vespa.ApplicationID) (int64, error) { sessionPath, err := c.applicationFilePath(app, "session_id") if err != nil { diff --git a/client/go/cmd/helpers.go b/client/go/cmd/helpers.go index 4768290e33e..54d8798b71d 100644 --- a/client/go/cmd/helpers.go +++ b/client/go/cmd/helpers.go @@ -153,10 +153,17 @@ func getConsoleURL() string { } func getApiURL() string { - if getSystem() == "publiccd" { - return "https://api.vespa-external-cd.aws.oath.cloud:4443" + if vespa.Auth0AccessTokenEnabled() { + if getSystem() == "publiccd" { + return "https://api.vespa-external-cd.aws.oath.cloud:443" + } + return "https://api.vespa-external.aws.oath.cloud:443" + } else { + if getSystem() == "publiccd" { + return "https://api.vespa-external-cd.aws.oath.cloud:4443" + } + return "https://api.vespa-external.aws.oath.cloud:4443" } - return "https://api.vespa-external.aws.oath.cloud:4443" } func getTarget() vespa.Target { @@ -174,9 +181,12 @@ func getTarget() vespa.Target { fatalErr(err, "Could not load config") return nil } - apiKey, err := ioutil.ReadFile(cfg.APIKeyPath(deployment.Application.Tenant)) - if err != nil { - fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") + var apiKey []byte = nil + if !vespa.Auth0AccessTokenEnabled() { + apiKey, err = ioutil.ReadFile(cfg.APIKeyPath(deployment.Application.Tenant)) + if err != nil { + fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") + } } privateKeyFile, err := cfg.PrivateKeyPath(deployment.Application) if err != nil { @@ -201,7 +211,8 @@ func getTarget() vespa.Target { vespa.LogOptions{ Writer: stdout, Level: vespa.LogLevel(logLevelArg), - }) + }, + cfg.AuthConfigPath()) } fatalErrHint(fmt.Errorf("Invalid target: %s", targetType), "Valid targets are 'local', 'cloud' or an URL") return nil @@ -232,11 +243,13 @@ func getDeploymentOpts(cfg *Config, pkg vespa.ApplicationPackage, target vespa.T fatalErrHint(fmt.Errorf("Missing certificate in application package"), "Applications in Vespa Cloud require a certificate", "Try 'vespa cert'") return opts } - var err error - opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant) - if err != nil { - fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") - return opts + if !vespa.Auth0AccessTokenEnabled() { + var err error + opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant) + if err != nil { + fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") + return opts + } } opts.Deployment = deployment } diff --git a/client/go/cmd/login.go b/client/go/cmd/login.go new file mode 100644 index 00000000000..767d462b0be --- /dev/null +++ b/client/go/cmd/login.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/vespa-engine/vespa/client/go/cli" + "github.com/vespa-engine/vespa/client/go/vespa" +) + +func init() { + if vespa.Auth0AccessTokenEnabled() { + rootCmd.AddCommand(loginCmd) + } +} + +var loginCmd = &cobra.Command{ + Use: "login", + Args: cobra.NoArgs, + Short: "Authenticate the Vespa CLI", + Example: "$ vespa login", + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + cfg, err := LoadConfig() + if err != nil { + return err + } + c, err := cli.GetCli(cfg.AuthConfigPath()) + if err != nil { + return err + } + _, err = cli.RunLogin(ctx, c, false) + return err + }, +} diff --git a/client/go/go.mod b/client/go/go.mod index 27faff3fd0b..70eea958933 100644 --- a/client/go/go.mod +++ b/client/go/go.mod @@ -3,12 +3,23 @@ module github.com/vespa-engine/vespa/client/go go 1.15 require ( + github.com/briandowns/spinner v1.16.0 + github.com/fatih/color v1.10.0 // indirect + github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 + github.com/lestrrat-go/jwx v1.2.9 github.com/logrusorgru/aurora/v3 v3.0.0 - github.com/mattn/go-colorable v0.0.9 - github.com/mattn/go-isatty v0.0.3 + github.com/mattn/go-colorable v0.1.8 + github.com/mattn/go-isatty v0.0.13 + github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 + github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.8.1 + github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/testify v1.7.0 + github.com/zalando/go-keyring v0.1.1 + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/client/go/go.sum b/client/go/go.sum index 0462d0575a1..59656af2b35 100644 --- a/client/go/go.sum +++ b/client/go/go.sum @@ -45,6 +45,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/briandowns/spinner v1.16.0 h1:DFmp6hEaIx2QXXuqSJmtfSBSAjRmpGiKG6ip2Wm/yOs= +github.com/briandowns/spinner v1.16.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -57,9 +59,14 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g= +github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -68,12 +75,18 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/goccy/go-json v0.7.10 h1:ulhbuNe1JqE68nMRXXTJRrUu0uhouf0VevLINxQq4Ec= +github.com/goccy/go-json v0.7.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -165,6 +178,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd h1:nIzoSW6OhhppWLm4yqBwZsKJlAayUu5FGozhrF3ETSM= +github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd/go.mod h1:MEQrHur0g8VplbLOv5vXmDzacSaH9Z7XhcgsSh1xciU= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -180,14 +195,31 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc= +github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.9 h1:kS8kLI4oaBYJJ6u6rpbPI0tDYVCqo0P5u8vv1zoQ49U= +github.com/lestrrat-go/jwx v1.2.9/go.mod h1:25DcLbNWArPA/Ew5CcBmewl32cJKxOk5cbepBsIJFzw= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -204,7 +236,11 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc= +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -235,6 +271,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -249,6 +287,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zalando/go-keyring v0.1.1 h1:w2V9lcx/Uj4l+dzAf1m9s+DJ1O8ROkEHnynonHjTcYE= +github.com/zalando/go-keyring v0.1.1/go.mod h1:OIC+OZ28XbmwFxU/Rp9V7eKzZjamBJwRzC8UFJH9+L8= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -269,6 +309,9 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -367,6 +410,7 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -376,9 +420,11 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -403,8 +449,11 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -575,8 +624,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/client/go/util/spinner.go b/client/go/util/spinner.go new file mode 100644 index 00000000000..1deb4296d28 --- /dev/null +++ b/client/go/util/spinner.go @@ -0,0 +1,63 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package util + +import ( + "os" + "time" + + "github.com/briandowns/spinner" + "github.com/pkg/errors" +) + +const ( + spinnerTextEllipsis = "..." + spinnerTextDone = "done" + spinnerTextFailed = "failed" + spinnerColor = "blue" +) + +var messages = os.Stderr + +func Spinner(text string, fn func() error) error { + initialMsg := text + spinnerTextEllipsis + " " + doneMsg := initialMsg + spinnerTextDone + "\n" + failMsg := initialMsg + spinnerTextFailed + "\n" + + return loading(initialMsg, doneMsg, failMsg, fn) +} + +func loading(initialMsg, doneMsg, failMsg string, fn func() error) error { + done := make(chan struct{}) + errc := make(chan error) + go func() { + defer close(done) + + s := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(messages)) + s.Prefix = initialMsg + s.FinalMSG = doneMsg + s.HideCursor = true + s.Writer = messages + + if err := s.Color(spinnerColor); err != nil { + panic(Error(err, "failed setting spinner color")) + } + + s.Start() + err := <-errc + if err != nil { + s.FinalMSG = failMsg + } + + s.Stop() + }() + + err := fn() + errc <- err + <-done + return err +} + +func Error(e error, message string) error { + return errors.Wrap(e, message) +} diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go index 3718c7d813a..9c5fb3a12ac 100644 --- a/client/go/vespa/deploy.go +++ b/client/go/vespa/deploy.go @@ -327,12 +327,12 @@ func Submit(opts DeploymentOpts) error { Header: make(http.Header), } request.Header.Set("Content-Type", writer.FormDataContentType()) - signer := NewRequestSigner(opts.Deployment.Application.SerializedForm(), opts.APIKey) - if err := signer.SignRequest(request); err != nil { + serviceDescription := "Submit service" + sigKeyId := opts.Deployment.Application.SerializedForm() + if err := opts.Target.PrepareApiRequest(request, sigKeyId); err != nil { return err } - serviceDescription := "Submit service" - response, err := util.HttpDo(request, time.Minute*10, serviceDescription) + response, err := util.HttpDo(request, time.Minute*10, sigKeyId) if err != nil { return err } @@ -344,7 +344,7 @@ func checkDeploymentOpts(opts DeploymentOpts) error { if !opts.ApplicationPackage.HasCertificate() { return fmt.Errorf("%s: missing certificate in package", opts) } - if opts.APIKey == nil { + if !Auth0AccessTokenEnabled() && opts.APIKey == nil { return fmt.Errorf("%s: missing api key", opts.String()) } return nil @@ -363,13 +363,11 @@ func uploadApplicationPackage(url *url.URL, opts DeploymentOpts) (int64, error) Header: header, Body: ioutil.NopCloser(zipReader), } - if opts.APIKey != nil { - signer := NewRequestSigner(opts.Deployment.Application.SerializedForm(), opts.APIKey) - if err := signer.SignRequest(request); err != nil { - return 0, err - } - } serviceDescription := "Deploy service" + sigKeyId := opts.Deployment.Application.SerializedForm() + if err := opts.Target.PrepareApiRequest(request, sigKeyId); err != nil { + return 0, err + } response, err := util.HttpDo(request, time.Minute*10, serviceDescription) if err != nil { return 0, err diff --git a/client/go/vespa/target.go b/client/go/vespa/target.go index 8a09440f5cc..f497bf5b3cd 100644 --- a/client/go/vespa/target.go +++ b/client/go/vespa/target.go @@ -6,13 +6,16 @@ import ( "crypto/tls" "encoding/json" "fmt" + "github.com/vespa-engine/vespa/client/go/cli" "io" "io/ioutil" "math" "net/http" "net/url" + "os" "sort" "strconv" + "strings" "time" "github.com/vespa-engine/vespa/client/go/util" @@ -35,6 +38,7 @@ type Service struct { BaseURL string Name string TLSOptions TLSOptions + Target *Target } // Target represents a Vespa platform, running named Vespa services. @@ -47,6 +51,8 @@ type Target interface { // PrintLog writes the logs of this deployment using given options to control output. PrintLog(options LogOptions) error + + PrepareApiRequest(req *http.Request, sigKeyId string) error } // TLSOptions configures the certificate to use for service requests. @@ -66,11 +72,21 @@ type LogOptions struct { Level int } +func Auth0AccessTokenEnabled() bool { + v, present := os.LookupEnv("VESPA_CLI_OAUTH2_DEVICE_FLOW") + if !present { + return false + } + return strings.ToLower(v) == "true" || v == "1" || v == "" +} + type customTarget struct { targetType string baseURL string } +func (t *customTarget) PrepareApiRequest(req *http.Request, sigKeyId string) error { return nil } + // Do sends request to this service. Any required authentication happens automatically. func (s *Service) Do(request *http.Request, timeout time.Duration) (*http.Response, error) { if s.TLSOptions.KeyPair.Certificate != nil { @@ -192,8 +208,9 @@ type cloudTarget struct { tlsOptions TLSOptions logOptions LogOptions - queryURL string - documentURL string + queryURL string + documentURL string + authConfigPath string } func (t *cloudTarget) Type() string { return t.targetType } @@ -221,6 +238,30 @@ func (t *cloudTarget) Service(name string, timeout time.Duration, runID int64) ( return nil, fmt.Errorf("unknown service: %s", name) } +func (t *cloudTarget) PrepareApiRequest(req *http.Request, sigKeyId string) error { + if Auth0AccessTokenEnabled() { + if err := t.addAuth0AccessToken(req); err != nil { + return err + } + } else if t.apiKey != nil { + signer := NewRequestSigner(sigKeyId, t.apiKey) + if err := signer.SignRequest(req); err != nil { + return err + } + } + return nil +} + +func (t *cloudTarget) addAuth0AccessToken(request *http.Request) error { + c, err := cli.GetCli(t.authConfigPath) + tenant, err := c.PrepareTenant(cli.ContextWithCancel()) + if err != nil { + return err + } + request.Header.Set("Authorization", "Bearer "+tenant.AccessToken) + return nil +} + func (t *cloudTarget) logsURL() string { return fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/environment/%s/region/%s/logs", t.apiURL, @@ -233,7 +274,6 @@ func (t *cloudTarget) PrintLog(options LogOptions) error { if err != nil { return err } - signer := NewRequestSigner(t.deployment.Application.SerializedForm(), t.apiKey) lastFrom := options.From requestFunc := func() *http.Request { fromMillis := lastFrom.Unix() * 1000 @@ -244,9 +284,7 @@ func (t *cloudTarget) PrintLog(options LogOptions) error { q.Set("to", strconv.FormatInt(toMillis, 10)) } req.URL.RawQuery = q.Encode() - if err := signer.SignRequest(req); err != nil { - panic(err) - } + t.PrepareApiRequest(req, t.deployment.Application.SerializedForm()) return req } logFunc := func(status int, response []byte) (bool, error) { @@ -280,16 +318,15 @@ func (t *cloudTarget) PrintLog(options LogOptions) error { } func (t *cloudTarget) waitForEndpoints(timeout time.Duration, runID int64) error { - signer := NewRequestSigner(t.deployment.Application.SerializedForm(), t.apiKey) if runID > 0 { - if err := t.waitForRun(signer, runID, timeout); err != nil { + if err := t.waitForRun(runID, timeout); err != nil { return err } } - return t.discoverEndpoints(signer, timeout) + return t.discoverEndpoints(timeout) } -func (t *cloudTarget) waitForRun(signer *RequestSigner, runID int64, timeout time.Duration) error { +func (t *cloudTarget) waitForRun(runID int64, timeout time.Duration) error { runURL := fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/job/%s-%s/run/%d", t.apiURL, t.deployment.Application.Tenant, t.deployment.Application.Application, t.deployment.Application.Instance, @@ -303,7 +340,7 @@ func (t *cloudTarget) waitForRun(signer *RequestSigner, runID int64, timeout tim q := req.URL.Query() q.Set("after", strconv.FormatInt(lastID, 10)) req.URL.RawQuery = q.Encode() - if err := signer.SignRequest(req); err != nil { + if err := t.PrepareApiRequest(req, t.deployment.Application.SerializedForm()); err != nil { panic(err) } return req @@ -353,7 +390,7 @@ func (t *cloudTarget) printLog(response jobResponse, last int64) int64 { return response.LastID } -func (t *cloudTarget) discoverEndpoints(signer *RequestSigner, timeout time.Duration) error { +func (t *cloudTarget) discoverEndpoints(timeout time.Duration) error { deploymentURL := fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/environment/%s/region/%s", t.apiURL, t.deployment.Application.Tenant, t.deployment.Application.Application, t.deployment.Application.Instance, @@ -362,7 +399,7 @@ func (t *cloudTarget) discoverEndpoints(signer *RequestSigner, timeout time.Dura if err != nil { return err } - if err := signer.SignRequest(req); err != nil { + if err := t.PrepareApiRequest(req, t.deployment.Application.SerializedForm()); err != nil { return err } var endpointURL string @@ -409,14 +446,16 @@ func CustomTarget(baseURL string) Target { } // CloudTarget creates a Target for the Vespa Cloud platform. -func CloudTarget(apiURL string, deployment Deployment, apiKey []byte, tlsOptions TLSOptions, logOptions LogOptions) Target { +func CloudTarget(apiURL string, deployment Deployment, apiKey []byte, tlsOptions TLSOptions, logOptions LogOptions, + authConfigPath string) Target { return &cloudTarget{ - apiURL: apiURL, - targetType: cloudTargetType, - deployment: deployment, - apiKey: apiKey, - tlsOptions: tlsOptions, - logOptions: logOptions, + apiURL: apiURL, + targetType: cloudTargetType, + deployment: deployment, + apiKey: apiKey, + tlsOptions: tlsOptions, + logOptions: logOptions, + authConfigPath: authConfigPath, } } diff --git a/client/go/vespa/target_test.go b/client/go/vespa/target_test.go index ed924059297..d4d23901513 100644 --- a/client/go/vespa/target_test.go +++ b/client/go/vespa/target_test.go @@ -143,7 +143,10 @@ func createCloudTarget(t *testing.T, url string, logWriter io.Writer) Target { x509KeyPair, err := tls.X509KeyPair(kp.Certificate, kp.PrivateKey) assert.Nil(t, err) - apiKey, err := CreateAPIKey() + var apiKey []byte = nil + if !Auth0AccessTokenEnabled() { + apiKey, err = CreateAPIKey() + } assert.Nil(t, err) target := CloudTarget( @@ -154,7 +157,7 @@ func createCloudTarget(t *testing.T, url string, logWriter io.Writer) Target { }, apiKey, TLSOptions{KeyPair: x509KeyPair}, - LogOptions{Writer: logWriter}) + LogOptions{Writer: logWriter}, "") if ct, ok := target.(*cloudTarget); ok { ct.apiURL = url } else { diff --git a/config/src/main/java/com/yahoo/config/subscription/ConfigSubscriber.java b/config/src/main/java/com/yahoo/config/subscription/ConfigSubscriber.java index f5ed79a1d44..07132c460f9 100644 --- a/config/src/main/java/com/yahoo/config/subscription/ConfigSubscriber.java +++ b/config/src/main/java/com/yahoo/config/subscription/ConfigSubscriber.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Level; import java.util.logging.Logger; import static java.util.logging.Level.FINE; @@ -293,10 +294,13 @@ public class ConfigSubscriber implements AutoCloseable { // Keep on polling the subscriptions until we have a new generation across the board, or it times out for (ConfigHandle<? extends ConfigInstance> h : subscriptionHandles) { ConfigSubscription<? extends ConfigInstance> subscription = h.subscription(); + log.log(Level.FINEST, () -> "Calling nextConfig for " + subscription.getKey()); if ( ! subscription.nextConfig(timeLeftMillis)) { // This subscriber has no new state and we know it has exhausted all time + log.log(Level.FINEST, () -> "No new config for " + subscription.getKey()); return false; } + log.log(Level.FINEST, () -> "Got new generation or config for " + subscription.getKey()); throwIfExceptionSet(subscription); ConfigSubscription.ConfigState<? extends ConfigInstance> config = subscription.getConfigState(); if (currentGen == null) currentGen = config.getGeneration(); @@ -322,6 +326,7 @@ public class ConfigSubscriber implements AutoCloseable { if (reconfigDue) { // This indicates the clients will possibly reconfigure their services, so "reset" changed-logic in subscriptions. // Also if appropriate update the changed flag on the handler, which clients use. + log.log(Level.FINE, () -> "Reconfig will happen for generation " + generation); markSubsChangedSeen(currentGen); synchronized (monitor) { generation = currentGen; diff --git a/container-disc/src/main/sh/vespa-start-container-daemon.sh b/container-disc/src/main/sh/vespa-start-container-daemon.sh index 8c122d3170e..ded38e9f7c9 100755 --- a/container-disc/src/main/sh/vespa-start-container-daemon.sh +++ b/container-disc/src/main/sh/vespa-start-container-daemon.sh @@ -129,9 +129,14 @@ configure_memory() { available=`free -m | grep Mem | tr -s ' ' | cut -f2 -d' '` if hash cgget 2>/dev/null; then # TODO: Create vespa_cgget for this and remove dependency on libcgroup-tools - available_cgroup_bytes=$(cgget -nv -r memory.limit_in_bytes /) + available_cgroup_bytes=$(cgget -nv -r memory.limit_in_bytes / 2>&1) if [ $? -ne 0 ]; then - available_cgroup_bytes=$(vespa_cg2get memory.max) + if [[ "$available_cgroup_bytes" =~ "Cgroup is not mounted" ]]; then + available_cgroup_bytes=$(vespa_cg2get memory.max) + else + echo "$available_cgroup_bytes" >&2 + fi + # If command failed or returned value is 'max' assign a big value (default in CGroup v1) if ! [[ "$available_cgroup_bytes" =~ ^[0-9]+$ ]]; then available_cgroup_bytes=$(((1 << 63) -1)) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java index be533b0c53b..16f12b3ac07 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java @@ -243,7 +243,8 @@ public class RoutingController { Instance instance = application.require(instanceName); boolean registerLegacyNames = requiresLegacyNames(application.deploymentSpec(), instanceName); Set<ContainerEndpoint> containerEndpoints = new HashSet<>(); - EndpointList endpoints = declaredEndpointsOf(application); + DeploymentId deployment = new DeploymentId(instance.id(), zone); + EndpointList endpoints = declaredEndpointsOf(application).targets(deployment); EndpointList globalEndpoints = endpoints.scope(Endpoint.Scope.global); // Add endpoints backed by a rotation, and register them in DNS if necessary for (var assignedRotation : instance.rotations()) { @@ -280,9 +281,7 @@ public class RoutingController { } // Add endpoints not backed by a rotation (i.e. other routing methods so that the config server always knows // about global names, even when not using rotations) - DeploymentId deployment = new DeploymentId(instance.id(), zone); globalEndpoints.not().requiresRotation() - .targets(deployment) .groupingBy(Endpoint::cluster) .forEach((clusterId, clusterEndpoints) -> { containerEndpoints.add(new ContainerEndpoint(clusterId.value(), @@ -290,10 +289,8 @@ public class RoutingController { clusterEndpoints.mapToList(Endpoint::dnsName))); }); // Add application endpoints - EndpointList applicationEndpoints = endpoints.scope(Endpoint.Scope.application) - .not().direct() // These are handled by RoutingPolicies - .targets(deployment); - for (var endpoint : applicationEndpoints) { + EndpointList applicationEndpoints = endpoints.scope(Endpoint.Scope.application); + for (var endpoint : applicationEndpoints.shared()) { // DNS for non-shared endpoints is handled by RoutingPolicies Set<ZoneId> targetZones = endpoint.targets().stream() .map(t -> t.deployment().zoneId()) .collect(Collectors.toUnmodifiableSet()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java index f626d832b6a..f9fd02fbf56 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java @@ -78,6 +78,11 @@ public class EndpointList extends AbstractFilteringList<Endpoint, EndpointList> return matching(endpoint -> endpoint.routingMethod().isDirect()); } + /** Returns the subset of endpoints that use shared routing */ + public EndpointList shared() { + return matching(endpoint -> endpoint.routingMethod().isShared()); + } + public static EndpointList copyOf(Collection<Endpoint> endpoints) { return new EndpointList(endpoints, false); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java index 1e8e444896f..9472801ef2c 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java @@ -6,6 +6,7 @@ import com.yahoo.component.Version; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationId; import com.yahoo.config.application.api.ValidationOverrides; +import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.CloudName; @@ -233,35 +234,58 @@ public class ControllerTest { @Test public void testDnsUpdatesForGlobalEndpoint() { - var context = tester.newDeploymentContext("tenant1", "app1", "default"); + var betaContext = tester.newDeploymentContext("tenant1", "app1", "beta"); + var defaultContext = tester.newDeploymentContext("tenant1", "app1", "default"); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() + .instances("beta,default") .endpoint("default", "foo") .region("us-west-1") .region("us-central-1") // Two deployments should result in each DNS alias being registered once .build(); - context.submit(applicationPackage).deploy(); + betaContext.submit(applicationPackage).deploy(); + + { // Expected rotation names are passed to beta instance deployments + Collection<Deployment> betaDeployments = betaContext.instance().deployments().values(); + assertFalse(betaDeployments.isEmpty()); + for (Deployment deployment : betaDeployments) { + assertEquals("Rotation names are passed to config server in " + deployment.zone(), + Set.of("rotation-id-01", + "beta--app1--tenant1.global.vespa.oath.cloud"), + tester.configServer().containerEndpointNames(betaContext.deploymentIdIn(deployment.zone()))); + } + betaContext.flushDnsUpdates(); + } - Collection<Deployment> deployments = context.instance().deployments().values(); - assertFalse(deployments.isEmpty()); - for (Deployment deployment : deployments) { - assertEquals("Rotation names are passed to config server in " + deployment.zone(), - Set.of("rotation-id-01", - "app1--tenant1.global.vespa.oath.cloud"), - tester.configServer().containerEndpointNames(context.deploymentIdIn(deployment.zone()))); + { // Expected rotation names are passed to default instance deployments + Collection<Deployment> defaultDeployments = defaultContext.instance().deployments().values(); + assertFalse(defaultDeployments.isEmpty()); + for (Deployment deployment : defaultDeployments) { + assertEquals("Rotation names are passed to config server in " + deployment.zone(), + Set.of("rotation-id-02", + "app1--tenant1.global.vespa.oath.cloud"), + tester.configServer().containerEndpointNames(defaultContext.deploymentIdIn(deployment.zone()))); + } + defaultContext.flushDnsUpdates(); } - context.flushDnsUpdates(); - assertEquals(1, tester.controllerTester().nameService().records().size()); + Map<String, String> rotationCnames = Map.of("beta--app1--tenant1.global.vespa.oath.cloud", "rotation-fqdn-01.", + "app1--tenant1.global.vespa.oath.cloud", "rotation-fqdn-02."); + rotationCnames.forEach((cname, data) -> { + var record = tester.controllerTester().findCname(cname); + assertTrue(record.isPresent()); + assertEquals(cname, record.get().name().asString()); + assertEquals(data, record.get().data().asString()); + }); - var record = tester.controllerTester().findCname("app1--tenant1.global.vespa.oath.cloud"); - assertTrue(record.isPresent()); - assertEquals("app1--tenant1.global.vespa.oath.cloud", record.get().name().asString()); - assertEquals("rotation-fqdn-01.", record.get().data().asString()); + Map<ApplicationId, List<String>> globalDnsNamesByInstance = Map.of(betaContext.instanceId(), List.of("beta--app1--tenant1.global.vespa.oath.cloud"), + defaultContext.instanceId(), List.of("app1--tenant1.global.vespa.oath.cloud")); - List<String> globalDnsNames = tester.controller().routing().readDeclaredEndpointsOf(context.instanceId()) - .scope(Endpoint.Scope.global) - .mapToList(Endpoint::dnsName); - assertEquals(List.of("app1--tenant1.global.vespa.oath.cloud"), globalDnsNames); + globalDnsNamesByInstance.forEach((instance, dnsNames) -> { + List<String> actualDnsNames = tester.controller().routing().readDeclaredEndpointsOf(instance) + .scope(Endpoint.Scope.global) + .mapToList(Endpoint::dnsName); + assertEquals("Global DNS names for " + instance, dnsNames, actualDnsNames); + }); } @Test diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java index e65f6e087c5..7619cf71f1a 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java @@ -280,7 +280,7 @@ public class MetricsReporterTest { context.submit(applicationPackage).deploy(); reporter.maintain(); - assertEquals("Deployment queues name services requests", 15, metrics.getMetric(MetricsReporter.NAME_SERVICE_REQUESTS_QUEUED).intValue()); + assertEquals("Deployment queues name services requests", 6, metrics.getMetric(MetricsReporter.NAME_SERVICE_REQUESTS_QUEUED).intValue()); context.flushDnsUpdates(); reporter.maintain(); diff --git a/filedistribution/src/main/java/com/yahoo/vespa/filedistribution/FileReceiver.java b/filedistribution/src/main/java/com/yahoo/vespa/filedistribution/FileReceiver.java index fbc17293e8d..89a77599909 100644 --- a/filedistribution/src/main/java/com/yahoo/vespa/filedistribution/FileReceiver.java +++ b/filedistribution/src/main/java/com/yahoo/vespa/filedistribution/FileReceiver.java @@ -187,7 +187,7 @@ public class FileReceiver { private static void moveFileToDestination(File tempFile, File destination) { try { Files.move(tempFile.toPath(), destination.toPath()); - log.log(Level.FINE, () -> "File moved from " + tempFile.getAbsolutePath()+ " to " + destination.getAbsolutePath()); + log.log(Level.FINEST, () -> "File moved from " + tempFile.getAbsolutePath()+ " to " + destination.getAbsolutePath()); } catch (FileAlreadyExistsException e) { // Don't fail if it already exists (we might get the file from several config servers when retrying, servers are down etc. // so it might be written already). Delete temp file/dir in that case, to avoid filling the disk. @@ -239,7 +239,7 @@ public class FileReceiver { } private void receiveFilePart(Request req) { - log.log(Level.FINE, () -> "Received method call '" + req.methodName() + "' with parameters : " + req.parameters()); + log.log(Level.FINEST, () -> "Received method call '" + req.methodName() + "' with parameters : " + req.parameters()); FileReference reference = new FileReference(req.parameters().get(0).asString()); int sessionId = req.parameters().get(1).asInt32(); diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index d72582287a9..ed12529f91e 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -295,7 +295,7 @@ public class Flags { public static final UnboundBooleanFlag DELETE_UNMAINTAINED_CERTIFICATES = defineFeatureFlag( "delete-unmaintained-certificates", false, - List.of("andreer"), "2021-09-23", "2021-11-11", + List.of("andreer"), "2021-09-23", "2021-12-11", "Whether to delete certificates that are known by provider but not by controller", "Takes effect on next run of EndpointCertificateMaintainer" ); diff --git a/searchcore/src/tests/proton/bucketdb/bucketdb/bucketdb_test.cpp b/searchcore/src/tests/proton/bucketdb/bucketdb/bucketdb_test.cpp index 94300d4abac..8f10f4b8045 100644 --- a/searchcore/src/tests/proton/bucketdb/bucketdb/bucketdb_test.cpp +++ b/searchcore/src/tests/proton/bucketdb/bucketdb/bucketdb_test.cpp @@ -1,8 +1,11 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/document/base/documentid.h> #include <vespa/searchcore/proton/bucketdb/bucket_db_explorer.h> #include <vespa/searchcore/proton/bucketdb/bucketdb.h> +#include <vespa/searchcore/proton/bucketdb/remove_batch_entry.h> #include <vespa/vespalib/data/slime/slime.h> #include <vespa/vespalib/stllike/asciistream.h> +#include <vespa/vespalib/util/stringfmt.h> #include <vespa/vespalib/testkit/testapp.h> #include <vespa/log/log.h> @@ -29,6 +32,24 @@ constexpr uint32_t DOCSIZE_2(10000u); typedef BucketInfo::ReadyState RS; typedef SubDbType SDT; +namespace { + +constexpr uint32_t bucket_bits = 16; + +uint32_t num_buckets() { return (1u << bucket_bits); } + +BucketId make_bucket_id(uint32_t n) { + return BucketId(bucket_bits, n & (num_buckets() - 1)); +} + +GlobalId make_gid(uint32_t n, uint32_t i) +{ + DocumentId id(vespalib::make_string("id::test:n=%u:%u", n & (num_buckets() - 1), i)); + return id.getGlobalId(); +} + +} + void assertDocCount(uint32_t ready, uint32_t notReady, @@ -70,12 +91,20 @@ struct Fixture Fixture() : _db() {} + void add(const GlobalId &gid, const Timestamp ×tamp, uint32_t docSize, SubDbType subDbType) { + BucketId bucket(bucket_bits, gid.convertToBucketId().getRawId()); + _db.add(gid, bucket, timestamp, docSize, subDbType); + } const BucketState &add(const Timestamp ×tamp, uint32_t docSize, SubDbType subDbType) { return _db.add(GID_1, BUCKET_1, timestamp, docSize, subDbType); } const BucketState &add(const Timestamp ×tamp, SubDbType subDbType) { return add(timestamp, DOCSIZE_1, subDbType); } + void remove(const GlobalId& gid, const Timestamp ×tamp, uint32_t docSize, SubDbType subDbType) { + BucketId bucket(bucket_bits, gid.convertToBucketId().getRawId()); + _db.remove(gid, bucket, timestamp, docSize, subDbType); + } BucketState remove(const Timestamp ×tamp, uint32_t docSize, SubDbType subDbType) { _db.remove(GID_1, BUCKET_1, timestamp, docSize, subDbType); return get(); @@ -83,8 +112,14 @@ struct Fixture BucketState remove(const Timestamp ×tamp, SubDbType subDbType) { return remove(timestamp, DOCSIZE_1, subDbType); } + void remove_batch(const std::vector<RemoveBatchEntry> &removed, SubDbType sub_db_type) { + _db.remove_batch(removed, sub_db_type); + } + BucketState get(BucketId bucket_id) const { + return _db.get(bucket_id); + } BucketState get() const { - return _db.get(BUCKET_1); + return get(BUCKET_1); } BucketChecksum getChecksum(const Timestamp ×tamp, uint32_t docSize, SubDbType subDbType) { BucketDB db; @@ -181,6 +216,36 @@ TEST_F("require that bucket checksum ignores document sizes", Fixture) EXPECT_EQUAL(state1.getChecksum(), state2.getChecksum()); } +TEST_F("require that remove batch works", Fixture) +{ + f.add(make_gid(4, 1), Timestamp(10), 100, SDT::READY); + f.add(make_gid(4, 2), Timestamp(11), 104, SDT::READY); + f.add(make_gid(4, 3), Timestamp(12), 102, SDT::READY); + f.add(make_gid(5, 4), Timestamp(13), 200, SDT::READY); + f.add(make_gid(5, 5), Timestamp(14), 270, SDT::READY); + f.add(make_gid(5, 6), Timestamp(15), 1000, SDT::READY); + auto state1 = f.get(make_bucket_id(4)); + EXPECT_EQUAL(306u, state1.getReadyDocSizes()); + EXPECT_EQUAL(3u, state1.getReadyCount()); + auto state2 = f.get(make_bucket_id(5)); + EXPECT_EQUAL(1470u, state2.getReadyDocSizes()); + EXPECT_EQUAL(3u, state2.getReadyCount()); + std::vector<RemoveBatchEntry> removed; + removed.emplace_back(make_gid(4, 1), make_bucket_id(4), Timestamp(10), 100); + removed.emplace_back(make_gid(4, 3), make_bucket_id(4), Timestamp(12), 102); + removed.emplace_back(make_gid(5, 5), make_bucket_id(5), Timestamp(14), 270); + f.remove_batch(removed, SDT::READY); + auto state3 = f.get(make_bucket_id(4)); + EXPECT_EQUAL(104u, state3.getReadyDocSizes()); + EXPECT_EQUAL(1u, state3.getReadyCount()); + auto state4 = f.get(make_bucket_id(5)); + EXPECT_EQUAL(1200u, state4.getReadyDocSizes()); + EXPECT_EQUAL(2u, state4.getReadyCount()); + f.remove(make_gid(4, 2), Timestamp(11), 104, SDT::READY); + f.remove(make_gid(5, 4), Timestamp(13), 200, SDT::READY); + f.remove(make_gid(5, 6), Timestamp(15), 1000, SDT::READY); +} + TEST("require that bucket db can be explored") { BucketDBOwner db; diff --git a/searchcore/src/tests/proton/matching/partial_result/partial_result_test.cpp b/searchcore/src/tests/proton/matching/partial_result/partial_result_test.cpp index 139288f6b6f..1fadd3993ff 100644 --- a/searchcore/src/tests/proton/matching/partial_result/partial_result_test.cpp +++ b/searchcore/src/tests/proton/matching/partial_result/partial_result_test.cpp @@ -23,7 +23,7 @@ void checkMerge(const std::vector<double> &a, const std::vector<double> &b, EXPECT_EQUAL(a.size() + b.size(), res_a.totalHits()); ASSERT_EQUAL(expect.size(), res_a.size()); for (size_t i = 0; i < expect.size(); ++i) { - EXPECT_EQUAL(expect[i], res_a.hit(i)._rankValue); + EXPECT_EQUAL(expect[i], res_a.hit(i).getRank()); } } @@ -70,10 +70,10 @@ TEST("require that partial results can be created without sort data") { res.totalHits(1000); EXPECT_EQUAL(1000u, res.totalHits()); ASSERT_EQUAL(2u, res.size()); - EXPECT_EQUAL(1u, res.hit(0)._docId); - EXPECT_EQUAL(10.0, res.hit(0)._rankValue); - EXPECT_EQUAL(2u, res.hit(1)._docId); - EXPECT_EQUAL(5.0, res.hit(1)._rankValue); + EXPECT_EQUAL(1u, res.hit(0).getDocId()); + EXPECT_EQUAL(10.0, res.hit(0).getRank()); + EXPECT_EQUAL(2u, res.hit(1).getDocId()); + EXPECT_EQUAL(5.0, res.hit(1).getRank()); } TEST("require that partial results can be created with sort data") { @@ -90,12 +90,12 @@ TEST("require that partial results can be created with sort data") { res.totalHits(1000); EXPECT_EQUAL(1000u, res.totalHits()); ASSERT_EQUAL(2u, res.size()); - EXPECT_EQUAL(1u, res.hit(0)._docId); - EXPECT_EQUAL(10.0, res.hit(0)._rankValue); + EXPECT_EQUAL(1u, res.hit(0).getDocId()); + EXPECT_EQUAL(10.0, res.hit(0).getRank()); EXPECT_EQUAL(str1.data(), res.sortData(0).first); EXPECT_EQUAL(str1.size(), res.sortData(0).second); - EXPECT_EQUAL(2u, res.hit(1)._docId); - EXPECT_EQUAL(5.0, res.hit(1)._rankValue); + EXPECT_EQUAL(2u, res.hit(1).getDocId()); + EXPECT_EQUAL(5.0, res.hit(1).getRank()); EXPECT_EQUAL(str2.data(), res.sortData(1).first); EXPECT_EQUAL(str2.size(), res.sortData(1).second); } @@ -133,10 +133,10 @@ TEST("require that lower docid is preferred when sorting on rank") { res_c.add(search::RankedHit(1, 1.0)); res_a.merge(res_b); ASSERT_EQUAL(1u, res_a.size()); - EXPECT_EQUAL(2u, res_a.hit(0)._docId); + EXPECT_EQUAL(2u, res_a.hit(0).getDocId()); res_a.merge(res_c); ASSERT_EQUAL(1u, res_a.size()); - EXPECT_EQUAL(1u, res_a.hit(0)._docId); + EXPECT_EQUAL(1u, res_a.hit(0).getDocId()); } TEST("require that lower docid is preferred when using sortspec") { @@ -149,10 +149,10 @@ TEST("require that lower docid is preferred when using sortspec") { res_c.add(search::RankedHit(1, 1.0), PartialResult::SortRef(foo.data(), foo.size())); res_a.merge(res_b); ASSERT_EQUAL(1u, res_a.size()); - EXPECT_EQUAL(2u, res_a.hit(0)._docId); + EXPECT_EQUAL(2u, res_a.hit(0).getDocId()); res_a.merge(res_c); ASSERT_EQUAL(1u, res_a.size()); - EXPECT_EQUAL(1u, res_a.hit(0)._docId); + EXPECT_EQUAL(1u, res_a.hit(0).getDocId()); } TEST_MAIN() { TEST_RUN_ALL(); } diff --git a/searchcore/src/vespa/searchcore/proton/bucketdb/bucketdb.cpp b/searchcore/src/vespa/searchcore/proton/bucketdb/bucketdb.cpp index 998da6b5789..c7a3103af22 100644 --- a/searchcore/src/vespa/searchcore/proton/bucketdb/bucketdb.cpp +++ b/searchcore/src/vespa/searchcore/proton/bucketdb/bucketdb.cpp @@ -1,14 +1,18 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "bucketdb.h" +#include "remove_batch_entry.h" #include <cassert> #include <algorithm> +#include <optional> using document::GlobalId; using storage::spi::BucketChecksum; namespace proton { +using bucketdb::RemoveBatchEntry; + BucketDB::BucketDB() : _map(), _cachedBucketId(), @@ -65,6 +69,20 @@ BucketDB::remove(const GlobalId &gid, } void +BucketDB::remove_batch(const std::vector<RemoveBatchEntry> &removed, SubDbType sub_db_type) +{ + std::optional<BucketId> prev_bucket_id; + BucketState* state = nullptr; + for (auto &entry : removed) { + if (!prev_bucket_id.has_value() || prev_bucket_id.value() != entry.get_bucket_id()) { + state = &_map[entry.get_bucket_id()]; + prev_bucket_id = entry.get_bucket_id(); + } + state->remove(entry.get_gid(), entry.get_timestamp(), entry.get_doc_size(), sub_db_type); + } +} + +void BucketDB::modify(const GlobalId &gid, const BucketId &oldBucketId, const Timestamp &oldTimestamp, uint32_t oldDocSize, const BucketId &newBucketId, const Timestamp &newTimestamp, uint32_t newDocSize, diff --git a/searchcore/src/vespa/searchcore/proton/bucketdb/bucketdb.h b/searchcore/src/vespa/searchcore/proton/bucketdb/bucketdb.h index 1723609e48e..2ea7594bde1 100644 --- a/searchcore/src/vespa/searchcore/proton/bucketdb/bucketdb.h +++ b/searchcore/src/vespa/searchcore/proton/bucketdb/bucketdb.h @@ -7,6 +7,8 @@ #include <vespa/persistence/spi/result.h> #include <map> +namespace proton::bucketdb { class RemoveBatchEntry; } + namespace proton { class BucketDB @@ -42,6 +44,8 @@ public: const BucketId &bucketId, const Timestamp ×tamp, uint32_t docSize, SubDbType subDbType); + void remove_batch(const std::vector<bucketdb::RemoveBatchEntry> &removed, SubDbType sub_db_type); + void modify(const GlobalId &gid, const BucketId &oldBucketId, const Timestamp &oldTimestamp, uint32_t oldDocSize, const BucketId &newBucketId, const Timestamp &newTimestamp, uint32_t newDocSize, diff --git a/searchcore/src/vespa/searchcore/proton/bucketdb/remove_batch_entry.h b/searchcore/src/vespa/searchcore/proton/bucketdb/remove_batch_entry.h new file mode 100644 index 00000000000..1ab1adb1add --- /dev/null +++ b/searchcore/src/vespa/searchcore/proton/bucketdb/remove_batch_entry.h @@ -0,0 +1,36 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/document/base/globalid.h> +#include <vespa/document/bucket/bucketid.h> +#include <persistence/spi/types.h> + + +namespace proton::bucketdb { + +/* + * Class containing meta data for a single document being removed from + * bucket db. + */ +class RemoveBatchEntry { + document::GlobalId _gid; + document::BucketId _bucket_id; + storage::spi::Timestamp _timestamp; + uint32_t _doc_size; +public: + RemoveBatchEntry(const document::GlobalId& gid, const document::BucketId& bucket_id, const storage::spi::Timestamp& timestamp, uint32_t doc_size) noexcept + : _gid(gid), + _bucket_id(bucket_id), + _timestamp(timestamp), + _doc_size(doc_size) + { + } + + const document::GlobalId& get_gid() const noexcept { return _gid; } + const document::BucketId& get_bucket_id() const noexcept { return _bucket_id; } + const storage::spi::Timestamp& get_timestamp() const noexcept { return _timestamp; } + uint32_t get_doc_size() const noexcept { return _doc_size; } +}; + +} diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.cpp b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.cpp index 3d7e1c1c774..13d4a39c8b1 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.cpp +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.cpp @@ -9,6 +9,7 @@ #include <vespa/persistence/spi/bucket_limits.h> #include <vespa/searchcore/proton/bucketdb/bucketsessionbase.h> #include <vespa/searchcore/proton/bucketdb/joinbucketssession.h> +#include <vespa/searchcore/proton/bucketdb/remove_batch_entry.h> #include <vespa/searchcore/proton/bucketdb/splitbucketsession.h> #include <vespa/searchlib/attribute/load_utils.h> #include <vespa/searchlib/attribute/readerbase.h> @@ -30,6 +31,7 @@ LOG_SETUP(".proton.documentmetastore"); using document::BucketId; using document::GlobalId; using proton::bucketdb::BucketState; +using proton::bucketdb::RemoveBatchEntry; using proton::documentmetastore::GidToLidMapKey; using search::AttributeVector; using search::FileReader; @@ -681,13 +683,15 @@ DocumentMetaStore::removeBatch(const std::vector<DocId> &lidsToRemove, const uin remove_batch_internal_btree(removed); _lidAlloc.unregister_lids(lidsToRemove); { - bucketdb::Guard bucketGuard = _bucketDB->takeGuard(); - // TODO: add remove_batch() method to BucketDB + std::vector<RemoveBatchEntry> bdb_removed; + bdb_removed.reserve(removed.size()); for (const auto& lid_and_meta : removed) { auto& meta = lid_and_meta.second; - bucketGuard->remove(meta.getGid(), meta.getBucketId().stripUnused(), - meta.getTimestamp(), meta.getDocSize(), _subDbType); + bdb_removed.emplace_back(meta.getGid(), meta.getBucketId().stripUnused(), + meta.getTimestamp(), meta.getDocSize()); } + bucketdb::Guard bucketGuard = _bucketDB->takeGuard(); + bucketGuard->remove_batch(bdb_removed, _subDbType); } incGeneration(); if (_op_listener) { diff --git a/searchcore/src/vespa/searchcore/proton/matching/partial_result.cpp b/searchcore/src/vespa/searchcore/proton/matching/partial_result.cpp index 6ae97a125ad..432752d69d0 100644 --- a/searchcore/src/vespa/searchcore/proton/matching/partial_result.cpp +++ b/searchcore/src/vespa/searchcore/proton/matching/partial_result.cpp @@ -7,10 +7,10 @@ namespace proton::matching { namespace { bool before(const search::RankedHit &a, const search::RankedHit &b) { - if (a._rankValue != b._rankValue) { - return (a._rankValue > b._rankValue); + if (a.getRank() != b.getRank()) { + return (a.getRank() > b.getRank()); } - return (a._docId < b._docId); + return (a.getDocId() < b.getDocId()); } void mergeHits(size_t maxHits, diff --git a/searchcore/src/vespa/searchcore/proton/matching/result_processor.cpp b/searchcore/src/vespa/searchcore/proton/matching/result_processor.cpp index 341bd3bb855..f332ca5ec26 100644 --- a/searchcore/src/vespa/searchcore/proton/matching/result_processor.cpp +++ b/searchcore/src/vespa/searchcore/proton/matching/result_processor.cpp @@ -108,7 +108,7 @@ ResultProcessor::extract_docid_ordering(const PartialResult &result) const std::vector<std::pair<uint32_t,uint32_t>> list; list.reserve(est_size); for (size_t i = _offset; i < result.size(); ++i) { - list.emplace_back(result.hit(i)._docId, list.size()); + list.emplace_back(result.hit(i).getDocId(), list.size()); } std::sort(list.begin(), list.end(), [](const auto &a, const auto &b){ return (a.first < b.first); }); return list; @@ -142,11 +142,11 @@ ResultProcessor::makeReply(PartialResultUP full_result) for (size_t i = 0; i < hitcnt; ++i) { search::engine::SearchReply::Hit &dst = r.hits[i]; const search::RankedHit &src = result.hit(hitOffset + i); - uint32_t docId = src._docId; + uint32_t docId = src.getDocId(); if (metaStore.getGidEvenIfMoved(docId, gid)) { dst.gid = gid; } - dst.metric = src._rankValue; + dst.metric = src.getRank(); LOG(debug, "convertLidToGid: hit[%zu]: lid(%u) -> gid(%s)", i, docId, dst.gid.toString().c_str()); } if (result.hasSortData() && (hitcnt > 0)) { diff --git a/searchcore/src/vespa/searchcore/proton/persistenceengine/persistenceengine.cpp b/searchcore/src/vespa/searchcore/proton/persistenceengine/persistenceengine.cpp index 2e1fc74037c..04d91b9c028 100644 --- a/searchcore/src/vespa/searchcore/proton/persistenceengine/persistenceengine.cpp +++ b/searchcore/src/vespa/searchcore/proton/persistenceengine/persistenceengine.cpp @@ -364,7 +364,7 @@ PersistenceEngine::putAsync(const Bucket &bucket, Timestamp ts, storage::spi::Do return onComplete->onComplete(std::make_unique<Result>(Result::ErrorType::PERMANENT_ERROR, make_string("No handler for document type '%s'", docType.toString().c_str()))); } - auto transportContext = std::make_shared<AsyncTranportContext>(1, std::move(onComplete)); + auto transportContext = std::make_shared<AsyncTransportContext>(1, std::move(onComplete)); handler->handlePut(feedtoken::make(std::move(transportContext)), bucket, ts, std::move(doc)); } @@ -384,7 +384,7 @@ PersistenceEngine::removeAsync(const Bucket& b, Timestamp t, const DocumentId& d return onComplete->onComplete(std::make_unique<RemoveResult>(Result::ErrorType::PERMANENT_ERROR, make_string("No handler for document type '%s'", docType.toString().c_str()))); } - auto transportContext = std::make_shared<AsyncTranportContext>(1, std::move(onComplete)); + auto transportContext = std::make_shared<AsyncTransportContext>(1, std::move(onComplete)); handler->handleRemove(feedtoken::make(std::move(transportContext)), b, t, did); } @@ -436,7 +436,7 @@ PersistenceEngine::updateAsync(const Bucket& b, Timestamp t, DocumentUpdate::SP if (handler == nullptr) { return onComplete->onComplete(std::make_unique<UpdateResult>(Result::ErrorType::PERMANENT_ERROR, make_string("No handler for document type '%s'", docType.toString().c_str()))); } - auto transportContext = std::make_shared<AsyncTranportContext>(1, std::move(onComplete)); + auto transportContext = std::make_shared<AsyncTransportContext>(1, std::move(onComplete)); handler->handleUpdate(feedtoken::make(std::move(transportContext)), b, t, std::move(upd)); } @@ -555,7 +555,7 @@ PersistenceEngine::createBucketAsync(const Bucket &b, Context &, OperationComple LOG(spam, "createBucket(%s)", b.toString().c_str()); HandlerSnapshot snap = getHandlerSnapshot(rguard, b.getBucketSpace()); - auto transportContext = std::make_shared<AsyncTranportContext>(snap.size(), std::move(onComplete)); + auto transportContext = std::make_shared<AsyncTransportContext>(snap.size(), std::move(onComplete)); while (snap.handlers().valid()) { IPersistenceHandler *handler = snap.handlers().get(); snap.handlers().next(); @@ -575,7 +575,7 @@ PersistenceEngine::deleteBucketAsync(const Bucket& b, Context&, OperationComplet LOG(spam, "deleteBucket(%s)", b.toString().c_str()); HandlerSnapshot snap = getHandlerSnapshot(rguard, b.getBucketSpace()); - auto transportContext = std::make_shared<AsyncTranportContext>(snap.size(), std::move(onComplete)); + auto transportContext = std::make_shared<AsyncTransportContext>(snap.size(), std::move(onComplete)); while (snap.handlers().valid()) { IPersistenceHandler *handler = snap.handlers().get(); snap.handlers().next(); diff --git a/searchcore/src/vespa/searchcore/proton/persistenceengine/transport_latch.cpp b/searchcore/src/vespa/searchcore/proton/persistenceengine/transport_latch.cpp index 61c240f0d6a..8a0955b3147 100644 --- a/searchcore/src/vespa/searchcore/proton/persistenceengine/transport_latch.cpp +++ b/searchcore/src/vespa/searchcore/proton/persistenceengine/transport_latch.cpp @@ -67,7 +67,7 @@ TransportLatch::send(ResultUP result, bool documentWasFound) _latch.countDown(); } -AsyncTranportContext::AsyncTranportContext(uint32_t cnt, OperationComplete::UP onComplete) +AsyncTransportContext::AsyncTransportContext(uint32_t cnt, OperationComplete::UP onComplete) : TransportMerger(cnt > 1), _countDown(cnt), _onComplete(std::move(onComplete)) @@ -78,17 +78,17 @@ AsyncTranportContext::AsyncTranportContext(uint32_t cnt, OperationComplete::UP o } void -AsyncTranportContext::completeIfDone() { +AsyncTransportContext::completeIfDone() { _countDown--; if (_countDown == 0) { _onComplete->onComplete(std::move(_result)); } } -AsyncTranportContext::~AsyncTranportContext() = default; +AsyncTransportContext::~AsyncTransportContext() = default; void -AsyncTranportContext::send(ResultUP result, bool documentWasFound) +AsyncTransportContext::send(ResultUP result, bool documentWasFound) { mergeResult(std::move(result), documentWasFound); } diff --git a/searchcore/src/vespa/searchcore/proton/persistenceengine/transport_latch.h b/searchcore/src/vespa/searchcore/proton/persistenceengine/transport_latch.h index 084ad0e8d10..91cadfedcd5 100644 --- a/searchcore/src/vespa/searchcore/proton/persistenceengine/transport_latch.h +++ b/searchcore/src/vespa/searchcore/proton/persistenceengine/transport_latch.h @@ -63,7 +63,7 @@ public: * Implementation of FeedToken::ITransport for async handling of the async reply for an operation. * Uses an internal count to keep track the number of the outstanding replies. */ -class AsyncTranportContext : public TransportMerger { +class AsyncTransportContext : public TransportMerger { private: using Result = storage::spi::Result; using OperationComplete = storage::spi::OperationComplete; @@ -72,8 +72,8 @@ private: OperationComplete::UP _onComplete; void completeIfDone() override; public: - AsyncTranportContext(uint32_t cnt, OperationComplete::UP); - ~AsyncTranportContext() override; + AsyncTransportContext(uint32_t cnt, OperationComplete::UP); + ~AsyncTransportContext() override; void send(ResultUP result, bool documentWasFound) override; }; diff --git a/searchlib/src/tests/attribute/searchcontext/searchcontext_test.cpp b/searchlib/src/tests/attribute/searchcontext/searchcontext_test.cpp index 4076194542a..de54386e4af 100644 --- a/searchlib/src/tests/attribute/searchcontext/searchcontext_test.cpp +++ b/searchlib/src/tests/attribute/searchcontext/searchcontext_test.cpp @@ -487,7 +487,7 @@ SearchContextTest::checkResultSet(const ResultSet & rs, const DocSet & expected, ASSERT_TRUE(array != nullptr); uint32_t i = 0; for (auto iter = expected.begin(); iter != expected.end(); ++iter, ++i) { - EXPECT_TRUE(array[i]._docId == *iter); + EXPECT_TRUE(array[i].getDocId() == *iter); } } } @@ -1517,10 +1517,10 @@ SearchContextTest::requireThatSearchIsWorkingAfterClearDoc(const vespalib::strin EXPECT_EQUAL(4u, rs->getNumHits()); ASSERT_TRUE(4u == rs->getNumHits()); const RankedHit * array = rs->getArray(); - EXPECT_EQUAL(1u, array[0]._docId); - EXPECT_EQUAL(2u, array[1]._docId); - EXPECT_EQUAL(3u, array[2]._docId); - EXPECT_EQUAL(4u, array[3]._docId); + EXPECT_EQUAL(1u, array[0].getDocId()); + EXPECT_EQUAL(2u, array[1].getDocId()); + EXPECT_EQUAL(3u, array[2].getDocId()); + EXPECT_EQUAL(4u, array[3].getDocId()); } a->clearDoc(1); a->clearDoc(3); @@ -1529,8 +1529,8 @@ SearchContextTest::requireThatSearchIsWorkingAfterClearDoc(const vespalib::strin ResultSetPtr rs = performSearch(v, term); EXPECT_EQUAL(2u, rs->getNumHits()); const RankedHit * array = rs->getArray(); - EXPECT_EQUAL(2u, array[0]._docId); - EXPECT_EQUAL(4u, array[1]._docId); + EXPECT_EQUAL(2u, array[0].getDocId()); + EXPECT_EQUAL(4u, array[1].getDocId()); } } @@ -1578,9 +1578,9 @@ SearchContextTest::requireThatSearchIsWorkingAfterLoadAndClearDoc(const vespalib const RankedHit * array = rs->getArray(); for (uint32_t i = 0; i < 14; ++i) { if (i < 5) { - EXPECT_EQUAL(i + 1, array[i]._docId); + EXPECT_EQUAL(i + 1, array[i].getDocId()); } else - EXPECT_EQUAL(i + 2, array[i]._docId); + EXPECT_EQUAL(i + 2, array[i].getDocId()); } } ValueType buf; @@ -1682,15 +1682,15 @@ SearchContextTest::requireThatFlagAttributeIsWorkingWhenNewDocsAreAdded() { ResultSetPtr rs = performSearch(fa, "<24"); EXPECT_EQUAL(2u, rs->getNumHits()); - EXPECT_EQUAL(1u, rs->getArray()[0]._docId); - EXPECT_EQUAL(2u, rs->getArray()[1]._docId); + EXPECT_EQUAL(1u, rs->getArray()[0].getDocId()); + EXPECT_EQUAL(2u, rs->getArray()[1].getDocId()); } { ResultSetPtr rs = performSearch(fa, "24"); EXPECT_EQUAL(3u, rs->getNumHits()); - EXPECT_EQUAL(1u, rs->getArray()[0]._docId); - EXPECT_EQUAL(2u, rs->getArray()[1]._docId); - EXPECT_EQUAL(4u, rs->getArray()[2]._docId); + EXPECT_EQUAL(1u, rs->getArray()[0].getDocId()); + EXPECT_EQUAL(2u, rs->getArray()[1].getDocId()); + EXPECT_EQUAL(4u, rs->getArray()[2].getDocId()); } } { @@ -1717,15 +1717,15 @@ SearchContextTest::requireThatFlagAttributeIsWorkingWhenNewDocsAreAdded() EXPECT_EQUAL(exp50.size(), rs1->getNumHits()); EXPECT_EQUAL(exp50.size(), rs2->getNumHits()); for (size_t j = 0; j < exp50.size(); ++j) { - EXPECT_EQUAL(exp50[j], rs1->getArray()[j]._docId); - EXPECT_EQUAL(exp50[j], rs2->getArray()[j]._docId); + EXPECT_EQUAL(exp50[j], rs1->getArray()[j].getDocId()); + EXPECT_EQUAL(exp50[j], rs2->getArray()[j].getDocId()); } } { ResultSetPtr rs = performSearch(fa, "60"); EXPECT_EQUAL(exp60.size(), rs->getNumHits()); for (size_t j = 0; j < exp60.size(); ++j) { - EXPECT_EQUAL(exp60[j], rs->getArray()[j]._docId); + EXPECT_EQUAL(exp60[j], rs->getArray()[j].getDocId()); } } } diff --git a/searchlib/src/tests/grouping/grouping_test.cpp b/searchlib/src/tests/grouping/grouping_test.cpp index ef4930de8ce..2eab66cb3b7 100644 --- a/searchlib/src/tests/grouping/grouping_test.cpp +++ b/searchlib/src/tests/grouping/grouping_test.cpp @@ -105,7 +105,7 @@ public: hit._rankValue = rank; _hits.push_back(hit); for (uint32_t pos = (_hits.size() - 1); - pos > 0 && (_hits[pos]._rankValue > _hits[pos - 1]._rankValue); + pos > 0 && (_hits[pos].getRank() > _hits[pos - 1].getRank()); --pos) { std::swap(_hits[pos], _hits[pos - 1]); diff --git a/searchlib/src/tests/groupingengine/groupingengine_benchmark.cpp b/searchlib/src/tests/groupingengine/groupingengine_benchmark.cpp index 66fa359f1a3..e82079073e7 100644 --- a/searchlib/src/tests/groupingengine/groupingengine_benchmark.cpp +++ b/searchlib/src/tests/groupingengine/groupingengine_benchmark.cpp @@ -88,7 +88,7 @@ public: hit._rankValue = rank; _hits.push_back(hit); for (uint32_t pos = (_hits.size() - 1); - pos > 0 && (_hits[pos]._rankValue > _hits[pos - 1]._rankValue); + pos > 0 && (_hits[pos].getRank() > _hits[pos - 1].getRank()); --pos) { std::swap(_hits[pos], _hits[pos - 1]); diff --git a/searchlib/src/tests/groupingengine/groupingengine_test.cpp b/searchlib/src/tests/groupingengine/groupingengine_test.cpp index a0179c36c23..d54b68388e4 100644 --- a/searchlib/src/tests/groupingengine/groupingengine_test.cpp +++ b/searchlib/src/tests/groupingengine/groupingengine_test.cpp @@ -87,7 +87,7 @@ public: hit._rankValue = rank; _hits.push_back(hit); for (uint32_t pos = (_hits.size() - 1); - pos > 0 && (_hits[pos]._rankValue > _hits[pos - 1]._rankValue); + pos > 0 && (_hits[pos].getRank() > _hits[pos - 1].getRank()); --pos) { std::swap(_hits[pos], _hits[pos - 1]); diff --git a/searchlib/src/tests/hitcollector/hitcollector_test.cpp b/searchlib/src/tests/hitcollector/hitcollector_test.cpp index 617e0e85824..ed68c47ea23 100644 --- a/searchlib/src/tests/hitcollector/hitcollector_test.cpp +++ b/searchlib/src/tests/hitcollector/hitcollector_test.cpp @@ -70,8 +70,8 @@ void checkResult(const ResultSet & rs, const std::vector<RankedHit> & exp) ASSERT_EQUAL(rs.getArrayUsed(), exp.size()); for (uint32_t i = 0; i < exp.size(); ++i) { - EXPECT_EQUAL(rh[i]._docId, exp[i]._docId); - EXPECT_EQUAL(rh[i]._rankValue + 1.0, exp[i]._rankValue + 1.0); + EXPECT_EQUAL(rh[i].getDocId(), exp[i].getDocId()); + EXPECT_EQUAL(rh[i].getRank() + 1.0, exp[i].getRank() + 1.0); } } else { ASSERT_TRUE(rs.getArray() == nullptr); diff --git a/searchlib/src/tests/sortresults/sorttest.cpp b/searchlib/src/tests/sortresults/sorttest.cpp index cd892800ca5..bbd6d0b72ce 100644 --- a/searchlib/src/tests/sortresults/sorttest.cpp +++ b/searchlib/src/tests/sortresults/sorttest.cpp @@ -41,17 +41,17 @@ test_sort(unsigned int caseNum, unsigned int n, unsigned int ntop) } FastS_SortResults(array, n, ntop); - minmax = array[ntop - 1]._rankValue; + minmax = array[ntop - 1].getRank(); for(i = 0; i < n; i++) { if (i < ntop && i > 0 - && array[i]._rankValue > array[i - 1]._rankValue) { + && array[i].getRank() > array[i - 1].getRank()) { printf("ERROR: rank(%d) > rank(%d)\n", i, i - 1); ok = false; break; } if (i >= ntop && - array[i]._rankValue > minmax) { + array[i].getRank() > minmax) { printf("ERROR: rank(%d) > rank(%d)\n", i, ntop - 1); ok = false; diff --git a/searchlib/src/tests/sortspec/multilevelsort.cpp b/searchlib/src/tests/sortspec/multilevelsort.cpp index f438fce0e7f..576e1d1336c 100644 --- a/searchlib/src/tests/sortspec/multilevelsort.cpp +++ b/searchlib/src/tests/sortspec/multilevelsort.cpp @@ -275,21 +275,21 @@ MultilevelSortTest::sortAndCheck(const std::vector<Spec> &spec, uint32_t num, for (uint32_t j = 0; j < spec.size(); ++j) { int cmp = 0; if (spec[j]._type == RANK) { - if (hits[i]._rankValue < hits[i+1]._rankValue) { + if (hits[i].getRank() < hits[i+1].getRank()) { cmp = -1; - } else if (hits[i]._rankValue > hits[i+1]._rankValue) { + } else if (hits[i].getRank() > hits[i+1].getRank()) { cmp = 1; } } else if (spec[j]._type == DOCID) { - if (hits[i]._docId < hits[i+1]._docId) { + if (hits[i].getDocId() < hits[i+1].getDocId()) { cmp = -1; - } else if (hits[i]._docId > hits[i+1]._docId) { + } else if (hits[i].getDocId() > hits[i+1].getDocId()) { cmp = 1; } } else { AttributeVector *av = vec[spec[j]._name].get(); cmp = compare(av, spec[j]._type, - hits[i]._docId, hits[i+1]._docId); + hits[i].getDocId(), hits[i+1].getDocId()); } if (spec[j]._asc) { EXPECT_TRUE(cmp <= 0); diff --git a/searchlib/src/vespa/searchlib/aggregation/grouping.cpp b/searchlib/src/vespa/searchlib/aggregation/grouping.cpp index 68098d6c35a..f373b5fc0b3 100644 --- a/searchlib/src/vespa/searchlib/aggregation/grouping.cpp +++ b/searchlib/src/vespa/searchlib/aggregation/grouping.cpp @@ -205,13 +205,13 @@ void Grouping::postProcess() void Grouping::aggregateWithoutClock(const RankedHit * rankedHit, unsigned int len) { for(unsigned int i(0); i < len; i++) { - aggregate(rankedHit[i]._docId, rankedHit[i]._rankValue); + aggregate(rankedHit[i].getDocId(), rankedHit[i].getRank()); } } void Grouping::aggregateWithClock(const RankedHit * rankedHit, unsigned int len) { for(unsigned int i(0); (i < len) && !hasExpired(); i++) { - aggregate(rankedHit[i]._docId, rankedHit[i]._rankValue); + aggregate(rankedHit[i].getDocId(), rankedHit[i].getRank()); } } diff --git a/searchlib/src/vespa/searchlib/attribute/loadedenumvalue.h b/searchlib/src/vespa/searchlib/attribute/loadedenumvalue.h index b31e726b103..fdf9ab624ad 100644 --- a/searchlib/src/vespa/searchlib/attribute/loadedenumvalue.h +++ b/searchlib/src/vespa/searchlib/attribute/loadedenumvalue.h @@ -29,7 +29,7 @@ public: uint64_t operator()(const LoadedEnumAttribute &v) { - return (static_cast<uint64_t>(v._enum) << 32) | v._docId; + return (static_cast<uint64_t>(v._enum) << 32) | v.getDocId(); } }; diff --git a/searchlib/src/vespa/searchlib/attribute/loadedvalue.h b/searchlib/src/vespa/searchlib/attribute/loadedvalue.h index 701ccdc902c..b8f938838d2 100644 --- a/searchlib/src/vespa/searchlib/attribute/loadedvalue.h +++ b/searchlib/src/vespa/searchlib/attribute/loadedvalue.h @@ -103,9 +103,10 @@ public: T _value; uint32_t _eidx; }; + uint32_t _docId; uint32_t _idx; - vespalib::datastore::EntryRef _pidx; + vespalib::datastore::EntryRef _pidx; private: int32_t _weight; Value _value; diff --git a/searchlib/src/vespa/searchlib/common/sortresults.cpp b/searchlib/src/vespa/searchlib/common/sortresults.cpp index 7a54de708d0..7510ae162ce 100644 --- a/searchlib/src/vespa/searchlib/common/sortresults.cpp +++ b/searchlib/src/vespa/searchlib/common/sortresults.cpp @@ -51,7 +51,7 @@ FastS_insertion_sort(RankedHit a[], uint32_t n) for (i=1; i<n ; i++) { swap = a[i]; j = i; - while (R(swap._rankValue) > R(a[j-1]._rankValue)) { + while (R(swap.getRank()) > R(a[j-1].getRank())) { a[j] = a[j-1]; if (!(--j)) break;; } @@ -74,13 +74,13 @@ FastS_radixsort(RankedHit a[], uint32_t n, uint32_t ntop) memset(cnt, 0, 256*sizeof(uint32_t)); // Count occurrences [NB: will fail with n < 3] for(i = 0; i < n - 3; i += 4) { - cnt[(R(a[i]._rankValue) >> SHIFT) & 0xFF]++; - cnt[(R(a[i + 1]._rankValue) >> SHIFT) & 0xFF]++; - cnt[(R(a[i + 2]._rankValue) >> SHIFT) & 0xFF]++; - cnt[(R(a[i + 3]._rankValue) >> SHIFT) & 0xFF]++; + cnt[(R(a[i].getRank()) >> SHIFT) & 0xFF]++; + cnt[(R(a[i + 1].getRank()) >> SHIFT) & 0xFF]++; + cnt[(R(a[i + 2].getRank()) >> SHIFT) & 0xFF]++; + cnt[(R(a[i + 3].getRank()) >> SHIFT) & 0xFF]++; } for(; i < n; i++) - cnt[(R(a[i]._rankValue) >> SHIFT) & 0xFF]++; + cnt[(R(a[i].getRank()) >> SHIFT) & 0xFF]++; // Accumulate cnt positions sorted = (cnt[0]==n); @@ -109,14 +109,14 @@ FastS_radixsort(RankedHit a[], uint32_t n, uint32_t ntop) // Grab first element to move j = ptr[i]; swap = a[j]; - k = (R(swap._rankValue) >> SHIFT) & 0xFF; + k = (R(swap.getRank()) >> SHIFT) & 0xFF; // Swap into correct class until cycle completed if (i!=k) { do { temp = a[ptr[k]]; a[ptr[k]++] = swap; - k = (R((swap = temp)._rankValue) >> SHIFT) & 0xFF; + k = (R((swap = temp).getRank()) >> SHIFT) & 0xFF; remain--; } while (i!=k); // Place last element in cycle @@ -265,11 +265,11 @@ FastS_SortSpec::initSortData(const RankedHit *hits, uint32_t n) written = sizeof(hits->_docId) + sizeof(_partitionId); break; case ASC_RANK: - serializeForSort<convertForSort<search::HitRank, true> >(hits[i]._rankValue, mySortData); + serializeForSort<convertForSort<search::HitRank, true> >(hits[i].getRank(), mySortData); written = sizeof(hits->_rankValue); break; case DESC_RANK: - serializeForSort<convertForSort<search::HitRank, false> >(hits[i]._rankValue, mySortData); + serializeForSort<convertForSort<search::HitRank, false> >(hits[i].getRank(), mySortData); written = sizeof(hits->_rankValue); break; case ASC_VECTOR: diff --git a/searchlib/src/vespa/searchlib/fef/termfieldmatchdata.cpp b/searchlib/src/vespa/searchlib/fef/termfieldmatchdata.cpp index 77c2aa3072a..ea278ebf607 100644 --- a/searchlib/src/vespa/searchlib/fef/termfieldmatchdata.cpp +++ b/searchlib/src/vespa/searchlib/fef/termfieldmatchdata.cpp @@ -18,7 +18,7 @@ TermFieldMatchData::TermFieldMatchData() : } TermFieldMatchData::TermFieldMatchData(const TermFieldMatchData & rhs) : - _docId(rhs._docId), + _docId(rhs.getDocId()), _fieldId(rhs._fieldId), _flags(rhs._flags), _sz(0), diff --git a/searchlib/src/vespa/searchlib/memoryindex/field_index_remover.h b/searchlib/src/vespa/searchlib/memoryindex/field_index_remover.h index f8328d15289..429eea038c9 100644 --- a/searchlib/src/vespa/searchlib/memoryindex/field_index_remover.h +++ b/searchlib/src/vespa/searchlib/memoryindex/field_index_remover.h @@ -21,6 +21,7 @@ private: struct WordFieldDocTuple { vespalib::datastore::EntryRef _wordRef; uint32_t _docId; + WordFieldDocTuple() noexcept : _wordRef(0), _docId(0) diff --git a/searchlib/src/vespa/searchlib/queryeval/hitcollector.cpp b/searchlib/src/vespa/searchlib/queryeval/hitcollector.cpp index b2b1d49bae9..3293019e538 100644 --- a/searchlib/src/vespa/searchlib/queryeval/hitcollector.cpp +++ b/searchlib/src/vespa/searchlib/queryeval/hitcollector.cpp @@ -195,7 +195,7 @@ mergeHitsIntoResultSet(const std::vector<HitCollector::Hit> &hits, ResultSet &re uint32_t rhCur(0); uint32_t rhEnd(result.getArrayUsed()); for (const auto &hit : hits) { - while (rhCur != rhEnd && result[rhCur]._docId != hit.first) { + while (rhCur != rhEnd && result[rhCur].getDocId() != hit.first) { // just set the iterators right ++rhCur; } diff --git a/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.h b/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.h index a94f6087a0d..d0a75930ed5 100644 --- a/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.h +++ b/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.h @@ -77,8 +77,8 @@ public: bool operator<(const PendingOp &rhs) const { if (_wordIdx != rhs._wordIdx) return _wordIdx < rhs._wordIdx; - if (_docId != rhs._docId) - return _docId < rhs._docId; + if (_docId != rhs.getDocId()) + return _docId < rhs.getDocId(); return _seq < rhs._seq; } }; |